杜洪波
2025-05-08 dc8fcd5b11652ad3f08bc33e2e3caef62cc89cc9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
package com.product.system.backup.service;
 
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
 
import org.springframework.stereotype.Service;
 
import com.alibaba.druid.util.StringUtils;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.SftpProgressMonitor;
import com.product.core.config.Global;
import com.product.system.backup.entity.BackupLogger;
 
/**
 *     系统备份
 *    数据库备份:每天执行代码备份
 *    附件备份:每天执行代码备份当天附件
 *    遗留问题:附件被修改,不能锁定非当天被修改的附件
 */
@Service("systemBackService")
public class SystemBackupService {
    
    // 备份配置
    Properties config;
 
    // 备份日志
    BackupLogger log;
    
    // 数字时间和数字日期(用做文件夹或者文件名)
    String NUMBER_TIME;    //例如(20250428091001)
    String NUMBER_DATE; //例如(20240428)
    
    // 数字日期格式
    SimpleDateFormat numberTimeFormat = new SimpleDateFormat("yyyyMMddHHmmss");
    SimpleDateFormat numberDateFormat = new SimpleDateFormat("yyyyMMdd");
 
    // 备份配置文件
    private static final String CONFIG_FILE_PATH = "systemBackup.properties";
    
    /**
     *     系统备份入口
     *     调用地点:定时任务配置功能(bean)
     */
    public void systemBackupInit(){
        log = new BackupLogger();
        // 在日志中记录操作系统信息,便于调试
        log.writeInfo("【系统备份入口】操作系统: " + System.getProperty("os.name"), BackupLogger.INFO_TYPE);
        log.writeInfo("【系统备份入口】文件分隔符: " + File.separator, BackupLogger.INFO_TYPE);
        log.writeInfo("【系统备份入口】系统备份开始..................", BackupLogger.INFO_TYPE);
        NUMBER_TIME = numberTimeFormat.format(new Date());
        NUMBER_DATE = numberDateFormat.format(new Date());
        log.writeInfo("【系统备份入口】系统日期:" + NUMBER_DATE, BackupLogger.INFO_TYPE);
        log.writeInfo("【系统备份入口】系统时间:" + NUMBER_TIME, BackupLogger.INFO_TYPE);
        backupProcess();
        log.closeLogger();
    }
 
    /**
     * 数据备份进程
     */
    public void backupProcess() {
        log.writeInfo("【系统备份进程】系统备份进程开始..................", BackupLogger.INFO_TYPE);
        long start = System.currentTimeMillis();
        boolean status= true;
        // 第一步:初始配文件参数,当前目生成时间戳的文件夹
        status = initSystemConfig();
        if (!status)
            return;
        // 第二步:执行数据库备份
        status = runExpDataBase();
        if (!status)
            return;
        
        // 第三步:执行对数据库文件、工程代码、上传文件做压缩备份
        status = zipDataBackup();
        if (!status)
            return;
        // 第四步:上传压缩备份文件到FTP
        status = uploadBackupMachine();
//        uploadBackupMachine2();
        if (!status)
            return;
        // 第五步:清除数据
//        clearKeepData();
 
        long end = System.currentTimeMillis();
        log.writeInfo("【系统备份进程】系统备份进程结束,耗时.................." + ((end - start) / 1000) + "秒", BackupLogger.INFO_TYPE);
        System.out.println("系统备份耗时.................." + ((end - start) / 1000) + "秒");
    }
 
