Java实现大文件分片上传,并暂存到Mysql数据库

本案例用springboot实现了大文件分片上传、上传完成后自动将文件合成为大文件的相关功能
前后端代码以及sql都在下面贴出来了

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>分片上传</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
分片上传
 
<form enctype="multipart/form-data">
    <input type="file" name="fileInput" id="fileInput">
    <input type="button" value="计算文件MD5" onclick="calculateFileMD5()">
    <input type="button" value="上传" onclick="uploadFile()">
    <input type="button" value="检测文件完整性" onclick="checkFile()">
</form>
 
<p>
    文件MD5:
    <span id="fileMd5"></span>
</p>
<p>
    上传结果:
    <span id="uploadResult"></span>
</p>
<p>
    检测文件完整性:
    <span id="checkFileRes"></span>
</p>
 
 
<script>
    //每片的大小
    var chunkSize = 1 * 1024 * 1024;// 1兆
    var uploadResult = document.getElementById("uploadResult")
    var fileMd5Span = document.getElementById("fileMd5")
    var checkFileRes = document.getElementById("checkFileRes")
    var  fileMd5;
 
 
    function  calculateFileMD5(){
        var fileInput = document.getElementById('fileInput');
        var file = fileInput.files[0];
        getFileMd5(file).then((md5) => {
            console.info(md5)
            fileMd5=md5;
            fileMd5Span.innerHTML=md5;
        })
    }
 
    function uploadFile() {
        var fileInput = document.getElementById('fileInput');
        var file = fileInput.files[0];
        console.log(file);
        if (!file) return;
        // if (!fileMd5) return;
 
 
        //获取到文件
        let fileArr = this.sliceFile(file);
        //保存文件名称
        let fileName = file.name;
 
        fileArr.forEach((e, i) => {
            //创建formdata对象
            let data = new FormData();
            data.append("fileName", fileName)
            data.append("chunks", fileArr.length)
            data.append("chunkSize", chunkSize)
            data.append("chunk", i)
            data.append("md5", fileMd5)
            data.append("file", new File([e],fileName));
            upload(data);
        })
 
 
    }
 
    /**
     * 计算文件md5值
     */
    function getFileMd5(file) {
        return new Promise((resolve, reject) => {
            let fileReader = new FileReader()
            fileReader.onload = function (event) {
                let fileMd5 = SparkMD5.ArrayBuffer.hash(event.target.result)
                resolve(fileMd5)
            }
            fileReader.readAsArrayBuffer(file)
        })
    }
 
 
   function upload(data) {
       var xhr = new XMLHttpRequest();
       // 当上传完成时调用
       xhr.onload = function () {
           if (xhr.status === 200) {
               uploadResult.append( '上传成功分片:' +data.get("chunkNumber")+'\t' ) ;
           }
       }
       xhr.onerror = function () {
           uploadResult.innerHTML = '上传失败';
       }
       // 发送请求
       xhr.open('POST', 'http://127.0.0.1:9088/sliceUpload', true);
       xhr.send(data);
    }
 
    function checkFile() {
        var xhr = new XMLHttpRequest();
        // 当上传完成时调用
        xhr.onload = function () {
            if (xhr.status === 200) {
                checkFileRes.innerHTML = '检测文件完整性成功:' + xhr.responseText;
            }
        }
        xhr.onerror = function () {
            checkFileRes.innerHTML = '检测文件完整性失败';
        }
        // 发送请求
        xhr.open('POST', 'http://127.0.0.1:9088/checkFile', true);
        let data = new FormData();
        data.append("md5", fileMd5)
        xhr.send(data);
    }
 
    function sliceFile(file) {
        const chunks = [];
        let start = 0;
        let end;
        while (start < file.size) {
            end = Math.min(start + chunkSize, file.size);
            chunks.push(file.slice(start, end));
            start = end;
        }
        return chunks;
    }
 
</script>
 
</body>
</html>

相关数据库表

