文件操作基架,包含简单文件,分片上传,垃圾文件清理-银发服务2

1.简单文件的上传

1.1.大体结构图

在这里插入图片描述

1.2.数据库表的结构

在这里插入图片描述

**注:**数据库表只保存上传的信息,具体的文件保存在OSS中

1.3.Controller层

@PostMapping(value = "up-load")
@ApiOperation(value = "文件上传-简单上传",notes = "文件上传-简单上传")
@ApiImplicitParams({
    @ApiImplicitParam(paramType = "form", name = "file", value = "文件对象", required = true, dataTypeClass = MultipartFile.class)
})
@ApiOperationSupport(includeParameters = {"fileVO.businessType","fileVO.bucketName","fileVO.storeFlag","fileVO.autoCatalog"})
//1.MultipartFile是Spring框架处理文件的接口
public ResponseResult<FileVO> upLoad(
        @RequestParam("file") MultipartFile file,
        FileVO fileVO) throws IOException {
    fileVO.setCompanyNo(SubjectContent.getCompanyNo());
    //2.我自定义的一个文件的构造类,下文会给出
    UploadMultipartFile uploadMultipartFile = UploadMultipartFile
        .builder()
        .originalFilename(file.getOriginalFilename())
        .fileByte(IOUtils.toByteArray(file.getInputStream()))
        .build();
    //执行文件上传
    FileVO fileVOResult = fileService.upLoad(uploadMultipartFile, fileVO);
    return ResponseResultBuild.successBuild(fileVOResult);
}


//自定义文件类,将文件的内容直接转化为字节,因为文件较小,所以这么处理很方便
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UploadMultipartFile implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "文件名称")
    public String originalFilename;

    @ApiModelProperty(value = "文件数组")
    public byte[] fileByte;

}

1.4.Service层

@Override
@Caching(
    evict = {
        @CacheEvict(value = FileCacheConstant.PAGE,allEntries = true),
        @CacheEvict(value = FileCacheConstant.BUSINESS_KEY,allEntries = true)},
    put={@CachePut(value =FileCacheConstant.BASIC,key = "#result.id")})
@Transactional
public FileVO upLoad(UploadMultipartFile multipartFile, FileVO fileVO) throws ProjectException {
    //1.流式处理,因为后续阿里云的操作都需要流
    ByteArrayInputStream byteArrayInputStream =new ByteArrayInputStream(multipartFile.getFileByte());
    try {
        //2.文件重命名
        String filename = identifierGenerator.nextId(fileVO)+"-"+multipartFile.getOriginalFilename();;
        fileVO.setFileName(filename);
        //3.文件后缀名
        String suffix = fileVO.getFileName().substring(fileVO.getFileName().lastIndexOf("."));
        fileVO.setSuffix(suffix);
        //3.调用简单上传,其实就是调用阿里云的OSS,具体的教程可以看其他教程
        String pathUrl = fileStorageAdapter.uploadFile(fileVO, byteArrayInputStream);
        //4.用到我自定义的转化类型方法,将数据保存在数据库中
        File file = BeanConv.toBean(fileVO, File.class);
        file.setStatus(FileConstant.STATUS_SUCCEED);
        file.setPathUrl(pathUrl);
        boolean flag = save(file);
        if (!flag){
            throw new ProjectException(FileEnum.UPLOAD_FAIL);
        }
        //5.补全完整路径,这一部分是返回给前端的数据,让其有地址能显示出来
        pathUrl = fileUrlContext.getFileUrl(fileVO.getStoreFlag(), pathUrl);
        fileVO.setId(file.getId());
        fileVO.setPathUrl(pathUrl);
        //6.这是垃圾文件处理的内容,后面具体会提到
        Long messageId = (Long) identifierGenerator.nextId(fileVO);
        MqMessage mqMessage = MqMessage.builder()
            .id(messageId)
            .title("file-message")
            .content(JSONObject.toJSONString(fileVO))
            .messageType("file-request")
            .produceTime(Timestamp.valueOf(LocalDateTime.now()))
            .sender("system")
            .build();
        Message<MqMessage> message = MessageBuilder.withPayload(mqMessage).setHeader("x-delay", fileDelayTime).build();
        fileSource.fileOutput().send(message);
        return fileVO;
        //7.异常处理情况
    }catch (Exception e) {
        log.error("文件上传异常:{}", ExceptionsUtil.getStackTraceAsString(e));
        throw new ProjectException(FileEnum.UPLOAD_FAIL);
    }finally {
        if (byteArrayInputStream != null) {
            try {
                byteArrayInputStream.close();
            } catch (Exception e) {
                log.error("文件上传操作失败:{}", ExceptionsUtil.getStackTraceAsString(e));
            }
        }
    }
}