    /**
     * 第一步:初始配置文件参数,当前目录生成时间戳的文件夹
     */
    public boolean initSystemConfig() {
        log.writeInfo("【初始配置文件】开始初始系统配置文件..................", BackupLogger.INFO_TYPE);
        try (InputStream  reader = getClass().getClassLoader().getResourceAsStream(CONFIG_FILE_PATH)) {
            // 读取备份配置文件
            config = new Properties();
            config.load(reader);
            // 系统附件存放目录(当天附件)
            config.setProperty("DOCUMENT_ROOT", System.getProperty("user.dir") + File.separator + Global.getSystemConfig("local.dir", "") + File.separator + "00000000-0000-0000-0000-000000000000" + File.separator + NUMBER_DATE);
            // 数据库备份目录(数据库备份根目录+时间文件名+.sql)
            config.setProperty("DATABASE_BACKUP", config.getProperty("DATABASE_ROOT") + File.separator + NUMBER_TIME + ".sql");
            // 备份目标文件(ZIP备份根目录+时间文件名+.zip)
            config.setProperty("ZIPFILE_BACKUP", config.getProperty("ZIPFILE_ROOT") + File.separator + NUMBER_TIME + ".zip");
            // 创建ZIP文件根目录
            File zipFileRoot = new File(config.getProperty("ZIPFILE_ROOT"));
            if (!zipFileRoot.exists()) {
                if (!zipFileRoot.mkdirs()) {
                    log.writeInfo("【初始配置文件】无法创建目录: " + zipFileRoot.getAbsolutePath(), BackupLogger.ERROR_TYPE);
                    return false;
                }
            }
            log.writeInfo("【初始配置文件】DATABASE_ROOT=" + config.getProperty("DATABASE_ROOT"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】DATABASE_BACKUP=" + config.getProperty("DATABASE_BACKUP"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】DOCUMENT_ROOT=" + config.getProperty("DOCUMENT_ROOT"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】DOCUMENT_BACKUP=" + config.getProperty("DOCUMENT_BACKUP"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】ZIPFILE_ROOT=" + config.getProperty("ZIPFILE_ROOT"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】ZIPFILE_BACKUP=" + config.getProperty("ZIPFILE_BACKUP"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】初始系统配置文件结束.................", BackupLogger.INFO_TYPE);
        } catch (Exception e) {
            e.printStackTrace();
            log.writeInfo("【初始配置文件】系统初始配置文件失败," + new File("config.properties").getAbsolutePath() + "," + e.getMessage(),
                    BackupLogger.ERROR_TYPE);
            return false;
        }
        return true;
    }
    
    /**
     *     第二步:备份数据库文件
     */
    public boolean runExpDataBase() {
        log.writeInfo("【备份数据库】开始备份数据库..................", BackupLogger.INFO_TYPE);
        String databaseHost = config.getProperty("DATABASE_HOST");
        String databasePort = config.getProperty("DATABASE_PORT");
        String databaseName = config.getProperty("DATABASE_NAME");
        String databaseUser = config.getProperty("DATABASE_USER");
        String databasePwd = config.getProperty("DATABASE_PWD");
        
        log.writeInfo("【备份数据库】数据库HOST:" + databaseHost, BackupLogger.INFO_TYPE);
        log.writeInfo("【备份数据库】数据库PORT:" + databasePort, BackupLogger.INFO_TYPE);
        
        File backupFile = new File(config.getProperty("DATABASE_BACKUP"));
        
        // 确保备份文件的目录存在
        if (!backupFile.getParentFile().exists()) {
            backupFile.getParentFile().mkdirs();
        }
        log.writeInfo("【备份数据库】数据库备份文件完整目录:" + backupFile.getAbsolutePath(), BackupLogger.INFO_TYPE);
 
        ProcessBuilder processBuilder = new ProcessBuilder(
            "mysqldump", 
            "-h" + databaseHost, 
            "-P" + databasePort, 
            "-u" + databaseUser, 
            "-p" + databasePwd, 
            databaseName
        );
 
        try {
            Process process = processBuilder.start();
            // 读取mysqldump的输出并写入到备份文件
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                 FileWriter writer = new FileWriter(backupFile)) {
                
                String line;
                while ((line = reader.readLine()) != null) {
                    writer.write(line + System.lineSeparator());
                }
            }
 
            // 读取错误流并记录日志
            try (BufferedReader errorReader = new BufferedReader(
                new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
                String errorLine;
                while ((errorLine = errorReader.readLine()) != null) {
                    log.writeInfo("【备份数据库】错误输出: " + errorLine, BackupLogger.ERROR_TYPE);
                }
            }
     
            int exitCode = process.waitFor();
            if (exitCode == 0) {
                log.writeInfo("【备份数据库】数据库备份成功:" + backupFile.getAbsolutePath(), BackupLogger.INFO_TYPE);
            } else {
                log.writeInfo("【备份数据库】数据库备份失败,退出码: " + exitCode, BackupLogger.ERROR_TYPE);
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.writeInfo("【备份数据库】数据库备份失败:" + e.getMessage(), BackupLogger.ERROR_TYPE);
            return false;
        }
        return true;
    }
 
    private static void addFolderToZip(File folder, String parentPath, ZipOutputStream zos) throws IOException {
        File[] files = folder.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    // 递归处理子目录,保持路径结构
                    addFolderToZip(file, parentPath + file.getName() + "/", zos);
                } else {
                    // 添加文件到ZIP,保持路径结构
                    addFileToZip(file, parentPath + file.getName(), zos);
                }
            }
        }
    }
    
    private static void addFileToZip(File file, String entryName, ZipOutputStream zos) throws IOException {
        try (FileInputStream fis = new FileInputStream(file)) {
            zos.putNextEntry(new ZipEntry(entryName));
            byte[] buffer = new byte[1024];
            int length;
            while ((length = fis.read(buffer)) >= 0) {
                zos.write(buffer, 0, length);
            }
            zos.closeEntry();
        }
    }
    
    /**
     *     第三步:压缩备份文件
     */
    public boolean zipDataBackup() {
        log.writeInfo("【压缩备份文件】开始压缩备份文件..................", BackupLogger.INFO_TYPE);
        String documentPath = config.getProperty("DOCUMENT_ROOT");
        String databasePath = config.getProperty("DATABASE_BACKUP");
        String zipFilePath = config.getProperty("ZIPFILE_BACKUP");
        log.writeInfo("【压缩备份文件】附件存储目录路径:" + documentPath, BackupLogger.INFO_TYPE);
        log.writeInfo("【压缩备份文件】数据库备份完整路径:" + databasePath, BackupLogger.INFO_TYPE);
        log.writeInfo("【压缩备份文件】备份压缩文件完整路径:" + zipFilePath, BackupLogger.INFO_TYPE);
        try (FileOutputStream fos = new FileOutputStream(zipFilePath);
               ZipOutputStream zos = new ZipOutputStream(fos)) {
            
            log.writeInfo("【压缩备份文件】压缩系统附件", BackupLogger.INFO_TYPE);
            // 压缩文件夹(保留完整路径结构)
            // 注意:这里我们传递了根目录名称作为初始parentPath
            addFolderToZip(new File(documentPath), NUMBER_DATE + "/", zos);
    
            log.writeInfo("【压缩备份文件】压缩数据库备份文件", BackupLogger.INFO_TYPE);
            // 压缩SQL文件(放在指定路径下)
            addFileToZip(new File(databasePath), "database/" + NUMBER_TIME + ".sql", zos);
    
            log.writeInfo("【压缩备份文件】压缩完成", BackupLogger.INFO_TYPE);
        } catch (IOException e) {
            e.printStackTrace();
            log.writeInfo("【压缩备份文件】压缩失败:" + e.getMessage(), BackupLogger.ERROR_TYPE);
            return false;
        }
        return true;
    }
    
    /**
     *     第四步:SFTP发送上传备用机
     */
    public boolean uploadBackupMachine() {
        log.writeInfo("【上传备份文件】开始上传备份文件..................", BackupLogger.INFO_TYPE);
        String localFile = config.getProperty("ZIPFILE_BACKUP"); // 本地文件路径
        String remotePath = config.getProperty("UPLOAD_BACKUP_DIR") + NUMBER_TIME + ".zip"; // 远程文件路径
        int port = Integer.valueOf(config.getProperty("UPLOAD_SFTP_PORT")); //SSH端口
        String host = config.getProperty("UPLOAD_SFTP_HOST"); // 备用服务器的IP地址
        String user = config.getProperty("UPLOAD_SFTP_USER"); // 备用服务器的用户名
        String password = config.getProperty("UPLOAD_SFTP_PWD"); // 备用服务器的密码
        log.writeInfo("【上传备份文件】上传备用机地址:" + host, BackupLogger.INFO_TYPE);
        log.writeInfo("【上传备份文件】上传备用机端口:" + port, BackupLogger.INFO_TYPE);
        log.writeInfo("【上传备份文件】上传备用机目录:" + remotePath, BackupLogger.INFO_TYPE);
        if (StringUtils.isEmpty(host) || StringUtils.isEmpty(user) || StringUtils.isEmpty(password)) {
            log.writeInfo("【上传备份文件】上传备用机地址SFTP信息未配置完整,不予上传", BackupLogger.INFO_TYPE);
            return true;
        }
        try {
             JSch jsch = new JSch();
             Session session = jsch.getSession(user, host, port);
             session.setPassword(password);
             session.setConfig("StrictHostKeyChecking", "no"); // 仅限测试环境
             session.connect(5000); // 设置连接超时时间
  
             Channel channel = session.openChannel("sftp");
             channel.connect(5000); // 设置通道超时时间
             ChannelSftp sftpChannel = (ChannelSftp) channel;
             
             // 确保远程目录存在(关键步骤)
             String remoteDir = remotePath.substring(0, remotePath.lastIndexOf('/'));
             if (!remoteDir.startsWith("/")) {
                 remoteDir = "/" + remoteDir;
             }
             // 创建对应目录文件夹
             createRemoteDirectory(sftpChannel, remoteDir);
             sftpChannel.put(localFile, remotePath, new SftpProgressMonitor() {
                 public void init(int op, String src, String dest, long max) {
                     log.writeInfo("【上传备份文件】开始传输: " + src + " -> " + dest, BackupLogger.INFO_TYPE);
                 }
                 public boolean count(long count) { return true; }
                 public void end() { log.writeInfo("【上传备份文件】传输完成", BackupLogger.INFO_TYPE); }
             });
            sftpChannel.exit();
            session.disconnect();
            log.writeInfo("【上传备份文件】上传备份文件成功", BackupLogger.INFO_TYPE);
        } catch (JSchException | SftpException e) {
            e.printStackTrace();
            log.writeInfo("【上传备份文件】上传备份文件失败:" + e.getMessage(), BackupLogger.ERROR_TYPE);
            return false;
        }
        return true;
    }
    
    /**
     *  递归创建远程目录
     * @param sftpChannel
     * @param remoteDir
     * @throws SftpException
     */
    private void createRemoteDirectory(ChannelSftp sftpChannel, String remoteDir) throws SftpException {
        try {
            sftpChannel.cd(remoteDir);
        } catch (SftpException e) {
            if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
                if (remoteDir.length() == 3 && remoteDir.charAt(1) == ':') {
                    // 跳过根目录
                    return;
                }
                int pos = remoteDir.lastIndexOf('/');
                if (pos > 0) {
                    createRemoteDirectory(sftpChannel, remoteDir.substring(0, pos));
                }
                sftpChannel.mkdir(remoteDir);
                log.writeInfo("【上传备份文件】创建目录成功: " + remoteDir, BackupLogger.INFO_TYPE);
            } else {
                throw e;
            }
        }
    }
}