hsy
昨天 81730da6fe27689cfa3a63692a26d1b87d6db045
feat(system): 新增 mysqldump 数据库备份接口

- 新增 `GET /admin-api/system/database/backup` 接口,包含 `@PreAuthorize` 权限校验。
- Service 层解析 JDBC 动态调用本地 `mysqldump` 导出数据库。
- 通过设置 `Content-Length` 响应头支持前端进度条读取,确保流传输完成后及时清除服务器临时生成的 `.sql` 文件。
已添加3个文件
143 ■■■■■ 文件已修改
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/database/DatabaseBackupController.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupServiceImpl.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/database/DatabaseBackupController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.system.controller.admin.database;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.system.service.database.DatabaseBackupService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
@Tag(name = "管理后台 - æ•°æ®åº“备份")
@RestController
@RequestMapping("/system/database")
public class DatabaseBackupController {
    @Resource
    private DatabaseBackupService databaseBackupService;
    @GetMapping("/backup")
    @Operation(summary = "下载数据库备份")
    @PreAuthorize("@ss.hasPermission('system:database:backup')")
    public void backupDatabase(HttpServletResponse response) throws Exception {
        databaseBackupService.backupDatabase(response);
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package cn.iocoder.yudao.module.system.service.database;
import jakarta.servlet.http.HttpServletResponse;
public interface DatabaseBackupService {
    /**
     * å¤‡ä»½æ•°æ®åº“,并将 SQL æ–‡ä»¶å†™å…¥åˆ° HTTP å“åº”中
     *
     * @param response HTTP å“åº”
     */
    void backupDatabase(HttpServletResponse response) throws Exception;
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/database/DatabaseBackupServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,100 @@
package cn.iocoder.yudao.module.system.service.database;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
@Slf4j
public class DatabaseBackupServiceImpl implements DatabaseBackupService {
    @Value("${spring.datasource.dynamic.datasource.master.url:}")
    private String dbUrl;
    @Value("${spring.datasource.dynamic.datasource.master.username:root}")
    private String dbUsername;
    @Value("${spring.datasource.dynamic.datasource.master.password:}")
    private String dbPassword;
    @Override
    public void backupDatabase(HttpServletResponse response) throws Exception {
        if (dbUrl == null || dbUrl.isEmpty()) {
            throw new ServiceException(500, "未找到数据库连接配置");
        }
        // è§£æž 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("无法解析 JDBC URL: {}, ä½¿ç”¨é»˜è®¤é…ç½®", dbUrl);
            dbName = "ruoyi-vue-pro"; // é»˜è®¤åï¼Œå¦‚果有的话
        }
        String timestamp = DateUtil.format(new Date(), "yyyyMMdd_HHmmss");
        String fileName = "backup_" + timestamp + ".sql";
        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);
            }
            // è®¾ç½®å“åº”头
            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);
        }
    }
}