文章目录

  • 前言
  • 一、项目现阶段部分效果演示
  • 二、项目数据库部分表设计
  • 2.1. 创建user表、file表、logs表等sql语句
  • 2.2 FileMapper.xml和FileMapper接口
  • 三、项目前端页面开发
  • 3.1. 前端主要页面html部分源码:
  • 3.2. javascript部分
  • 四、后端接口开发
  • 4.1. 文件工具类
  • 4.2. restful接口(restcontroller类)
  • 4.3. FileService文件服务接口
  • 五、总结
  • 六、更新



前言

因为开发中很多时候都需要上传和下载文件,所以想开发出一个模块,用于文件的上传和下载,自然而然也就想到了网盘,因为是个人开发,所以版本项目应该会持续迭代,优化,此外,技术栈是springboot+mybatis+jquery+bootstrap。以下是第一版本的个人网盘项目介绍,主要介绍开发的过程,以及一些源码的分享。


一、项目现阶段部分效果演示

1.登录(浏览器输入http://localhost:8080/):

Springboot个人网盘源码 springboot网盘项目_List


2.登录成功后进入个人网盘页面(默认加载视频文件列表):

Springboot个人网盘源码 springboot网盘项目_xml_02


3.文件上传:

Springboot个人网盘源码 springboot网盘项目_java_03


4.音视频预览:

Springboot个人网盘源码 springboot网盘项目_java_04


5.图片预览:

Springboot个人网盘源码 springboot网盘项目_List_05


Springboot个人网盘源码 springboot网盘项目_xml_06


6.文档预览,包括office文档的预览功能,支持的格式有(.txt,.xml,.html,.jsp,.java,doc,docx,ppt,xlsx等):

Springboot个人网盘源码 springboot网盘项目_java_07


word文档预览:

Springboot个人网盘源码 springboot网盘项目_Springboot个人网盘源码_08


excel文档预览:

Springboot个人网盘源码 springboot网盘项目_List_09


ppt预览:

Springboot个人网盘源码 springboot网盘项目_java_10


其它文档预览:

Springboot个人网盘源码 springboot网盘项目_xml_11

7.删除,分享,下载等操作:

Springboot个人网盘源码 springboot网盘项目_web_12

二、项目数据库部分表设计

2.1. 创建user表、file表、logs表等sql语句

--  角色表
create table if not exists role(
	role_id int primary key not null auto_increment comment '角色ID',
	role_name varchar(128) unique not null comment '角色名称'
);
 
 -- 用户表
create table if not exists user (
   user_id int primary key not null auto_increment comment '用户ID,主键自增',
   user_name varchar(32) not null unique comment '用户名',
   password varchar(128) not null comment '密码',
   real_name varchar(128) default null comment '真实姓名',
   email varchar(128) default null comment '电子邮箱',
   telephone char(11) default null comment '手机号码',
   introduction varchar(300) default null comment '个人简介',
   address varchar(128) default null comment '住址',
   sex char(2) default '男' not null comment '性别',
   picture varchar(128)	default null comment '用户头像存放路径',
   is_active int default null comment '状态,0:未激活;1:正常;2:禁用',
   disk_size varchar(128) default '10G' comment '网盘大小',
   used_size varchar(128) default '0' comment '已用大小',
   create_time datetime default null comment '创建时间',
   role_id int comment '角色',
   foreign key(role_id) references role(role_id)
);
-- 文件表
create table if not exists file (
	file_id int primary key not null auto_increment comment '文件ID,主键自增',
	file_name varchar(300) default null comment '文件名',
	file_path varchar(300) default null comment '存储路径',
	file_size varchar(200) default null comment '文件大小',
	file_type varchar(120) default null comment '文件类型',
	download_counts integer default null comment '文件下载次数',
	upload_time datetime default null comment '上传时间',
	modify_time datetime default null comment '修改时间',
	is_delete int comment '是否被删除',
	is_folder int comment '是否是文件夹',
	file_md5 varchar(300) default null comment'文件md5',
	parent_id int default null comment '父级目录ID',
	user_id int not null comment '用户id',
	foreign key (user_id) references user(user_id),
	foreign key (parent_id) references file(file_id)
);
-- 分享文件表
create table if not exists sharefiles(
	share_id int primary key not null auto_increment comment '文件分享ID,主键自增',
	share_link varchar(300) not null comment '文件分享链接',
	is_cancel int default null comment '是否取消分享',
	share_time datetime default null comment '分享时间',
	file_id int comment '文件ID',
	foreign key (file_id) references file(file_id)
);
-- 日志表
create table if not exists logs(
	log_id int primary key not null auto_increment comment '日志Id主键自增',
	message varchar(300) default null comment '消息名称',
	operation varchar(300) default null comment '操作',
	operate_time datetime default null comment '操作日期',
	exception varchar(300) default null comment '异常信息',
	log_level varchar(300) default null comment '日志级别',
	user_id int not null comment '用户ID',
	foreign key(user_id) references user(user_id) 
);

2.2 FileMapper.xml和FileMapper接口

FileMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.filemanager.mapper.FileMapper">
	<!--一对一级联,即一个文件对应一个用户 -->
	<resultMap type="File" id="FileMap">
	    <id property="file_id" column="file_id"/>
	    <result property="file_name" column="file_name"/>
	    <result property="file_path" column="file_path"/>
	    <result property="file_size" column="file_size"/>
	    <result property="file_type" column="file_type"/>
	    <result property="download_counts" column="download_counts"/>
	    <result property="upload_time" column="upload_time"/>
	    <result property="is_delete" column="is_delete"/>
	    <!-- 映射User属性 -->
	    <association property="user" javaType="com.filemanager.entity.User">
	        <id property="user_id" column="user_id"/>
	        <result property="user_name" column="user_name"/>
	        <result property="password" column="password"/>
	        <result property="sex" column="sex"/>
	    </association>
	</resultMap>
	
    <select id="countFileNum" resultType="int">
       select count(*) from file
    </select>
    <select id="findFakeDeleteFiles" parameterType="int" resultMap="FileMap">
        select * from file f,user u where u.user_id = #{user_id} and u.user_id = f.user_id and f.is_delete =1
    </select>
    <select id="findAllByUserId" parameterType="int" resultMap="FileMap">
        select * from file f,user u where u.user_id = #{user_id} and u.user_id = f.user_id and f.is_delete =0
    </select>
    <select id="findAllByType" resultMap="FileMap">
    	select * from file f,user u
    	where f.user_id = #{user_id} and f.file_type = #{file_type} and f.user_id = u.user_id and f.is_delete =0
    </select>
    <select id="findById" parameterType="int" resultMap="FileMap">
    	select * from file f,user u
    	where f.file_id =#{file_id} and u.user_id = f.user_id and f.is_delete =0
    </select>
    <select id="getMaxId" resultType="int">
		select max(file_id) from file
    </select>
    <insert id="addFile" parameterType="com.filemanager.entity.File">
    	insert into file(file_id,file_name,file_path,file_size,file_type,download_counts,upload_time,is_delete,user_id)
    	values(#{file_id},#{file_name},#{file_path},#{file_size},#{file_type},#{download_counts},#{upload_time},#{is_delete},#{user.user_id});
    </insert>
    
    <delete id="deleteFile" parameterType="int" >
    	delete from file where file_id = #{file_id}
    </delete>
	
    <update id="fakeDeleteFile" parameterType="int" >
    	update file
    	set 
		is_delete=#{is_delete}
    	where file_id = #{file_id} 
    </update>
	
    <update id="updateFile" parameterType="com.filemanager.entity.File" >
    	update file
    	set 
    	file_id = #{file_id},file_name = #{file_name},file_path = #{file_path},
    	file_path = #{file_path},file_size = #{file_size},file_type = #{file_type},
    	download_counts = #{download_counts},upload_time = #{upload_time},is_delete=#{is_delete},user_id = #{user.user_id}
    	where file_id = #{file_id} 
    </update>
    
</mapper>

FileMapper接口:

/**
 * 
 */
package com.filemanager.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.filemanager.entity.File;
/**
 * @author chenyujie
 * @Time   2020年8月19日下午6:40:26
 */
@Mapper	//指定这是一个操作数据库的mapper,
//使用这个注解之后,可以不再启动类上加上@MapperScan; 当然加上@MapperScan之后,也可以不用这个注解
public interface FileMapper {
	List<File> findFakeDeleteFiles(int user_id);//查询某个用户放入回收站的文件
	List<File> findAllByUserId(int user_id);//查找某个用户的所有文件,不包括放入回收站的文件
	List<File> findAllByType(@Param("user_id")int user_id,@Param("file_type")String file_type);//查询用户某个类型的所有文件,不包括放入回收站的文件
	File findById(int file_id);//根据文件id查找文件,不包括放入回收站的文件
	public int addFile(File file);//增加文件
	public int deleteFile(int file_id);//删除单个文件,真正从数据库中删除
	public int deleteFiles(int user_id);//删除某用户的所有文件,真正从数据库中删除
	public int fakeDeleteFile(int file_id,int is_delete);//将文件设置为已删除或者不删除,等价于放入回收站或者移出回收站,而不是真正的删除
	public int updateFile(File file);//更新文件
	public int getMaxId();
	public int countFileNum();//统计数据库中文件的数量
}

三、项目前端页面开发

3.1. 前端主要页面html部分源码:

<body style="margin-top:30px ; height:500px; overflow:scroll;" >
    <div class="container" style="margin-top:30px ;">
        <div class="row">
            <div class="col-md-2 column">
                <figure class="figure">
                    <img alt="140x140" src="v3/default3.jpg" class="figure-img img-fluid rounded-circle" />
                    <figcaption class="figure-caption text-center">admin</figcaption>
                </figure>
                <ul class="nav nav-pills flex-column" >
                    <li class="nav-item">
                      <a class="nav-link active" href="#" id="video"  value="video" onclick="selectType(this.id)">视频</a>
                    </li>
                    <li class="nav-item">
                      <a class="nav-link" href="#" id="audio"  value="audio" onclick="selectType(this.id)">音频</a>
                    </li>
                    <li class="nav-item">
                      <a class="nav-link" href="#" id="documents"  value="documents" onclick="selectType(this.id)">文档</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#" id="image"  value="image" onclick="selectType(this.id)">图片</a>
                    </li>
                    <li class="nav-item">
                      <a class="nav-link " href="#" id="other"  value="other" onclick="selectType(this.id)">其它</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link " href="#" id="favorites"  value="favorites" onclick="selectType(this.id)">收藏</a>
                    </li>
                    <li class="nav-item">
                    <a class="nav-link " href="#" id="recycle"  value="recycle" onclick="selectType(this.id)">回收站</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link " href="#" id="site_resources"  value="site_resources" onclick="selectType(this.id)">站内资源</a>
                    </li>
                    <li class="nav-item">
                    <a class="nav-link " id="exit"  value="exit" onclick="selectType(this.id)" href="#">退出</a>
                    </li>
                </ul>

            </div>

            <div class="col-md-10 column" id="file_scan_div">
                <h3>我的云盘</h3>
                <!-- 下面部分是模态框预览视频div,放在此处是为了能够更好的再页面显示 -->
                <button type="button" hidden="hidden" class="btn btn-primary" id="myButton" data-toggle="modal" data-target=".bs-example-modal-lg">播放视频</button>
                <div class="modal fade bs-example-modal-lg modal-body" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" id="myModal"  >
                    <div class="modal-dialog modal-lg" role="document">
                        <!-- 模态框头部 -->
                        <div class="modal-header">
                            <h5 class="modal-title text-warning" id="showResourceName">模态框头部</h5>
                            <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
                        </div>
                        <!-- modal content-->
                        <div class="modal-content">
                                <div class="modal-body" width="auto" height="500px" style="padding:0" id="modal-body">
                                    <video id = "videoPlay" width="100%" height="500px" controls="controls"  preload="none"  >
                                        <source src=""  type="video/mp4">
                                        <source src="" type="video/ogg">
                                        <source src="" type="video/webm">
                                            你的浏览器不支持该视频播放!请换过浏览器或者升级!
                                    </video>
    
                                </div>
                        </div>
                    </div>
                </div>
                <!--模态框div--- -->

                <!-- 非模态框部分div -->
                <div id="unmodal_div">
                    <form>
                        <div class="input-group mb-3">
                            <input  type="text" id="lastName" class="form-control" placeholder="输入查询内容/文件链接">
                            <button id="search" class="btn btn-outline-success my-2 my-sm-0" type="button">查询</button>
                            <button onclick="upload()" class="btn btn-outline-success my-2 my-sm-0" type="button">上传</button>
                        </div>  
                        
                        <div id="static_button" class="btn-group" role="group" aria-label="Basic example" style="display: none;">
                            <button type="button" class="btn btn-secondary">分享</button>
                            <button type="button" class="btn btn-secondary" onclick='downloadAllfiles()'>下载</button>
                            <button type="button" class="btn btn-secondary" onclick="deleteAllSelectedFiles()">删除</button>
                        </div>
      
      
                    </form>      
                    <br> 
                    <table id="files_table" class="table table-hover">
                        <thead>
                            <tr>
                                <th>
                                    <div class="custom-control custom-checkbox check_box" >
                                        <input type="checkbox" class="custom-control-input" id="checkboxAll" onchange="selectAll()">
                                        <label class="custom-control-label" for="checkboxAll"></label>
                                    </div>                          
                                </th>
                                <th class="fileName">
                                    文件名
                                </th>
                                <th>
                                    大小
                                </th>
                                <th>
                                    下载次数
                                </th>
                                <th class="date_time">
                                    上传时间
                                </th>
                                <th>
                                    预览
                                </th>
                                <th style="width:150px">
                                    分享/下载/删除
                                </th>
                            </tr>
                        </thead>
                        <tbody></tbody>
                    </table>
                    <div class="row justify-content-center">
                        <div class="align-self-center">
                            <ul class="pagination">
                                <li class="page-item"><a class="page-link" href="#">上一页</a></li>
                                <li class="page-item active"><a class="page-link" href="#">1</a></li>
                                <li class="page-item"><a class="page-link" href="#">2</a></li>
                                <li class="page-item"><a class="page-link" href="#">3</a></li>
                                <li class="page-item"><a class="page-link" href="#">下一页</a></li>
                            </ul>
                        </div>
                    </div>
                </div>




            </div>

            <div id="file_upload_div" style="display: none;" >
                <form  enctype="multipart/form-data">
                    <h3>文件上传:</h3><br>
                    <button id="scanFile" type="button" class="btn btn-info" onclick="selectFile()">浏览</button>
                    <button id="uploadFile" type="button" class="btn btn-info" onclick="uploadFiles()">上传</button><br>
                    <h4>已选文件:</h4><br>
                    <input id="hidden_input" type="file" style="display: none;"/>
                    <table class="table table-hover" id="display_files">
                        <tbody>

                        </tbody>
                    </table>
                </form>
            </div>

        </div>
    </div>
    <div class="jumbotron text-center" style="margin-bottom:0">
        <p>有问题请发邮件至@2023748976@qq.com</p>
    </div>
</body>
</html>

3.2. javascript部分

<script>
    var files;//文件对象
    var type = "video";//左侧栏里面的类型
    var lastLinkId = 'video';//左侧栏里面上个点击的链接id
    //文件删除部分
    function fakeDeleteFile(id){
        var index = parseInt(id.split('_')[2]);
        var file_id = files[index].file_id;
        $.ajax({
            type: "delete",
            url: "http://localhost:8080/api/v1/files/gabage/"+file_id,
            success: function (response) {        
                alert("删除"+files[index].file_name+"成功!");
            }
        });
    }
    //删除所有选中的文件
    function deleteAllSelectedFiles(){
        var files_id = new Array();//此处只能这样定义,然后用Push添加数据
        for(var i =0;i<files.length;i++){
            if(document.getElementById("checkbox"+i).checked == true){
                files_id.push(files[i].file_id);
            }
        }
        console.log(files_id);
        $.ajax({
            type: "delete",
            url: "http://localhost:8080/api/v1/files/gabages/"+files_id,
            traditional:true, //默认false,加入traditional防止深度序列化
            success: function (response) {        
                alert("批量删除成功!");
            }
        });
    }
    //文件下载部分js
    function downloadFile(id){
        var index = parseInt(id.split('_')[2]);
        var file_id = files[index].file_id;
        window.location.href= "http://localhost:8080/api/v1/files/singleFile/"+file_id;
    }
    //下载当前所有文件
    function downloadAllfiles(){
        for(var i =0;i<files.length;i++){
            if(document.getElementById("checkbox"+i).checked == true){
                var file_id = files[i].file_id;
                window.location.href= "http://localhost:8080/api/v1/files/singleFile/"+file_id;
            }
        }
    }
    //初始化页面数据
    $(document).ready(function(){
        searchFiles();
    }); 
    //选左侧边框
    function selectType(id){
        type = $('#'+id).attr('value');

        $('#'+id).attr('class','nav-link active');
        $('#'+lastLinkId).attr('class','nav-link');
        lastLinkId = id;//记录点击的链接id
        showScanFileDiv();//显示浏览文件信息div
        if(type == 'favorites'){//收藏
            searchFiles();
        }else if(type == 'recycle'){//回收站
            searchFiles();
        }else if(type == 'site_resources'){//站内资源
            searchFiles();
        }else if(type == 'exit'){//退出
            searchFiles();
        }else if(type == 'image'){//音视频,文档,图片,其它
            searchImageInfo();
        }else{
            searchFiles();
        }
    }
    //选择全部复选框
    function selectAll(){
        //将所有的下标选上
        for(let i=0;i<files.length;i++){
            // $('#checkbox'+i+'').checked = true;//此句无法生效
            document.getElementById("checkbox"+i).checked = document.getElementById("checkboxAll").checked;
        }
        //使隐藏的三个按钮显现,或者隐藏
        if(document.getElementById("checkboxAll").checked == true)
            $("#static_button").attr("style","display:block;");//显示div;
        else
            $("#static_button").attr("style","display:none;");//隐藏div
    }

    //点击左侧链接后触发查询函数
    function searchFiles(){
        var functionType;
        if(type == 'documents'){
            functionType = 'preViewDocuments(this.id)';
        }else if(type =='video'|| type =='audio'){
            functionType = 'previewVideo(this.id)';
        }else{
            functionType = 'preViewDocuments(this.id)';
            //functionType = 'unPreview()';
        }
        $.ajax({
            type: "get",
            url: "http://localhost:8080/api/v1/files/type?user_id="+1+"&file_type="+type,
            success: function (response) {        
                files = response;
                //先清空表格内容
                $("#files_table tbody").html("");
                //再往table 中添加数据
                let tbody;
                for(let i =0;i<files.length;i++){
                    tbody +="<tr>";
                    tbody += "<td> <div class ='custom-control custom-checkbox'><input type='checkbox' class='custom-control-input' id='"+'checkbox'+i+ "'><label class='custom-control-label' for='"+'checkbox'+i+ "'></label> </div> </td>" + 
                    " <td>"+files[i].file_name+"</td>"+
                    " <td>"+files[i].file_size+"</td>"+
                    " <td>"+files[i].download_counts+"</td>"+
                    " <td>"+dateToString(files[i].upload_time)+"</td>"+
                    " <td>"+"<button id='"+"preview_"+i+"' class='btn btn-link' οnclick='"+functionType+"'>预览</button>"+"</td>"+
                    " <td>"+
                    "<div class='dropdown show'>"+ 
                        "<button  class='btn  btn-link dropdown-toggle' type='button' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>操作</button >" +
                        "<div class='dropdown-menu' aria-labelledby='dropdownMenuLink'>"+
                        "<button  class='dropdown-item' type='button' id='"+'dropdown_share_'+i+"'>分享</button >"+    
                        "<button  class='dropdown-item' type='button' id='"+'dropdown_download_'+i+"' οnclick='downloadFile(this.id)'>下载</button >"+
                        "<button  class='dropdown-item' type='button' id='"+'dropdown_delete_'+i+"' οnclick='fakeDeleteFile(this.id)'>删除</button >"+
                        "</div>"+
                    "</div>"+
                    "</td>";
                }
                $('#files_table tbody').append(tbody);
            }
        });
    }

    //无法预览的类型
    function unPreview() {
        alert('该类型暂时无法预览!');
        return;
    }

    //将date型转换为string 
    function dateToString(date_time){ 
        var datetime = new Date(date_time);
        var year = datetime.getFullYear();
        var month = datetime.getMonth()+1;//js从0开始取 
        var date = datetime.getDate(); 
        var hour = datetime.getHours(); 
        var minutes = datetime.getMinutes(); 
        var second = datetime.getSeconds();

        if(month<10){
            month = "0" + month;
        }
        if(date<10){
            date = "0" + date;
        }
        if(hour <10){
            hour = "0" + hour;
        }
        if(minutes <10){
            minutes = "0" + minutes;
        }
        if(second <10){
            second = "0" + second ;
        }
        var time = year+"-"+month+"-"+date+" "+hour+":"+minutes+":"+second; 
        return time;
    }
    

    //文件上传部分js函数
    var filesArray = [];//存放文件数组
    
    //用于切换div,显示上传页面
    function upload(){
        hideScanFileDiv();
    }
    //隐藏上传div,显示浏览文件div
    function showScanFileDiv(){
        $("#file_upload_div").attr("style","display:none;");//隐藏div
        $("#file_scan_div").attr("style","display:block;");//显示div;
    }
    //显示上传div,隐藏浏览文件div
    function hideScanFileDiv(){
        $("#file_scan_div").attr("style","display:none;");//隐藏div
        $("#file_upload_div").attr("style","display:block;");//显示div;
    }
    function selectFile(){
        //间接调用input = file,目的是改变默认按钮名字
        $('#hidden_input').click();
    }
    //真正的input= file被点击事件
    $("#hidden_input").change(function (even) {
        // 获取input
        var fileName = $(this).val();
        var file = $(this)[0].files[0];
        //是否选择了文件
        if (fileName != '') {
            filesArray.push(file);//保存选择的文件
            // $("#display_files").append("<div><p>" + fileName + "</p></div>");

            $('#display_files tbody').append("<tr><td>"+fileName+"</td></tr>");
        }
    });

    //上传文件
    function uploadFiles(){
        var form = new FormData();
        //获取user信息
        var user = {
            user_id:1,
            user_name:'admin',
            password:'123456',
            sex:'男',
            files:null
        };
        //遍历数据,手动注入formData
        for (var i = 0; i < filesArray.length; i++) {
            let tempFile = filesArray[i];
            if(tempFile != "-1"){//-1表示被删除了
                form.append("files", tempFile);
            }
        }
        form.append("user", JSON.stringify(user));
        $.ajax({
            type: 'POST',
            url: 'http://localhost:8080/api/v1/files/multipleFiles',
            data: form,
            processData: false,     // 告诉jquery要传输data对象,必须设置,保证你提交的数据和后台返回是同步的
            contentType: false,     // 告诉jquery不需要增加请求头对于contentType的设置
            success: function (arg) {
                alert('上传成功!');
                //清空之前的文件选择信息
                $("#display_files tbody").html("");
                filesArray = [];
            }
        })
    }
    //视频预览部分js函数
    //当点击按钮的时候,将video的src更换
    function previewVideo(id){

        $('#unmodal_div').attr("style","display:none;");//隐藏非模态框div

        //根据出入的按钮id获取files数组下标 
        var index = id.split('_');
        var file_id = files[parseInt(index[1])].file_id;
        //清空模态框
        $('#modal-body').html('');
        //添加video控件
        $('#modal-body').append("<video id = 'videoPlay' width='100%'' height='500px' controls='controls'  preload='none' >"+
                                "<source  type='video/mp4'> <source type='video/ogg'> <source  type='video/webm'>"+
                                "你的浏览器不支持该视频播放!请换过浏览器或者升级!</video>");
        $('#videoPlay').attr('src','http://localhost:8080/api/v1/files/videos/'+file_id);
        //显示视频标题
        document.getElementById('showResourceName').innerHTML = '视频名称:'+files[parseInt(index[1])].file_name;
        //触发模态框点击
        $('#myButton').click();
        

    }
    $('#myModal').on('hidden.bs.modal', function (e) {
        //模态框隐藏的时候,暂停视频播放
        var myVideo = document.getElementById("videoPlay");   //获取视频video
        $("#unmodal_div").attr("style","display:block;");//显示非模态框div;
        if(myVideo != null){
            myVideo.pause();
            //$('#myModal').modal('dispose');
        }
    })

    //图片处理js
    //查找图片信息
    function searchImageInfo(){
        $.ajax({
            type: "get",
            url: "http://localhost:8080/api/v1/files/type?user_id="+1+"&file_type="+type,
            success: function (response) {        
                files = response;
                //先清空表格内容
                $("#files_table tbody").html("");
                //再往table 中添加数据
                let tbody;
                for(let i =0;i<files.length;i++){
                    tbody +="<tr>";
                    tbody += "<td> <div class ='custom-control custom-checkbox'><input type='checkbox' class='custom-control-input' id='"+'checkbox'+i+ "'><label class='custom-control-label' for='"+'checkbox'+i+ "'></label> </div> </td>" + 
                    " <td>"+files[i].file_name+"</td>"+
                    " <td>"+files[i].file_size+"</td>"+
                    " <td>"+files[i].download_counts+"</td>"+
                    " <td>"+dateToString(files[i].upload_time)+"</td>"+
                    " <td>"+"<img src='http://localhost:8080/api/v1/files/images/"+files[i].file_id+"' id=' "+"image_"+i+"' οnclick='dealImageClick(this.id)' class='img-fluid' alt='Responsive image'>"+"</td>"+//此处加载图片
                    " <td>"+
                    "<div class='dropdown show'>"+ 
                        "<button  class='btn  btn-link dropdown-toggle' type='button' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>操作</button >" +
                        "<div class='dropdown-menu' aria-labelledby='dropdownMenuLink'>"+
                        "<button  class='dropdown-item' type='button' id='"+'dropdown_share_'+i+"'>分享</button >"+    
                        "<button  class='dropdown-item' type='button' id='"+'dropdown_download_'+i+"' οnclick='downloadFile(this.id)'>下载</button >"+
                        "<button  class='dropdown-item' type='button' id='"+'dropdown_delete_'+i+"' οnclick='fakeDeleteFile(this.id)'>删除</button >"+
                        "</div>"+
                    "</div>"+
                    "</td>";
                }
                $('#files_table tbody').append(tbody);
            }
        });
    }
    //图片点击事件处理
    function dealImageClick(imageId){
        var index  = imageId.split('_')[1];
        $('#modal-body').html('');//清空模态框body中的内容
        $('#unmodal_div').attr("style","display:none;");//隐藏非模态框div
        $('#modal-body').append("<img src='http://localhost:8080/api/v1/files/images/"+files[parseInt(index)].file_id+"' class='img-fluid' width='100%' height='500px' alt='Responsive image'>");
        //显示图片标题
        document.getElementById('showResourceName').innerHTML = '图片名称:'+files[parseInt(index)].file_name;
        //触发模态框点击
        $('#myButton').click();
    }
    //文档显示js函数
    function preViewDocuments(id){
        var index  = id.split('_')[1];
        $('#modal-body').html('');//清空模态框body中的内容
        var file_id = files[parseInt(index)].file_id;
        var documentString ='';
        $.ajax({
            type: "GET",
            url: "http://localhost:8080/api/v1/files/text/"+file_id,
            dataType: "text",
            success: function (response) {     
                documentString = response;
                if(documentString == 'can not preview'){//文件类型无法预览
                    unPreview();
                }else{
                    $('#unmodal_div').attr("style","display:none;");//隐藏非模态框div
                    //下面模态框的设置必须放在下面,不然会失效,因为ajax是异步发送消息的
                    $('#modal-body').append("<div class='text-light bg-dark' style='height:500px; overflow:scroll;' <textarea id='textcontent'>>"+"</textarea></div>");
                    document.getElementById('textcontent').innerText = ""+documentString;
                    //显示文档标题
                    document.getElementById('showResourceName').innerHTML = '文档名称:'+files[parseInt(index)].file_name;
                    //触发模态框点击
                    $('#myButton').click();
                }
            }
        })
    }
    </script>

四、后端接口开发

4.1. 文件工具类

public class FileTools {

    /**
     * 存储文件到系统
     * @param file 文件,path 文件路径
     * @return 文件名
     */
    public static String storeFile(MultipartFile file,String path) {
        String fileName = StringUtils.cleanPath(file.getOriginalFilename());
        Path fileStorageLocation = Paths.get(path).toAbsolutePath().normalize();
        try {
            if(fileName.contains("..")) {
                throw new FileException("文件路径出错...." + fileName);
            }
            Path targetLocation = fileStorageLocation.resolve(fileName);
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
            return fileName;
        } catch (IOException ex) {
            throw new FileException("存储文件失败 ...." + fileName + "请重试", ex);
        }
    }
    /**
     * 加载文件
     * @param path 文件路径
     * @return 文件资源
     */
    public static Resource loadFileAsResource(String file_name) {
        try {
            Path filePath = Paths.get(file_name).toAbsolutePath().normalize();
            String fileName = filePath.getFileName().toString();
            Resource resource = new UrlResource(filePath.toUri());
            if(resource.exists()) {
                return resource;
            } else {
                throw new FileException("文件不存在 " + fileName);
            }
        } catch (MalformedURLException  ex) {
            throw new FileException("文件找不到 ...." + ex);
        }
    }
    /**
     * 根据文件后缀判断文件类型
     * @param 文件名
     * @return 该文件所属类型
     */
    public static String transforType(String fileName) {
		if(fileName == null){
			fileName = "emptyFileName";
			return fileName;
		}else {
		    //获取文件后缀名并转化为小写,用于后续比较
			String fileType = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length()).toLowerCase();
			//创建图片类型数组
			String img[] = { "bmp", "jpg", "jpeg", "png", "tiff", "gif", "pcx", "tga", "exif", "fpx", "svg", "psd",
			"cdr", "pcd", "dxf", "ufo", "eps", "ai", "raw", "wmf" };
			for(int i = 0; i < img.length; i++){
				if (img[i].equals(fileType)) {
					return "image";
				}
			}
			//创建文档类型数组
			String document[] = { "txt", "doc", "docx", "xls","xlsx", "htm", "html", "jsp", "rtf", "wpd", "pdf", "ppt"};
			for (int i = 0; i < document.length; i++) {
				if (document[i].equals(fileType)) {
					return "documents";
				}
			}
			// 创建视频类型数组
			String video[] = { "mp4", "avi", "mov", "wmv", "asf", "navi", "3gp", "mkv", "f4v", "rmvb", "webm" };
			for (int i = 0; i < video.length; i++) {
				if (video[i].equals(fileType)) {
					return "video";
				}
			}
			// 创建音频类型数组
			String music[] = { "mp3", "wma", "wav", "mod", "ra", "cd", "md", "asf", "aac", "vqf", "ape", "mid", "ogg",
			"m4a", "vqf" };
			for (int i = 0; i < music.length; i++) {
				if (music[i].equals(fileType)) {
					return "audio";
				}
			}
		}
		return "other";
    }
    /**
     * 根据文件字节大小,转换成相应的B,KB,MB,GB
     * @param 文件的字节大小
     * @return 转换单位后的文件大小
     */
    public static String transforSize(long size) {
        //获取到的size bites,表示字节大小
        int GB = 1024 * 1024 * 1024;//定义GB的计算常量
        int MB = 1024 * 1024;//定义MB的计算常量
        int KB = 1024;//定义KB的计算常量
        DecimalFormat df = new DecimalFormat("0.00");//格式化小数
        String resultSize = "";
        if (size / GB >= 1) {
            //如果当前Byte的值大于等于1GB
            resultSize = df.format(size / (float) GB) + "GB";
        } else if (size / MB >= 1) {
            //如果当前Byte的值大于等于1MB
            resultSize = df.format(size / (float) MB) + "MB";
        } else if (size / KB >= 1) {
            //如果当前Byte的值大于等于1KB
            resultSize = df.format(size / (float) KB) + "KB";
        } else {
            resultSize = size + "B";
        }
        return resultSize;
    }
    /**
     * 读取文本文件,返回一个字符串
     * @param 文件路径
     * @return 文本内容
     */
    public static String readText(String fileName) {
		StringBuffer stringList = new StringBuffer();
		System.out.println(fileName);
        try {
        	File file = new File(fileName);
    		if (!file.exists()) {
    			System.out.println("该文件不存在!");
    			return stringList.toString();
    		}
        	InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8");
        	BufferedReader in = new BufferedReader(isr); 
            String str;
            while ((str = in.readLine()) != null) {
            	//str = str.replace(" ", " ");
                stringList.append(str+"\n");//为了在前端正常显示,加上换行和空格转换符号,如果需要在后端显示,则去掉,注意:前端如果用innerText
                									//显示的话,则不需要在后端加
            }
            in.close();
        } catch (IOException e) {
        	System.out.println("文件为空,读取出错");
        }
        return stringList.toString();
    }
    
    /**
     * 从文件中按行读取,返回List<String>
     * @param 文件路径
     * @return 文本内容List<String>
     */
	public static List<String> readStringList(String fileName){
		List<String> stringList = new ArrayList<String>();
        try {
        	File file = new File(fileName);
    		if (!file.exists()) {
    			System.out.println("该文件不存在!");
    			return stringList;
    		}
        	InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8");
        	BufferedReader in = new BufferedReader(isr); 
            String str;
            while ((str = in.readLine()) != null) {
                stringList.add(str);
            }
            in.close();
        } catch (IOException e) {
        	System.out.println("文件为空,读取出错");
        }
        return stringList;
	}
	
    /**
     * 判断哪些是可以预览,也就是读取的文本文件
     * @param 文件路径
     * @return 是否可以读取
     */
	public static boolean canPreView(String fileName) {
	    //获取文件后缀名并转化为小写,用于后续比较
		String fileType = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length()).toLowerCase();
		// 创建可以预览文本类型数组
		String canPreviewArray[] = { "txt", "java", "xml", "py","lrc","sql","html","jsp"};
		for (int i = 0; i < canPreviewArray.length; i++) {
			if (canPreviewArray[i].equals(fileType)) {
				return true;
			}
		}
		return false;
	}
}

