IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Springboot文件管理 -- 实现上传下载显示删除等接口详细解析 附代码(全) -> 正文阅读

[Java知识库]Springboot文件管理 -- 实现上传下载显示删除等接口详细解析 附代码(全)

前言

对于该篇文章涉及的知识点可看我之前的文章:
java知识点细节:java框架零基础从入门到精通的学习路线(超全)
springboot知识点:springboot从入门到精通(全)

细节知识点具体有如下:@GetMapping、@PostMapping 和 @RequestMapping详细区别附实战代码(全)

IO流以及缓冲区输入输出基础知识:

  1. java NIO从入门到精通(全)
  2. javaSE从入门到精通的二十万字总结(二)

以上知识点查漏补缺或者在下面正文中遇到了可对应进行学习即可

1. 知识点补充

文件的上传下载等借口,使用最多的两个类File以及MultipartFile类,先看源代码讲解

1.1 File类

关于File类可看我之前的文章:java关于File类源码的详细分析 附代码(全)

1.2 MultipartFile类

上传多文件主要用到这个接口:MultipartFile file
单文件为file,多文件上传即存到数组,通过遍历一个个取出即可

源码:

public interface MultipartFile extends InputStreamSource {
    String getName();

    @Nullable
    String getOriginalFilename();

    @Nullable
    String getContentType();

    boolean isEmpty();

    long getSize();

    byte[] getBytes() throws IOException;

    InputStream getInputStream() throws IOException;

    default Resource getResource() {
        return new MultipartFileResource(this);
    }

    void transferTo(File dest) throws IOException, IllegalStateException;

    default void transferTo(Path dest) throws IOException, IllegalStateException {
        FileCopyUtils.copy(this.getInputStream(), Files.newOutputStream(dest));
    }
}

该接口有多个方法属性
大致如下:

参数描述
getName()文件格式
getOriginalFilename()文件名
getContentType文件类型
isEmpty()文件是否为空
getSize()文件大小

上传一张照片的时候,输出的结果大致如下:
在这里插入图片描述
文件中的一些其他参数,比如上传时间、文件后缀等可自个优化

获取文件后缀的定义可通过substring截取
函数如下:

fileName.substring(fileName.lastIndexOf("."));

2. 本地测试

所谓的本地接口,也就是通过本地进行上传下载删除更改文件目录显示等接口
加深各个函数之间的运用之后,在对应进行实战开发

2.1 上传文件

函数名如下:

@RestController
@RequestMapping("file")
@Slf4j
public class filecontroller2 {

对于slf4j的注解可看我这两文章:(主要是打印日志的文件)

