feat(dbms): 增加数据库备份任务停止重启功能和MySQL支持

- 添加了备份任务停止和重启接口及实现
- 实现了对MySQL数据库的支持,包括数据库名配置
- 重构了数据库连接和备份操作的SPI架构
- 优化了备份文件删除逻辑,支持目录递归删除
- 增加了连接名称唯一性校验
- 完善了备份任务状态管理和错误处理机制
- 更新了数据库连接参数验证逻辑
This commit is contained in:
2026-06-09 13:14:43 +08:00
parent 5f6c10b9cb
commit 36962221f5
47 changed files with 2553 additions and 227 deletions

View File

@@ -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_<taskNo>.json`
- MySQL `JDBC_EXPORT` 会按任务号创建独立备份目录,每张表独立 CSV默认按 512MB 分片:
- 数据分片文件:`<table>_part001_yyyyMMdd_<taskNo>.csv`
- 元数据文件:`mysql_jdbc_export_metadata_yyyyMMdd_<taskNo>.json`
- 备份任务支持停止和重新开始:
- `POST /database/backups/tasks/stop`
- `POST /database/backups/tasks/restart`
- `JDBC_EXPORT` 恢复依赖元数据文件,不再允许缺少元数据直接发起恢复。
- 删除备份文件时,会校验目标路径必须位于受管备份目录下,避免误删非备份文件。
@@ -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` 编译、测试或真实库联调。

View File

@@ -35,5 +35,10 @@
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.3</version>
</dependency>
</dependencies>
</project>

View File

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

View File

@@ -0,0 +1,541 @@
package com.njcn.gather.systemops.database.component;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam;
import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.function.BooleanSupplier;
/**
* JDBC 表数据导出与恢复组件。
*/
@Component
@RequiredArgsConstructor
public class JdbcExportComponent {
private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("^[A-Za-z][A-Za-z0-9_#$]*$");
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final ObjectMapper objectMapper;
public void exportCsv(Connection jdbcConnection, String ownerName, DatabaseBackupParam.CreateParam param,
Path dataFilePath, Path metadataFilePath) throws Exception {
Files.createDirectories(dataFilePath.getParent());
if (metadataFilePath.getParent() != null) {
Files.createDirectories(metadataFilePath.getParent());
}
List<TableMetadata> metadataList = new ArrayList<>();
try (BufferedWriter writer = Files.newBufferedWriter(dataFilePath, StandardCharsets.UTF_8)) {
for (String tableName : param.getTargetNames()) {
metadataList.add(exportTable(jdbcConnection, ownerName, tableName, param, writer));
}
}
try (BufferedWriter metadataWriter = Files.newBufferedWriter(metadataFilePath, StandardCharsets.UTF_8)) {
objectMapper.writeValue(metadataWriter, metadataList);
}
}
public void importCsv(Connection jdbcConnection, Path dataFilePath, Path metadataFilePath, String dbType,
String restoreMode, String targetOwnerName) throws Exception {
String metadataText = new String(Files.readAllBytes(metadataFilePath), StandardCharsets.UTF_8);
if (metadataText.trim().startsWith("{")) {
importCsvV2(jdbcConnection, metadataFilePath, dbType, restoreMode, targetOwnerName);
return;
}
List<TableMetadata> metadataList = Arrays.asList(objectMapper.readValue(metadataFilePath.toFile(), TableMetadata[].class));
Map<String, TableMetadata> metadataMap = new LinkedHashMap<>();
for (TableMetadata metadata : metadataList) {
metadataMap.put(metadata.getFullTableName(), metadata);
}
jdbcConnection.setAutoCommit(false);
try (BufferedReader reader = Files.newBufferedReader(dataFilePath, StandardCharsets.UTF_8)) {
try {
String line;
TableMetadata currentMetadata = null;
List<String> currentColumns = null;
while ((line = reader.readLine()) != null) {
if (line.startsWith("-- TABLE ")) {
currentMetadata = metadataMap.get(line.substring("-- TABLE ".length()).trim());
if (currentMetadata == null) {
throw new IllegalArgumentException("未找到表元数据:" + line);
}
currentColumns = null;
prepareTargetTable(jdbcConnection, currentMetadata, dbType, restoreMode, targetOwnerName);
continue;
}
if (currentMetadata == null) {
continue;
}
if (currentColumns == null) {
currentColumns = parseCsvLine(line);
continue;
}
List<String> values = parseCsvLine(line);
insertRow(jdbcConnection, currentMetadata, currentColumns, values, dbType, restoreMode, targetOwnerName);
}
jdbcConnection.commit();
} catch (Exception exception) {
jdbcConnection.rollback();
throw exception;
}
}
}
public ExportManifest exportMysqlCsvV2(Connection jdbcConnection, String databaseName, String taskNo,
DatabaseBackupParam.CreateParam param, Path backupDirectory,
Path metadataFilePath, int fetchSize, long maxPartBytes,
BooleanSupplier cancelled) throws Exception {
Files.createDirectories(backupDirectory);
ExportManifest manifest = new ExportManifest();
manifest.setVersion(2);
manifest.setDbType("MYSQL");
manifest.setBackupStrategy("JDBC_EXPORT");
manifest.setTaskNo(taskNo);
manifest.setDatabaseName(databaseName);
List<TableExportMetadata> tableMetadataList = new ArrayList<>();
manifest.setTables(tableMetadataList);
for (String tableName : param.getTargetNames()) {
checkCancelled(cancelled, backupDirectory);
tableMetadataList.add(exportMysqlTableV2(jdbcConnection, tableName, param, backupDirectory, taskNo,
fetchSize, maxPartBytes, cancelled));
}
try (BufferedWriter metadataWriter = Files.newBufferedWriter(metadataFilePath, StandardCharsets.UTF_8)) {
objectMapper.writeValue(metadataWriter, manifest);
}
return manifest;
}
private TableMetadata exportTable(Connection connection, String ownerName, String tableName,
DatabaseBackupParam.CreateParam param, BufferedWriter writer) throws Exception {
String normalizedOwner = normalizeOwner(ownerName);
String normalizedTable = normalizeMysqlIdentifier(tableName);
String fullTableName = buildFullTableName(normalizedOwner, normalizedTable);
String querySql = buildQuerySql(fullTableName, param);
TableMetadata metadata = new TableMetadata();
metadata.setOwnerName(normalizedOwner);
metadata.setTableName(normalizedTable);
metadata.setFullTableName(fullTableName);
metadata.setTimeColumn(StrUtil.isBlank(param.getTimeColumn()) ? null : normalizeMysqlIdentifier(param.getTimeColumn()));
metadata.setStartTime(param.getStartTime() == null ? null : param.getStartTime().format(DATE_TIME_FORMATTER));
metadata.setEndTime(param.getEndTime() == null ? null : param.getEndTime().format(DATE_TIME_FORMATTER));
writer.write("-- TABLE " + fullTableName);
writer.newLine();
try (PreparedStatement statement = connection.prepareStatement(querySql)) {
fillQueryParams(statement, param);
try (ResultSet resultSet = statement.executeQuery()) {
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
int columnCount = resultSetMetaData.getColumnCount();
List<String> columnNames = new ArrayList<>();
List<String> columnTypes = new ArrayList<>();
for (int i = 1; i <= columnCount; i++) {
String columnName = resultSetMetaData.getColumnName(i);
columnNames.add(normalizeMysqlIdentifier(columnName));
columnTypes.add(resultSetMetaData.getColumnTypeName(i));
if (i > 1) {
writer.write(",");
}
writer.write(escape(columnName));
}
writer.newLine();
long rowCount = 0L;
while (resultSet.next()) {
for (int i = 1; i <= columnCount; i++) {
if (i > 1) {
writer.write(",");
}
writer.write(escape(resultSet.getString(i)));
}
writer.newLine();
rowCount++;
}
metadata.setColumnNames(columnNames);
metadata.setColumnTypes(columnTypes);
metadata.setRowCount(rowCount);
return metadata;
}
}
}
private TableExportMetadata exportMysqlTableV2(Connection connection, String tableName,
DatabaseBackupParam.CreateParam param, Path backupDirectory,
String taskNo, int fetchSize, long maxPartBytes,
BooleanSupplier cancelled) throws Exception {
String normalizedTable = normalizeIdentifier(tableName);
String querySql = buildQuerySql(normalizedTable, param);
TableExportMetadata metadata = new TableExportMetadata();
metadata.setTableName(normalizedTable);
metadata.setFullTableName(normalizedTable);
metadata.setTimeColumn(StrUtil.isBlank(param.getTimeColumn()) ? null : normalizeIdentifier(param.getTimeColumn()));
metadata.setStartTime(param.getStartTime() == null ? null : param.getStartTime().format(DATE_TIME_FORMATTER));
metadata.setEndTime(param.getEndTime() == null ? null : param.getEndTime().format(DATE_TIME_FORMATTER));
metadata.setColumns(new ArrayList<>());
metadata.setParts(new ArrayList<>());
try (PreparedStatement statement = connection.prepareStatement(querySql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
statement.setFetchSize(fetchSize);
fillQueryParams(statement, param);
try (ResultSet resultSet = statement.executeQuery()) {
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
int columnCount = resultSetMetaData.getColumnCount();
List<String> columnNames = new ArrayList<>();
for (int i = 1; i <= columnCount; i++) {
String columnName = resultSetMetaData.getColumnName(i);
columnNames.add(columnName);
ColumnMetadata columnMetadata = new ColumnMetadata();
columnMetadata.setName(columnName);
columnMetadata.setType(resultSetMetaData.getColumnTypeName(i));
metadata.getColumns().add(columnMetadata);
}
PartWriter partWriter = openPartWriter(backupDirectory, normalizedTable, taskNo,
metadata.getParts().size() + 1, columnNames);
metadata.getParts().add(partWriter.getPart());
long totalRows = 0L;
try {
while (resultSet.next()) {
checkCancelled(cancelled, backupDirectory);
if (partWriter.shouldRotate(maxPartBytes)) {
partWriter.close();
partWriter = openPartWriter(backupDirectory, normalizedTable, taskNo,
metadata.getParts().size() + 1, columnNames);
metadata.getParts().add(partWriter.getPart());
}
partWriter.writeRow(resultSet, columnCount);
totalRows++;
}
} finally {
partWriter.close();
}
metadata.setRowCount(totalRows);
return metadata;
}
}
}
private String buildQuerySql(String fullTableName, DatabaseBackupParam.CreateParam param) {
StringBuilder sql = new StringBuilder("SELECT * FROM ").append(fullTableName);
if (param.getStartTime() != null && param.getEndTime() != null && StrUtil.isNotBlank(param.getTimeColumn())) {
sql.append(" WHERE ").append(normalizeIdentifier(param.getTimeColumn())).append(" BETWEEN ? AND ?");
}
return sql.toString();
}
private void fillQueryParams(PreparedStatement statement, DatabaseBackupParam.CreateParam param) throws Exception {
if (param.getStartTime() != null && param.getEndTime() != null && StrUtil.isNotBlank(param.getTimeColumn())) {
statement.setString(1, param.getStartTime().format(DATE_TIME_FORMATTER));
statement.setString(2, param.getEndTime().format(DATE_TIME_FORMATTER));
}
}
private void prepareTargetTable(Connection connection, TableMetadata metadata, String dbType, String restoreMode,
String targetOwnerName) throws Exception {
if (!"TRUNCATE".equalsIgnoreCase(restoreMode)
&& !("REPLACE".equalsIgnoreCase(restoreMode) && !isMysql(dbType))) {
return;
}
String fullTargetName = buildTargetTableName(metadata, targetOwnerName);
try (Statement statement = connection.createStatement()) {
statement.execute("TRUNCATE TABLE " + fullTargetName);
}
}
private void insertRow(Connection connection, TableMetadata metadata, List<String> columns,
List<String> values, String dbType, String restoreMode, String targetOwnerName) throws Exception {
String fullTargetName = buildTargetTableName(metadata, targetOwnerName);
StringBuilder placeholders = new StringBuilder();
for (int i = 0; i < columns.size(); i++) {
if (i > 0) {
placeholders.append(",");
}
placeholders.append("?");
}
String sql = buildInsertSql(dbType, restoreMode, fullTargetName, columns, placeholders.toString());
try (PreparedStatement statement = connection.prepareStatement(sql)) {
for (int i = 0; i < columns.size(); i++) {
statement.setString(i + 1, i < values.size() ? values.get(i) : null);
}
statement.executeUpdate();
}
}
private String buildInsertSql(String dbType, String restoreMode, String fullTargetName, List<String> columns,
String placeholders) {
String command = "INSERT INTO";
if (isMysql(dbType) && "SKIP".equalsIgnoreCase(restoreMode)) {
// MySQL 跳过重复主键行,避免普通恢复因历史数据重复而整体失败。
command = "INSERT IGNORE INTO";
} else if (isMysql(dbType) && "REPLACE".equalsIgnoreCase(restoreMode)) {
command = "REPLACE INTO";
}
return command + " " + fullTargetName + " (" + String.join(",", columns) + ") VALUES (" + placeholders + ")";
}
private void importCsvV2(Connection jdbcConnection, Path metadataFilePath, String dbType, String restoreMode,
String targetOwnerName) throws Exception {
ExportManifest manifest = objectMapper.readValue(metadataFilePath.toFile(), ExportManifest.class);
jdbcConnection.setAutoCommit(false);
try {
for (TableExportMetadata tableMetadata : manifest.getTables()) {
prepareTargetTable(jdbcConnection, toLegacyMetadata(tableMetadata), dbType, restoreMode, targetOwnerName);
for (FilePartMetadata part : tableMetadata.getParts()) {
importPart(jdbcConnection, metadataFilePath.getParent(), tableMetadata, part, dbType, restoreMode, targetOwnerName);
}
}
jdbcConnection.commit();
} catch (Exception exception) {
jdbcConnection.rollback();
throw exception;
}
}
private void importPart(Connection jdbcConnection, Path backupDirectory, TableExportMetadata tableMetadata,
FilePartMetadata part, String dbType, String restoreMode, String targetOwnerName) throws Exception {
Path partPath = backupDirectory.resolve(part.getFileName()).normalize();
if (!partPath.startsWith(backupDirectory.normalize())) {
throw new IllegalArgumentException("备份分片路径不在元数据目录内:" + part.getFileName());
}
try (BufferedReader reader = Files.newBufferedReader(partPath, StandardCharsets.UTF_8)) {
List<String> columns = null;
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("-- TABLE ")) {
continue;
}
if (columns == null) {
columns = parseCsvLine(line);
continue;
}
List<String> values = parseCsvLine(line);
insertRow(jdbcConnection, toLegacyMetadata(tableMetadata), columns, values, dbType, restoreMode, targetOwnerName);
}
}
}
private boolean isMysql(String dbType) {
return DatabaseOpsConst.DB_TYPE_MYSQL.equalsIgnoreCase(dbType);
}
private PartWriter openPartWriter(Path backupDirectory, String tableName, String taskNo, int partIndex,
List<String> columnNames) throws IOException {
String rawName = tableName.toLowerCase(Locale.ROOT) + "_part" + String.format("%03d", partIndex) + ".csv";
String fileName = DatabaseFileNameUtil.appendTodayWithTask(rawName, taskNo);
Path filePath = backupDirectory.resolve(fileName).normalize();
BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8);
writer.write("-- TABLE " + tableName);
writer.newLine();
for (int i = 0; i < columnNames.size(); i++) {
if (i > 0) {
writer.write(",");
}
writer.write(escape(columnNames.get(i)));
}
writer.newLine();
FilePartMetadata part = new FilePartMetadata();
part.setFileName(fileName);
part.setFilePath(filePath.toString());
part.setRowCount(0L);
part.setFileSize(0L);
return new PartWriter(writer, filePath, part);
}
private TableMetadata toLegacyMetadata(TableExportMetadata metadata) {
TableMetadata legacy = new TableMetadata();
legacy.setOwnerName(null);
legacy.setTableName(metadata.getTableName());
legacy.setFullTableName(metadata.getFullTableName());
legacy.setTimeColumn(metadata.getTimeColumn());
legacy.setStartTime(metadata.getStartTime());
legacy.setEndTime(metadata.getEndTime());
legacy.setRowCount(metadata.getRowCount());
List<String> columnNames = new ArrayList<>();
List<String> columnTypes = new ArrayList<>();
for (ColumnMetadata column : metadata.getColumns()) {
columnNames.add(column.getName());
columnTypes.add(column.getType());
}
legacy.setColumnNames(columnNames);
legacy.setColumnTypes(columnTypes);
return legacy;
}
private void checkCancelled(BooleanSupplier cancelled, Path backupDirectory) {
if (cancelled != null && cancelled.getAsBoolean()) {
throw new IllegalStateException("备份任务已停止,已生成文件保留在:" + backupDirectory);
}
}
private String buildTargetTableName(TableMetadata metadata, String targetOwnerName) {
String owner = normalizeOwner(StrUtil.blankToDefault(targetOwnerName, metadata.getOwnerName()));
return buildFullTableName(owner, metadata.getTableName());
}
private String buildFullTableName(String ownerName, String tableName) {
if (StrUtil.isBlank(ownerName)) {
return tableName;
}
return ownerName + "." + tableName;
}
private String normalizeOwner(String ownerName) {
if (StrUtil.isBlank(ownerName)) {
return null;
}
return normalizeIdentifier(ownerName);
}
private List<String> parseCsvLine(String line) {
List<String> result = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean quoted = false;
for (int i = 0; i < line.length(); i++) {
char currentChar = line.charAt(i);
if (currentChar == '"') {
if (quoted && i + 1 < line.length() && line.charAt(i + 1) == '"') {
current.append('"');
i++;
} else {
quoted = !quoted;
}
continue;
}
if (currentChar == ',' && !quoted) {
result.add(current.toString());
current.setLength(0);
continue;
}
current.append(currentChar);
}
result.add(current.toString());
return result;
}
private String escape(String value) {
if (value == null) {
return "";
}
return "\"" + value.replace("\"", "\"\"") + "\"";
}
private String normalizeIdentifier(String value) {
if (value == null || !IDENTIFIER_PATTERN.matcher(value).matches()) {
throw new IllegalArgumentException("数据库对象名称格式不正确:" + value);
}
return value.trim().toUpperCase(Locale.ROOT);
}
private String normalizeMysqlIdentifier(String value) {
if (value == null || !IDENTIFIER_PATTERN.matcher(value).matches()) {
throw new IllegalArgumentException("数据库对象名称格式不正确:" + value);
}
return value.trim();
}
@Data
public static class TableMetadata {
private String ownerName;
private String tableName;
private String fullTableName;
private List<String> columnNames;
private List<String> columnTypes;
private String timeColumn;
private String startTime;
private String endTime;
private Long rowCount;
}
@Data
public static class ExportManifest {
private Integer version;
private String dbType;
private String backupStrategy;
private String taskNo;
private String databaseName;
private List<TableExportMetadata> tables;
}
@Data
public static class TableExportMetadata {
private String tableName;
private String fullTableName;
private String timeColumn;
private String startTime;
private String endTime;
private List<ColumnMetadata> columns;
private Long rowCount;
private List<FilePartMetadata> parts;
}
@Data
public static class ColumnMetadata {
private String name;
private String type;
}
@Data
public static class FilePartMetadata {
private String fileName;
private String filePath;
private Long rowCount;
private Long fileSize;
}
private class PartWriter {
private final BufferedWriter writer;
private final Path filePath;
private final FilePartMetadata part;
private PartWriter(BufferedWriter writer, Path filePath, FilePartMetadata part) {
this.writer = writer;
this.filePath = filePath;
this.part = part;
}
private FilePartMetadata getPart() {
return part;
}
private boolean shouldRotate(long maxPartBytes) throws IOException {
writer.flush();
return part.getRowCount() > 0 && Files.size(filePath) >= maxPartBytes;
}
private void writeRow(ResultSet resultSet, int columnCount) throws Exception {
for (int i = 1; i <= columnCount; i++) {
if (i > 1) {
writer.write(",");
}
writer.write(escape(resultSet.getString(i)));
}
writer.newLine();
part.setRowCount(part.getRowCount() + 1);
}
private void close() throws IOException {
writer.close();
part.setFileSize(Files.exists(filePath) ? Files.size(filePath) : 0L);
}
}
}