4.2. restful接口(restcontroller类)

@RestController
@CrossOrigin
@RequestMapping("/api/v1/files")
public class FileRestController {
    private static final Logger logger = LoggerFactory.getLogger(FileRestController.class);
    private static String filePath = "D:/aplus";
    @Autowired
    private FileService fileService;
    
    /**
     * 根据用户id查询所有文件
     * @param 用户id
     * @return 该用户所有的文件信息
     */
    @RequestMapping(value="/{user_id}", method = RequestMethod.GET, produces = "application/json")
    public List<File> findAll(@PathVariable int user_id){
        return fileService.findAllByUserId(user_id);
    }
    /**
     * 根据用户id和文件的类型查询所有文件
     * @param 用户id,文件类型 file_type
     * @return 此类型所有文件信息
     */
    @RequestMapping(value="/type", method = RequestMethod.GET, produces = "application/json")
    public List<File> find1(int user_id,String file_type){
    	
//    	List<File> files = fileService.findAllByType(user_id, file_type);
    	//转换日期格式,实际考虑,此处也可不转换,而是在前端页面显示的时候再转换,另外,存入数据库中的时候也可以转换后再存,即将数据库中字段改成字符串,而不是Date,可以节省查询时间
//        for(int i=0;i<files.size();i++) {
//        	files.get(i).setUpload_time(TimeTransforTools.transforDateToString(files.get(i).getUpload_time()););
//        }
    	return fileService.findAllByType(user_id, file_type);
    }
    