  1. java常见log日志的使用方法详细解析
  2. 出现SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“.的解决方法

上传的接口赋值一个文件名(埋下伏笔,已经写死只能从这个文件进行上传,后续会对应进行优化):

@Value("${file.upload.url}")
private String uploadFilePath;

注解的引入在配置文件中(application.properties):file.upload.url=E:/upload

之后可以通过uuid的随机赋值文件名

//路径 + UUID(文件名)
String realPath = uploadFilePath + "/" + UUID.randomUUID().toString().replaceAll("-", "");
File dest = new File(realPath);
//判断是否存在这个目录,如果不存在就在当前路径下创建
if (!dest.exists() && !dest.isDirectory()) {
    //创建多级文件夹
    dest.getParentFile().mkdirs();
}

也可以使用当前的filename进行命名:

File dest = new File(uploadFilePath +'/'+ fileName);
if (!dest.getParentFile().exists()) {
    dest.getParentFile().mkdirs();
}

完整示例代码如下:

以下为多文件上传,如果改动为单文件,只需要将其数组以及遍历去除即可

//  http://127.0.0.1:8080/file/upload
// 填写files的参数,问号不用带
/*
* 上传多个文件
* */
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public String FileUpload(@RequestParam("files") MultipartFile[] files){
    //终端以json格式传输,可以使用JSONObject这个类,将其json格式打在显示上
    JSONObject object=new JSONObject();

    //遍历各个文件
    for(int i=0;i<files.length;i++){
        String fileName = files[i].getOriginalFilename();

        //判断是否存在这个目录,如果不存在就在当前路径下创建
        File dest = new File(uploadFilePath +'/'+ fileName);
        if (!dest.getParentFile().exists()) {
            dest.getParentFile().mkdirs();
        }

        try {
            //上传到服务器
            files[i].transferTo(dest);
            object.put("success","上传成功");
        } catch (Exception e) {
            object.put("fail","上传失败,重新上传");
        }
    }

    return object.toString();
}

对应通过接口测试如下:

测试接口可通过PostMan、ApiPost等软件,可看我这篇文章的讲解:国货之光的API管理软件 - Apipost

在这里插入图片描述

2.2 下载文件

通过对应的流进行下载,之后别忘记关闭流的传输

//http://127.0.0.1:8080/file/download?fileName=1.png
//@GetMapping("/download")
@RequestMapping(value = "/download",  method = RequestMethod.GET)
public String fileDownLoad(HttpServletResponse response, @RequestParam("fileName") String fileName){
    File file = new File(uploadFilePath +'/'+ fileName);
    if(!file.exists()){
        return "下载文件不存在";
    }
    response.reset();
    response.setCharacterEncoding("utf-8");
    response.setHeader("Content-Type", "multipart/form-data");
    response.setHeader("Content-Disposition", "attachment;filename=" + fileName );

    try(BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
        BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
    ) {
        byte[] buff = new byte[1024];
        int len = 0;
        while ((len = bis.read(buff)) != -1) {
            bos.write(buff, 0, len);
            bos.flush();
        }
    } catch (IOException e) {
        return "下载失败";
    }
    return "下载成功";
}

处理中断输入输出流的时候的异常,加入finally比较保守
改进代码如下:

//http://127.0.0.1:8080/file/download?fileName=1.png
//@GetMapping("/download")
@RequestMapping(value = "/download",  method = RequestMethod.GET)
public String fileDownLoad(HttpServletResponse response, @RequestParam("fileName") String fileName){
    File file = new File(uploadFilePath +'/'+ fileName);
    if(!file.exists()){
        return "下载文件不存在";
    }
    response.reset();
    response.setCharacterEncoding("utf-8");
    response.setHeader("Content-Type", "multipart/form-data");
    response.setHeader("Content-Disposition", "attachment;filename=" + fileName );

    FileInputStream fis = null;
    BufferedInputStream bis = null;
    try {
        fis = new FileInputStream(file);
        bis = new BufferedInputStream(fis);
        BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
        byte[] buff = new byte[1024];
        int len = 0;
        while ((len = bis.read(buff)) != -1) {
            bos.write(buff, 0, len);
            bos.flush();
        }
        return "下载成功";
    } catch (IOException e) {
        e.printStackTrace();
        return "下载失败";
    }finally {
        if (bis != null) {
            try {
                bis.close();
            } catch (IOException e) {
                e.printStackTrace();
                return "输入流异常";
            }
        }
        if (fis != null) {
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
                return "文件流异常";
            }
        }
    }

}

2.3 删除文件

可以使用FileSystemUtils的类对应进行删除

// http://127.0.0.1:8080/file3/delete  path是路径
// @PostMapping ("/delete")
@RequestMapping(value = "/delete",  method = RequestMethod.POST)
public Boolean DeletePathFile(@RequestParam("path")String path){
    return FileSystemUtils.deleteRecursively(new File(path));
}

通过api接口测试中显示true,这是因为这个类本身返回值就是Boolean值,如果删除成功返回true
在这里插入图片描述

2.4 显示所有目录

该demo在本地中只能显示该路径下(已经写死)的所有目录文件

//  http://127.0.0.1:8080/file/getAllDirs
/*
* 获取当前路径下的所有目录
* */
@RequestMapping(value = "/getAllDirs", method = RequestMethod.POST)
public Object getAllDirs(String filepath ) {
    Map<String, Object> map = new HashMap<>();
    try {
    	// 获取目录 主要是通过递归
        List<String> dirList = getAllDir(uploadFilePath,true);
        map.put("data", dirList);
        map.put("code", 200);
        map.put("查询目录", uploadFilePath);
        map.put("message", "查询成功");
    } catch (Exception e) {
        e.printStackTrace();
        map.put("查询目录", "无此目录");
        map.put("message", "查询失败");
    }
    return map;
}

该递归目录的算法如下:

// 获取当前路径下的所有文件/文件夹
public List<String> getAllDir(String directoryPath, boolean isDirectory) {
    List<String> list = new ArrayList<String>();
    // 将其String转换为file文件
    File file = new File(directoryPath);

    // 当前如果目录不存在,或者路径是一个文件,直接返回空列表
    // 此处的路径本身已经写死了,变成一个目录,所以不用在判断是不是文件,或者路径是不是存在

    // 将其文件存储在文件中放置在数组内部
    // 将其每个文件
    File[] files = file.listFiles();

    for (int i = 0;i< files.length; i++) {
        if (files[i].isDirectory()) {
            if (isDirectory) {
                // 一个个对应添加到其list中,也就是绝对路径
                list.add(files[i].getAbsolutePath());
            }
            // 递归嵌套遍历类似谷粒商城的三级分组,或者lc 中的递归算法
            list.addAll(getAllDir(files[i].getAbsolutePath(), isDirectory));
        }
    }
    return list;
}

