From 30a6da9dd62728fe4a329a5f9723e18e33d5c334 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 31 八月 2026 17:04:12 +0800
Subject: [PATCH] refactor(database): 优化数据库备份实现支持多种部署环境

---
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupServiceImpl.java |  186 +++++++++++++++++++++++++++++++---------------
 1 files changed, 124 insertions(+), 62 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 d033692..69903fa 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
@@ -98,63 +98,110 @@
             throw new ServiceException(500, "鏃犳硶瑙f瀽鏁版嵁搴撳浠借繛鎺ヤ俊鎭紝璇峰湪 yudao.database-backup 涓厤缃� host/port/database");
         }
         // localhost 缁熶竴璧� TCP 127.0.0.1锛歮ysqldump 瀵� localhost 鍙兘璧� socket/loopback锛�
-        // 鑰屽鍣ㄥ唴 root@localhost 璁よ瘉瑕佹眰涓� TCP root@'%' 涓嶅悓锛屽己鍒� TCP 閬垮厤璁よ瘉澶辫触
+        // 鑰屼笉鍚岀幆澧� 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");
+        }
+        if (candidates.isEmpty()) {
+            throw new ServiceException(500, "鏈娴嬪埌鍙敤鐨� mysqldump 鐜锛氭湰鏈烘湭瀹夎 MySQL 瀹㈡埛绔紝涔熸病鏈夎繍琛屼腑鐨� MySQL Docker 瀹瑰櫒銆�"
+                    + "璇峰畨瑁� MySQL 瀹㈡埛绔紝鎴栭�氳繃 yudao.database-backup.command 鎵嬪姩鎸囧畾澶囦唤鍛戒护锛堝 docker exec mysql8 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);
+            }
+        }
+        throw new ServiceException(500, "鏁版嵁搴撳浠藉け璐ワ紙宸插皾璇� " + candidates.size() + " 绉嶆柟寮忥級锛�" + String.join(" 锝� ", failures));
+    }
 
+    /**
+     * 鏋勫缓鏈満 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(commandParts);
-            // 鏁版嵁搴撳瘑鐮侀�氳繃鐜鍙橀噺浼犻�掞紝閬垮厤鍑虹幇鍦ㄥ懡浠よ鍙婅繘绋嬪垪琛�
+            ProcessBuilder pb = new ProcessBuilder(command);
+            // 瀵嗙爜閫氳繃鐜鍙橀噺浼犻�掞紝閬垮厤鍑虹幇鍦ㄥ懡浠よ鍙婅繘绋嬪垪琛�
             if (StrUtil.isNotEmpty(password)) {
                 pb.environment().put("MYSQL_PWD", password);
             }
@@ -174,9 +221,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);
             }
 
@@ -184,30 +231,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