1.5.效果图

在这里插入图片描述

在这里插入图片描述

可以看到,目前是没有business_id这个字段的,因为我还没有提交,只有提交才能有business_id字段如果在此时我没有点击保存而是直接退出,这就成了垃圾文件,所以这就涉及到了垃圾自动回收的功能,具体的垃圾回收在介绍完所有文件的上传机制的时候统一讲述。

2.简单文件的替换操作

2.1.Controller层

@PutMapping(value = "replace-bind-batch-file")
@ApiOperation(value = "移除业务原图片,并绑定新的图片到业务上",notes = "移除业务原图片,并绑定新的图片到业务上")
@ApiImplicitParam(name = "fileVOs",value = "附件对象",required = true,dataType = "FileVO")
//与简单文件上传不一样,替换操作的参数是带有BusinessId的
public ResponseResult<Boolean> replaceBindBatchFile(@RequestBody List<FileVO> fileVOs){
    Boolean flag = fileService.replaceBindBatchFile(fileVOs);
    return ResponseResultBuild.successBuild(flag);
}

2.2.Service层

@Override
@Transactional
@Caching(evict = {@CacheEvict(value = FileCacheConstant.PAGE,allEntries = true),
        @CacheEvict(value = FileCacheConstant.BASIC,allEntries = true),
        @CacheEvict(value = FileCacheConstant.LIST,allEntries = true),
        @CacheEvict(value = FileCacheConstant.BUSINESS_KEY,key = "#fileVOs.get(0).getBusinessId()")})
public Boolean replaceBindBatchFile(List<FileVO> fileVOs) {
    try {
        //1.查询新老图片id集合,注意,只有BusinessId才能被查出来,垃圾文件如果没删除也删不出来
        QueryWrapper<File> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(File::getBusinessId,fileVOs.get(0).getBusinessId());
        List<File> oldList = list(queryWrapper);
        List<Long> oldIds = oldList.stream().map(File::getId).collect(Collectors.toList());
        List<Long> newIds = fileVOs.stream().map(FileVO::getId).collect(Collectors.toList());
        //2.我的前端最多可以传三张图片,所以这里处理的是集合的差
        List<Long> delIds = oldIds.stream().filter(n -> {
            return !newIds.contains(n);
        }).collect(Collectors.toList());
        //3.删除图片,这个方法既删除了OSS中的图片,也删除了数据库的记录
        if (!EmptyUtil.isNullOrEmpty(delIds)){
            deleteInIds(delIds);
        }
        //4.绑定新图片
        List<FileVO> newFiles = fileVOs.stream().filter(n -> {
            return !oldIds.contains(n.getId());
        }).collect(Collectors.toList());
        if (!EmptyUtil.isNullOrEmpty(newFiles)){
            List<FileVO> fileVOsResult = bindBatchFile(newFiles);
            return !EmptyUtil.isNullOrEmpty(fileVOsResult);
        }
        return true;
    }catch (Exception e){
        log.error("查询文件分页异常:{}", ExceptionsUtil.getStackTraceAsString(e));
        throw new ProjectException(FileEnum.BIND_FAIL);
    }
}

2.3.实际效果图

在这里插入图片描述

3.大文件的分片上传

3.1.大体结构图

在这里插入图片描述

3.2.文件初始化

前端调佣初始化接口,告知三方对象存储我需要进行文件分片上传,三方对象存储收到信息后返回uploadId。

在这里插入图片描述

controller层

    @PostMapping(value = "initiate-multipart-up-load")
    @ApiOperation(value = "文件分片上传-初始化",notes = "文件分片上传-初始化")
    @ApiImplicitParam(name = "fileVO",value = "文件对象",required = true,dataType = "FileVO")
    public ResponseResult<FileVO> initiateMultipartUpload(
            @RequestBody FileVO fileVO){
        fileVO.setCompanyNo(SubjectContent.getCompanyNo());
        //初始化上传Id
        FileVO fileVOResult = fileService.initiateMultipartUpload(fileVO);
        return ResponseResultBuild.successBuild(fileVOResult);
    }