View File

@@ -0,0 +1,89 @@
package com.njcn.gather.systemops.database.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO;
import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Oracle JDBC 连接与元数据探测组件。
*/
@Component
public class OracleJdbcComponent {
public DatabaseTestResultVO test(DatabaseConnection connection, String password) {
DatabaseTestResultVO result = new DatabaseTestResultVO();
try (Connection ignored = openConnection(connection, password)) {
result.setSuccess(true);
result.setMessage("连接成功");
} catch (Exception exception) {
result.setSuccess(false);
result.setMessage(exception.getMessage());
}
return result;
}
public List<DatabaseTableVO> listTables(DatabaseConnection connection, String password, String schemaName) throws Exception {
String owner = StrUtil.blankToDefault(schemaName, connection.getSchemaName());
if (StrUtil.isBlank(owner)) {
owner = connection.getUsername();
}
owner = owner.trim().toUpperCase(Locale.ROOT);
String sql = "SELECT t.owner, t.table_name, t.num_rows, o.last_ddl_time, c.comments "
+ "FROM all_tables t "
+ "LEFT JOIN all_tab_comments c "
+ "ON t.owner = c.owner AND t.table_name = c.table_name "
+ "LEFT JOIN all_objects o "
+ "ON t.owner = o.owner AND t.table_name = o.object_name AND o.object_type = 'TABLE' "
+ "WHERE t.owner = ? ORDER BY t.table_name";
try (Connection jdbcConnection = openConnection(connection, password);
PreparedStatement statement = jdbcConnection.prepareStatement(sql)) {
statement.setString(1, owner);
try (ResultSet resultSet = statement.executeQuery()) {
List<DatabaseTableVO> result = new ArrayList<>();
while (resultSet.next()) {
DatabaseTableVO table = new DatabaseTableVO();
table.setOwner(resultSet.getString("owner"));
table.setTableName(resultSet.getString("table_name"));
table.setEngine(DatabaseOpsConst.DB_TYPE_ORACLE);
table.setTableRows(getLongValue(resultSet, "num_rows"));
Timestamp updateTime = resultSet.getTimestamp("last_ddl_time");
table.setUpdateTime(updateTime == null ? null : updateTime.toLocalDateTime());
table.setComments(resultSet.getString("comments"));
result.add(table);
}
return result;
}
}
}
private Long getLongValue(ResultSet resultSet, String columnName) throws Exception {
long value = resultSet.getLong(columnName);
return resultSet.wasNull() ? null : value;
}
public Connection openConnection(DatabaseConnection connection, String password) throws Exception {
if (StrUtil.isBlank(password)) {
throw new IllegalArgumentException("数据库密码不能为空");
}
return DriverManager.getConnection(buildJdbcUrl(connection), connection.getUsername(), password);
}
public String buildJdbcUrl(DatabaseConnection connection) {
if (DatabaseOpsConst.CONNECT_TYPE_SID.equalsIgnoreCase(connection.getConnectType())) {
return "jdbc:oracle:thin:@" + connection.getHost() + ":" + connection.getPort() + ":" + connection.getSid();
}
return "jdbc:oracle:thin:@//" + connection.getHost() + ":" + connection.getPort() + "/" + connection.getServiceName();
}
}

View File

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

View File

@@ -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 = "确认删除";

View File