    /**
     * 单个文件上传,接收前台上传的文件和用户信息
     * @param file 文件,user 用户信息字符串
     * @return 上传后的文件信息
     */
    @RequestMapping(value = "/singleFile", method = RequestMethod.POST, produces = "application/json")
    public File uploadSingleFile(@RequestParam("file") MultipartFile file,String user){
    	JSONObject jsonObjects =  JSONObject.fromObject(user.toString());//java 转json
    	User tempUser = (User) JSONObject.toBean(jsonObjects,User.class);//json转换成java对象
    	//将文件存入系统中
    	String fileName = FileTools.storeFile(file,filePath);
    	//根据文件名后缀,划分文件类型
    	String file_type = FileTools.transforType(fileName);
    	//将字节为单位的文件大小转换成合适的单位,比如B,KB,MB,GB
    	String file_size = FileTools.transforSize(file.getSize());
        //生成file对象,存入数据库中
        File tempFile = new File(fileService.getMaxId()+1, 
        		file.getOriginalFilename(), filePath+"/"+file.getOriginalFilename(),
        		file_size, file_type, 0, new Date(),0,tempUser);
        fileService.addFile(tempFile);
        return tempFile;
    }
    
    /**
     * 多个文件上传,接收前台上传的文件和用户信息
     * @param files 文件数组,user 用户信息字符串
     * @return 上传后的所有文件信息
     */
    @RequestMapping(value = "/multipleFiles", method = RequestMethod.POST, produces = "application/json")
    public List<File> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files,String user) {
       List<File> list = new ArrayList<>();
       if (files != null) {
    	   for(int i=0;i<files.length;i++) {
        	   File uploadFileResponse = uploadSingleFile(files[i],user);
               list.add(uploadFileResponse);
    	   }
       }
       return list;
    }
    /**
     * 单个文件下载
     * @param 文件id
     * @return 文件资源
     * @throws IOException 
     */
    @RequestMapping(value = "/singleFile/{file_id}", method = RequestMethod.GET, produces = "application/json")
    public ResponseEntity<Resource> downloadSingleFile(@PathVariable int file_id, HttpServletRequest request) throws IOException {
    	String fileName = fileService.findById(file_id).getFile_path();
        Resource resource = FileTools.loadFileAsResource(fileName);
        String contentType = null;
        try {
            request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
        } catch (IOException e) {
            logger.info("Could not determine file type.");
        }
        if(contentType == null) {
            contentType = "application/octet-stream";
        }
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType))
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" +resource.getFilename()+ "\"")
                .body(resource);
    }
    
    /**
     * 获取文本文件内容,并且返回字符串
     * @param 
     * @return 上传后的所有文件信息
     */
    @RequestMapping(value = "/text/{file_id}", method = RequestMethod.GET)
    public String getTextContent(@PathVariable int file_id) {
    	File tempFile =fileService.findById(file_id);
    	String file_path = tempFile.getFile_path();
    	if(FileTools.canPreView(file_path)) {
    		return FileTools.readText(file_path);
    	}
    	return "can not preview";
    }
    
    /**
     * 将文件放入回收站,即不是真正的删除
     * @param 文件id
     * @return 
     */
    @RequestMapping(value = "/gabage/{file_id}", method = RequestMethod.DELETE)
    public int fakeDeleteFile(@PathVariable int file_id) {
    	
    	return fileService.fakeDeleteFile(file_id, 1);
    }
    /**
     * 将文件批量放入回收站,即不是真正的删除
     * @param 文件id数组
     * @return 
     */
    @RequestMapping(value = "/gabages/{files_id}", method = RequestMethod.DELETE)
    public int fakeDeleteFiles(@PathVariable int[] files_id) {
    	for(int i=0;i<files_id.length;i++) {
    		fileService.fakeDeleteFile(files_id[i], 1);
    	}
    	return 1;
    }
    
    /**
     * 将文件拿出回收站
     * @param 文件id
     * @return 上传后的所有文件信息
     */
    @RequestMapping(value = "/gabage/{file_id}", method = RequestMethod.PUT)
    public int cancelDeleteFile(@PathVariable int file_id) {
    	
    	return fileService.fakeDeleteFile(file_id, 0);
    }
    
    /**
     * 将文件批量拿出回收站
     * @param 文件id数组
     * @return 上传后的所有文件信息
     */
    @RequestMapping(value = "/gabages/{files_id}", method = RequestMethod.PUT)
    public int cancelDeleteFiles(@PathVariable int[] files_id) {
    	for(int i=0;i<files_id.length;i++) {
    		fileService.fakeDeleteFile(files_id[i], 0);
    	}
    	return 1;
    }
    
    /**
     * 真正删除文件
     * @param 
     * @return 
     */
    @RequestMapping(value = "/{file_id}", method = RequestMethod.DELETE)
    public int realDeleteFile(@PathVariable int file_id) {
    	
    	return fileService.deleteFile(file_id);
    }
    
    /**
     * 真正批量删除文件
     * @param 
     * @return 
     */
    @RequestMapping(value = "/array/{files_id}", method = RequestMethod.DELETE)
    public int realDeleteFiles(@PathVariable int[] files_id) {
    	for(int i=0;i<files_id.length;i++) {
    		fileService.deleteFile(files_id[i]);
    	}
    	return 1;
    }    
}