Service层

@Override
@Caching(evict = {
        @CacheEvict(value = FileCacheConstant.PAGE,allEntries = true),
        @CacheEvict(value = FileCacheConstant.BUSINESS_KEY,allEntries = true)},
    put={@CachePut(value =FileCacheConstant.BASIC,key = "#result.id")})
@Transactional
public FileVO initiateMultipartUpload(FileVO fileVO) {
    try {
        //1.文件重命名
        String filename = identifierGenerator.nextId(fileVO)+"-"+fileVO.getFileName();
        fileVO.setFileName(filename);
        //2.文件后缀名
        String suffix = fileVO.getFileName().substring(fileVO.getFileName().lastIndexOf("."));
        fileVO.setSuffix(suffix);
        //3.分片上传-初始化,具体的代码下文给出
        File file  =  fileStorageAdapter.initiateMultipartUpload(fileVO);
        //4.保存数据库
        file.setBusinessType(fileVO.getBusinessType());
        file.setSuffix(suffix);
        file.setStoreFlag(fileVO.getStoreFlag());
        file.setMd5(fileVO.getMd5());
        file.setCompanyNo(fileVO.getCompanyNo());
        file.setStatus(FileConstant.STATUS_SENDING);
        boolean flag = save(file);
        if (!flag){
            throw new ProjectException(FileEnum.UPLOAD_FAIL);
        }
        //5.补全完整路径
        FileVO fileVOResult = BeanConv.toBean(file, FileVO.class);
        String pathUrl = fileUrlContext.getFileUrl(fileVOResult.getStoreFlag(), fileVOResult.getPathUrl());
        fileVOResult.setPathUrl(pathUrl);
        //6.这里依然是垃圾文件处理的机制,如果我们只提交了文件没有将其保存的话不会有业务id,所以是垃圾文件,会被我们定时清除
        Long messageId = (Long) identifierGenerator.nextId(fileVOResult);
        MqMessage mqMessage = MqMessage.builder()
            .id(messageId)
            .title("file-message")
            .content(JSONObject.toJSONString(fileVOResult))
            .messageType("file-request")
            .produceTime(Timestamp.valueOf(LocalDateTime.now()))
            .sender("system")
            .build();
        Message<MqMessage> message = MessageBuilder.withPayload(mqMessage).setHeader("x-delay", fileDelayTime).build();
        fileSource.fileOutput().send(message);
        return fileVOResult;
    } catch (Exception e) {
        log.error("文件上传初始化异常:{}", ExceptionsUtil.getStackTraceAsString(e));
        throw new ProjectException(FileEnum.INIT_UPLOAD_FAIL);
    }
}

//3.具体OSS部分初始化代码
@Override
    public File initiateMultipartUpload(String suffix, String filename, String bucketName, boolean autoCatalog) {
        // 是否自动生成存储路径并设置文件路径和名称(Key)
        String key = filename;
        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key);
        // 如果需要在初始化分片时设置请求头,请参考以下示例代码。
        ObjectMetadata metadata = fileMetaHandler(suffix);
        metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
        // 指定该Object的网页缓存行为。
        metadata.setCacheControl("no-cache");
        // 指定该Object被下载时的名称。
        metadata.setContentDisposition("inline;filename="+key);
        // 指定该Object的内容编码格式。
        metadata.setContentEncoding(OSSConstants.DEFAULT_CHARSET_NAME);
         //指定请求
        request.setObjectMetadata(metadata);
        // 初始化分片。
        InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request);
        // 设置权限(公开读)
        ossClient.setBucketAcl(bucketName, CannedAccessControlList.PublicRead);
        // 返回uploadId,它是分片上传事件的唯一标识。您可以根据该uploadId发起相关的操作,例如取消分片上传、查询分片上传等。
        return File.builder().bucketName(bucketName).pathUrl(key).fileName(key).uploadId(upresult.getUploadId()).build();
    }

3.3.文件分片

前端把大文件拆分多个小文件,每次携带uploadId及分片文件进行上传,三方对象存储收到信息后返回分片partETag信息

在这里插入图片描述

在这里插入图片描述

controller层