@@ -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<Boolean> stop(@RequestBody @Validated DatabaseBackupParam.StopParam param) {
String methodDescribe = getMethodDescribe("stop");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.stopBackupTask(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("重新开始备份任务")
@PostMapping("/tasks/restart")
public HttpResult<DatabaseTaskCreateVO> restart(@RequestBody @Validated DatabaseBackupParam.RestartParam param) {
String methodDescribe = getMethodDescribe("restart");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, databaseOperationTaskService.restartBackupTask(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询备份文件")
@PostMapping("/files/list")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,10 @@ public interface DatabaseOperationTaskService extends IService<DatabaseOperation
DatabaseTaskVO getStatus(String taskId);
boolean stopBackupTask(DatabaseBackupParam.StopParam param);
DatabaseTaskCreateVO restartBackupTask(DatabaseBackupParam.RestartParam param);
boolean deleteTask(String taskId, String confirmText);
boolean existsRunningTask(String connectionId);

View File

@@ -24,7 +24,9 @@ import org.springframework.transaction.annotation.Transactional;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 数据库备份文件服务实现。
@@ -63,9 +65,9 @@ public class DatabaseBackupFileServiceImpl extends ServiceImpl<DatabaseBackupFil
if (file == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "备份文件不存在或已删除");
}
deletePhysicalFile(file, file.getFilePath());
deletePhysicalFile(file, file.getLogFilePath());
deletePhysicalFile(file, file.getMetadataFilePath());
deletePhysicalPath(file, file.getFilePath());
deletePhysicalPath(file, file.getLogFilePath());
deletePhysicalPath(file, file.getMetadataFilePath());
file.setState(DatabaseOpsConst.STATE_DELETED);
file.setUpdateTime(LocalDateTime.now());
return this.updateById(file);
@@ -73,14 +75,14 @@ public class DatabaseBackupFileServiceImpl extends ServiceImpl<DatabaseBackupFil
@Override
public void validateBackupFileReadable(DatabaseBackupFile backupFile) {
validateReadablePath(backupFile, backupFile.getFilePath(), "备份文件", false);
validateReadablePath(backupFile, backupFile.getFilePath(), "备份文件", false, true);
validateReadablePath(backupFile, backupFile.getMetadataFilePath(), "备份元数据文件",
StrUtil.isBlank(backupFile.getMetadataFilePath()));
StrUtil.isBlank(backupFile.getMetadataFilePath()), false);
if (StrUtil.isBlank(backupFile.getChecksum())) {
throw new BusinessException(CommonResponseEnum.FAIL, "备份文件缺少校验值");
}
Path filePath = resolveManagedPath(backupFile, backupFile.getFilePath());
String actualChecksum = DatabaseChecksumUtil.sha256(filePath);
Path checksumPath = resolveChecksumPath(backupFile);
String actualChecksum = DatabaseChecksumUtil.sha256(checksumPath);
if (!backupFile.getChecksum().equalsIgnoreCase(actualChecksum)) {
throw new BusinessException(CommonResponseEnum.FAIL, "备份文件校验失败");
}
@@ -100,20 +102,41 @@ public class DatabaseBackupFileServiceImpl extends ServiceImpl<DatabaseBackupFil
return path;
}
Path primaryFilePath = DatabasePathUtil.normalize(backupFile.getFilePath());
if (primaryFilePath != null && primaryFilePath.getParent() != null
&& DatabasePathUtil.isUnder(path, primaryFilePath.getParent())) {
return path;
if (primaryFilePath != null) {
Path allowedRoot = Files.isDirectory(primaryFilePath) ? primaryFilePath : primaryFilePath.getParent();
if (allowedRoot != null && DatabasePathUtil.isUnder(path, allowedRoot)) {
return path;
}
}
throw new BusinessException(CommonResponseEnum.FAIL, "文件路径不在允许的备份目录内");
}
private void deletePhysicalFile(DatabaseBackupFile backupFile, String filePath) {
private Path resolveChecksumPath(DatabaseBackupFile backupFile) {
Path metadataPath = resolveManagedPath(backupFile, backupFile.getMetadataFilePath());
if (metadataPath != null && Files.exists(metadataPath) && !Files.isDirectory(metadataPath)) {
return metadataPath;
}
Path filePath = resolveManagedPath(backupFile, backupFile.getFilePath());
if (filePath == null || !Files.exists(filePath) || Files.isDirectory(filePath)) {
throw new BusinessException(CommonResponseEnum.FAIL, "备份校验文件不存在");
}
return filePath;
}
private void deletePhysicalPath(DatabaseBackupFile backupFile, String filePath) {
if (StrUtil.isBlank(filePath)) {
return;
}
try {
Path path = resolveManagedPath(backupFile, filePath);
if (path != null && Files.exists(path) && !Files.isDirectory(path)) {
if (path == null || !Files.exists(path)) {
return;
}
if (Files.isDirectory(path)) {
try (Stream<Path> paths = Files.walk(path)) {
paths.sorted(Comparator.reverseOrder()).forEach(this::deleteSinglePath);
}
} else {
Files.delete(path);
}
} catch (BusinessException exception) {
@@ -123,7 +146,16 @@ public class DatabaseBackupFileServiceImpl extends ServiceImpl<DatabaseBackupFil
}
}
private void validateReadablePath(DatabaseBackupFile backupFile, String filePath, String fileType, boolean allowBlank) {
private void deleteSinglePath(Path path) {
try {
Files.deleteIfExists(path);
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "删除物理文件失败:" + exception.getMessage());
}
}
private void validateReadablePath(DatabaseBackupFile backupFile, String filePath, String fileType,
boolean allowBlank, boolean allowDirectory) {
if (StrUtil.isBlank(filePath)) {
if (allowBlank) {
return;
@@ -131,9 +163,12 @@ public class DatabaseBackupFileServiceImpl extends ServiceImpl<DatabaseBackupFil
throw new BusinessException(CommonResponseEnum.FAIL, fileType + "路径不能为空");
}
Path path = resolveManagedPath(backupFile, filePath);
if (path == null || !Files.exists(path) || Files.isDirectory(path)) {
if (path == null || !Files.exists(path)) {
throw new BusinessException(CommonResponseEnum.FAIL, fileType + "不存在");
}
if (Files.isDirectory(path) && !allowDirectory) {
throw new BusinessException(CommonResponseEnum.FAIL, fileType + "不能是目录");
}
}
private DatabaseBackupFileVO toVO(DatabaseBackupFile file) {

View File

@@ -8,7 +8,6 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.systemops.database.component.DatabasePasswordComponent;
import com.njcn.gather.systemops.database.component.OracleJdbcComponent;
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
import com.njcn.gather.systemops.database.mapper.DatabaseConnectionMapper;
import com.njcn.gather.systemops.database.pojo.param.DatabaseConnectionParam;
@@ -18,6 +17,8 @@ import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO;
import com.njcn.gather.systemops.database.service.DatabaseConnectionService;
import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService;
import com.njcn.gather.systemops.database.support.spi.DatabaseConnectionOperator;
import com.njcn.gather.systemops.database.support.spi.DatabaseOperatorRegistry;
import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil;
import com.njcn.web.factory.PageFactory;
import lombok.RequiredArgsConstructor;
@@ -38,7 +39,7 @@ import java.util.stream.Collectors;
public class DatabaseConnectionServiceImpl extends ServiceImpl<DatabaseConnectionMapper, DatabaseConnection> implements DatabaseConnectionService {
private final DatabasePasswordComponent databasePasswordComponent;
private final OracleJdbcComponent oracleJdbcComponent;
private final DatabaseOperatorRegistry databaseOperatorRegistry;
private final ObjectProvider<DatabaseOperationTaskService> databaseOperationTaskServiceProvider;
@Override
@@ -61,6 +62,7 @@ public class DatabaseConnectionServiceImpl extends ServiceImpl<DatabaseConnectio
public boolean addConnection(DatabaseConnectionParam param) {
DatabaseConnection connection = new DatabaseConnection();
fillConnection(connection, param, true);
checkConnectionNameUnique(connection.getConnectionName());
connection.setId(DatabaseOpsIdUtil.uuid());
connection.setState(DatabaseOpsConst.STATE_ENABLED);
connection.setCreateTime(LocalDateTime.now());
@@ -95,12 +97,10 @@ public class DatabaseConnectionServiceImpl extends ServiceImpl<DatabaseConnectio
@Transactional(rollbackFor = Exception.class)
public DatabaseTestResultVO testConnection(DatabaseConnectionParam.TestParam param) {
DatabaseConnection connection = resolveTestConnection(param);
DatabaseTestResultVO result = oracleJdbcComponent.test(connection, resolvePassword(connection, param.getTemporaryPassword()));
DatabaseConnectionOperator operator = databaseOperatorRegistry.getConnectionOperator(connection.getDbType());
DatabaseTestResultVO result = operator.test(connection, resolvePassword(connection, param.getTemporaryPassword()));
if (StrUtil.isNotBlank(connection.getId())) {
connection.setLastTestStatus(Boolean.TRUE.equals(result.getSuccess()) ? "SUCCESS" : "FAIL");
connection.setLastTestMessage(result.getMessage());
connection.setLastTestTime(LocalDateTime.now());
this.updateById(connection);
updateLastTestResult(connection.getId(), result);
}
return result;
}
@@ -109,7 +109,10 @@ public class DatabaseConnectionServiceImpl extends ServiceImpl<DatabaseConnectio
public List<DatabaseTableVO> 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<DatabaseConnectio
private DatabaseConnection resolveTestConnection(DatabaseConnectionParam.TestParam param) {
if (StrUtil.isNotBlank(param.getConnectionId())) {
return requireEnabled(param.getConnectionId());
DatabaseConnection savedConnection = requireEnabled(param.getConnectionId());
if (param.getConnection() == null) {
return savedConnection;
}
DatabaseConnection connection = new DatabaseConnection();
fillConnection(connection, param.getConnection(), true, true);
connection.setId(savedConnection.getId());
if (StrUtil.isBlank(param.getConnection().getPassword())) {
// 已有连接测试编辑后参数时,未传密码则复用库里保存的密码。
connection.setPasswordCipher(savedConnection.getPasswordCipher());
}
return connection;
}
if (param.getConnection() == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "连接测试参数不能为空");
@@ -151,20 +165,47 @@ public class DatabaseConnectionServiceImpl extends ServiceImpl<DatabaseConnectio
return connection;
}
private void updateLastTestResult(String connectionId, DatabaseTestResultVO result) {
this.lambdaUpdate()
.set(DatabaseConnection::getLastTestStatus, Boolean.TRUE.equals(result.getSuccess()) ? "SUCCESS" : "FAIL")
.set(DatabaseConnection::getLastTestMessage, result.getMessage())
.set(DatabaseConnection::getLastTestTime, LocalDateTime.now())
.eq(DatabaseConnection::getId, connectionId)
.update();
}
private String resolveTablesPassword(DatabaseConnection connection, DatabaseConnectionParam.TablesParam param) {
if (StrUtil.isNotBlank(param.getTemporaryPassword())) {
return param.getTemporaryPassword();
}
if (StrUtil.isNotBlank(param.getPassword())) {
return param.getPassword();
}
if (param.getConnection() != null && StrUtil.isNotBlank(param.getConnection().getPassword())) {
return param.getConnection().getPassword();
}
if (StrUtil.isNotBlank(param.getPasswordCipher())) {
return databasePasswordComponent.resolveRuntimePassword(param.getPasswordCipher(), null);
}
return resolvePassword(connection, null);
}
private void fillConnection(DatabaseConnection connection, DatabaseConnectionParam param, boolean create) {
fillConnection(connection, param, create, false);
}
private void fillConnection(DatabaseConnection connection, DatabaseConnectionParam param, boolean create,
boolean allowTemporaryPasswordOnly) {
validateConnectionParam(param);
String dbType = resolveDbType(param.getDbType());
validateConnectionParam(param, dbType);
connection.setConnectionName(param.getConnectionName().trim());
connection.setDbType(DatabaseOpsConst.DB_TYPE_ORACLE);
connection.setDbType(dbType);
connection.setHost(param.getHost().trim());
connection.setPort(param.getPort());
connection.setConnectType(resolveConnectType(param.getConnectType()));
connection.setServiceName(trimToNull(param.getServiceName()));
connection.setSid(trimToNull(param.getSid()));
connection.setConnectType(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? resolveConnectType(param.getConnectType()) : null);
connection.setServiceName(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? trimToNull(param.getServiceName()) : null);
connection.setSid(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? trimToNull(param.getSid()) : null);
connection.setDatabaseName(DatabaseOpsConst.DB_TYPE_MYSQL.equals(dbType) ? trimToNull(param.getDatabaseName()) : null);
connection.setSchemaName(trimToNull(param.getSchemaName()));
connection.setUsername(param.getUsername().trim());
connection.setSavePassword(param.getSavePassword() == null ? DatabaseOpsConst.SAVE_PASSWORD_YES : param.getSavePassword());
@@ -180,19 +221,38 @@ public class DatabaseConnectionServiceImpl extends ServiceImpl<DatabaseConnectio
} else if (create && StrUtil.isBlank(param.getPassword()) && !allowTemporaryPasswordOnly) {
throw new BusinessException(CommonResponseEnum.FAIL, "保存密码时密码不能为空");
}
connection.setDirectoryName(trimToNull(param.getDirectoryName()));
connection.setDirectoryPath(trimToNull(param.getDirectoryPath()));
connection.setDirectoryName(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? trimToNull(param.getDirectoryName()) : null);
connection.setDirectoryPath(DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? trimToNull(param.getDirectoryPath()) : null);
connection.setExtraConfigJson(trimToNull(param.getExtraConfigJson()));
connection.setRemark(param.getRemark());
}
private void validateConnectionParam(DatabaseConnectionParam param) {
String connectType = resolveConnectType(param.getConnectType());
if (DatabaseOpsConst.CONNECT_TYPE_SERVICE_NAME.equals(connectType) && StrUtil.isBlank(param.getServiceName())) {
throw new BusinessException(CommonResponseEnum.FAIL, "SERVICE_NAME 连接方式下服务名不能为空");
private void validateConnectionParam(DatabaseConnectionParam param, String dbType) {
if (DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType)) {
String connectType = resolveConnectType(param.getConnectType());
if (DatabaseOpsConst.CONNECT_TYPE_SERVICE_NAME.equals(connectType) && StrUtil.isBlank(param.getServiceName())) {
throw new BusinessException(CommonResponseEnum.FAIL, "SERVICE_NAME 连接方式下服务名不能为空");
}
if (DatabaseOpsConst.CONNECT_TYPE_SID.equals(connectType) && StrUtil.isBlank(param.getSid())) {
throw new BusinessException(CommonResponseEnum.FAIL, "SID 连接方式下 SID 不能为空");
}
return;
}
if (DatabaseOpsConst.CONNECT_TYPE_SID.equals(connectType) && StrUtil.isBlank(param.getSid())) {
throw new BusinessException(CommonResponseEnum.FAIL, "SID 连接方式下 SID 不能为空");
if (StrUtil.isBlank(param.getDatabaseName())) {
throw new BusinessException(CommonResponseEnum.FAIL, "MYSQL 数据库名不能为空");
}
}
/**
* 新增连接时,连接名称在有效记录中必须唯一。
*/
private void checkConnectionNameUnique(String connectionName) {
long count = this.lambdaQuery()
.eq(DatabaseConnection::getConnectionName, connectionName)
.eq(DatabaseConnection::getState, DatabaseOpsConst.STATE_ENABLED)
.count();
if (count > 0) {
throw new BusinessException(CommonResponseEnum.FAIL, "连接名称已存在");
}
}
@@ -200,6 +260,21 @@ public class DatabaseConnectionServiceImpl extends ServiceImpl<DatabaseConnectio
return StrUtil.blankToDefault(connectType, DatabaseOpsConst.CONNECT_TYPE_SERVICE_NAME).trim().toUpperCase(Locale.ROOT);
}
private String resolveDbType(String dbType) {
String resolved = StrUtil.blankToDefault(dbType, DatabaseOpsConst.DB_TYPE_ORACLE).trim().toUpperCase(Locale.ROOT);
if (!DatabaseOpsConst.DB_TYPE_ORACLE.equals(resolved) && !DatabaseOpsConst.DB_TYPE_MYSQL.equals(resolved)) {
throw new BusinessException(CommonResponseEnum.FAIL, "不支持的数据库类型:" + dbType);
}
return resolved;
}
private String resolveSchemaOrDatabase(DatabaseConnectionParam.TablesParam param, DatabaseConnection connection) {
if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(connection.getDbType())) {
return StrUtil.blankToDefault(param.getSchemaName(), connection.getDatabaseName());
}
return param.getSchemaName();
}
private String trimToNull(String value) {
return StrUtil.isBlank(value) ? null : value.trim();
}

View File

@@ -8,14 +8,10 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.systemops.database.component.DataPumpCommandExecutor;
import com.njcn.gather.systemops.database.component.JdbcExportComponent;
import com.njcn.gather.systemops.database.config.DbmsProperties;
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
import com.njcn.gather.systemops.database.mapper.DatabaseOperationTaskMapper;
import com.njcn.gather.systemops.database.pojo.enums.BackupModeEnum;
import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum;
import com.njcn.gather.systemops.database.pojo.enums.FileFormatEnum;
import com.njcn.gather.systemops.database.pojo.enums.OperationTypeEnum;
import com.njcn.gather.systemops.database.pojo.enums.TaskStatusEnum;
import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam;
@@ -27,10 +23,9 @@ import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskVO;
import com.njcn.gather.systemops.database.service.DatabaseBackupFileService;
import com.njcn.gather.systemops.database.service.DatabaseConnectionService;
import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService;
import com.njcn.gather.systemops.database.util.DatabaseChecksumUtil;
import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil;
import com.njcn.gather.systemops.database.support.spi.DatabaseBackupOperator;
import com.njcn.gather.systemops.database.support.spi.DatabaseOperatorRegistry;
import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil;
import com.njcn.gather.systemops.database.util.DatabasePathUtil;
import com.njcn.web.factory.PageFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -39,8 +34,6 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Locale;
@@ -57,9 +50,7 @@ public class DatabaseOperationTaskServiceImpl extends ServiceImpl<DatabaseOperat
private final DatabaseConnectionService databaseConnectionService;
private final DatabaseBackupFileService databaseBackupFileService;
private final DataPumpCommandExecutor dataPumpCommandExecutor;
private final JdbcExportComponent jdbcExportComponent;
private final DbmsProperties dbmsProperties;
private final DatabaseOperatorRegistry databaseOperatorRegistry;
private final ObjectMapper objectMapper;
@Resource(name = "dbmsTaskExecutorService")
private ExecutorService dbmsTaskExecutorService;
@@ -95,11 +86,41 @@ public class DatabaseOperationTaskServiceImpl extends ServiceImpl<DatabaseOperat
@Override
public DatabaseTaskVO getStatus(String taskId) {
DatabaseOperationTask task = this.getById(taskId);
if (task == null || !Integer.valueOf(DatabaseOpsConst.STATE_ENABLED).equals(task.getState())) {
throw new BusinessException(CommonResponseEnum.FAIL, "任务不存在或已删除");
return toVO(requireEnabledTask(taskId));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean stopBackupTask(DatabaseBackupParam.StopParam param) {
DatabaseOperationTask task = requireEnabledTask(param.getTaskId());
if (!OperationTypeEnum.BACKUP.name().equals(task.getOperationType())) {
throw new BusinessException(CommonResponseEnum.FAIL, "仅支持停止备份任务");
}
return toVO(task);
if (!TaskStatusEnum.WAITING.name().equals(task.getTaskStatus())
&& !TaskStatusEnum.RUNNING.name().equals(task.getTaskStatus())) {
throw new BusinessException(CommonResponseEnum.FAIL, "仅等待中或运行中的任务允许停止");
}
task.setTaskStatus(TaskStatusEnum.CANCELLED.name());
task.setResultMessage("用户请求停止备份任务");
task.setFinishedAt(LocalDateTime.now());
task.setUpdateTime(LocalDateTime.now());
return this.updateById(task);
}
@Override
@Transactional(rollbackFor = Exception.class)
public DatabaseTaskCreateVO restartBackupTask(DatabaseBackupParam.RestartParam param) {
DatabaseOperationTask sourceTask = requireEnabledTask(param.getTaskId());
if (!OperationTypeEnum.BACKUP.name().equals(sourceTask.getOperationType())) {
throw new BusinessException(CommonResponseEnum.FAIL, "仅支持重新开始备份任务");
}
if (!TaskStatusEnum.FAIL.name().equals(sourceTask.getTaskStatus())
&& !TaskStatusEnum.CANCELLED.name().equals(sourceTask.getTaskStatus())) {
throw new BusinessException(CommonResponseEnum.FAIL, "仅失败或已取消的任务允许重新开始");
}
DatabaseBackupParam.CreateParam createParam = readCreateParam(sourceTask.getRequestParamJson());
createParam.setTemporaryPassword(param.getTemporaryPassword());
return createBackupTask(createParam);
}
@Override
@@ -108,10 +129,7 @@ public class DatabaseOperationTaskServiceImpl extends ServiceImpl<DatabaseOperat
if (!DatabaseOpsConst.CONFIRM_DELETE.equals(confirmText)) {
throw new BusinessException(CommonResponseEnum.FAIL, "确认文案不正确");
}
DatabaseOperationTask task = this.getById(taskId);
if (task == null || !Integer.valueOf(DatabaseOpsConst.STATE_ENABLED).equals(task.getState())) {
throw new BusinessException(CommonResponseEnum.FAIL, "任务不存在或已删除");
}
DatabaseOperationTask task = requireEnabledTask(taskId);
if (TaskStatusEnum.RUNNING.name().equals(task.getTaskStatus()) || TaskStatusEnum.WAITING.name().equals(task.getTaskStatus())) {
throw new BusinessException(CommonResponseEnum.FAIL, "运行中的任务不能删除");
}
@@ -132,113 +150,32 @@ public class DatabaseOperationTaskServiceImpl extends ServiceImpl<DatabaseOperat
private void executeBackupTask(String taskId, DatabaseBackupParam.CreateParam param) {
DatabaseOperationTask task = this.getById(taskId);
try {
if (task == null || TaskStatusEnum.CANCELLED.name().equals(task.getTaskStatus())) {
return;
}
markRunning(task);
DatabaseConnection connection = databaseConnectionService.requireEnabled(task.getConnectionId());
connection.setSchemaName(task.getSchemaName());
String password = databaseConnectionService.resolvePassword(connection, param.getTemporaryPassword());
DatabaseBackupFile backupFile;
if (BackupStrategyEnum.DATA_PUMP.name().equals(task.getBackupStrategy())) {
backupFile = executeDataPumpBackup(task, connection, password, param);
} else {
backupFile = executeJdbcExportBackup(task, connection, password, param);
DatabaseBackupOperator operator = databaseOperatorRegistry.getBackupOperator(connection.getDbType(), task.getBackupStrategy());
DatabaseBackupFile backupFile = operator.executeBackup(task, connection, password, param);
task = this.getById(taskId);
if (task == null || TaskStatusEnum.CANCELLED.name().equals(task.getTaskStatus())) {
return;
}
databaseBackupFileService.save(backupFile);
markSuccess(task, "备份任务执行成功");
} catch (Exception exception) {
log.error("数据库备份任务失败taskId={}", taskId, exception);
task = this.getById(taskId);
if (task != null && TaskStatusEnum.CANCELLED.name().equals(task.getTaskStatus())) {
markCancelled(task, exception.getMessage());
return;
}
markFail(task, exception.getMessage());
}
}
private DatabaseBackupFile executeDataPumpBackup(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 executeJdbcExportBackup(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);
jdbcExportComponent.exportCsv(connection, password, param, dataFilePath, metadataFilePath);
return buildBackupFile(task, connection, param, FileFormatEnum.CSV.name(), fileName, dataFilePath, null, null, metadataFilePath);
}
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(DatabaseOpsConst.STATE_ENABLED);
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);
}
private DatabaseOperationTask buildBackupTask(DatabaseBackupParam.CreateParam param, DatabaseConnection connection) {
DatabaseOperationTask task = new DatabaseOperationTask();
task.setId(DatabaseOpsIdUtil.uuid());
@@ -246,9 +183,9 @@ public class DatabaseOperationTaskServiceImpl extends ServiceImpl<DatabaseOperat
task.setConnectionId(connection.getId());
task.setDbType(connection.getDbType());
task.setOperationType(OperationTypeEnum.BACKUP.name());
task.setBackupStrategy(resolveBackupStrategy(param.getBackupStrategy()));
task.setBackupStrategy(resolveBackupStrategy(param.getBackupStrategy(), connection.getDbType()));
task.setTaskStatus(TaskStatusEnum.WAITING.name());
task.setSchemaName(StrUtil.blankToDefault(param.getSchemaName(), connection.getSchemaName()));
task.setSchemaName(resolveSchemaName(param, connection));
task.setTargetNamesJson(writeJson(param.getTargetNames()));
task.setRequestParamJson(writeJsonWithoutPassword(param));
task.setProgressPercent(BigDecimal.ZERO);
@@ -259,15 +196,16 @@ public class DatabaseOperationTaskServiceImpl extends ServiceImpl<DatabaseOperat
}
private void validateBackupParam(DatabaseBackupParam.CreateParam param, DatabaseConnection connection) {
if (!DatabaseOpsConst.DB_TYPE_ORACLE.equals(connection.getDbType())) {
throw new BusinessException(CommonResponseEnum.FAIL, "一期仅支持 ORACLE");
}
if (param.getTargetNames() == null || param.getTargetNames().isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "备份表不能为空");
}
if (StrUtil.isBlank(StrUtil.blankToDefault(param.getSchemaName(), connection.getSchemaName()))) {
if (DatabaseOpsConst.DB_TYPE_ORACLE.equals(connection.getDbType())
&& StrUtil.isBlank(StrUtil.blankToDefault(param.getSchemaName(), connection.getSchemaName()))) {
throw new BusinessException(CommonResponseEnum.FAIL, "备份 Schema 不能为空");
}
if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(connection.getDbType()) && StrUtil.isBlank(connection.getDatabaseName())) {
throw new BusinessException(CommonResponseEnum.FAIL, "MYSQL 数据库名不能为空");
}
String backupMode = StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT);
if (BackupModeEnum.TIME_RANGE.name().equals(backupMode)
&& (param.getStartTime() == null || param.getEndTime() == null)) {
@@ -282,22 +220,37 @@ public class DatabaseOperationTaskServiceImpl extends ServiceImpl<DatabaseOperat
&& (param.getMaxFileSizeMb() == null || param.getMaxFileSizeMb() <= 0)) {
throw new BusinessException(CommonResponseEnum.FAIL, "按大小分片必须传入大于 0 的文件大小");
}
if (BackupStrategyEnum.JDBC_EXPORT.name().equals(resolveBackupStrategy(param.getBackupStrategy()))
&& BackupModeEnum.TIME_RANGE.name().equals(backupMode)
&& StrUtil.isBlank(param.getTimeColumn())) {
throw new BusinessException(CommonResponseEnum.FAIL, "JDBC 按时间备份必须传入时间字段");
if (BackupModeEnum.TIME_RANGE.name().equals(backupMode) && StrUtil.isBlank(param.getTimeColumn())) {
throw new BusinessException(CommonResponseEnum.FAIL, "按时间备份必须传入时间字段");
}
resolveBackupStrategy(param.getBackupStrategy(), connection.getDbType());
}
private String resolveBackupStrategy(String backupStrategy) {
String value = StrUtil.blankToDefault(backupStrategy, BackupStrategyEnum.DATA_PUMP.name()).trim().toUpperCase(Locale.ROOT);
private String resolveBackupStrategy(String backupStrategy, String dbType) {
String value = StrUtil.blankToDefault(backupStrategy,
DatabaseOpsConst.DB_TYPE_ORACLE.equals(dbType) ? BackupStrategyEnum.DATA_PUMP.name() : BackupStrategyEnum.JDBC_EXPORT.name())
.trim()
.toUpperCase(Locale.ROOT);
try {
return BackupStrategyEnum.valueOf(value).name();
BackupStrategyEnum strategyEnum = BackupStrategyEnum.valueOf(value);
if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(dbType) && BackupStrategyEnum.DATA_PUMP == strategyEnum) {
throw new BusinessException(CommonResponseEnum.FAIL, "MYSQL 不支持 DATA_PUMP");
}
return strategyEnum.name();
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "不支持的备份策略:" + backupStrategy);
}
}
private String resolveSchemaName(DatabaseBackupParam.CreateParam param, DatabaseConnection connection) {
if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(connection.getDbType())) {
return StrUtil.blankToDefault(param.getSchemaName(), connection.getDatabaseName());
}
return StrUtil.blankToDefault(param.getSchemaName(), connection.getSchemaName());
}
private void markRunning(DatabaseOperationTask task) {
task.setTaskStatus(TaskStatusEnum.RUNNING.name());
task.setStartedAt(LocalDateTime.now());
@@ -315,6 +268,9 @@ public class DatabaseOperationTaskServiceImpl extends ServiceImpl<DatabaseOperat
}
private void markFail(DatabaseOperationTask task, String message) {
if (task == null) {
return;
}
task.setTaskStatus(TaskStatusEnum.FAIL.name());
task.setResultMessage(message);
task.setFinishedAt(LocalDateTime.now());
@@ -322,6 +278,22 @@ public class DatabaseOperationTaskServiceImpl extends ServiceImpl<DatabaseOperat
this.updateById(task);
}
private void markCancelled(DatabaseOperationTask task, String message) {
task.setTaskStatus(TaskStatusEnum.CANCELLED.name());
task.setResultMessage(StrUtil.blankToDefault(message, "备份任务已停止"));
task.setFinishedAt(LocalDateTime.now());
task.setUpdateTime(LocalDateTime.now());
this.updateById(task);
}
private DatabaseOperationTask requireEnabledTask(String taskId) {
DatabaseOperationTask task = this.getById(taskId);
if (task == null || !Integer.valueOf(DatabaseOpsConst.STATE_ENABLED).equals(task.getState())) {
throw new BusinessException(CommonResponseEnum.FAIL, "任务不存在或已删除");
}
return task;
}
private String writeJson(Object value) {
try {
return objectMapper.writeValueAsString(value);
@@ -337,6 +309,14 @@ public class DatabaseOperationTaskServiceImpl extends ServiceImpl<DatabaseOperat
return writeJson(copy);
}
private DatabaseBackupParam.CreateParam readCreateParam(String requestParamJson) {
try {
return objectMapper.readValue(requestParamJson, DatabaseBackupParam.CreateParam.class);
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.JSON_CONVERT_EXCEPTION, exception.getMessage());
}
}
private DatabaseTaskCreateVO toCreateVO(DatabaseOperationTask task) {
DatabaseTaskCreateVO vo = new DatabaseTaskCreateVO();
vo.setTaskId(task.getId());

View File

@@ -5,13 +5,9 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.systemops.database.component.DataPumpCommandExecutor;
import com.njcn.gather.systemops.database.component.JdbcExportComponent;
import com.njcn.gather.systemops.database.component.OracleJdbcComponent;
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
import com.njcn.gather.systemops.database.mapper.DatabaseRestoreRecordMapper;
import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum;
import com.njcn.gather.systemops.database.pojo.enums.FileFormatEnum;
import com.njcn.gather.systemops.database.pojo.enums.OperationTypeEnum;
import com.njcn.gather.systemops.database.pojo.enums.RestoreModeEnum;
import com.njcn.gather.systemops.database.pojo.enums.TaskStatusEnum;
@@ -25,7 +21,9 @@ import com.njcn.gather.systemops.database.service.DatabaseBackupFileService;
import com.njcn.gather.systemops.database.service.DatabaseConnectionService;
import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService;
import com.njcn.gather.systemops.database.service.DatabaseRestoreService;
import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil;
import com.njcn.gather.systemops.database.support.spi.DatabaseConnectionOperator;
import com.njcn.gather.systemops.database.support.spi.DatabaseOperatorRegistry;
import com.njcn.gather.systemops.database.support.spi.DatabaseRestoreOperator;
import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -34,7 +32,6 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
@@ -50,9 +47,7 @@ public class DatabaseRestoreServiceImpl extends ServiceImpl<DatabaseRestoreRecor
private final DatabaseConnectionService databaseConnectionService;
private final DatabaseOperationTaskService databaseOperationTaskService;
private final DatabaseBackupFileService databaseBackupFileService;
private final DataPumpCommandExecutor dataPumpCommandExecutor;
private final JdbcExportComponent jdbcExportComponent;
private final OracleJdbcComponent oracleJdbcComponent;
private final DatabaseOperatorRegistry databaseOperatorRegistry;
private final ObjectMapper objectMapper;
@Resource(name = "dbmsTaskExecutorService")
private ExecutorService dbmsTaskExecutorService;
@@ -87,20 +82,8 @@ public class DatabaseRestoreServiceImpl extends ServiceImpl<DatabaseRestoreRecor
DatabaseBackupFile backupFile = requireBackupFile(record.getBackupFileId());
databaseBackupFileService.validateBackupFileReadable(backupFile);
String password = databaseConnectionService.resolvePassword(connection, param.getTemporaryPassword());
if (BackupStrategyEnum.DATA_PUMP.name().equals(backupFile.getBackupStrategy())) {
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());
}
} else if (FileFormatEnum.CSV.name().equalsIgnoreCase(backupFile.getFileFormat())) {
Path dataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getFilePath());
Path metadataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getMetadataFilePath());
jdbcExportComponent.importCsv(connection, password, dataFilePath, metadataFilePath, record.getRestoreMode(), record.getTargetSchemaName());
} else {
throw new BusinessException(CommonResponseEnum.FAIL, "暂不支持的恢复文件格式:" + backupFile.getFileFormat());
}
DatabaseRestoreOperator operator = databaseOperatorRegistry.getRestoreOperator(connection.getDbType(), backupFile.getBackupStrategy());
operator.executeRestore(task, record, backupFile, connection, password, param);
record.setResultMessage("恢复任务执行成功");
record.setUpdateTime(LocalDateTime.now());
this.updateById(record);
@@ -125,7 +108,8 @@ public class DatabaseRestoreServiceImpl extends ServiceImpl<DatabaseRestoreRecor
}
databaseBackupFileService.validateBackupFileReadable(backupFile);
String password = databaseConnectionService.resolvePassword(connection, param.getTemporaryPassword());
if (!Boolean.TRUE.equals(oracleJdbcComponent.test(connection, password).getSuccess())) {
DatabaseConnectionOperator operator = databaseOperatorRegistry.getConnectionOperator(connection.getDbType());
if (!Boolean.TRUE.equals(operator.test(connection, password).getSuccess())) {
throw new BusinessException(CommonResponseEnum.FAIL, "目标连接测试失败,不能创建恢复任务");
}
if (BackupStrategyEnum.DATA_PUMP.name().equals(backupFile.getBackupStrategy())) {
@@ -148,7 +132,7 @@ public class DatabaseRestoreServiceImpl extends ServiceImpl<DatabaseRestoreRecor
task.setOperationType(OperationTypeEnum.RESTORE.name());
task.setBackupStrategy(backupFile.getBackupStrategy());
task.setTaskStatus(TaskStatusEnum.WAITING.name());
task.setSchemaName(StrUtil.blankToDefault(param.getTargetSchemaName(), connection.getSchemaName()));
task.setSchemaName(resolveTargetSchemaName(param, connection));
task.setTargetNamesJson(backupFile.getTargetNamesJson());
task.setRequestParamJson(writeJsonWithoutPassword(param));
task.setProgressPercent(BigDecimal.ZERO);
@@ -168,7 +152,7 @@ public class DatabaseRestoreServiceImpl extends ServiceImpl<DatabaseRestoreRecor
record.setConnectionId(connection.getId());
record.setDbType(connection.getDbType());
record.setRestoreMode(restoreMode);
record.setTargetSchemaName(StrUtil.blankToDefault(param.getTargetSchemaName(), connection.getSchemaName()));
record.setTargetSchemaName(resolveTargetSchemaName(param, connection));
record.setTargetNamesJson(backupFile.getTargetNamesJson());
record.setTableExistsAction(restoreMode);
record.setOverwriteConfirmed(DatabaseOpsConst.CONFIRM_OVERWRITE.equals(param.getOverwriteConfirmText()) ? 1 : 0);
@@ -178,6 +162,13 @@ public class DatabaseRestoreServiceImpl extends ServiceImpl<DatabaseRestoreRecor
return record;
}
private String resolveTargetSchemaName(DatabaseRestoreParam.CreateParam param, DatabaseConnection connection) {
if (DatabaseOpsConst.DB_TYPE_MYSQL.equals(connection.getDbType())) {
return StrUtil.blankToDefault(param.getTargetSchemaName(), connection.getDatabaseName());
}
return StrUtil.blankToDefault(param.getTargetSchemaName(), connection.getSchemaName());
}
private DatabaseBackupFile requireBackupFile(String backupFileId) {
DatabaseBackupFile backupFile = databaseBackupFileService.getById(backupFileId);
if (backupFile == null || !Integer.valueOf(DatabaseOpsConst.STATE_ENABLED).equals(backupFile.getState())) {
@@ -195,10 +186,6 @@ public class DatabaseRestoreServiceImpl extends ServiceImpl<DatabaseRestoreRecor
}
}
private String buildRestoreLogName(DatabaseOperationTask task) {
return DatabaseFileNameUtil.appendTodayWithTask(task.getTaskNo() + "_restore.log", task.getTaskNo());
}
private void markRunning(DatabaseOperationTask task) {
task.setTaskStatus(TaskStatusEnum.RUNNING.name());
task.setStartedAt(LocalDateTime.now());

View File

@@ -0,0 +1,102 @@
package com.njcn.gather.systemops.database.support.mysql;
import cn.hutool.core.util.StrUtil;
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO;
import com.njcn.gather.systemops.database.support.spi.DatabaseConnectionOperator;
import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* MySQL 连接能力实现。
*/
@Component
public class MysqlConnectionOperator implements DatabaseConnectionOperator {
@Override
public boolean support(String dbType) {
return DatabaseOpsConst.DB_TYPE_MYSQL.equalsIgnoreCase(dbType);
}
@Override
public DatabaseTestResultVO test(DatabaseConnection connection, String password) {
DatabaseTestResultVO result = new DatabaseTestResultVO();
try (Connection ignored = openConnection(connection, password)) {
result.setSuccess(true);
result.setMessage("连接成功");
} catch (Exception exception) {
result.setSuccess(false);
result.setMessage(exception.getMessage());
}
return result;
}
@Override
public List<DatabaseTableVO> listTables(DatabaseConnection connection, String password, String schemaOrDatabaseName) throws Exception {
String databaseName = StrUtil.blankToDefault(schemaOrDatabaseName, connection.getDatabaseName());
String sql = "SELECT t.table_schema, t.table_name, t.auto_increment, t.update_time, "
+ "t.data_length, t.engine, t.table_rows, t.table_comment, "
+ "MAX(CASE WHEN c.extra LIKE '%auto_increment%' THEN 1 ELSE 0 END) AS has_auto_increment "
+ "FROM information_schema.tables t "
+ "LEFT JOIN information_schema.columns c "
+ "ON t.table_schema = c.table_schema AND t.table_name = c.table_name "
+ "WHERE t.table_schema = ? AND t.table_type = 'BASE TABLE' "
+ "GROUP BY t.table_schema, t.table_name, t.auto_increment, t.update_time, "
+ "t.data_length, t.engine, t.table_rows, t.table_comment "
+ "ORDER BY t.table_name";
try (Connection jdbcConnection = openConnection(connection, password);
PreparedStatement statement = jdbcConnection.prepareStatement(sql)) {
statement.setString(1, databaseName);
try (ResultSet resultSet = statement.executeQuery()) {
List<DatabaseTableVO> result = new ArrayList<>();
while (resultSet.next()) {
DatabaseTableVO table = new DatabaseTableVO();
table.setOwner(resultSet.getString("table_schema").toUpperCase(Locale.ROOT));
table.setTableName(resultSet.getString("table_name"));
if (resultSet.getInt("has_auto_increment") == 1) {
fillAutoIncrement(table, defaultZero(getLongValue(resultSet, "auto_increment")));
}
Timestamp updateTime = resultSet.getTimestamp("update_time");
table.setUpdateTime(updateTime == null ? null : updateTime.toLocalDateTime());
table.setDataLength(getLongValue(resultSet, "data_length"));
table.setEngine(resultSet.getString("engine"));
table.setTableRows(getLongValue(resultSet, "table_rows"));
table.setComments(resultSet.getString("table_comment"));
result.add(table);
}
return result;
}
}
}
private Long getLongValue(ResultSet resultSet, String columnName) throws Exception {
long value = resultSet.getLong(columnName);
return resultSet.wasNull() ? null : value;
}
private Long defaultZero(Long value) {
return value == null ? 0L : value;
}
private void fillAutoIncrement(DatabaseTableVO table, Long autoIncrement) {
table.setAutoIncrementValue(autoIncrement);
table.setAutoIncrement(autoIncrement);
}
private Connection openConnection(DatabaseConnection connection, String password) throws Exception {
if (StrUtil.isBlank(password)) {
throw new IllegalArgumentException("数据库密码不能为空");
}
return DriverManager.getConnection(MysqlJdbcUrlUtil.build(connection), connection.getUsername(), password);
}
}

View File

@@ -0,0 +1,146 @@
package com.njcn.gather.systemops.database.support.mysql;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.systemops.database.component.JdbcExportComponent;
import com.njcn.gather.systemops.database.config.DbmsProperties;
import com.njcn.gather.systemops.database.mapper.DatabaseOperationTaskMapper;
import com.njcn.gather.systemops.database.pojo.enums.BackupModeEnum;
import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum;
import com.njcn.gather.systemops.database.pojo.enums.FileFormatEnum;
import com.njcn.gather.systemops.database.pojo.enums.TaskStatusEnum;
import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam;
import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask;
import com.njcn.gather.systemops.database.support.spi.DatabaseBackupOperator;
import com.njcn.gather.systemops.database.util.DatabaseChecksumUtil;
import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil;
import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil;
import com.njcn.gather.systemops.database.util.DatabasePathUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.time.LocalDateTime;
import java.util.Locale;
import java.util.stream.Stream;
/**
* MySQL JDBC_EXPORT 大数据量备份实现。
*/
@Component
@RequiredArgsConstructor
public class MysqlJdbcExportBackupOperator implements DatabaseBackupOperator {
private final JdbcExportComponent jdbcExportComponent;
private final DbmsProperties dbmsProperties;
private final DatabaseOperationTaskMapper databaseOperationTaskMapper;
@Override
public boolean support(String dbType, String backupStrategy) {
return "MYSQL".equalsIgnoreCase(dbType) && BackupStrategyEnum.JDBC_EXPORT.name().equals(backupStrategy);
}
@Override
public DatabaseBackupFile executeBackup(DatabaseOperationTask task, DatabaseConnection connection, String password,
DatabaseBackupParam.CreateParam param) throws Exception {
Path backupDirectory = buildManagedPath(dbmsProperties.getBackup().getStoragePath(), task.getTaskNo());
String metadataFileName = DatabaseFileNameUtil.appendTodayWithTask("mysql_jdbc_export_metadata.json", task.getTaskNo());
Path metadataFilePath = backupDirectory.resolve(metadataFileName).normalize();
int fetchSize = positiveOrDefault(dbmsProperties.getBackup().getMysqlFetchSize(), 1000);
long maxPartBytes = resolveMaxPartBytes(param);
try (Connection jdbcConnection = DriverManager.getConnection(MysqlJdbcUrlUtil.build(connection), connection.getUsername(), password)) {
jdbcExportComponent.exportMysqlCsvV2(jdbcConnection, connection.getDatabaseName(), task.getTaskNo(), param,
backupDirectory, metadataFilePath, fetchSize, maxPartBytes, () -> isTaskCancelled(task.getId()));
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, exception.getMessage() + ",导出目录:" + backupDirectory);
}
return buildBackupFile(task, connection, param, backupDirectory, metadataFilePath);
}
private boolean isTaskCancelled(String taskId) {
DatabaseOperationTask task = databaseOperationTaskMapper.selectById(taskId);
return task != null && TaskStatusEnum.CANCELLED.name().equals(task.getTaskStatus());
}
private DatabaseBackupFile buildBackupFile(DatabaseOperationTask task, DatabaseConnection connection,
DatabaseBackupParam.CreateParam param, Path backupDirectory,
Path metadataFilePath) throws Exception {
if (!Files.exists(metadataFilePath)) {
throw new BusinessException(CommonResponseEnum.FAIL, "备份元数据文件未生成");
}
DatabaseBackupFile file = new DatabaseBackupFile();
file.setId(DatabaseOpsIdUtil.uuid());
file.setTaskId(task.getId());
file.setConnectionId(connection.getId());
file.setDbType(connection.getDbType());
file.setBackupStrategy(task.getBackupStrategy());
file.setFileFormat(FileFormatEnum.CSV.name());
file.setSchemaName(task.getSchemaName());
file.setTargetNamesJson(task.getTargetNamesJson());
file.setBackupMode(StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT));
file.setBackupStartTime(param.getStartTime());
file.setBackupEndTime(param.getEndTime());
file.setTimeColumn(param.getTimeColumn());
file.setDirectoryName(null);
file.setDumpFileName(null);
file.setLogFileName(null);
file.setFileName(backupDirectory.getFileName().toString());
file.setFilePath(backupDirectory.toString());
file.setLogFilePath(null);
file.setMetadataFilePath(metadataFilePath.toString());
file.setFileSize(readDirectoryFileSize(backupDirectory));
file.setChecksum(DatabaseChecksumUtil.sha256(metadataFilePath));
file.setState(1);
file.setCreateTime(LocalDateTime.now());
file.setUpdateTime(LocalDateTime.now());
return file;
}
private long resolveMaxPartBytes(DatabaseBackupParam.CreateParam param) {
Integer maxFileSizeMb = param.getMaxFileSizeMb();
if (maxFileSizeMb == null || maxFileSizeMb <= 0) {
maxFileSizeMb = positiveOrDefault(dbmsProperties.getBackup().getDefaultMaxFileSizeMb(), 512);
}
return maxFileSizeMb.longValue() * 1024L * 1024L;
}
private int positiveOrDefault(Integer value, int defaultValue) {
return value == null || value <= 0 ? defaultValue : value;
}
private Long readDirectoryFileSize(Path directory) {
try {
if (directory != null && Files.exists(directory) && Files.isDirectory(directory)) {
final long[] total = new long[]{0L};
try (Stream<Path> paths = Files.walk(directory)) {
paths.filter(path -> Files.exists(path) && !Files.isDirectory(path))
.forEach(path -> {
try {
total[0] += Files.size(path);
} catch (Exception ignored) {
// 忽略单个文件大小读取失败,避免影响备份记录生成。
}
});
}
return total[0];
}
} catch (Exception ignored) {
return null;
}
return null;
}
private Path buildManagedPath(String rootPath, String directoryName) {
Path root = DatabasePathUtil.normalize(rootPath);
if (root == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "备份目录未配置");
}
return root.resolve(directoryName).normalize();
}
}

View File

@@ -0,0 +1,48 @@
package com.njcn.gather.systemops.database.support.mysql;
import com.njcn.gather.systemops.database.component.JdbcExportComponent;
import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum;
import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam;
import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask;
import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord;
import com.njcn.gather.systemops.database.service.DatabaseBackupFileService;
import com.njcn.gather.systemops.database.support.spi.DatabaseRestoreOperator;
import org.springframework.stereotype.Component;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
/**
* MySQL JDBC_EXPORT 恢复实现。
*/
@Component
public class MysqlJdbcExportRestoreOperator implements DatabaseRestoreOperator {
private final JdbcExportComponent jdbcExportComponent;
private final DatabaseBackupFileService databaseBackupFileService;
public MysqlJdbcExportRestoreOperator(JdbcExportComponent jdbcExportComponent,
DatabaseBackupFileService databaseBackupFileService) {
this.jdbcExportComponent = jdbcExportComponent;
this.databaseBackupFileService = databaseBackupFileService;
}
@Override
public boolean support(String dbType, String backupStrategy) {
return "MYSQL".equalsIgnoreCase(dbType) && BackupStrategyEnum.JDBC_EXPORT.name().equals(backupStrategy);
}
@Override
public void executeRestore(DatabaseOperationTask task, DatabaseRestoreRecord record, DatabaseBackupFile backupFile,
DatabaseConnection connection, String password, DatabaseRestoreParam.CreateParam param) throws Exception {
Path dataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getFilePath());
Path metadataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getMetadataFilePath());
try (Connection jdbcConnection = DriverManager.getConnection(MysqlJdbcUrlUtil.build(connection), connection.getUsername(), password)) {
jdbcExportComponent.importCsv(jdbcConnection, dataFilePath, metadataFilePath,
connection.getDbType(), record.getRestoreMode(), null);
}
}
}