2.5 显示所有文件

//  http://127.0.0.1:8080/file/getAllFiles
/*
 * 获取当前路径下的所有文件
 * */
@RequestMapping(value = "/getAllFiles", method = RequestMethod.GET)
public Object getAllFiles() {
    Map<String, Object> dataMap = new HashMap<>();
    Map<String, Object> listMap = new HashMap<>();
    try {
        List<Object> list = getAllFilesmethod(uploadFilePath);
        listMap.put("list",list);
        dataMap.put("data", listMap);
        dataMap.put("code", 0);
        dataMap.put("message", "查询成功");
    } catch (Exception e) {
        e.printStackTrace();
        dataMap.put("data", "");
        dataMap.put("code", 500);
        dataMap.put("message", "查询失败");
    }
    log.info(""+dataMap);
    return dataMap;
}

获取所有文件的方法:

public  List<Object> getAllFilesmethod(String directoryPath) {
    List<Object> list = new ArrayList<Object>();
    File file1 = new File(directoryPath);
    File[] files = file1.listFiles();
    for (File file : files) {
        Map<String, String> map = new HashMap<>();
        if (file.isFile()) {
            map.put("fileName", file.getName());
            map.put("filePath", file.getAbsolutePath());
            list.add(map);
        }
    }
    return list;
}

如果想显示文件的上传时间或者是 电脑内部文件的上传时间(通过fs自带的类可以获取)
上传时间的方法如下:new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()

显示文件大小可以通过MultipartFile类中的ile.getSize()获取文件大小
但是这种大小只有数字,转换为B、KB等规格
需要增加一种判断,具体如下:

public String transferfileSize(long fileLen) {
    DecimalFormat df = new DecimalFormat("#.00");
    DecimalFormat d = new DecimalFormat("#");
    String fileSize ;
    if (fileLength < 1024) {
        fileSize = d.format((double) fileLen) + "B";
    } else if (fileLength < 1048576) {
        fileSize = df.format((double) fileLen/ 1024) + "KB";
    } else if (fileLength < 1073741824) {
        fileSize = df.format((double) fileLen/ 1048576) + "MB";
    } else {
        fileSize = df.format((double) fileLen/ 1073741824) + "GB";
    }
    return fileSize;
}

2.6 更改 文件/目录 名

controller代码模块和上面大同小异,区别在于核心方法
此处post出核心模块