@PostMapping(value = "up-load-part")
@ApiOperation(value = "文件分片上传-上传分片",notes = "文件分片上传-上传分片")
@ApiImplicitParams({
    @ApiImplicitParam(paramType = "form", name = "file", value = "文件对象", required = true, dataTypeClass = MultipartFile.class)
})
public ResponseResult<String> uploadPart(
        @RequestParam("file") MultipartFile file,
        FilePartVO filePartVO)throws IOException {
    filePartVO.setCompanyNo(SubjectContent.getCompanyNo());
    //构建文件上次对象
    UploadMultipartFile uploadMultipartFile = UploadMultipartFile
        .builder()
        .originalFilename(file.getOriginalFilename())
        .fileByte(IOUtils.toByteArray(file.getInputStream()))
        .build();
    //上传分片返回partETagJson
    String partETagJson = fileService.uploadPart(uploadMultipartFile,filePartVO);
    return ResponseResultBuild.successBuild(partETagJson);
}

Service层

@Override
@Transactional
public String uploadPart(UploadMultipartFile multipartFile, FilePartVO filePartVO) {
    try {
        //1.上传分片数据,上传到OSS中,具体代码下文给出
        String partETagString = fileStorageAdapter.uploadPart(filePartVO, new ByteArrayInputStream(multipartFile.getFileByte()));
        //2.保存分片信息,数据库中保存分片信息
        FilePart filePart = BeanConv.toBean(filePartVO, FilePart.class);
        filePart.setUploadResult(partETagString);
        filePartService.save(filePart);
        return partETagString;
    }catch (Exception e) {
        log.error("文件分片上传异常:{}", ExceptionsUtil.getStackTraceAsString(e));
        throw new ProjectException(FileEnum.UPLOAD_PART_FAIL);
    }
}

//1.具体的OSS操作代码
    @Override
    public String uploadPart(String upLoadId, String filename, int partNumber, long partSize, String bucketName, InputStream inputStream) {
        //封装分片上传请求
        UploadPartRequest uploadPartRequest = new UploadPartRequest();
         uploadPartRequest.setUploadId(upLoadId);
        //part大小 1-10000
        uploadPartRequest.setPartNumber(partNumber);
        uploadPartRequest.setPartSize(partSize);
        //文件上传的bucketName
        uploadPartRequest.setBucketName(bucketName);
        //分片文件
        uploadPartRequest.setInputStream(inputStream);
        uploadPartRequest.setKey(filename);
        // 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。
        UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
        log.info("{}文件第 {} 片上传成功,上传结果:{}", upLoadId, uploadPartRequest.getPartNumber(),JSON.toJSON(uploadPartResult));
        return JSONObject.toJSONString(uploadPartResult.getPartETag());
    }

3.4.合并分片

在这里插入图片描述

controller层

    @PostMapping(value = "complete-multipart-up-load")
    @ApiOperation(value = "文件分片上传-合并分片",notes = "文件分片上传-合并分片")
    @ApiImplicitParam(name = "fileVO",value = "文件对象",required = true,dataType = "FileVO")
    public ResponseResult<String> completeMultipartUpload(
            @RequestBody FileVO fileVO)throws IOException {
        //问上传分片返回partETagJson
        String eTagJson = fileService.completeMultipartUpload(fileVO);
        return ResponseResultBuild.successBuild(eTagJson);
    }

Service层

@Override
@Transactional
public String completeMultipartUpload(FileVO fileVO) {
    try {
        //1.移除分片记录
        Boolean flag = filePartService.deleteFilePartByUpLoadId(fileVO.getUploadId());
        if (!flag){
            throw new ProjectException(FileEnum.COMPLETE_PART_FAIL);
        }
        //2.修改文件记录状态
        fileVO.setStatus(FileConstant.STATUS_SUCCEED);
        flag = updateById(BeanConv.toBean(fileVO,File.class));
        if (!flag){
            throw new ProjectException(FileEnum.COMPLETE_PART_FAIL);
        }
        //3.合并结果,调用阿里OSS,具体代码在下面
        return fileStorageAdapter.completeMultipartUpload(fileVO);
    } catch (Exception e) {
        log.error("文件分片上传异常:{}", ExceptionsUtil.getStackTraceAsString(e));
        throw new ProjectException(FileEnum.COMPLETE_PART_FAIL);
    }
}