View File

@@ -0,0 +1,18 @@
package com.njcn.gather.systemops.database.support.mysql;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
/**
* MySQL JDBC URL 构造工具。
*/
public final class MysqlJdbcUrlUtil {
private MysqlJdbcUrlUtil() {
}
public static String build(DatabaseConnection connection) {
return "jdbc:mysql://" + connection.getHost() + ":" + connection.getPort() + "/" + connection.getDatabaseName()
+ "?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai"
+ "&useCursorFetch=true&connectTimeout=5000&socketTimeout=30000";
}
}

View File

@@ -0,0 +1,37 @@
package com.njcn.gather.systemops.database.support.oracle;
import com.njcn.gather.systemops.database.component.OracleJdbcComponent;
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO;
import com.njcn.gather.systemops.database.support.spi.DatabaseConnectionOperator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Oracle 连接能力实现。
*/
@Component
@RequiredArgsConstructor
public class OracleConnectionOperator implements DatabaseConnectionOperator {
private final OracleJdbcComponent oracleJdbcComponent;
@Override
public boolean support(String dbType) {
return DatabaseOpsConst.DB_TYPE_ORACLE.equalsIgnoreCase(dbType);
}
@Override
public DatabaseTestResultVO test(DatabaseConnection connection, String password) {
return oracleJdbcComponent.test(connection, password);
}
@Override
public List<DatabaseTableVO> listTables(DatabaseConnection connection, String password, String schemaOrDatabaseName) throws Exception {
return oracleJdbcComponent.listTables(connection, password, schemaOrDatabaseName);
}
}

