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 |  464 +++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 412 insertions(+), 52 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 28bf9bd..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,24 +3,48 @@
 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 jakarta.servlet.http.HttpServletResponse;
+import javax.sql.DataSource;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+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;
+import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 @Service
 @Slf4j
 public class DatabaseBackupServiceImpl implements DatabaseBackupService {
+
+    /** 澶囦唤瓒呮椂鏃堕棿锛堝垎閽燂級 */
+    private static final long BACKUP_TIMEOUT_MINUTES = 10;
+    /** MySQL 瀹瑰櫒鍐呴儴绔彛 */
+    private static final String DOCKER_MYSQL_PORT = "3306";
 
     @Value("${spring.datasource.dynamic.datasource.master.url:}")
     private String dbUrl;
@@ -31,70 +55,406 @@
     @Value("${spring.datasource.dynamic.datasource.master.password:}")
     private String dbPassword;
 
+    /** 澶囦唤鍛戒护锛屽彲鏄惧紡鎸囧畾锛堝 docker exec mysql8 mysqldump锛夛紱鐣欑┖鍒欒嚜鍔ㄦ娴嬫湰鍦� mysqldump 鎴� Docker MySQL 瀹瑰櫒 */
+    @Value("${yudao.database-backup.command:}")
+    private String backupCommand;
+
+    /** 澶囦唤鐩爣鏁版嵁搴� host锛岀暀绌哄垯浠庢暟鎹簮 JDBC URL 瑙f瀽 */
+    @Value("${yudao.database-backup.host:}")
+    private String backupHost;
+
+    /** 澶囦唤鐩爣鏁版嵁搴� port锛岀暀绌哄垯浠庢暟鎹簮 JDBC URL 瑙f瀽 */
+    @Value("${yudao.database-backup.port:}")
+    private String backupPort;
+
+    /** 澶囦唤鐩爣鏁版嵁搴撳悕锛岀暀绌哄垯浠庢暟鎹簮 JDBC URL 瑙f瀽 */
+    @Value("${yudao.database-backup.database:}")
+    private String backupDatabase;
+
+    /** 澶囦唤璐﹀彿锛岀暀绌哄垯浣跨敤鏁版嵁婧愯处鍙� */
+    @Value("${yudao.database-backup.username:}")
+    private String backupUsername;
+
+    /** 澶囦唤瀵嗙爜锛岀暀绌哄垯浣跨敤鏁版嵁婧愬瘑鐮� */
+    @Value("${yudao.database-backup.password:}")
+    private String backupPassword;
+
+    /** 涓绘暟鎹簮锛岀敤浜� JDBC 瀵煎嚭鍏滃簳 */
+    @Resource
+    private DataSource dataSource;
+
     @Override
     public void backupDatabase(HttpServletResponse response) throws Exception {
-        if (dbUrl == null || dbUrl.isEmpty()) {
+        if (StrUtil.isEmpty(dbUrl)) {
             throw new ServiceException(500, "鏈壘鍒版暟鎹簱杩炴帴閰嶇疆");
         }
 
-        // 瑙f瀽 JDBC URL锛屾彁鍙� host, port, dbname
-        // 鏍煎紡: jdbc:mysql://localhost:3306/ruoyi-vue-pro?useSSL=false
-        Pattern pattern = Pattern.compile("jdbc:mysql://([^:]+):(\\d+)/([^?]+)");
-        Matcher matcher = pattern.matcher(dbUrl);
-        String host = "127.0.0.1";
-        String port = "3306";
-        String dbName = "";
-        
-        if (matcher.find()) {
-            host = matcher.group(1);
-            port = matcher.group(2);
-            dbName = matcher.group(3);
-        } else {
-            // fallback
-            log.warn("鏃犳硶瑙f瀽 JDBC URL: {}, 浣跨敤榛樿閰嶇疆", dbUrl);
-            dbName = "ruoyi-vue-pro"; // 榛樿鍚嶏紝濡傛灉鏈夌殑璇�
+        // 澶囦唤杩炴帴鐨勬暟鎹簱淇℃伅锛氫紭鍏堜娇鐢� yudao.database-backup 閰嶇疆锛屾湭閰嶇疆鐨勯儴鍒嗕粠鏁版嵁婧� JDBC URL 瑙f瀽鍏滃簳
+        String dbHost = backupHost;
+        String dbPort = backupPort;
+        String dbName = backupDatabase;
+        if (StrUtil.isEmpty(dbHost) || StrUtil.isEmpty(dbPort) || StrUtil.isEmpty(dbName)) {
+            Matcher matcher = Pattern.compile("jdbc:mysql://([^:]+):(\\d+)/([^?]+)").matcher(dbUrl);
+            if (matcher.find()) {
+                if (StrUtil.isEmpty(dbHost)) {
+                    dbHost = matcher.group(1);
+                }
+                if (StrUtil.isEmpty(dbPort)) {
+                    dbPort = matcher.group(2);
+                }
+                if (StrUtil.isEmpty(dbName)) {
+                    dbName = matcher.group(3);
+                }
+            }
         }
+        if (StrUtil.isEmpty(dbHost) || StrUtil.isEmpty(dbPort) || StrUtil.isEmpty(dbName)) {
+            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;
 
+        // 缁勮鍊欓�夊浠藉懡浠わ紝鎸変紭鍏堢骇渚濇灏濊瘯锛氭墜鍔ㄩ厤缃� > 鏈満 mysqldump > Docker 瀹瑰櫒 mysqldump
+        List<List<String>> candidates = new ArrayList<>();
+        List<String> labels = new ArrayList<>();
+        if (StrUtil.isNotEmpty(backupCommand)) {
+            candidates.add(splitCommand(backupCommand));
+            labels.add("鎵嬪姩閰嶇疆鍛戒护[" + backupCommand + "]");
+        }
+        if (isLocalMysqldumpAvailable()) {
+            candidates.add(buildLocalMysqldumpCommand(dbHost, dbPort, username, password, dbName));
+            labels.add("鏈満 mysqldump");
+        }
+        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";
+        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 {
-            // 鏋勫缓 mysqldump 鍛戒护
-            String[] command = {
-                    "mysqldump",
-                    "-h" + host,
-                    "-P" + port,
-                    "-u" + dbUsername,
-                    "-p" + dbPassword,
-                    dbName,
-                    "-r", tempFile.getAbsolutePath()
-            };
-
-            ProcessBuilder pb = new ProcessBuilder(command);
-            Process process = pb.start();
-            int exitCode = process.waitFor();
-
-            if (exitCode != 0) {
-                String error = IoUtil.read(process.getErrorStream(), StandardCharsets.UTF_8);
-                log.error("鏁版嵁搴撳浠藉け璐�, exitCode: {}, error: {}", exitCode, error);
-                throw new ServiceException(500, "鏁版嵁搴撳浠藉け璐�: " + error);
+            String error = exportByJdbc(tempFile);
+            if (error == null) {
+                log.info("鏁版嵁搴撳浠芥垚鍔燂紝浣跨敤鏂瑰紡锛欽DBC 瀵煎嚭鍏滃簳");
+                downloadAttachment(response, tempFile, fileName);
+                return;
             }
-
-            // 璁剧疆鍝嶅簲澶�
-            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);
-            }
-
+            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);
+            }
+            Process process = pb.start();
+
+            // 鍚庡彴璇诲彇 stderr锛岄槻姝㈢閬撶紦鍐插尯鍐欐弧瀵艰嚧杩涚▼闃诲
+            StringBuilder stderr = new StringBuilder();
+            Thread stderrThread = new Thread(() -> {
+                try (BufferedReader reader = new BufferedReader(
+                        new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        stderr.append(line).append(System.lineSeparator());
+                    }
+                } catch (IOException ignored) {
+                }
+            });
+            stderrThread.start();
+
+            // mysqldump 杈撳嚭鍐欏叆鏈湴涓存椂鏂囦欢锛坉ocker exec 鍦烘櫙杈撳嚭涓嶄細钀藉湪瀹瑰櫒鍐呮枃浠剁郴缁燂級
+            try (InputStream in = process.getInputStream();
+                 OutputStream out = new FileOutputStream(outputFile)) {
+                IoUtil.copy(in, out);
+            }
+
+            boolean finished = process.waitFor(BACKUP_TIMEOUT_MINUTES, TimeUnit.MINUTES);
+            stderrThread.join();
+            if (!finished) {
+                process.destroyForcibly();
+                return "鎵ц瓒呮椂锛堣秴杩� " + BACKUP_TIMEOUT_MINUTES + " 鍒嗛挓锛�";
+            }
+            if (process.exitValue() != 0) {
+                return "exitCode=" + process.exitValue() + "锛�" + stderr.toString().trim();
+            }
+            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() {
+        try {
+            Process process = new ProcessBuilder("mysqldump", "--version").redirectErrorStream(true).start();
+            boolean finished = process.waitFor(5, TimeUnit.SECONDS);
+            if (!finished) {
+                process.destroyForcibly();
+                return false;
+            }
+            return process.exitValue() == 0;
+        } catch (IOException e) {
+            return false;
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return false;
+        }
+    }
+
+    /**
+     * 鏌ユ壘杩愯涓殑 MySQL Docker 瀹瑰櫒锛屼紭鍏堥�夋嫨绔彛鏄犲皠涓庢暟鎹簮绔彛涓�鑷寸殑瀹瑰櫒
+     *
+     * @return 瀹瑰櫒鍚嶏紱鏈壘鍒拌繑鍥� null
+     */
+    private String findMysqlDockerContainer(String dbPort) {
+        try {
+            Process ps = new ProcessBuilder("docker", "ps", "--format", "{{.ID}}|{{.Names}}|{{.Image}}")
+                    .redirectErrorStream(true).start();
+            String output = IoUtil.read(ps.getInputStream(), StandardCharsets.UTF_8);
+            if (!ps.waitFor(10, TimeUnit.SECONDS) || ps.exitValue() != 0) {
+                return null;
+            }
+            List<String[]> containers = new ArrayList<>();
+            for (String line : output.split("\n")) {
+                String[] parts = line.trim().split("\\|");
+                if (parts.length == 3 && parts[2].startsWith("mysql")) {
+                    containers.add(parts);
+                }
+            }
+            if (containers.isEmpty()) {
+                return null;
+            }
+            // 浼樺厛閫夋嫨绔彛鏄犲皠涓庢暟鎹簮绔彛涓�鑷寸殑瀹瑰櫒
+            for (String[] container : containers) {
+                Process portProcess = new ProcessBuilder("docker", "port", container[0], "3306/tcp")
+                        .redirectErrorStream(true).start();
+                String portOutput = IoUtil.read(portProcess.getInputStream(), StandardCharsets.UTF_8);
+                if (portProcess.waitFor(10, TimeUnit.SECONDS) && portProcess.exitValue() == 0
+                        && portOutput.contains(dbPort)) {
+                    return container[1];
+                }
+            }
+            log.warn("鏈尮閰嶅埌绔彛鏄犲皠鐨� MySQL 瀹瑰櫒锛屼娇鐢ㄧ涓�涓�: {}", containers.get(0)[1]);
+            return containers.get(0)[1];
+        } catch (IOException e) {
+            log.warn("妫�娴� Docker 鐜澶辫触: {}", e.getMessage());
+            return null;
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return null;
+        }
+    }
+}
\ No newline at end of file

--
Gitblit v1.9.3