CREATE TABLE `gz_upload_file` (
  `id` bigint(20) NOT NULL,
  `state` int(1) DEFAULT '0' COMMENT '上传状态:0-上传未完成;1-已上传;2-已上传ftp;',
  `file_name` varchar(50) DEFAULT NULL COMMENT '文件名称',
  `file_path` varchar(255) DEFAULT NULL COMMENT '上传本地路径',
  `chunks` int(10) DEFAULT NULL COMMENT '分片总数',
  `chunk_size` bigint(20) DEFAULT NULL COMMENT '分片大小',
  `md5` varchar(50) DEFAULT NULL COMMENT 'MD ',
  `created_date` datetime DEFAULT NULL,
  `deleted` varchar(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文件上传任务表';
//
CREATE TABLE `gz_file_slice` (
  `id` bigint(20) DEFAULT NULL,
  `md5` varchar(50) DEFAULT NULL COMMENT '文件md5值',
  `state` int(1) DEFAULT NULL COMMENT '分片上传结果:0-未完成;1-已完成;',
  `chunk` int(10) DEFAULT NULL COMMENT '分片序号',
  `size` bigint(20) DEFAULT NULL COMMENT '当前分片的大小',
  `bytes` longblob COMMENT '分片文件字节数组',
  `created_date` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文件分片存储表';

java代码
这里实现了两种将分片合成为目标文件的方式

  • 本地存储
  • 将文件上传到ftp服务器

Maven依赖包

        <!-- https://mvnrepository.com/artifact/commons-net/commons-net -->
        <dependency>
            <groupId>commons-net</groupId>
            <artifactId>commons-net</artifactId>
            <version>3.9.0</version>
        </dependency>
package com.za.edu.service;

import cn.hutool.core.util.CharsetUtil;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import com.sun.jndi.toolkit.url.UrlUtil;
import com.za.edu.base.BaseBean;
import com.za.edu.bean.FileSlice;
import com.za.edu.bean.UploadFile;
import com.za.edu.dto.FileUploadDTO;
import com.za.edu.enums.UploadStatus;
import com.za.edu.exception.BusinessException;
import com.za.edu.mapper.FileSliceMapper;
import com.za.edu.mapper.UploadFileMapper;
import com.za.edu.sandbox.TenantContext;
import com.za.edu.utils.ExceptionUtil;
import com.za.edu.utils.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Slf4j
@Service
public class FileUploadService {

    // 全局变量来记录已上传的分片序号
    private static final ConcurrentHashMap<String, Integer> uploadChunksMap = new ConcurrentHashMap<>();

    @Resource
    private UploadFileMapper uploadFileMapper;
    @Resource
    private FileSliceMapper fileSliceMapper;
    @Resource
    FTPPoolService ftpPoolService;

    @Transactional
    public void sliceUpload(FileUploadDTO uploadDTO) throws IOException {
        ExceptionUtil.isNull(uploadDTO.getFile(), "文件不能为空");
        if(uploadDTO.getChunk() == 0){
        	//保存文件上传任务
            UploadFile uploadFile = uploadFileMapper.queryByMd5(uploadDTO.getMd5());
            if(uploadFile == null){
                uploadFile = new UploadFile();
                BeanUtils.copyProperties(uploadDTO, uploadFile);
                uploadFile.setId(UploadFile.idGenerator.nextId());
                uploadFile.setFileName(uploadDTO.getFileName());
                uploadFile.setFilePath(uploadDTO.getFilePath());
                uploadFileMapper.add(uploadFile);
            }
        }
		//保存分片
        FileSlice fileSlice = new FileSlice();
        fileSlice.setId(UploadFile.idGenerator.nextId());
        fileSlice.setMd5(uploadDTO.getMd5());
        fileSlice.setState(BaseBean.TRUE);
        fileSlice.setSize(uploadDTO.getSize());
        fileSlice.setChunk(uploadDTO.getChunk());
        fileSlice.setBytes(uploadDTO.getFile().getBytes());
        fileSliceMapper.add(fileSlice);
		//线程变量中保存已上传的分片序号
        uploadChunksMap.put(uploadDTO.getMd5()+"_"+fileSlice.getChunk(), fileSlice.getChunk());
		//当完成了90%的分片上传后,开启是否完成上传任务的判断
        if(fileSlice.getChunk() > (uploadDTO.getChunks()*0.9)){
            synchronized (uploadDTO.getMd5()){
                List<String> chunks = Lists.newArrayList(uploadChunksMap.keySet());
                System.out.println("当前已完成了"+chunks.size()+"分片上传");
                System.out.println("当前已完成了"+chunks.size()+"分片上传");
                if(chunks.size() == uploadDTO.getChunks()){
                    System.out.println("共计"+chunks.size()+"分片,已上传完成");
                    new Thread(() -> {
                        List<FileSlice> fileSlices = fileSliceMapper.queryByMd5(uploadDTO.getMd5());
                        /**   将文件写入本地  **/
                        File mergeFile = new File("G:\\Downloads\\"+uploadDTO.getFileName());
                        try(RandomAccessFile randomAccessFileWriter = new RandomAccessFile(mergeFile, "rw")){
                        for (FileSlice chunkFile : fileSlices) {
                            //单个字节的写入
    //                        for(byte b : chunkFile.getBytes()){
    //                            randomAccessFileWriter.write(b);
    //                        }
                            //批量字节的写入
                            randomAccessFileWriter.write(chunkFile.getBytes(), 0, chunkFile.getBytes().length);
                        }
                        }catch (Exception err){
                            log.error("", err);
                        }
                        /**   将文件写入ftp  **/
//                            FTPClient ftpClient = ftpPoolService.borrowObject();
//                            String fName = uploadDTO.getFileName();
//                            try {
//                                final String ffName = new String(fName.getBytes("GBK"), StandardCharsets.UTF_8);
//                                // 设置PassiveMode传输
//                                ftpClient.enterLocalPassiveMode();
//                                // 设置以二进制流的方式传输
//                                ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
//                                ftpClient.setControlEncoding(CharsetUtil.GBK);
//                                // 检查远程是否存在文件
//                                String uploadPath = "ftp";
//                                //切换到工作目录
//                                if (!ftpClient.changeWorkingDirectory(uploadPath)) {
//                                    ftpClient.makeDirectory(uploadPath);
//                                    ftpClient.changeWorkingDirectory(uploadPath);
//                                }
//                                for (FileSlice chunkFile : fileSlices) {
//                                    System.out.println("==============-----------------------------==================");
//                                    if(chunkFile.getChunk() == 0){
//                                        ftpClient.storeFile(ffName, new ByteArrayInputStream(chunkFile.getBytes(), 0, chunkFile.getBytes().length));
//                                        Thread.sleep(200);
//                                    }else{
//                                        ftpClient.appendFile(ffName, new ByteArrayInputStream(chunkFile.getBytes(), 0, chunkFile.getBytes().length));
//                                        Thread.sleep(200);
//                                    }
//                                    System.out.println("已完成了向FTP服务器上传第"+chunkFile.getChunk()+"分片数据");
//                                }
//
//                            }catch (Exception err){
//                                log.error("", err);
//                                throw new BusinessException(err.getMessage());
//                            }
                        //清空
                        fileSliceMapper.deleteByMd5(uploadDTO.getMd5());
                    }).start();
                }
            }
        }
    }
}
简单的购物车系统 购物车系统的整体介绍: 购物车系统是一个常见的电子商务功能模块,允许用户浏览商品、选择商品管理购物车中的商品,最终完成结算。让用户能暂存商品,对比后统一结算,模拟线下购物,使线上购物更便捷流畅。用户将商品加入购物车后可能会因凑单等增加购买量,还可利用购物车进行促销,如满减等,刺激消费。 购物车系统的功能介绍: 1. 商品管理功能 显示所有商品的列表,包括商品名称、价格、库存等信息。 支持按分类、价格区间或关键词搜索商品。 商品详情:用户可以查看单个商品的详细信息,如描述、规格、图片等。 2. 购物车功能 用户可以将商品添加到购物车。 添加时需检查商品库存,库存不足时提示用户。 查看购物车:用户可以查看购物车中的所有商品,包括商品名称、单价、数量和小计。显示购物车中商品的总数量和总价格。 修改商品数量:用户可以修改购物车中某个商品的数量。修改时需检查库存,库存不足时提示用户。 删除商品:用户可以从购物车中删除某个商品。 清空购物车:用户可以一键清空购物车中的所有商品。 3. 结算功能 计算总价:根据购物车中的商品数量和单价,计算总价格。支持折扣、优惠券等促销活动。 生成订单:用户确认购物车内容后,生成订单。订单包括商品列表、总价、用户信息等。 库存更新:结算成功后,更新商品的库存数量。 4. 用户交互功能 用户登录/注册:用户需要登录或注册后才能使用购物车功能。登录后可以查看历史订单。 订单历史:用户可以查看自己的历史订单记录。 退出登录:用户可以安全退出登录。 5. 系统管理功能 商品管理:管理员可以添加、修改、删除商品信息。 订单管理:管理员可以查看所有订单,处理订单状态(如发货、取消等)。 库存管理:管理员可以管理商品库存,及时补货。 用户管理:管理员可以查看和管理用户信息。 购物车系统的制作介绍: 常使用HTML、CSS构建,通过这些技术将购物车页面美观、交互性好地呈现给用户,实现添加、删除等操作的动态效果。借助后端编程语言JAVA实现业务逻辑,处理前端发送的请求,与数据库交互,完成添加商品、计算总价等功能。 购物车系统的实现计划: 商品管理功能(1-4):完成时限大纲 购物车功能(5-9):完成页面的基础制作 用户交互功能(10-13):完成后台的完成度 系统管理功能(14-18):完成整体的制作收费多少合适
最新发布
03-10
<think>嗯,用户让我介绍一下简单的购物车系统,还提到制作收费的问题。首先,我需要理解用户的需求。他们可能想开发一个购物车系统,或者了解其构成,以便预算或计划项目。用户提供的结构很详细,包括整体介绍、功能、制作和实现计划,最后问收费多少合适。 首先,我应该先按照用户提供的结构,分点介绍购物车系统的各个方面,确保覆盖所有提到的功能模块。然后,关于收费部分,用户可能想知道开发这样一个系统的成本估算。这里需要考虑开发方式的不同,比如自主开发、外包或使用SaaS,每种方式的成本差异很大。 用户可能没有明确说明他们的技术背景,所以需要用简单易懂的语言解释不同选项的优缺点。比如,自主开发成本低但需要技术能力,外包费用高但省时省力,SaaS适合快速上线但定制性差。还需要考虑功能复杂度,简单系统和复杂系统的费用不同,可能从几千到几十万不等。 另外,用户提到的实现计划中的时间线,比如各个功能模块的完成时限,可能需要结合开发周期来估算费用。比如,商品管理功能1-4天,可能对应开发的工作量,进而影响成本。 还要注意用户可能关心后续维护费用,比如服务器、域名、维护等,这些也需要在回答中提到,确保全面性。同时,提醒用户根据自身需求和预算选择合适方案,避免他们选择不适合的高成本方案。 最后,确保回答结构清晰,分点明确,使用中文,符合之前的系统级指令。避免使用专业术语过多,保持口语化,但又要准确可靠。检查LaTeX格式是否正确,虽然用户的问题中没有数学公式,但可能案例中的变量需要正确格式,比如价格区间或数量等。 总结,回答应包括购物车系统的详细介绍,功能模块,制作技术,然后分情况讨论收费,给出不同开发方式的费用范围和考虑因素,帮助用户做出决策。</think>### 购物车系统介绍与实现费用分析 #### 一、购物车系统整体结构 购物车系统是电商平台的核心模块,通过模拟线下购物流程,实现以下技术目标: $$系统总价 = \sum_{i=1}^{n}(商品单价_i \times 数量_i) - 优惠金额$$ 系统包含5大功能模块,通过前后端分离架构实现: #### 二、功能模块实现详解 1. **商品管理模块** - 前端实现:Vue/React动态渲染商品卡片,支持分类过滤 ```html <!-- 示例:商品卡片组件 --> <div class="product-card"> <img :src="product.image" alt="商品图"> <h3>{{ product.name }}</h3> <p>价格:¥{{ product.price }}</p> <button @click="addToCart(product)">加入购物车</button> </div> ``` 2. **购物车核心功能** - 数据结构设计: ```javascript // 购物车数据模型 { items: [ { id: "P001", name: "商品A", price: 99.99, quantity: 2, stock: 10 } ], total: 199.98, discount: 20.00 } ``` 3. **结算系统实现** - 关键算法: ```java // 订单金额计算示例 public BigDecimal calculateTotal(Cart cart) { BigDecimal subtotal = cart.getItems().stream() .map(item -> item.getPrice().multiply(new BigDecimal(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); return subtotal.subtract(cart.getDiscount()); } ``` #### 三、开发费用估算 根据系统复杂度和开发方式,费用差异显著: $$开发成本 = 基础功能成本 + (高级功能 \times 复杂度系数) + 维护成本$$ | 开发方式 | 费用范围 | 周期 | 适用场景 | |----------------|-------------|--------|--------------------------| | 自主开发 | ¥0-5万 | 2-6月 | 有技术团队 | | 外包开发 | ¥3万-30万 | 1-3月 | 定制化需求 | | SaaS解决方案 | ¥0.5-3万/年 | 即时 | 快速上线/中小商户 | **成本构成分析:** 1. 基础功能开发:¥3-8万(含商品管理+购物车核心功能) 2. 高级功能扩展: - 促销系统:+¥2-5万 - 库存预警:+¥1-3万 - 数据分析:+¥3-8万 3. 年度维护费用:约开发费用的15-20% #### 四、实施建议 1. **MVP开发策略**:先实现核心购物车功能(3-5万),后续迭代升级 2. **技术选型推荐**: - 前端:Vue3 + Element Plus - 后端:Spring Boot + MyBatis - 数据库MySQL + Redis缓存 3. **费用优化方案**: - 使用开源框架(如OpenCart)二次开发可降低30%成本 - 选择云服务商的全托管方案(如AWS Amplify)减少运维投入 **注**:实际报价需根据具体需求评估,建议准备详细的需求文档(PRD)获取精确报价。系统上线后还需预留每年¥1-3万的服务器/域名/维护费用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值