View File

@@ -0,0 +1,122 @@
package com.njcn.gather.systemops.database.support.oracle;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.systemops.database.component.DataPumpCommandExecutor;
import com.njcn.gather.systemops.database.pojo.enums.BackupModeEnum;
import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum;
import com.njcn.gather.systemops.database.pojo.enums.FileFormatEnum;
import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam;
import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask;
import com.njcn.gather.systemops.database.service.DatabaseBackupFileService;
import com.njcn.gather.systemops.database.support.spi.DatabaseBackupOperator;
import com.njcn.gather.systemops.database.util.DatabaseChecksumUtil;
import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil;
import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil;
import com.njcn.gather.systemops.database.util.DatabasePathUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.Locale;
/**
* Oracle DATA_PUMP 备份实现。
*/
@Component
@RequiredArgsConstructor
public class OracleDataPumpBackupOperator implements DatabaseBackupOperator {
private final DataPumpCommandExecutor dataPumpCommandExecutor;
private final DatabaseBackupFileService databaseBackupFileService;
@Override
public boolean support(String dbType, String backupStrategy) {
return "ORACLE".equalsIgnoreCase(dbType) && BackupStrategyEnum.DATA_PUMP.name().equals(backupStrategy);
}
@Override
public DatabaseBackupFile executeBackup(DatabaseOperationTask task, DatabaseConnection connection, String password,
DatabaseBackupParam.CreateParam param) {
String directoryName = StrUtil.blankToDefault(param.getDirectoryName(), connection.getDirectoryName());
if (StrUtil.isBlank(directoryName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "DATA_PUMP 备份需要 Oracle Directory 名称");
}
String baseName = buildBaseFileName(connection, task);
String dumpFileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + ".dmp", task.getTaskNo());
String logFileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + ".log", task.getTaskNo());
DataPumpCommandExecutor.CommandResult commandResult = dataPumpCommandExecutor.expdp(connection, password,
directoryName, dumpFileName, logFileName, param.getTargetNames());
if (!Boolean.TRUE.equals(commandResult.getSuccess())) {
throw new BusinessException(CommonResponseEnum.FAIL, "Data Pump 执行失败:" + commandResult.getOutput());
}
if (StrUtil.isBlank(connection.getDirectoryPath())) {
throw new BusinessException(CommonResponseEnum.FAIL, "Data Pump 备份需要配置可管理的 directoryPath");
}
Path dumpPath = buildManagedPath(connection.getDirectoryPath(), dumpFileName);
Path logPath = buildManagedPath(connection.getDirectoryPath(), logFileName);
return buildBackupFile(task, connection, param, FileFormatEnum.DMP.name(), dumpFileName, dumpPath, logFileName, logPath, null);
}
private DatabaseBackupFile buildBackupFile(DatabaseOperationTask task, DatabaseConnection connection,
DatabaseBackupParam.CreateParam param, String fileFormat, String fileName,
Path filePath, String logFileName, Path logFilePath, Path metadataFilePath) {
if (filePath == null || !Files.exists(filePath)) {
throw new BusinessException(CommonResponseEnum.FAIL, "备份文件未生成");
}
DatabaseBackupFile file = new DatabaseBackupFile();
file.setId(DatabaseOpsIdUtil.uuid());
file.setTaskId(task.getId());
file.setConnectionId(connection.getId());
file.setDbType(connection.getDbType());
file.setBackupStrategy(task.getBackupStrategy());
file.setFileFormat(fileFormat);
file.setSchemaName(task.getSchemaName());
file.setTargetNamesJson(task.getTargetNamesJson());
file.setBackupMode(StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT));
file.setBackupStartTime(param.getStartTime());
file.setBackupEndTime(param.getEndTime());
file.setTimeColumn(param.getTimeColumn());
file.setDirectoryName(StrUtil.blankToDefault(param.getDirectoryName(), connection.getDirectoryName()));
file.setDumpFileName(FileFormatEnum.DMP.name().equals(fileFormat) ? fileName : null);
file.setLogFileName(logFileName);
file.setFileName(fileName);
file.setFilePath(filePath.toString());
file.setLogFilePath(logFilePath == null ? null : logFilePath.toString());
file.setMetadataFilePath(metadataFilePath == null ? null : metadataFilePath.toString());
file.setFileSize(readFileSize(filePath));
file.setChecksum(DatabaseChecksumUtil.sha256(filePath));
file.setState(1);
file.setCreateTime(LocalDateTime.now());
file.setUpdateTime(LocalDateTime.now());
return file;
}
private Long readFileSize(Path filePath) {
try {
if (filePath != null && Files.exists(filePath) && !Files.isDirectory(filePath)) {
return Files.size(filePath);
}
} catch (Exception ignored) {
return null;
}
return null;
}
private Path buildManagedPath(String rootPath, String fileName) {
Path root = DatabasePathUtil.normalize(rootPath);
if (root == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "备份目录未配置");
}
return root.resolve(fileName).normalize();
}
private String buildBaseFileName(DatabaseConnection connection, DatabaseOperationTask task) {
return connection.getSchemaName() + "_" + task.getBackupStrategy().toLowerCase(Locale.ROOT);
}
}