4.3. FileService文件服务接口

public interface FileService {
	List<File> findFakeDeleteFiles(int user_id);//查询某个用户放入回收站的文件
	List<File> findAllByUserId(int user_id);//查找某个用户的所有文件,不包括放入回收站的文件
	List<File> findAllByType(@Param("user_id")int user_id,@Param("file_type")String file_type);//查询用户某个类型的所有文件,不包括放入回收站的文件
	File findById(int file_id);//根据文件id查找文件,不包括放入回收站的文件
	public int addFile(File file);//增加文件
	public int deleteFile(int file_id);//删除单个文件,真正从数据库中删除
	public int deleteFiles(int user_id);//删除某用户的所有文件,真正从数据库中删除
	public int fakeDeleteFile(int file_id,int is_delete);//将文件设置为已删除或者不删除,等价于放入回收站或者移出回收站,而不是真正的删除
	public int updateFile(File file);//更新文件
	public int getMaxId();
}

五、总结

这次花了四五天时间开发这个第一版网盘,考虑下次迭代将站内资源模块引入,然后可以接入各种资源模块,同时优化界面,并且实现offce文档的预览。代码届时会上传至github,以上就是个人网盘的开发流程。

六、更新


2020.9.5
更新文件存储方式:按照用户名创建子目录
考虑到之前文件都是存放在统一文件夹下,当文件数量过大的时候,会导致检索速度变得非常慢,所以创建子集目录可以加快检索速度。


2020.9.10
新增功能点:文件分享链接生成
首先将用户的id等参数采用MD5加密或者其它加密,然后生成一个参数追加到链接后面,同时设置提取密码,也可以设置不要提取密码,如果不设置提取密码,该文件默认共享到站内资源,数据库文件表中需要加上文件分享的加密后参数,因为MD5是不可逆的,所以不会暴露出用户信息,其次,当别人输入链接的时候,再输入正确的密码,则可以正确的提取该文件资源。


2020.9.15
新增功能点:能够预览office(doc,docx,xls,xlsx,ppt)文件
在后端将对应的office文档转换成pdf格式,然后将pdf格式文件流传递到前端页面,页面通过pdf.js显示pdf文件流内容