//3.具体代码
    @Override
    public String completeMultipartUpload(String upLoadId, List<String> partETags, String filename, String bucketName) {
        StopWatch st = new StopWatch();
        st.start();
        //转换jsonarray为list
        List<PartETag> partETagList =Lists.newArrayList();
        partETags.forEach(n->{
            partETagList.add(JSONObject.parseObject(n,PartETag.class));
        });
        CompleteMultipartUploadRequest completeMultipartUploadRequest =
                new CompleteMultipartUploadRequest(bucketName, filename, upLoadId, partETagList);
        log.info("{}文件上传完成,开始合并,partList:{}", upLoadId, partETags);
        // 完成分片上传。
        CompleteMultipartUploadResult completeMultipartUploadResult = ossClient.completeMultipartUpload(completeMultipartUploadRequest);
        st.stop();
        log.info("{}文件上传完成,上传结果:{},耗时:{}", upLoadId, JSON.toJSON(completeMultipartUploadResult), st.getTotalTimeMillis());
        return completeMultipartUploadResult.getETag();
    }

4.垃圾文件的处理

4.1.利用XXL-JOB进行定时清理

在这里插入图片描述

在使用下面的代码之前,需要自行配置XXL-JOB

执行器任务

@Component
public class ClearFileHandlerJob {

    @Autowired
    FileBusinessFeign fileBusinessFeign;

    @XxlJob(value = "clear-file")
    public ReturnT<String> execute(String param) {
        Boolean responseWrap = fileBusinessFeign.clearFile();
        if (responseWrap){
            ReturnT.SUCCESS.setMsg("计划任务:清理垃圾文件-成功");
            return ReturnT.SUCCESS;
        }
        ReturnT.FAIL.setMsg("计划任务:清理垃圾文件-失败");
        return ReturnT.FAIL;

    }
}

微服务接口

@FeignClient(value = "file-web",fallback = FileBusinessHystrix.class)
public interface FileBusinessFeign {

    /**
     * @Description 定时清理文件
     * @return
     */
    @DeleteMapping("file-feign/clear-file")
    Boolean clearFile();
}

控制层处理

@RestController
@RequestMapping("file-feign")
@Api(tags = "附件feign-controller")
@Slf4j
public class FileBusinessFeignController {
    @Autowired
    IFileService fileService;
    /**
     * @Description 定时清理文件
     * @return Boolean
     */
    @DeleteMapping("clear-file")
    @ApiOperation(value = "删除业务对应附件",notes = "删除业务对应附件")
    public Boolean clearFile(){
        return fileService.clearFile();
    }
}

业务服务层

@Override
    @Caching(evict = {@CacheEvict(value = FileCacheConstant.PAGE,allEntries = true),
            @CacheEvict(value = FileCacheConstant.BUSINESS_KEY,allEntries = true),
            @CacheEvict(value = FileCacheConstant.BASIC,allEntries = true)})
    @Transactional
    public Boolean clearFile() {
        try {
            //查询需要清理的文件
            List<FileVO> fileList = needClearFile();
            if (EmptyUtil.isNullOrEmpty(fileList)){
                return true;
            }
            List<Long> fileListIds = fileList.stream().map(FileVO::getId).collect(Collectors.toList());
            //移除数据库信息
            Boolean flag =  removeByIds(fileListIds);
            //移除对象存储数据
            for (FileVO fileVO : fileList) {
                fileStorageAdapter.delete(fileVO.getStoreFlag(),fileVO.getBucketName(),fileVO.getPathUrl());
            }
            return flag;
        }catch (Exception e){
            log.error("定时清理文件异常:{}", ExceptionsUtil.getStackTraceAsString(e));
            throw new ProjectException(FileEnum.CLEAR_FILE_TASK_FAIL);
        }
    }

4.2.利用延迟任务延迟清理

在这里插入图片描述

逻辑:当文件上传对象存储且保存数据库后,上传方法最后会发送延迟信息当前文件记录信息到RabbitMQ中,task-listener10分钟后消费到消息,然后通过openfegin调用file-web中的查询处理方法,如果当前文件记录还是没有businessId,则认为是垃圾文件进行删除

具体的实现不予给出,前面延迟视频的代码可以参考,不过直接使用新推出的delayed-message-exchange插件会更加方便哦

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值