View File

@@ -0,0 +1,47 @@
package com.njcn.gather.systemops.database.support.oracle;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.systemops.database.component.DataPumpCommandExecutor;
import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum;
import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam;
import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask;
import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord;
import com.njcn.gather.systemops.database.support.spi.DatabaseRestoreOperator;
import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil;
import org.springframework.stereotype.Component;
/**
* Oracle DATA_PUMP 恢复实现。
*/
@Component
public class OracleDataPumpRestoreOperator implements DatabaseRestoreOperator {
private final DataPumpCommandExecutor dataPumpCommandExecutor;
public OracleDataPumpRestoreOperator(DataPumpCommandExecutor dataPumpCommandExecutor) {
this.dataPumpCommandExecutor = dataPumpCommandExecutor;
}
@Override
public boolean support(String dbType, String backupStrategy) {
return "ORACLE".equalsIgnoreCase(dbType) && BackupStrategyEnum.DATA_PUMP.name().equals(backupStrategy);
}
@Override
public void executeRestore(DatabaseOperationTask task, DatabaseRestoreRecord record, DatabaseBackupFile backupFile,
DatabaseConnection connection, String password, DatabaseRestoreParam.CreateParam param) {
DataPumpCommandExecutor.CommandResult result = dataPumpCommandExecutor.impdp(connection, password,
backupFile.getDirectoryName(), backupFile.getDumpFileName(), buildRestoreLogName(task),
record.getTableExistsAction());
if (!Boolean.TRUE.equals(result.getSuccess())) {
throw new BusinessException(CommonResponseEnum.FAIL, "Data Pump 恢复失败:" + result.getOutput());
}
}
private String buildRestoreLogName(DatabaseOperationTask task) {
return DatabaseFileNameUtil.appendTodayWithTask(task.getTaskNo() + "_restore.log", task.getTaskNo());
}
}

