前段时间,因为各种原因,自己动手写了一个小的备份工具,用了一个星期,想想把它的设计思路放上来,当是笔记吧。
需求场景:这个工具起初的目的是为了解决朋友公司对其网络的限制(不可以用任何同步软件,git,外网SVN,U盘只读)。本来只是想做一个自动打包和发送邮件的工具,后来就发展成了这个。
软件功能:这个软件最终实现的功能包括1、读取配置文件,对配置文件中指定目录的文件进行日期检测,获取对应修改过的文件。2、将读出的文件进行压缩;3、将压缩文件发送到指定邮箱;4、对压缩文件进行历史版本的保留
根据最后的功能需求,工具最后选择采用的API包如下:
1、 使用commons-config实现配置文件的读写
2、 使用commons-io实现文件的检测和读写
3、 使用zip4j实现对文件的压缩
4、 使用javamail实现邮件的发送
系统最后的流程图如下:
主要的工具类如下:
1 //用于发送邮件的Bean 2 package com.sean.bean; 3 4 import java.io.File; 5 import java.io.IOException; 6 import java.io.InputStreamReader; 7 import java.io.UnsupportedEncodingException; 8 import java.util.Properties; 9 10 import javax.activation.DataHandler; 11 import javax.activation.FileDataSource; 12 import javax.mail.Address; 13 import javax.mail.BodyPart; 14 import javax.mail.Message; 15 import javax.mail.Message.RecipientType; 16 import javax.mail.MessagingException; 17 import javax.mail.NoSuchProviderException; 18 import javax.mail.Session; 19 import javax.mail.Transport; 20 import javax.mail.internet.AddressException; 21 import javax.mail.internet.InternetAddress; 22 import javax.mail.internet.MimeBodyPart; 23 import javax.mail.internet.MimeMessage; 24 import javax.mail.internet.MimeMultipart; 25 /** 26 * 邮件类 27 * @author Sean 28 * @blog http://www.cnblogs.com/Seanit/ 29 * @email [email protected] 30 * 2015-6-9 31 */ 32 public class EmailBean { 33 private Session session; 34 private MimeMessage message; 35 private Properties properties; 36 private MimeMultipart body; 37 38 /** 39 * 默认构造函数 40 */ 41 public EmailBean(){ 42 } 43 44 45 public EmailBean(Properties properties){ 46 this.properties=properties; 47 createMail(properties); 48 } 49 50 public EmailBean(String src){ 51 this.properties=new Properties(); 52 try { 53 properties.load(new InputStreamReader(ClassLoader.getSystemResourceAsStream(src),"utf-8")); 54 } catch (UnsupportedEncodingException e) { 55 // TODO Auto-generated catch block 56 e.printStackTrace(); 57 } catch (IOException e) { 58 // TODO Auto-generated catch block 59 e.printStackTrace(); 60 } 61 createMail(properties); 62 } 63 64 /** 65 * 创建连接 66 * @author Sean 67 * 2015-6-8 68 * @param properties 邮箱相关配置 69 */ 70 public void createMail(Properties properties) { 71 this.properties=properties; 72 try { 73 session=Session.getDefaultInstance(properties,null); 74 message=new MimeMessage(session); 75 body=new MimeMultipart(); 76 } catch (Exception e) { 77 e.printStackTrace(); 78 throw new RuntimeException("邮件初始化失败"); 79 } 80 } 81 82 /** 83 * 设置发件人 84 * @author Sean 85 * 2015-6-8 86 * @param from 发件人地址 87 * @return 成功返回true,失败防护false 88 */ 89 public boolean setFrom(String from){ 90 try { 91 message.setFrom(new InternetAddress(from)); 92 return true; 93 } catch (Exception e) { 94 e.printStackTrace(); 95 return false; 96 } 97 } 98 99 /** 100 * 设置收件人 101 * @author Sean 102 * 2015-6-8 103 * @param toSend 收件人邮箱地址 104 * @return 成功返回true,失败防护false 105 */ 106 public boolean setTOSend(String toSend){ 107 try { 108 message.setRecipients(RecipientType.TO, toSend); 109 return true; 110 } catch (MessagingException e) { 111 e.printStackTrace(); 112 return false; 113 } 114 } 115 116 /** 117 * 设置抄送人 118 * @author Sean 119 * 2015-6-8 120 * @param copyTo 抄送人地址 121 * @return 成功返回true,失败防护false 122 */ 123 public boolean setCopyTO(String copyTo){ 124 try { 125 message.setRecipients(RecipientType.CC, (Address[])InternetAddress.parse(copyTo)); 126 return true; 127 } catch (Exception e) { 128 e.printStackTrace(); 129 return false; 130 } 131 } 132 133 134 /** 135 * 设置主题 136 * @author Sean 137 * 2015-6-8 138 * @param subject 邮件主题 139 * @return 成功返回true,失败防护false 140 */ 141 public boolean setSubject(String subject){ 142 try { 143 message.setSubject(subject); 144 return true; 145 } catch (MessagingException e) { 146 e.printStackTrace(); 147 return false; 148 } 149 } 150 151 /** 152 * 设置邮件正文 153 * @author Sean 154 * 2015-6-8 155 * @param content 邮件正文 156 * @return 成功返回true,失败防护false 157 */ 158 public boolean setContent(String content){ 159 try { 160 BodyPart bodyPart=new MimeBodyPart(); 161 bodyPart.setContent(""+content, "text/html;charset=GBK"); 162 body.addBodyPart(bodyPart); 163 return true; 164 } catch (Exception e) { 165 e.printStackTrace(); 166 return false; 167 } 168 } 169 170 /** 171 * 添加附件 172 * @author Sean 173 * 2015-6-8 174 * @param src 附件地址 175 * @return 176 */ 177 public boolean addFiles(String src){ 178 try { 179 BodyPart bodyPart=new MimeBodyPart(); 180 FileDataSource file =new FileDataSource(src); 181 bodyPart.setDataHandler(new DataHandler(file)); 182 bodyPart.setFileName(file.getName()); 183 body.addBodyPart(bodyPart); 184 return true; 185 } catch (Exception e) { 186 e.printStackTrace(); 187 return false; 188 } 189 } 190 191 /** 192 * 发送邮件 193 * @author Sean 194 * 2015-6-8 195 * @return 196 */ 197 public boolean send(){ 198 Transport transport=null; 199 try { 200 message.setContent(body); 201 message.saveChanges(); 202 transport=session.getTransport(); 203 transport.connect(properties.getProperty("mail.smtp.host"), properties.getProperty("username"), properties.getProperty("password")); 204 transport.sendMessage(message, message.getRecipients(Message.RecipientType.TO)); 205 if(message.getRecipients(Message.RecipientType.CC)!=null){ 206 transport.sendMessage(message, message.getRecipients(Message.RecipientType.CC)); 207 } 208 return true; 209 } catch (Exception e) { 210 e.printStackTrace(); 211 return false; 212 }finally{ 213 try { 214 transport.close(); 215 } catch (MessagingException e) { 216 // TODO Auto-generated catch block 217 e.printStackTrace(); 218 } 219 } 220 } 221 }
1 //用于操作配置文件的bean 2 package com.sean.bean; 3 4 import org.apache.commons.configuration.ConfigurationException; 5 import org.apache.commons.configuration.PropertiesConfiguration; 6 /** 7 * 配置文件操作类 8 * 实现功能:对properties 文件进行读写操作 9 * @author Sean 10 * 2015-6-6 11 */ 12 public class PropertiesBean { 13 private String src=""; 14 private PropertiesConfiguration pc=null; 15 /** 16 * 默认构造函数 17 */ 18 public PropertiesBean() {} 19 public PropertiesBean(String src) { 20 this.src=src; 21 init(); 22 } 23 24 /** 25 * 初始化函数 26 * @author Sean 27 * 2015-6-6 28 */ 29 public void init(){ 30 if(src.trim().equals("")){ 31 throw new RuntimeException("The path is null"); 32 } 33 try { 34 pc=new PropertiesConfiguration(src); 35 } catch (ConfigurationException e) { 36 e.printStackTrace(); 37 } 38 } 39 40 /** 41 * 取值函数,根据对应的关键字获取对应的值 42 * @author Sean 43 * 2015-6-6 44 * @param key 关键字 45 * @return 返回关键字对应的值,若无该关键字,抛出异常 46 */ 47 public String getValue(String key){ 48 if(!pc.containsKey(key)){ 49 throw new RuntimeException("not such a key"); 50 } 51 return pc.getString(key); 52 } 53 54 /** 55 * 设置对应的值,传入键值对,根据关键字修改对应的值 56 * @author Sean 57 * 2015-6-6 58 * @param key 关键字 59 * @param value 值 60 */ 61 public void setValue(String key,String value){ 62 if(!pc.containsKey(key)){ 63 throw new RuntimeException("not such a key"); 64 } 65 pc.setProperty(key, value); 66 try { 67 pc.save(); 68 } catch (ConfigurationException e) { 69 // TODO Auto-generated catch block 70 e.printStackTrace(); 71 } 72 } 73 74 /** 75 * 设置配置文件的地址 76 * @author Sean 77 * 2015-6-6 78 * @param src 配置文件的路径 79 */ 80 public void setSrc(String src) { 81 this.src = src; 82 } 83 }
//用于进行IO操作的Bean package com.sean.bean; import java.io.File; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.apache.commons.io.FileUtils; /** * 文件操作工具类 * @author Sean * @blog http://www.cnblogs.com/Seanit/ * @email [email protected] * 2015-6-6 */ public class FilesBean { private List<File> list=null; public FilesBean(){} /** * 获取文件夹目录下的列表,目录为空返回空列表 * @param dir 传入对应文件夹目录 * @param ifFirst 控制递归时候的循环表示 * @return 返回list<File>文件列表 */ public List<File> listFiles(String dir,boolean ifFirst){ /** * 递归判断,仅在第一次调用函数的时候新建列表,为防止重复存入列表 */ if(ifFirst){ list=new ArrayList<File>(); } if (dir==null||dir=="") { return null; } for (File f : FileUtils.getFile(dir + "\\").listFiles()) { if (f.isDirectory()) { listFiles(f.getAbsolutePath(),false); } else { list.add(f); } } return list; } /** * 根据过滤条件获取列表 * @param dir 目录 * @param filter 过滤条件 * @param ifFirst 递归标识 * @return 返回文件列表 */ public List<File> listFiles(String dir,String filter,boolean ifFirst){ /** * 递归判断,仅在第一次调用函数的时候新建列表,为防止重复存入列表 */ if(ifFirst){ list=new ArrayList<File>(); } if (dir==null||dir=="") { return null; } for (File f : FileUtils.getFile(dir + "\\").listFiles()) { if (f.isDirectory()) { listFiles(f.getAbsolutePath(),filter,false); } else { if(!f.getName().endsWith(filter)){ list.add(f); } } } return list; } /** * 获取指定目录下,指定日期修改后的文件 * @param dir 传入指定检测目录 * @param date 传入指定修改日期 * @param ifFirst 控制递归时候的循环表示 * @return 返回获取的文件列表,若目录或日期为空,则返回null */ public List<File> getModifiedFiles(String dir,Date date,boolean ifFirst){ /** * 递归判断,仅在第一次调用函数的时候新建列表,为防止重复存入列表 */ if(ifFirst){ list=new ArrayList<File>(); } if (dir==null||dir==""||date==null) { return null; } for (File f : FileUtils.getFile(dir + "\\").listFiles()) { if (f.isDirectory()) { getModifiedFiles(f.getAbsolutePath(),date,false); } else { if (FileUtils.isFileNewer(f, date.getTime())) { list.add(f); } } } return list; } /** * 获取某一日期之前创建的文件 * @author Sean * 2015-6-9 * @param dir 文件路径 * @param ifFirst 控制递归时候的循环表示 * @param date 日期 * @return 返回列表 */ public List<File> getOlderFiles(String dir,Date date,boolean ifFirst){ /** * 递归判断,仅在第一次调用函数的时候新建列表,为防止重复存入列表 */ if(ifFirst){ list=new ArrayList<File>(); } if (dir==null||dir==""||date==null) { return null; } for (File f : FileUtils.getFile(dir + "\\").listFiles()) { if (f.isDirectory()) { getOlderFiles(f.getAbsolutePath(),date,false); } else { if (FileUtils.isFileOlder(f, date)) { list.add(f); } } } return list; } /** * 获取除过滤条件外的修改文件列表 * @param dir * @param date * @param filter * @param ifFirst * @return */ public List<File> getModifiedFiles(String dir,Date date,String filter,boolean ifFirst){ /** * 递归判断,仅在第一次调用函数的时候新建列表,为防止重复存入列表 */ if(ifFirst){ list=new ArrayList<File>(); } if (dir==null||dir==""||date==null) { return null; } for (File f : FileUtils.getFile(dir + "\\").listFiles()) { if (f.isDirectory()) { getModifiedFiles(f.getAbsolutePath(),date,filter,false); } else { if (FileUtils.isFileNewer(f, date.getTime())) { if(!f.getName().endsWith(filter)){ list.add(f); } } } } return list; } }
//用于压缩文件的工具类 package com.sean.utils; import java.io.File; import java.util.ArrayList; import net.lingala.zip4j.core.ZipFile; import net.lingala.zip4j.exception.ZipException; import net.lingala.zip4j.model.ZipParameters; import net.lingala.zip4j.util.Zip4jConstants; public class ZipUtil { /** * 压缩文件列表,无目录结构 * @author Sean * 2015-6-6 * @param zipSrc zip文件地址 * @param list 文件列表 * @return 若成功,返回文件地址,不成功返回null */ public static String zipFiles(String zipSrc,ArrayList<File> list){ ZipParameters zipParameters=new ZipParameters(); zipParameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); zipParameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_MAXIMUM); ZipFile zipFile=null; try { zipFile=new ZipFile(zipSrc); } catch (ZipException e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } try { zipFile.addFiles(list, zipParameters); } catch (ZipException e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } return zipSrc; } /** * 压缩文件夹 * @author Sean * 2015-6-6 * @param src 文件夹地址 * @param zipSrc 压缩文件存放地址 * @return 若成功,返回文件夹地址,若不成功,返回null */ public static String zipFolder(String src,String zipSrc){ ZipParameters zipParameters=new ZipParameters(); zipParameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); zipParameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_MAXIMUM); ZipFile zipFile=null; try { zipFile=new ZipFile(zipSrc); } catch (ZipException e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } try { zipFile.addFolder(src, zipParameters); } catch (ZipException e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } return zipSrc; } /** * 往zip文件中添加文件列表 * @author Sean * 2015-6-7 * @param zipSrc zip文件路径 * @param list 要添加的文件列表 * @param src 要添加到zip文件中的哪个路径 */ public static void addFileToZip(String zipSrc,ArrayList<File> list,String src){ ZipFile zipFile=null; try { zipFile=new ZipFile(zipSrc); } catch (ZipException e) { // TODO Auto-generated catch block e.printStackTrace(); } ZipParameters zipParameters=new ZipParameters(); zipParameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); zipParameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL); zipParameters.setRootFolderInZip(src); try { zipFile.addFiles(list, zipParameters); } catch (ZipException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * 添加文件列表到zip根目录下 * @author Sean * 2015-6-7 * @param zipSrc zip文件地址 * @param list 添加的文件列表 */ public static void addFileToZip(String zipSrc,ArrayList<File> list){ addFileToZip(zipSrc,list,""); } }
主流程: package com.sean.main; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import com.sean.bean.EmailBean; import com.sean.bean.FilesBean; import com.sean.bean.PropertiesBean; import com.sean.utils.ZipUtil; /** * change2Mail 主程序 * 检测文件修改情况并发送给指定邮箱 * @version 1.0 * @author sean * 2015-06-09 * */ public class Change2Mail { /** * @param args * @throws ParseException */ public static void main(String[] args) throws ParseException{ //获取properties操作类 PropertiesBean propertiesBean = new PropertiesBean("cfg.properties"); FilesBean filesBean=new FilesBean(); //邮件对象 EmailBean email=new EmailBean("cfg.properties"); String first = propertiesBean.getValue("first"); //当前日期 String today=new SimpleDateFormat("yyyyMMdd").format(new Date()); //邮件正文 String content=""; //首次使用判断 if (first.trim().equals("true")) { System.out.println("首次运行"); content+="欢迎首次使用change2Mail,<br/>以下是你的更改文件备份目录<br/>"; //获取检查文件目录 String cheakDir = propertiesBean.getValue("cheakDir"); if(!StringUtils.isEmpty(cheakDir)){ System.out.println("正在进行更改文件目录备份。。。。。。"); //目录分割 String[] dirs = cheakDir.split(";"); for (int i = 0; i < dirs.length; i++) { File tmp=new File(dirs[i]); List<File> list=filesBean.listFiles(dirs[i],true); //创建对应文件存储目录列表 File file=new File(ClassLoader.getSystemResource("").getPath()+tmp.getName()); if(!file.exists()){ try { file.createNewFile(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); System.out.println("创建目录列表记录文件失败。。。。。"); return ; } } //将列表写入文件 logFiles(file,list); //压缩文件并加入邮件 try { ZipUtil.zipFolder(dirs[i], "tmp\\"+tmp.getName()+today+".zip"); content+=tmp.getName()+"<br/>"; email.addFiles("tmp\\"+tmp.getName()+today+".zip"); } catch (Exception e) { e.printStackTrace(); System.out.println("压缩出错"); return ; } } } content+="以下是你的固定备份目录<br/>"; String zipDir = propertiesBean.getValue("zipDir"); if(!StringUtils.isEmpty(zipDir)){ System.out.println("正在进行固定备份目录备份。。。。。。"); String[] zipDirs = zipDir.split(";"); for (int i = 0; i < zipDirs.length; i++) { try { File tmp=new File(zipDirs[i]); ZipUtil.zipFolder(zipDirs[i], "tmp\\"+tmp.getName()+today+"bak.zip"); content+=tmp.getName()+"<br/>"; email.addFiles("tmp\\"+tmp.getName()+today+"bak.zip"); } catch (Exception e) { e.printStackTrace(); System.out.println("压缩出错"); return ; } } } System.out.println("更新配置文件。。。。。"); propertiesBean.setValue("first", "false"); propertiesBean.setValue("lastData", today); System.out.println("发送文件。。。。。"); email.setFrom(propertiesBean.getValue("username")); email.setTOSend(propertiesBean.getValue("toSend")); email.setContent(content); email.setSubject("change2Mail备份文件"+today); if(email.send()){ System.out.println("发送成功"); }else{ System.out.println("发送失败"); } } else { System.out.println("备份开始。。。。。"); String data=propertiesBean.getValue("lastData"); String filter=propertiesBean.getValue("filter"); String cheakDir = propertiesBean.getValue("cheakDir"); content+="你好,自"+data+"起的文件变动如下:<br/>"; if(!StringUtils.isEmpty(cheakDir)){ System.out.println("正在进行更改文件目录备份。。。。。。"); String[] dirs = cheakDir.split(";"); for (int i = 0; i < dirs.length; i++) { File tmp=new File(dirs[i]); File file=new File(ClassLoader.getSystemResource("").getPath()+tmp.getName()); if(!file.exists()){ try { file.createNewFile(); } catch (IOException e) { System.out.println("创建文档失败"); e.printStackTrace(); } } content+="文件夹"+tmp.getName()+":<br/>"; List<File> list=filesBean.getModifiedFiles(dirs[i],new SimpleDateFormat("yyyyMMdd").parse(data),filter,true); List<File> now =filesBean.listFiles(dirs[i],filter,true); List<File> news=new ArrayList<File>(); List<File> del=new ArrayList<File>(); List<File> modefy=new ArrayList<File>(); List<String> lines=null; try { lines=FileUtils.readLines(file); } catch (IOException e) { e.printStackTrace(); } System.out.println("检查新建和修改的文件。。。。。。"); for(File f:list){ if(!lines.contains(f.getAbsolutePath())){ news.add(f); }else{ modefy.add(f); } } System.out.println("检查删除的文件。。。。。。"); for(String f:lines){ File old=new File(f); if(!now.contains(old)){ del.add(old); } } logFiles(file,now); content+="删除文件:<br/>"; for(File f:del){ content+=" "+f.getName()+"<br/>"; } content+="新增文件:<br/>"; for(File f:news){ content+=" "+f.getName()+"<br/>"; } content+="修改文件:<br/>"; for(File f:modefy){ content+=" "+f.getName()+"<br/>"; } try { if(list.size()!=0){ list.add(file); ZipUtil.zipFiles("tmp\\"+tmp.getName()+today+".zip", (ArrayList<File>)list); email.addFiles("tmp\\"+tmp.getName()+today+".zip"); } } catch (Exception e) { e.printStackTrace(); System.out.println("压缩失败"); return; } } String zipDir = propertiesBean.getValue("zipDir"); if(!StringUtils.isEmpty(zipDir)){ System.out.println("正在进行固定备份目录备份。。。。。。"); String[] zipDirs = zipDir.split(";"); for (int i = 0; i < zipDirs.length; i++) { try { File tmp=new File(zipDirs[i]); ZipUtil.zipFolder(zipDirs[i], "tmp\\"+tmp.getName()+today+"bak.zip"); email.addFiles("tmp\\"+tmp.getName()+today+"bak.zip"); } catch (Exception e) { e.printStackTrace(); System.out.println("压缩出错"); return ; } } } System.out.println("更改配置文件。。。。。"); propertiesBean.setValue("lastData", today); System.out.println("发送文件。。。。。"); email.setFrom(propertiesBean.getValue("username")); email.setTOSend(propertiesBean.getValue("toSend")); email.setContent(content); email.setSubject("change2Mail备份文件"+today); if(email.send()){ System.out.println("发送成功"); }else{ System.out.println("发送失败"); } SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd"); long to = df.parse(today).getTime(); long todel=Integer.parseInt(propertiesBean.getValue("time"))*(1000 * 60 * 60 * 24); Date lastBak=new Date(to-todel) ; System.out.println("删除"+df.format(lastBak)+"之前保存的历史文件。。。。。"); List<File> fileToDel=filesBean.getOlderFiles("tmp\\", lastBak,true); for(File f:fileToDel){ try { FileUtils.deleteQuietly(f); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); System.out.println("删除"+f.getName()+"文件失败"); } } System.out.println(new Date()); } } } /** * 将文件列表的绝对地址输出到一个文件中 * @param file 输出文件 * @param list 文件列表 * @return 成功返回true,失败返回false */ public static boolean logFiles(File file, List<File> list) { List<String> lines=new ArrayList<String>(); try { for (File f : list) { lines.add(f.getAbsolutePath()); } FileUtils.writeLines(file, lines,false); return true; } catch (IOException e) { e.printStackTrace(); return false; } } }
配置文件: #autho:sean #data:2015/05/26 #email:[email protected]126.com #mail config javamail的配置 mail.debug = true mail.smtp.auth = true mail.smtp.host = smtp.126.com mail.transport.protocol = smtp #mail info 邮箱设置 username = [email protected].com password = 121212 toSend = 121212@qq.com #soft config 软件设置 #if the fisrt time to run 是否第一次运行 first = true #the path to check 检测的目录 cheakDir = F:\\myfolder\\BaseClass; #the path allways to zip 完全备份的目录 zipDir = F:\\myfolder\\myProject\\Change2Mail; #the data lastcheck 最后备份时间 lastData = 20150629 #storage life of the bak zip 保留文件 time = 5 #filter string 过滤条件 filter = .class
工具github地址:https://github.com/Seanid/change2mail
为了便于使用,工具最后导出为jar包,并编写了一个bat进行执行,加入了jre文件夹,可以再无jdk环境下使用。最后压缩包地址:http://pan.baidu.com/s/1i3vQJv3
本工具只是个人闲时开发,纯属娱乐,大神勿喷
时间: 2024-10-08 14:19:08