杜洪波
2025-07-02 7f132dd94c6ae3e5801d0885d905bcc9e1ed5ebf
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
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
package com.product.system.backup.service;
 
import java.io.BufferedReader;
import java.io.File;
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.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.zip.ZipOutputStream;
 
import org.springframework.beans.factory.annotation.Autowired;
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.core.dao.BaseDao;
import com.product.core.entity.DataTableEntity;
import com.product.core.entity.FieldSetEntity;
import com.product.core.service.support.AbstractBaseService;
import com.product.system.backup.entity.BackupLogger;
import com.product.system.backup.util.SystemBackupUtil;
 
@Service("systemBackService")
public class SystemBackupService extends AbstractBaseService{
 
    @Autowired
    BaseDao baseDao;
    
    // 备份配置
    Properties config;
 
    // 备份日志
    BackupLogger log;
    
    // 数字时间和数字日期(用做文件夹或者文件名)
    String NUMBER_TIME;    //例如(20250428091001)
    String NUMBER_DATE; //例如(20240428)
    String STANDARD_START_TIME;    // 例如(2024-04-28 09:10:00)
    String STANDARD_FINAL_TIME;    // 例如(2024-04-28 09:10:00)
    
    // 数字日期格式
    SimpleDateFormat numberTimeFormat = new SimpleDateFormat("yyyyMMddHHmm00");
    SimpleDateFormat numberDateFormat = new SimpleDateFormat("yyyyMMdd");
    SimpleDateFormat standardTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:00");
 
    // 备份配置文件
    private static final String CONFIG_FILE_PATH = "systemBackup.properties";
    
    /**
     *     系统备份判定
     *     调用地点:定时任务配置功能(bean)
     */
    public void systemBackupInit() {
        // 获取系统时间
        Date finalTime = new Date();
        Date startTime = null;
        // 获取系统最大备份时间
        StringBuilder sbSql = new StringBuilder();
        sbSql.append("SELECT end_time \n");
        sbSql.append("FROM product_sys_backup_log \n");
        sbSql.append("WHERE backup_status = 1 \n");
        sbSql.append("ORDER BY end_time DESC \n");
        sbSql.append("LIMIT 1 \n");
        FieldSetEntity fseMaxLogTime = baseDao.getFieldSetEntityBySQL(sbSql.toString(), new Object[] {}, false);
        if(fseMaxLogTime != null && !StringUtils.isEmpty(fseMaxLogTime.getString("end_time"))) {
            startTime = fseMaxLogTime.getDate("end_time");
        }
        // 初始备份日志数据
        FieldSetEntity fseBackLog = new FieldSetEntity("product_sys_backup_log");
        fseBackLog.setValue("start_time", startTime);
        fseBackLog.setValue("end_time", finalTime);
        
        // 执行备份
        boolean flag = backupProcess(startTime, finalTime);
        if (flag) {
            fseBackLog.setValue("backup_status", 1);
        } else {
            fseBackLog.setValue("backup_status", 0);
        }
        // 保存日志
        baseDao.saveFieldSetEntity(fseBackLog);
    }
 
    /**
     *     数据备份进程
     * @param startTime    备份开始时间
     * @param finalTime    备份截止时间
     * @return
     */
    public boolean backupProcess(Date startTime, Date finalTime) {
        // 初始日志文件
        log = new BackupLogger();
        log.writeInfo("【系统备份进程】系统备份进程开始..................", BackupLogger.INFO_TYPE);
        long start = System.currentTimeMillis();
        // 第一步:初始备份参数
        initBackupParam(startTime, finalTime);
        // 第二步:初始配文件参数,当前目生成时间戳的文件夹
        boolean status = initSystemConfig();
        if (!status)
            return false;
        // 第三步:执行数据库备份
        status = runExpDataBase();
        if (!status)
            return false;
        // 第四步:执行对数据库文件、工程代码、上传文件做压缩备份
        status = zipDataBackup();
        if (!status)
            return false;
        // 第五步:上传压缩备份文件到FTP
        status = sftpTransferService();
        if (!status)
            return false;
        // 第六步:清除数据
//        clearKeepData();
 
        long end = System.currentTimeMillis();
        log.writeInfo("【系统备份进程】系统备份进程结束,耗时.................." + ((end - start) / 1000) + "秒", BackupLogger.INFO_TYPE);
        // 关闭日志
        log.closeLogger();
        return status;
    }
 