// 更改目录
// http://127.0.0.1:8080/file/FixFileName 
// filePath为路径,newFileName为更改的名字
public String modifyFileName(String filePath, String newFileName) {
    File file = new File(filePath);
    // 判断原文件是否存在(防止文件名冲突)
    if (!file.exists()) { 
        return null;
    }
    // 去除前后的空格
    newFileName = newFileName.trim();
    // 文件名为空,则返回null
    if ("".equals(newFileName) || newFileName == null) 
        return null;
    String newFilePath = null;
    // 文件和文件夹直接更改名字,此处的demo为windows系统,如果要将其变换linux,需要增加判断
    newFilePath = filePath.substring(0, filePath.lastIndexOf("\\")) + "\\" + newFileName;
    try {
    	// 修改文件名,内部需要是文件类,所以将其转换
        file.renameTo(new File(newFilePath)); 
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
    return newFilePath;
}

2.7 创建 文件/目录

controller代码模块和上面大同小异,区别在于核心方法
此处post出核心模块

主要思想是,如果创建的这个目录或者文件存在的时候,对应在后面加上(i),通过遍历加上合适的i

// http://127.0.0.1:8080/file/makeDirs  filePath创建绝对路径

/*是否存在这个目录,创建目录
* */
public static boolean ismakeDirs(String path) {
    File file = new File(path);

	int i = 1; // 定义全局变量,主要用来增加i
    try {
        // 如果文件夹不存在且没有这个文件 直接创建
        // 这种情况比较简单
        if (!file.exists() && !file.isDirectory()) {
            file.mkdirs();
            return true;
        }else{
        	//获取最后一个分割符的后缀名
            String name = path.substring(path.lastIndexOf("/")+1,path.length());
            while(file.exists()) {
            	// 创建文件:上级目录 + 系统分割符 + 名字 + (i)
                file = new File(file.getParent()+ File.separator+name+"("+i+")");
                i++;
            }
            file.mkdirs();
            return true;
        }
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

3. 实战开发

思路:将其文件通过s3,获取到上传链接以及下载链接,之后将对应的信息都放置在mongo数据库中存储(比如文件名、上传时间、上传链接、下载链接等),之后通过对应操作进行增删改查

代码:以下代码只是提供思路(部分代码有些耦合,没有定义多一个mapper类),对接aws s3代码没post出来

三个注意点规范:

  1. 使用云服务aws s3的文件传输(对应服务器的传输需看好规范文档)
    对接服务器这块内容使用的是微服务项目的接口(各个项目独立分离,而且每个厂商服务器对接文档就不一样),以下实战中会有所省略这部分内容
  2. 数据存储到mongo数据库,可看我这篇文章:
    云服务器下载安装mongo数据库并远程连接详细图文版本(全)
    Java关于MongoTemplate的增删改查实战代码解析(全)
  3. 前端form表单的传输规范字段值(可以获取拿到什么字段以及传输显示什么字段等)

定义一个接口类:

public interface FileUploadService {

以及接口实现类:

@Slf4j
@Service
public class FileUploadServiceImpl implements FileUploadService {

3.1 上传文件

此处的上传可以通过任意位置,上传到字节流,在上传到服务器(上面的本地测试是写死某个路径)

ResultGeneralModel这个类,是自个封装的json格式返回结果(此处就不post出这个实体类)
类似的可以通过上面json或者定义一个map集合封装json格式仿照下

//todo 第一个接口为上传接口
// http://127.0.0.1:10000/file/upload
@RequestMapping(method = RequestMethod.POST, value =  "file/upload")
public ResultGeneralModel<String> fileUpload(@RequestParam("file") MultipartFile file) {

    LinkedList<String> list = fileUploadService.httpUpload(file);
    String getUploadUrl = list.getFirst();
    String getDownloadUrl = list.getLast();
    mongoService.uploadmongo(file.getOriginalFilename(), file.getSize(), getUploadUrl, getDownloadUrl);

    return ResultGeneralModel.newSuccess("成功上传");
}

httpUpload的核心方法如下:

@Override
public LinkedList<String> httpUpload(MultipartFile file) {
    String getUploadUrl;
    LinkedList<String> list = new LinkedList<>();
    SdkFileInfo sdkFileInfo = new SdkFileInfo();
    
    // 定义了一个sdkFileInfo的实体类,appid也可写死 也可通过form表单的传输引入
    // 此处post出思路引导
    int appid = sdkFileInfo.getAppid();
    
    try {
        list = uploadS3(appid, file ,sdkFileInfo);
        getUploadUrl = list.getFirst();
        if (null == getUploadUrl) {
            return null;
        }
        logger.info("file upload success");
    } catch (Exception e) {
        logger.error("file error");
    }
    return list;
}

具体上传到服务器的uploadS3的函数如下:
通过将其文件变成字节流在传输到服务器,之后获取其相关信息返回

public LinkedList<String> uploadS3(int appId, MultipartFile file, SdkFileInfo sdkFileInfo) {
    LinkedList<String> list = new LinkedList<>();
    // 获取s3的相关信息
    S3Info info = getS3Info(appId, file);
    // 判断
    list.add(info.getUpload_url());
    logger.info(info.getUpload_url());
    if (null == info) {
        logger.error("get s3 info error, user info:" + sdkFileInfo);
        return null;
    }
    byte[] bytes;
    try {
    	// 转换为字节流
        bytes = file.getBytes();
    } catch (IOException e) {
        logger.error("read file bytes fail, user info:" + sdkFileInfo, e);
        return null;
    }

	// 之后通过putUploadFile的方法进行传输,此处对应s3服务器的对接文档
	// 转换字节流对应传输,此方法没有post出来
    if (!httpService.putUploadFile(info.getUpload_url(), bytes)) {
        logger.error("upload file error, user info:" + sdkFileInfo);
        return null;
    }
    list.add(info.getDownload_url());
    logger.info(info.getDownload_url());
    return list;
}

具体获取S3的相关信息,首先需要账号密码以及字段值的配对

private S3Info getS3Info(int appId, MultipartFile file) {
	// 获取S3的相关信息(通过注解引入commonConfig 之后getS3Url获取url属性)
    String url = commonConfig.getS3Url();
    Map<String, String> params = new HashMap<>();
    // 对应传输进入属性
    params.put("product", "xxx");
    params.put("expire", String.valueOf(appId));

	// 获取其后缀,如果有后缀则添加进入,下载链接会有后缀,直接下载文件
    String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")+1);
    if(suffix != null){
        params.put("suffix",suffix);
    }

	// 判断url的参数结果
	// 通过注解引入httpService
	// 此处的代码需参考s3,同样也不post出代码
    String resultString = httpService.get(url, params);
    String dataString = httpService.httpResultCheck(url, params, resultString);
    return null != dataString ? JSON.parseObject(dataString, S3Info.class) : null;
}

对应的数据库那一块的处理,通过uploadmongo函数,该函数如下:

@Override
public void uploadmongo(String fileName, Long fileSize, String getUploadUrl, String getDownloadUrl){
    SdkInfo sdkInfo = new SdkInfo();
    sdkInfo.setCreateTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
    sdkInfo.setFileSize(fileSize);
    sdkInfo.setFileName(fileName);
    sdkInfo.setOperatorName("码农研究僧");
    sdkInfo.setUploadUrl(getUploadUrl);
    sdkInfo.setDownloadUrl(getDownloadUrl);
    SdkInfo insert = mongoTemplate.insert(sdkInfo);

    if(String.valueOf(insert) != null){
        logger.info(String.valueOf(insert));
    }else{
        logger.info("Failed to insert data");
    }

}

3.2 下载文件

思路:由于mongo的数据库以及存储了下载链接,通过查询mongo的数据库,之后对应的链接进行重定向即可下载

controller类:

// http://127.0.0.1:10000/file/download
@RequestMapping(value = "file/download" , method = RequestMethod.GET)
public void fileDownLoad(@RequestParam("_id") String id, HttpServletResponse response){
    String getDownloadUrl = mongoService.findmongo(id);
    try {
    	// 链接的重定向,使得直接条状下载
        response.sendRedirect(getDownloadUrl);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

接口:public String findmongo(String id);
接口实现类:

@Override
public String findmongo(String id){
	// 通过每一行的关键信息查找对应某一行
    Query query = new Query(Criteria.where("id").is(id));
    List<SdkInfo> configs = mongoTemplate.find(query, SdkInfo.class);
	
	// 获取这一行的对应下载数据的链接
    logger.info("文件下载链接:" + configs.get(0).getDownloadUrl());

    return configs.get(0).getDownloadUrl();
}

3.3 删除文件

思路:上传与删除一样,需要对服务器进行操作之后在对数据库进行操作

服务器的操作需要对接aws s3(省略)
删除数据库的代码模块(通过form表单 获取对应的某个字段来或者这一行数据,之后删除这一行数据即可)

@Override
public void deletemongo(String id) {
    Query query = new Query(Criteria.where("_id").is(id));
    mongoTemplate.remove(query, SdkInfo.class);
}

3.4 显示所有文件

对应回显在前端是通过什么字段值,做好规范约束
前端的规范约束是这个:

*接口需要支持的参数:
page: Int // 当前页码
size: Int // 每页条数

*接口返回的字段规范如下:
{
    code:0,
    data:{
        list:[
            {
                id: Int|String,
                ...
            }
        ]
    }
}

对应接口规范,代码为:

private final MongoService mongoService;

// http://127.0.0.1:10000/file/getAllFiles
@RequestMapping(value =  "file/getAllFiles" , method = RequestMethod.GET)
public Object  getAllFiles(@RequestParam("page") int page,
                           @RequestParam("size") int size){
    Map<String, Object> dataMap = new HashMap<>();
    Map<String, Object> listMap = new HashMap<>();
    try {

        List<Object> list = mongoService.showList();
        listMap.put("list",list);
        dataMap.put("data", listMap);
        dataMap.put("code", 0);
        dataMap.put("message", "查询成功");
    } catch (Exception e) {
        e.printStackTrace();
        dataMap.put("data", "");
        dataMap.put("code", 500);
        dataMap.put("message", "查询失败");
    }
    logger.info(""+dataMap);
    return dataMap;
}

书写接口类:

public List<Object> showList();

接口实现类:

// SdkInfo.class是我定义的一个类
@Override
public List<Object> showList() {
    Query query = new Query();
    List<SdkInfo> configs = mongoTemplate.find(query, SdkInfo.class);
    List<Object> list = new ArrayList<Object>();
    configs.forEach(config -> {
        Map<String, String> map = new HashMap<>();
        map.put("_id",config.getId());
        map.put("文件名",config.getFileName());
        map.put("文件大小", config.getFileSize());
        map.put("创建时间", config.getCreateTime());
        map.put("上传用户",config.getOperatorName());
        map.put("下载链接",config.getDownloadUrl());
        list.add(map);
    });
    return list;
}
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-08-06 10:29:56  更:2022-08-06 10:30:23 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 12:58:54-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码