View File

@@ -0,0 +1,116 @@
package com.njcn.gather.systemops.database.support.oracle;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.systemops.database.component.JdbcExportComponent;
import com.njcn.gather.systemops.database.component.OracleJdbcComponent;
import com.njcn.gather.systemops.database.config.DbmsProperties;
import com.njcn.gather.systemops.database.pojo.enums.BackupModeEnum;
import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum;
import com.njcn.gather.systemops.database.pojo.enums.FileFormatEnum;
import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam;
import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask;
import com.njcn.gather.systemops.database.support.spi.DatabaseBackupOperator;
import com.njcn.gather.systemops.database.util.DatabaseChecksumUtil;
import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil;
import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil;
import com.njcn.gather.systemops.database.util.DatabasePathUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.time.LocalDateTime;
import java.util.Locale;
/**
* Oracle JDBC_EXPORT 备份实现。
*/
@Component
@RequiredArgsConstructor
public class OracleJdbcExportBackupOperator implements DatabaseBackupOperator {
private final JdbcExportComponent jdbcExportComponent;
private final OracleJdbcComponent oracleJdbcComponent;
private final DbmsProperties dbmsProperties;
@Override
public boolean support(String dbType, String backupStrategy) {
return "ORACLE".equalsIgnoreCase(dbType) && BackupStrategyEnum.JDBC_EXPORT.name().equals(backupStrategy);
}
@Override
public DatabaseBackupFile executeBackup(DatabaseOperationTask task, DatabaseConnection connection, String password,
DatabaseBackupParam.CreateParam param) throws Exception {
String baseName = buildBaseFileName(connection, task);
String fileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + ".csv", task.getTaskNo());
String metadataFileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + "_metadata.json", task.getTaskNo());
Path dataFilePath = buildManagedPath(dbmsProperties.getBackup().getStoragePath(), fileName);
Path metadataFilePath = buildManagedPath(dbmsProperties.getBackup().getStoragePath(), metadataFileName);
try (Connection jdbcConnection = oracleJdbcComponent.openConnection(connection, password)) {
jdbcExportComponent.exportCsv(jdbcConnection, connection.getSchemaName(), param, dataFilePath, metadataFilePath);
}
return buildBackupFile(task, connection, param, fileName, dataFilePath, metadataFilePath);
}
private DatabaseBackupFile buildBackupFile(DatabaseOperationTask task, DatabaseConnection connection,
DatabaseBackupParam.CreateParam param, String fileName, Path filePath,
Path metadataFilePath) {
if (filePath == null || !Files.exists(filePath)) {
throw new BusinessException(CommonResponseEnum.FAIL, "备份文件未生成");
}
DatabaseBackupFile file = new DatabaseBackupFile();
file.setId(DatabaseOpsIdUtil.uuid());
file.setTaskId(task.getId());
file.setConnectionId(connection.getId());
file.setDbType(connection.getDbType());
file.setBackupStrategy(task.getBackupStrategy());
file.setFileFormat(FileFormatEnum.CSV.name());
file.setSchemaName(task.getSchemaName());
file.setTargetNamesJson(task.getTargetNamesJson());
file.setBackupMode(StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT));
file.setBackupStartTime(param.getStartTime());
file.setBackupEndTime(param.getEndTime());
file.setTimeColumn(param.getTimeColumn());
file.setDirectoryName(null);
file.setDumpFileName(null);
file.setLogFileName(null);
file.setFileName(fileName);
file.setFilePath(filePath.toString());
file.setLogFilePath(null);
file.setMetadataFilePath(metadataFilePath.toString());
file.setFileSize(readFileSize(filePath));
file.setChecksum(DatabaseChecksumUtil.sha256(filePath));
file.setState(1);
file.setCreateTime(LocalDateTime.now());
file.setUpdateTime(LocalDateTime.now());
return file;
}
private Long readFileSize(Path filePath) {
try {
if (filePath != null && Files.exists(filePath) && !Files.isDirectory(filePath)) {
return Files.size(filePath);
}
} catch (Exception ignored) {
return null;
}
return null;
}
private Path buildManagedPath(String rootPath, String fileName) {
Path root = DatabasePathUtil.normalize(rootPath);
if (root == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "备份目录未配置");
}
return root.resolve(fileName).normalize();
}
private String buildBaseFileName(DatabaseConnection connection, DatabaseOperationTask task) {
return connection.getSchemaName() + "_" + task.getBackupStrategy().toLowerCase(Locale.ROOT);
}
}

View File

@@ -0,0 +1,51 @@
package com.njcn.gather.systemops.database.support.oracle;
import com.njcn.gather.systemops.database.component.JdbcExportComponent;
import com.njcn.gather.systemops.database.component.OracleJdbcComponent;
import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum;
import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam;
import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask;
import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord;
import com.njcn.gather.systemops.database.service.DatabaseBackupFileService;
import com.njcn.gather.systemops.database.support.spi.DatabaseRestoreOperator;
import org.springframework.stereotype.Component;
import java.nio.file.Path;
import java.sql.Connection;
/**
* Oracle JDBC_EXPORT 恢复实现。
*/
@Component
public class OracleJdbcExportRestoreOperator implements DatabaseRestoreOperator {
private final JdbcExportComponent jdbcExportComponent;
private final OracleJdbcComponent oracleJdbcComponent;
private final DatabaseBackupFileService databaseBackupFileService;
public OracleJdbcExportRestoreOperator(JdbcExportComponent jdbcExportComponent,
OracleJdbcComponent oracleJdbcComponent,
DatabaseBackupFileService databaseBackupFileService) {
this.jdbcExportComponent = jdbcExportComponent;
this.oracleJdbcComponent = oracleJdbcComponent;
this.databaseBackupFileService = databaseBackupFileService;
}
@Override
public boolean support(String dbType, String backupStrategy) {
return "ORACLE".equalsIgnoreCase(dbType) && BackupStrategyEnum.JDBC_EXPORT.name().equals(backupStrategy);
}
@Override
public void executeRestore(DatabaseOperationTask task, DatabaseRestoreRecord record, DatabaseBackupFile backupFile,
DatabaseConnection connection, String password, DatabaseRestoreParam.CreateParam param) throws Exception {
Path dataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getFilePath());
Path metadataFilePath = databaseBackupFileService.resolveManagedPath(backupFile, backupFile.getMetadataFilePath());
try (Connection jdbcConnection = oracleJdbcComponent.openConnection(connection, password)) {
jdbcExportComponent.importCsv(jdbcConnection, dataFilePath, metadataFilePath,
connection.getDbType(), record.getRestoreMode(), record.getTargetSchemaName());
}
}
}

View File

@@ -0,0 +1,17 @@
package com.njcn.gather.systemops.database.support.spi;
import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam;
import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask;
/**
* 按数据库类型与备份策略隔离备份执行能力。
*/
public interface DatabaseBackupOperator {
boolean support(String dbType, String backupStrategy);
DatabaseBackupFile executeBackup(DatabaseOperationTask task, DatabaseConnection connection, String password,
DatabaseBackupParam.CreateParam param) throws Exception;
}

View File

@@ -0,0 +1,19 @@
package com.njcn.gather.systemops.database.support.spi;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTableVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTestResultVO;
import java.util.List;
/**
* 按数据库类型隔离连接测试与表查询能力。
*/
public interface DatabaseConnectionOperator {
boolean support(String dbType);
DatabaseTestResultVO test(DatabaseConnection connection, String password);
List<DatabaseTableVO> listTables(DatabaseConnection connection, String password, String schemaOrDatabaseName) throws Exception;
}

