From 7e69a1f3aef86c535ecbb781128b04950f235076 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 01 九月 2026 09:38:43 +0800
Subject: [PATCH] Merge branch 'refs/heads/dev_business' into dev_新疆_绿能
---
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupServiceImpl.java | 314 ++++++++++++++++++++++++++++++++++++++++++----------
1 files changed, 253 insertions(+), 61 deletions(-)
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupServiceImpl.java
index b78f3d1..0ff72a9 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupServiceImpl.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupServiceImpl.java
@@ -3,14 +3,18 @@
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
+import javax.sql.DataSource;
import java.io.BufferedReader;
+import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -18,8 +22,14 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
+import java.io.OutputStreamWriter;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.Statement;
+import java.sql.Types;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -69,6 +79,10 @@
@Value("${yudao.database-backup.password:}")
private String backupPassword;
+ /** 涓绘暟鎹簮锛岀敤浜� JDBC 瀵煎嚭鍏滃簳 */
+ @Resource
+ private DataSource dataSource;
+
@Override
public void backupDatabase(HttpServletResponse response) throws Exception {
if (StrUtil.isEmpty(dbUrl)) {
@@ -97,59 +111,222 @@
log.warn("鏃犳硶浠庨厤缃垨 JDBC URL 瑙f瀽瀹屾暣鐨勬暟鎹簱淇℃伅: host={}, port={}, db={}", dbHost, dbPort, dbName);
throw new ServiceException(500, "鏃犳硶瑙f瀽鏁版嵁搴撳浠借繛鎺ヤ俊鎭紝璇峰湪 yudao.database-backup 涓厤缃� host/port/database");
}
+ // localhost 缁熶竴璧� TCP 127.0.0.1锛歮ysqldump 瀵� localhost 鍙兘璧� socket/loopback锛�
+ // 鑰屼笉鍚岀幆澧� root@localhost 鐨勮璇佽姹傚樊寮傚ぇ锛屽己鍒� TCP 鏇寸ǔ瀹�
+ if ("localhost".equalsIgnoreCase(dbHost)) {
+ dbHost = "127.0.0.1";
+ }
String username = StrUtil.isEmpty(backupUsername) ? dbUsername : backupUsername;
String password = StrUtil.isEmpty(backupPassword) ? dbPassword : backupPassword;
- // 妫�娴嬮儴缃茬幆澧冿紝鏋勫缓澶囦唤鍛戒护
- List<String> commandParts = new ArrayList<>();
- String connectHost = dbHost;
- String connectPort = dbPort;
+ // 缁勮鍊欓�夊浠藉懡浠わ紝鎸変紭鍏堢骇渚濇灏濊瘯锛氭墜鍔ㄩ厤缃� > 鏈満 mysqldump > Docker 瀹瑰櫒 mysqldump
+ List<List<String>> candidates = new ArrayList<>();
+ List<String> labels = new ArrayList<>();
if (StrUtil.isNotEmpty(backupCommand)) {
- // 鎵嬪姩鎸囧畾鍛戒护锛屼紭鍏堜娇鐢�
- for (String token : backupCommand.trim().split("\\s+")) {
- commandParts.add(token);
- }
- log.info("浣跨敤鎵嬪姩閰嶇疆鐨勫浠藉懡浠�: {}", backupCommand);
- } else if (isLocalMysqldumpAvailable()) {
- // 鏈湴閮ㄧ讲锛氭湰鏈哄凡瀹夎 mysqldump
- commandParts.add("mysqldump");
- log.info("妫�娴嬪埌鏈満 mysqldump锛屼娇鐢ㄦ湰鍦板浠藉懡浠�");
- } else {
- // Docker 閮ㄧ讲锛氭煡鎵捐繍琛屼腑鐨� MySQL 瀹瑰櫒锛岄�氳繃 docker exec 璋冪敤瀹瑰櫒鍐� mysqldump
- String container = findMysqlDockerContainer(dbPort);
- if (StrUtil.isEmpty(container)) {
- throw new ServiceException(500, "鏈娴嬪埌鍙敤鐨� mysqldump 鐜锛氭湰鏈烘湭瀹夎 MySQL 瀹㈡埛绔紝"
- + "涔熸湭鍙戠幇杩愯涓殑 MySQL Docker 瀹瑰櫒銆傚彲閫氳繃閰嶇疆 yudao.database-backup.command 鎵嬪姩鎸囧畾澶囦唤鍛戒护");
- }
- commandParts.add("docker");
- commandParts.add("exec");
- // docker exec 涓嶄細杞彂瀹夸富鏈虹幆澧冨彉閲忥紝瀵嗙爜闇�閫氳繃 -e 鏄惧紡浼犲叆瀹瑰櫒
- if (StrUtil.isNotEmpty(password)) {
- commandParts.add("-e");
- commandParts.add("MYSQL_PWD=" + password);
- }
- commandParts.add(container);
- commandParts.add("mysqldump");
- // 瀹瑰櫒鍐呭浐瀹氫娇鐢� 127.0.0.1:3306 璧� TCP 杩炴帴鑷韩锛岀粫寮� socket 璁よ瘉宸紓
- connectHost = "127.0.0.1";
- connectPort = DOCKER_MYSQL_PORT;
- log.info("妫�娴嬪埌 Docker 閮ㄧ讲锛屼娇鐢ㄥ鍣� [{}] 鎵ц mysqldump", container);
+ candidates.add(splitCommand(backupCommand));
+ labels.add("鎵嬪姩閰嶇疆鍛戒护[" + backupCommand + "]");
}
- commandParts.add("-h" + connectHost);
- commandParts.add("-P" + connectPort);
- commandParts.add("-u" + username);
- if (StrUtil.isEmpty(password)) {
- commandParts.add("--skip-password");
+ if (isLocalMysqldumpAvailable()) {
+ candidates.add(buildLocalMysqldumpCommand(dbHost, dbPort, username, password, dbName));
+ labels.add("鏈満 mysqldump");
}
- commandParts.add(dbName);
+ String container = findMysqlDockerContainer(dbPort);
+ if (StrUtil.isNotEmpty(container)) {
+ candidates.add(buildDockerExecCommand(container, username, password, dbName));
+ labels.add("Docker 瀹瑰櫒[" + container + "] mysqldump");
+ }
+ log.info("鏁版嵁搴撳浠藉�欓�夋柟寮忥細{}", labels);
+ // 渚濇灏濊瘯鎵�鏈夊閮ㄥ懡浠ゆ柟寮�
+ List<String> failures = new ArrayList<>();
String timestamp = DateUtil.format(new Date(), "yyyyMMdd_HHmmss");
String fileName = "backup_" + timestamp + ".sql";
- File tempFile = File.createTempFile("backup_", ".sql");
+ for (int i = 0; i < candidates.size(); i++) {
+ File tempFile = File.createTempFile("backup_", ".sql");
+ try {
+ String error = runMysqldump(candidates.get(i), password, tempFile);
+ if (error == null) {
+ log.info("鏁版嵁搴撳浠芥垚鍔燂紝浣跨敤鏂瑰紡锛歿}", labels.get(i));
+ downloadAttachment(response, tempFile, fileName);
+ return;
+ }
+ log.warn("澶囦唤鏂瑰紡 [{}] 澶辫触锛歿}", labels.get(i), error);
+ failures.add(labels.get(i) + "锛�" + error);
+ } finally {
+ FileUtil.del(tempFile);
+ }
+ }
+ // 澶栭儴鍛戒护鍏ㄩ儴澶辫触鎴栦笉鍙敤鏃讹紝浣跨敤 JDBC 瀵煎嚭鍏滃簳锛屼繚璇佷换浣曢儴缃茬幆澧冿紙濡傚簲鐢ㄥ鍣ㄥ唴鏃� mysqldump/docker锛夐兘鑳藉浠�
+ File tempFile = File.createTempFile("backup_", ".sql");
try {
- ProcessBuilder pb = new ProcessBuilder(commandParts);
- // 鏁版嵁搴撳瘑鐮侀�氳繃鐜鍙橀噺浼犻�掞紝閬垮厤鍑虹幇鍦ㄥ懡浠よ鍙婅繘绋嬪垪琛�
+ String error = exportByJdbc(tempFile);
+ if (error == null) {
+ log.info("鏁版嵁搴撳浠芥垚鍔燂紝浣跨敤鏂瑰紡锛欽DBC 瀵煎嚭鍏滃簳");
+ downloadAttachment(response, tempFile, fileName);
+ return;
+ }
+ log.warn("JDBC 瀵煎嚭鍏滃簳澶辫触锛歿}", error);
+ failures.add("JDBC 瀵煎嚭鍏滃簳锛�" + error);
+ } finally {
+ FileUtil.del(tempFile);
+ }
+ throw new ServiceException(500, "鏁版嵁搴撳浠藉け璐ワ細" + String.join(" 锝� ", failures));
+ }
+
+ /**
+ * 閫氳繃 JDBC 鐩磋繛鏁版嵁搴撳鍑哄缓琛ㄨ鍙ヤ笌鍏ㄩ噺鏁版嵁锛屼綔涓哄閮ㄥ懡浠や笉鍙敤鏃剁殑鍏滃簳澶囦唤鏂瑰紡
+ *
+ * @return 鎵ц鎴愬姛杩斿洖 null锛涘け璐ヨ繑鍥為敊璇弿杩�
+ */
+ private String exportByJdbc(File outputFile) {
+ try (Connection conn = dataSource.getConnection();
+ BufferedWriter writer = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(outputFile), StandardCharsets.UTF_8))) {
+ writer.write("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n");
+ writer.write("/*!40101 SET NAMES utf8mb4 */;\n");
+ writer.write("SET TIME_ZONE='+00:00';\n");
+ writer.write("SET FOREIGN_KEY_CHECKS=0;\n");
+ writer.write("SET UNIQUE_CHECKS=0;\n");
+ writer.write("SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO';\n\n");
+ try (Statement st = conn.createStatement();
+ ResultSet rs = st.executeQuery("SHOW TABLES")) {
+ List<String> tables = new ArrayList<>();
+ while (rs.next()) {
+ tables.add(rs.getString(1));
+ }
+ for (String table : tables) {
+ exportTable(conn, writer, table);
+ }
+ }
+ writer.write("\nSET FOREIGN_KEY_CHECKS=1;\n");
+ writer.write("SET UNIQUE_CHECKS=1;\n");
+ writer.write("/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n");
+ return null;
+ } catch (Exception e) {
+ log.error("JDBC 瀵煎嚭鍏滃簳澶辫触", e);
+ return "JDBC 瀵煎嚭寮傚父: " + e.getMessage();
+ }
+ }
+
+ /**
+ * 瀵煎嚭鍗曞紶琛ㄧ殑寤鸿〃璇彞涓庡叏閮ㄦ暟鎹�
+ */
+ private void exportTable(Connection conn, BufferedWriter writer, String table) throws Exception {
+ String quoted = "`" + table.replace("`", "``") + "`";
+ // 寤鸿〃璇彞
+ try (Statement st = conn.createStatement();
+ ResultSet rs = st.executeQuery("SHOW CREATE TABLE " + quoted)) {
+ if (rs.next()) {
+ writer.write(rs.getString(2));
+ writer.write(";\n\n");
+ }
+ }
+ // 鍏ㄩ噺鏁版嵁锛屾祦寮忚鍙栭伩鍏嶅ぇ琛ㄥ崰鐢ㄨ繃澶氬唴瀛�
+ try (Statement st = conn.createStatement()) {
+ st.setFetchSize(Integer.MIN_VALUE);
+ try (ResultSet rs = st.executeQuery("SELECT * FROM " + quoted)) {
+ ResultSetMetaData meta = rs.getMetaData();
+ int columnCount = meta.getColumnCount();
+ int[] types = new int[columnCount];
+ for (int i = 0; i < columnCount; i++) {
+ types[i] = meta.getColumnType(i + 1);
+ }
+ while (rs.next()) {
+ StringBuilder sb = new StringBuilder("INSERT INTO ").append(quoted).append(" VALUES (");
+ for (int i = 0; i < columnCount; i++) {
+ if (i > 0) {
+ sb.append(',');
+ }
+ if (rs.getObject(i + 1) == null) {
+ sb.append("NULL");
+ continue;
+ }
+ int type = types[i];
+ if (type == Types.BIT) {
+ sb.append(rs.getBoolean(i + 1) ? '1' : '0');
+ } else if (isBinaryType(type)) {
+ sb.append("X'").append(HexUtil.encodeHexStr(rs.getBytes(i + 1))).append("'");
+ } else {
+ sb.append('\'').append(escapeSql(rs.getString(i + 1))).append('\'');
+ }
+ }
+ sb.append(");\n");
+ writer.write(sb.toString());
+ }
+ }
+ }
+ writer.write('\n');
+ }
+
+ /**
+ * 鏄惁浜岃繘鍒跺垪绫诲瀷锛屼簩杩涘埗鏁版嵁浠ュ崄鍏繘鍒跺瓧闈㈤噺瀵煎嚭閬垮厤涔辩爜
+ */
+ private boolean isBinaryType(int type) {
+ return type == Types.BINARY || type == Types.VARBINARY
+ || type == Types.LONGVARBINARY || type == Types.BLOB;
+ }
+
+ /**
+ * SQL 瀛楃涓插瓧闈㈤噺杞箟
+ */
+ private String escapeSql(String value) {
+ return value.replace("\\", "\\\\").replace("'", "''").replace("\0", "\\0");
+ }
+
+ /**
+ * 鏋勫缓鏈満 mysqldump 澶囦唤鍛戒护
+ */
+ private List<String> buildLocalMysqldumpCommand(String host, String port, String username, String password, String dbName) {
+ List<String> command = new ArrayList<>();
+ command.add("mysqldump");
+ // MySQL 8 caching_sha2_password 璁よ瘉榛樿涓嶈姹傛湇鍔$鍏挜锛屼細鎶� Access denied (using password: YES)
+ command.add("--get-server-public-key");
+ command.add("-h" + host);
+ command.add("-P" + port);
+ command.add("-u" + username);
+ if (StrUtil.isEmpty(password)) {
+ command.add("--skip-password");
+ }
+ command.add(dbName);
+ return command;
+ }
+
+ /**
+ * 鏋勫缓閫氳繃 docker exec 璋冪敤瀹瑰櫒鍐� mysqldump 鐨勫浠藉懡浠�
+ */
+ private List<String> buildDockerExecCommand(String container, String username, String password, String dbName) {
+ List<String> command = new ArrayList<>();
+ command.add("docker");
+ command.add("exec");
+ // docker exec 涓嶄細杞彂瀹夸富鏈虹幆澧冨彉閲忥紝瀵嗙爜闇�閫氳繃 -e 鏄惧紡浼犲叆瀹瑰櫒
+ if (StrUtil.isNotEmpty(password)) {
+ command.add("-e");
+ command.add("MYSQL_PWD=" + password);
+ }
+ command.add(container);
+ command.add("mysqldump");
+ command.add("--get-server-public-key");
+ // 瀹瑰櫒鍐呭浐瀹氳蛋 127.0.0.1:3306 鐨� TCP 杩炴帴鑷韩锛岀粫寮� socket/loopback 璁よ瘉宸紓
+ command.add("-h127.0.0.1");
+ command.add("-P" + DOCKER_MYSQL_PORT);
+ command.add("-u" + username);
+ if (StrUtil.isEmpty(password)) {
+ command.add("--skip-password");
+ }
+ command.add(dbName);
+ return command;
+ }
+
+ /**
+ * 鎵ц mysqldump 鍛戒护锛屽皢 stdout 鍐欏叆涓存椂鏂囦欢
+ *
+ * @return 鎵ц鎴愬姛杩斿洖 null锛涘け璐ヨ繑鍥為敊璇弿杩�
+ */
+ private String runMysqldump(List<String> command, String password, File outputFile) {
+ try {
+ ProcessBuilder pb = new ProcessBuilder(command);
+ // 瀵嗙爜閫氳繃鐜鍙橀噺浼犻�掞紝閬垮厤鍑虹幇鍦ㄥ懡浠よ鍙婅繘绋嬪垪琛�
if (StrUtil.isNotEmpty(password)) {
pb.environment().put("MYSQL_PWD", password);
}
@@ -169,9 +346,9 @@
});
stderrThread.start();
- // stdout 鍐欏叆鏈湴涓存椂鏂囦欢锛坉ocker exec 鍦烘櫙杈撳嚭涓嶄細钀藉湪瀹瑰櫒鍐呮枃浠剁郴缁燂級
+ // mysqldump 杈撳嚭鍐欏叆鏈湴涓存椂鏂囦欢锛坉ocker exec 鍦烘櫙杈撳嚭涓嶄細钀藉湪瀹瑰櫒鍐呮枃浠剁郴缁燂級
try (InputStream in = process.getInputStream();
- OutputStream out = new FileOutputStream(tempFile)) {
+ OutputStream out = new FileOutputStream(outputFile)) {
IoUtil.copy(in, out);
}
@@ -179,30 +356,45 @@
stderrThread.join();
if (!finished) {
process.destroyForcibly();
- throw new ServiceException(500, "鏁版嵁搴撳浠借秴鏃讹紙瓒呰繃 " + BACKUP_TIMEOUT_MINUTES + " 鍒嗛挓锛�");
+ return "鎵ц瓒呮椂锛堣秴杩� " + BACKUP_TIMEOUT_MINUTES + " 鍒嗛挓锛�";
}
if (process.exitValue() != 0) {
- log.error("鏁版嵁搴撳浠藉け璐�, exitCode: {}, error: {}", process.exitValue(), stderr);
- throw new ServiceException(500, "鏁版嵁搴撳浠藉け璐�: " + stderr);
+ return "exitCode=" + process.exitValue() + "锛�" + stderr.toString().trim();
}
-
- // 璁剧疆鍝嶅簲澶�
- response.setContentType("application/octet-stream; charset=UTF-8");
- response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(fileName, "UTF-8") + "\"");
- response.setContentLengthLong(tempFile.length());
-
- // 鍐欏叆鍝嶅簲娴�
- try (FileInputStream fis = new FileInputStream(tempFile);
- OutputStream os = response.getOutputStream()) {
- IoUtil.copy(fis, os);
- }
- } finally {
- // 鍒犻櫎涓存椂鏂囦欢
- FileUtil.del(tempFile);
+ return null;
+ } catch (IOException e) {
+ return "鍚姩鍛戒护澶辫触: " + e.getMessage();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return "鎵ц琚腑鏂�: " + e.getMessage();
}
}
/**
+ * 灏嗗浠芥枃浠朵綔涓洪檮浠惰緭鍑哄埌鍝嶅簲
+ */
+ private void downloadAttachment(HttpServletResponse response, File file, String fileName) throws IOException {
+ response.setContentType("application/octet-stream; charset=UTF-8");
+ response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(fileName, "UTF-8") + "\"");
+ response.setContentLengthLong(file.length());
+ try (FileInputStream fis = new FileInputStream(file);
+ OutputStream os = response.getOutputStream()) {
+ IoUtil.copy(fis, os);
+ }
+ }
+
+ /**
+ * 灏嗛厤缃殑鍛戒护瀛楃涓叉寜绌虹櫧鍒囧垎涓哄弬鏁板垪琛�
+ */
+ private List<String> splitCommand(String command) {
+ List<String> parts = new ArrayList<>();
+ for (String token : command.trim().split("\\s+")) {
+ parts.add(token);
+ }
+ return parts;
+ }
+
+ /**
* 妫�娴嬫湰鏈� mysqldump 鏄惁鍙敤
*/
private boolean isLocalMysqldumpAvailable() {
--
Gitblit v1.9.3