    /**
     * 第一步:初始备份参数
     * @param startTime    备份开始时间
     * @param finalTime    备份截止时间
     * @return
     */
    public boolean initBackupParam(Date startTime, Date finalTime){
        // 在日志中记录操作系统信息,便于调试
        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(finalTime);
        NUMBER_DATE = numberDateFormat.format(finalTime);
        if(startTime == null) {
            STANDARD_START_TIME = "2000-01-01 00:00:00";
        } else {
            STANDARD_START_TIME = standardTimeFormat.format(startTime);
        }
        STANDARD_FINAL_TIME = standardTimeFormat.format(finalTime);
        log.writeInfo("【初始备份参数】系统日期:" + NUMBER_DATE, BackupLogger.INFO_TYPE);
        log.writeInfo("【初始备份参数】系统时间:" + NUMBER_TIME, BackupLogger.INFO_TYPE);
        log.writeInfo("【初始备份参数】系统备份开始时间:" + STANDARD_START_TIME, BackupLogger.INFO_TYPE);
        log.writeInfo("【初始备份参数】系统备份截止时间:" + STANDARD_FINAL_TIME, BackupLogger.INFO_TYPE);
        
        return true;
    }
    
    /**
     * 第二步:初始配置文件参数,当前目录生成时间戳的文件夹
     */
    public boolean initSystemConfig() {
        log.writeInfo("【初始配置文件】开始初始系统配置文件..................", BackupLogger.INFO_TYPE);
        try (InputStream  reader = getClass().getClassLoader().getResourceAsStream(CONFIG_FILE_PATH)) {
            if (reader == null) {
                log.writeInfo("【初始配置文件】初始系统配置文件失败:" + CONFIG_FILE_PATH + "配置文件不存在", BackupLogger.ERROR_TYPE);
                return false;
            }
            // 读取备份配置文件
            config = new Properties();
            config.load(reader);
            // 获取系统文件路径
            String systemFileFolder = Global.getSystemConfig("local.dir", "");
            File newFile= new File(systemFileFolder);
            // 系统附件存放目录(../product-server-web/attachment/upload/)
            config.setProperty("DOCUMENT_ROOT", newFile.getAbsolutePath());
            // 数据库备份目录(数据库备份根目录+时间文件名+.sql,例如:E://backup/database/20250101090000.sql)
            config.setProperty("DATABASE_BACKUP", config.getProperty("DATABASE_ROOT") + File.separator + NUMBER_TIME + ".sql");
            // 备份ZIP文件(ZIP备份根目录+时间文件名+.zip,例如:E://backup/20250101090000.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("【初始配置文件】DOCUMENT_ROOT=" + config.getProperty("DOCUMENT_ROOT"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】DATABASE_ROOT=" + config.getProperty("DATABASE_ROOT"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】DATABASE_BACKUP=" + config.getProperty("DATABASE_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(), StandardCharsets.UTF_8));
                 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);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.writeInfo("【备份数据库】数据库备份失败:" + e.getMessage(), BackupLogger.ERROR_TYPE);
            backupFile.delete();
            return false;
        }
    }
 
    /**
     *     第四步:执行对数据库文件、工程代码、上传文件做压缩备份
     * @return
     */
    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)){
            StringBuilder sbSql = new StringBuilder();
            sbSql.append("SELECT SUBSTRING_INDEX(attachment_url, '/', -1) folder_name,attachment_title AS file_name,attachment_url AS folder_path \n");
            sbSql.append("FROM product_sys_attachments \n");
            sbSql.append("WHERE (created_utc_datetime > ? AND created_utc_datetime <= ?) \n");
            sbSql.append("OR (updated_utc_datetime > ? AND updated_utc_datetime <= ?) \n");
            DataTableEntity dtTable = baseDao.listTable(sbSql.toString(), new Object[] {STANDARD_START_TIME, STANDARD_FINAL_TIME, STANDARD_START_TIME, STANDARD_FINAL_TIME});
            if(dtTable != null && dtTable.getRows() > 0) {
                Map<String, List<String>> groupFolderFile = new HashMap<>();
                for(int i = 0; i < dtTable.getRows(); i++) {
                    String folderName = dtTable.getFieldSetEntity(i).getString("folder_path");
                    String fileName = dtTable.getFieldSetEntity(i).getString("file_name");
                    if (groupFolderFile.containsKey(folderName)) {
                        groupFolderFile.get(folderName).add(fileName);
                    } else {
                        groupFolderFile.put(folderName, new ArrayList<String>(Arrays.asList(fileName)));
                    }
                }
                log.writeInfo("【压缩备份文件】压缩系统增量附件", BackupLogger.INFO_TYPE);
                SystemBackupUtil.compressExistingFiles(groupFolderFile, documentPath, zos);
            }
            
            log.writeInfo("【压缩备份文件】压缩数据库备份文件", BackupLogger.INFO_TYPE);
            SystemBackupUtil.addFileToZip(new File(databasePath), "database/" + NUMBER_TIME + ".sql", zos);
            log.writeInfo("【压缩备份文件】压缩完成", BackupLogger.INFO_TYPE);
            return true;
        } catch (IOException e) {
            e.printStackTrace();
            log.writeInfo("【压缩备份文件】压缩失败:" + e.getMessage(), BackupLogger.ERROR_TYPE);
            return false;
        } finally {
            File file = new File(databasePath);
            file.delete();
            log.writeInfo("【压缩备份文件】清理数据库备份文件完成", BackupLogger.INFO_TYPE);
        }
    }
    
    /**
     * 第五步:SFTP连接:通过SFTP将备份文件上传备用机  或  从mysql服务器上获取备份文件
     * @param isUpload     是否上传
     * @return
     */
    public boolean sftpTransferService() {
        String localFilePath =  config.getProperty("ZIPFILE_BACKUP");    // 本地文件路径
        String sftpFilePath = config.getProperty("UPLOAD_BACKUP_DIR") + NUMBER_TIME + ".zip"; // SFTP服务文件路径;
            
        String host = config.getProperty("UPLOAD_SFTP_HOST"); // SFTP服务IP地址
        String port = config.getProperty("UPLOAD_SFTP_PORT"); // SSH端口
        String user = config.getProperty("UPLOAD_SFTP_USER"); // SFTP服务的用户名
        String password = config.getProperty("UPLOAD_SFTP_PWD"); // SFTP服务的密码
        log.writeInfo("【上传系统备份文件】开始上传系统备份文件..................", BackupLogger.INFO_TYPE);
        log.writeInfo("【上传系统备份文件】SFTP服务地址:" + host, BackupLogger.INFO_TYPE);
        log.writeInfo("【上传系统备份文件】SFTP服务端口:" + port, BackupLogger.INFO_TYPE);
        log.writeInfo("【上传系统备份文件】SFTP服务存储目录:" + sftpFilePath, BackupLogger.INFO_TYPE);
        log.writeInfo("【上传系统备份文件】当前服务存储目录:" + localFilePath, BackupLogger.INFO_TYPE);
        if (StringUtils.isEmpty(host) || StringUtils.isEmpty(port) || 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, Integer.valueOf(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 = sftpFilePath.substring(0, sftpFilePath.lastIndexOf('/'));
            if (!remoteDir.startsWith("/")) {
                remoteDir = "/" + remoteDir;
            }
            // 创建对应目录文件夹
            createRemoteDirectory(sftpChannel, remoteDir);
            sftpChannel.put(localFilePath, sftpFilePath, 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;
    }
    
    /**
     *  SFTP递归创建远程目录
     * @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;
            }
        }
    }
}