View File

@@ -0,0 +1,43 @@
package com.njcn.gather.systemops.database.support.spi;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 数据库能力路由注册器。
*/
@Component
@RequiredArgsConstructor
public class DatabaseOperatorRegistry {
private final List<DatabaseConnectionOperator> connectionOperators;
private final List<DatabaseBackupOperator> backupOperators;
private final List<DatabaseRestoreOperator> restoreOperators;
public DatabaseConnectionOperator getConnectionOperator(String dbType) {
return connectionOperators.stream()
.filter(operator -> operator.support(dbType))
.findFirst()
.orElseThrow(() -> new BusinessException(CommonResponseEnum.FAIL, "暂不支持的数据库类型:" + dbType));
}
public DatabaseBackupOperator getBackupOperator(String dbType, String backupStrategy) {
return backupOperators.stream()
.filter(operator -> operator.support(dbType, backupStrategy))
.findFirst()
.orElseThrow(() -> new BusinessException(CommonResponseEnum.FAIL,
"暂不支持的备份能力:" + dbType + "/" + backupStrategy));
}
public DatabaseRestoreOperator getRestoreOperator(String dbType, String backupStrategy) {
return restoreOperators.stream()
.filter(operator -> operator.support(dbType, backupStrategy))
.findFirst()
.orElseThrow(() -> new BusinessException(CommonResponseEnum.FAIL,
"暂不支持的恢复能力:" + dbType + "/" + backupStrategy));
}
}

View File

@@ -0,0 +1,18 @@
package com.njcn.gather.systemops.database.support.spi;
import com.njcn.gather.systemops.database.pojo.param.DatabaseRestoreParam;
import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask;
import com.njcn.gather.systemops.database.pojo.po.DatabaseRestoreRecord;
/**
* 按数据库类型与备份策略隔离恢复执行能力。
*/
public interface DatabaseRestoreOperator {
boolean support(String dbType, String backupStrategy);
void executeRestore(DatabaseOperationTask task, DatabaseRestoreRecord record, DatabaseBackupFile backupFile,
DatabaseConnection connection, String password, DatabaseRestoreParam.CreateParam param) throws Exception;
}

View File

@@ -0,0 +1,119 @@
CREATE TABLE IF NOT EXISTS `dbms_connection` (
`id` VARCHAR(64) NOT NULL COMMENT '主键',
`connection_name` VARCHAR(100) NOT NULL COMMENT '连接名称',
`db_type` VARCHAR(32) NOT NULL DEFAULT 'ORACLE' COMMENT '数据库类型ORACLE后续可扩展 MYSQL、INFLUXDB',
`host` VARCHAR(255) NOT NULL COMMENT '数据库主机地址',
`port` INT NOT NULL COMMENT '数据库端口',
`connect_type` VARCHAR(32) NULL COMMENT '连接类型SERVICE_NAME、SIDOracle 使用',
`service_name` VARCHAR(128) NULL COMMENT '服务名Oracle SERVICE_NAME 模式使用',
`sid` VARCHAR(128) NULL COMMENT 'SIDOracle SID 模式使用',
`database_name` VARCHAR(128) NULL COMMENT '数据库名或实例名,预留给 MySQL 等数据库使用',
`schema_name` VARCHAR(128) NULL COMMENT '默认 SchemaOracle 使用',
`username` VARCHAR(128) NOT NULL COMMENT '用户名',
`password_cipher` VARCHAR(1000) NULL COMMENT '保存的数据库密码;为空表示不保存密码,执行时临时输入',
`save_password` TINYINT NOT NULL DEFAULT 1 COMMENT '是否保存密码0-否1-是',
`directory_name` VARCHAR(128) NULL COMMENT '默认数据库目录对象名称Oracle Data Pump 使用',
`directory_path` VARCHAR(500) NULL COMMENT '目录对象对应物理路径,仅用于展示和校验',
`extra_config_json` JSON NULL COMMENT '扩展配置 JSON用于保存不同数据库的差异配置',
`remark` VARCHAR(500) NULL COMMENT '备注',
`last_test_status` VARCHAR(32) NULL COMMENT '最近连接测试状态SUCCESS、FAIL',
`last_test_message` VARCHAR(1000) NULL COMMENT '最近连接测试结果说明',
`last_test_time` DATETIME NULL COMMENT '最近连接测试时间',
`state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态0-删除1-正常',
`create_by` VARCHAR(64) NULL COMMENT '创建人',
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(64) NULL COMMENT '更新人',
`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_dbms_connection_state` (`state`),
KEY `idx_dbms_connection_db_type` (`db_type`),
KEY `idx_dbms_connection_name` (`connection_name`),
KEY `idx_dbms_connection_schema` (`schema_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库连接配置表';
CREATE TABLE IF NOT EXISTS `dbms_operation_task` (
`id` VARCHAR(64) NOT NULL COMMENT '主键',
`task_no` VARCHAR(64) NOT NULL COMMENT '任务编号',
`connection_id` VARCHAR(64) NOT NULL COMMENT '数据库连接配置 ID',
`db_type` VARCHAR(32) NOT NULL COMMENT '数据库类型ORACLE后续可扩展 MYSQL、INFLUXDB',
`operation_type` VARCHAR(32) NOT NULL COMMENT '操作类型BACKUP、RESTORE、DELETE',
`backup_strategy` VARCHAR(32) NULL COMMENT '备份策略DATA_PUMP、JDBC_EXPORT',
`task_status` VARCHAR(32) NOT NULL DEFAULT 'WAITING' COMMENT '任务状态WAITING、RUNNING、SUCCESS、FAIL、CANCELLED',
`schema_name` VARCHAR(128) NULL COMMENT '操作 Schema',
`target_names_json` JSON NULL COMMENT '操作对象名称列表 JSON例如表名列表',
`request_param_json` JSON NULL COMMENT '请求参数快照 JSON不保存运行时密码',
`result_message` VARCHAR(2000) NULL COMMENT '执行结果或失败原因',
`progress_percent` DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '任务进度百分比',
`started_at` DATETIME NULL COMMENT '任务开始时间',
`finished_at` DATETIME NULL COMMENT '任务结束时间',
`state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态0-删除1-正常',
`create_by` VARCHAR(64) NULL COMMENT '创建人',
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(64) NULL COMMENT '更新人',
`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_dbms_operation_task_no` (`task_no`),
KEY `idx_dbms_operation_connection` (`connection_id`),
KEY `idx_dbms_operation_db_type` (`db_type`),
KEY `idx_dbms_operation_type_status` (`operation_type`, `task_status`),
KEY `idx_dbms_operation_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库运维任务表';
CREATE TABLE IF NOT EXISTS `dbms_backup_file` (
`id` VARCHAR(64) NOT NULL COMMENT '主键',
`task_id` VARCHAR(64) NOT NULL COMMENT '备份任务 ID',
`connection_id` VARCHAR(64) NOT NULL COMMENT '数据库连接配置 ID',
`db_type` VARCHAR(32) NOT NULL COMMENT '数据库类型ORACLE后续可扩展 MYSQL、INFLUXDB',
`backup_strategy` VARCHAR(32) NOT NULL COMMENT '备份策略DATA_PUMP、JDBC_EXPORT',
`file_format` VARCHAR(32) NOT NULL COMMENT '文件格式DMP、SQL、CSV',
`schema_name` VARCHAR(128) NULL COMMENT '备份 Schema',
`target_names_json` JSON NULL COMMENT '备份对象名称列表 JSON例如表名列表',
`backup_mode` VARCHAR(32) NOT NULL DEFAULT 'FULL_TABLE' COMMENT '备份模式FULL_TABLE、TIME_RANGE、SIZE_SPLIT',
`backup_start_time` DATETIME NULL COMMENT '按时间备份开始时间',
`backup_end_time` DATETIME NULL COMMENT '按时间备份结束时间',
`time_column` VARCHAR(128) NULL COMMENT '按时间备份使用的时间字段',
`directory_name` VARCHAR(128) NULL COMMENT '数据库目录对象名称Oracle Data Pump 使用',
`dump_file_name` VARCHAR(255) NULL COMMENT 'Data Pump dump 文件名',
`log_file_name` VARCHAR(255) NULL COMMENT 'Data Pump log 文件名',
`file_name` VARCHAR(255) NOT NULL COMMENT '主备份文件名,需包含 _yyyyMMdd',
`file_path` VARCHAR(1000) NOT NULL COMMENT '服务端记录的备份文件路径或目录对象映射路径',
`log_file_path` VARCHAR(1000) NULL COMMENT '备份日志文件路径',
`metadata_file_path` VARCHAR(1000) NULL COMMENT 'JDBC_EXPORT 元数据文件路径',
`file_size` BIGINT NULL COMMENT '文件大小,单位字节',
`checksum` VARCHAR(128) NULL COMMENT '文件校验值',
`state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态0-删除1-正常',
`create_by` VARCHAR(64) NULL COMMENT '创建人',
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(64) NULL COMMENT '更新人',
`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_dbms_backup_task` (`task_id`),
KEY `idx_dbms_backup_connection` (`connection_id`),
KEY `idx_dbms_backup_db_type` (`db_type`),
KEY `idx_dbms_backup_strategy` (`backup_strategy`),
KEY `idx_dbms_backup_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库备份文件记录表';
CREATE TABLE IF NOT EXISTS `dbms_restore_record` (
`id` VARCHAR(64) NOT NULL COMMENT '主键',
`task_id` VARCHAR(64) NOT NULL COMMENT '恢复任务 ID',
`backup_file_id` VARCHAR(64) NOT NULL COMMENT '备份文件 ID',
`connection_id` VARCHAR(64) NOT NULL COMMENT '目标数据库连接配置 ID',
`db_type` VARCHAR(32) NOT NULL COMMENT '数据库类型ORACLE后续可扩展 MYSQL、INFLUXDB',
`restore_mode` VARCHAR(32) NOT NULL DEFAULT 'SKIP' COMMENT '恢复模式SKIP、APPEND、TRUNCATE、REPLACE',
`target_schema_name` VARCHAR(128) NULL COMMENT '目标 Schema',
`target_names_json` JSON NULL COMMENT '恢复对象名称列表 JSON例如表名列表',
`table_exists_action` VARCHAR(32) NULL COMMENT 'Data Pump TABLE_EXISTS_ACTIONSKIP、APPEND、TRUNCATE、REPLACE',
`overwrite_confirmed` TINYINT NOT NULL DEFAULT 0 COMMENT '是否已确认覆盖类操作0-否1-是',
`result_message` VARCHAR(2000) NULL COMMENT '恢复结果说明',
`state` TINYINT NOT NULL DEFAULT 1 COMMENT '状态0-删除1-正常',
`create_by` VARCHAR(64) NULL COMMENT '创建人',
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(64) NULL COMMENT '更新人',
`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_dbms_restore_task` (`task_id`),
KEY `idx_dbms_restore_backup_file` (`backup_file_id`),
KEY `idx_dbms_restore_connection` (`connection_id`),
KEY `idx_dbms_restore_db_type` (`db_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库恢复记录表';

View File

@@ -0,0 +1,14 @@
INSERT INTO `cn_tool`.`sys_function`
(`Id`, `Pid`, `Pids`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `Type`, `Remark`, `State`, `Create_By`, `Create_Time`, `Update_By`, `Update_Time`)
VALUES
('9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '0', '0', '系统运维', 'systemOps', '/systemOps', '/systemOps/index', 'Aim', 50, 0, '系统运维', 1, 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:00:00', 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:00:00');
INSERT INTO `cn_tool`.`sys_function`
(`Id`, `Pid`, `Pids`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `Type`, `Remark`, `State`, `Create_By`, `Create_Time`, `Update_By`, `Update_Time`)
VALUES
('2a7e5d9c1f4b4386b0c9e6f3a8d21754', '9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '0,9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '数据库监控', 'database', '/systemOps/database', '/systemOps/database/index', 'Monitor', 100, 0, '数据库监控', 1, 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:10:00', 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:10:00');
INSERT INTO `cn_tool`.`sys_function`
(`Id`, `Pid`, `Pids`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `Type`, `Remark`, `State`, `Create_By`, `Create_Time`, `Update_By`, `Update_Time`)
VALUES
('7c6d4a1e9b2f43c8a5e0d3f6b9c21875', '9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '0,9f3b2c7a6e8d4b91a5c0f2d7e6a3b841', '系统部署', 'deploy', '/systemOps/deploy', '/systemOps/deploy/index', 'Upload', 110, 0, '系统部署', 1, 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:20:00', 'f8516cc81d964cd8b4b771a3b3985cd4', '2026-05-20 10:20:00');