吉安感知网项目-后端
linwei
2026-01-26 dc8cdc5affb564ed5a098a1cbad25dd6e0c5a054
文件下载
3 files modified
172 ■■■■■ changed files
drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java 41 ●●●●● patch | view | raw | blame | history
drone-ops/drone-resource/src/main/java/org/sxkj/resource/service/IAttachService.java 9 ●●●●● patch | view | raw | blame | history
drone-ops/drone-resource/src/main/java/org/sxkj/resource/service/impl/AttachServiceImpl.java 122 ●●●●● patch | view | raw | blame | history
drone-ops/drone-resource/src/main/java/org/sxkj/resource/controller/AttachController.java
@@ -206,7 +206,48 @@
         return R.status(attachService.updateById(attach));
     }
    // 流式附件下载接口,支持下载zip压缩包,不使用本地存储
    @ApiOperation(value = "流式附件下载接口", notes = "使用流方式返回数据,不使用本地存储")
    @PostMapping("/downloadByByte")
    public void downloadByByte(
        @ApiParam(value = "附件下载参数") @RequestBody AttachmentDownloadParam param,
        HttpServletResponse response) throws IOException {
        // 设置文件名
        String timestamp = DateTimeUtil.format(LocalDateTime.now(), "yyyyMMdd_HHmmss");
        String fileName = "attachments_" + timestamp + ".zip";
        // 设置响应头
        response.setContentType("application/zip");
        response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()));
        // 获取响应输出流
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            Boolean result = attachService.downloadByByte(param, outputStream);
            if (!result) {
                return;
            }
        } catch (Exception e) {
            if (!response.isCommitted()) {
                response.reset();
                response.setContentType("application/json;charset=UTF-8");
                ResponseResult errorResult = ResponseResult.error("下载文件失败: " + e.getMessage());
                // 使用输出流写入错误信息,避免getWriter()冲突
                try {
                    byte[] errorBytes = JSON.toJSONString(errorResult).getBytes(StandardCharsets.UTF_8);
                    outputStream.write(errorBytes);
                } catch (IOException ioException) {
                    log.error("Error writing error response: {}", ioException.getMessage());
                }
            } else {
                log.error("Cannot send error response: response already committed. Exception: {}", e.getMessage());
            }
        }
    }
    /**
     * 删除 附件表及对应的附件信息
drone-ops/drone-resource/src/main/java/org/sxkj/resource/service/IAttachService.java
@@ -212,4 +212,13 @@
     */
    List<AttachTypeStatisticsVO> calculateTheThreeDimensionalArea(String deviceSn, String startDate, String endDate, String resultTypes, String areaCode);
    /**
     * 流式附件下载
     * @param param 下载参数
     * @param outputStream 输出流
     * @return 处理结果
     * @throws IOException IO异常
     */
    Boolean downloadByByte(AttachmentDownloadParam param, java.io.OutputStream outputStream) throws IOException;
}
drone-ops/drone-resource/src/main/java/org/sxkj/resource/service/impl/AttachServiceImpl.java
@@ -23,6 +23,8 @@
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.annotations.Param;
@@ -41,6 +43,7 @@
import org.sxkj.common.enums.AttachResultTypeEnum;
import org.sxkj.common.enums.TypeOfOutcome;
import org.sxkj.common.func.Streams;
import org.sxkj.common.model.ResponseResult;
import org.sxkj.common.query.PaginationUtils;
import org.sxkj.common.redis.RedisOpsUtils;
import org.sxkj.common.utils.*;
@@ -63,6 +66,9 @@
import org.sxkj.tools.model.TifConvertResult;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
@@ -70,6 +76,9 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
 * 附件表 服务实现类
@@ -770,4 +779,117 @@
        return attachTypeStatisticsVOS;
    }
    @Override
    public Boolean downloadByByte(AttachmentDownloadParam param, OutputStream outputStream) throws IOException {
        // 如果附件id集合并且任务id集合参数都为空,就判断起止时间是否都有为空,如果都为空,则返回错误
        param.setAreaCode(HeaderUtils.formatAreaCode(param.getAreaCode()));
        if (CollectionUtils.isEmpty(param.getAttachIds()) && CollectionUtils.isEmpty(param.getWayLineJobIds())) {
            // 判断起止时间是否为空
            if (StringUtils.isEmpty(param.getStartTime()) || StringUtils.isEmpty(param.getEndTime())) {
                return false;
            }
        }
        // 获取附件列表
        List<AttachVO> attachList = getAttachList(param);
        if (CollectionUtils.isEmpty(attachList)) {
            return false;
        }
        try {
            // 直接使用传入的输出流创建ZipOutputStream
            ZipOutputStream zos = new ZipOutputStream(outputStream);
            // 使用BEST_SPEED而不是BEST_COMPRESSION,提高速度
            zos.setLevel(Deflater.BEST_SPEED);
            // 使用较小的缓冲区,减少内存使用
            final int BUFFER_SIZE = 1024 * 1024; // 1MB
            byte[] buffer = new byte[BUFFER_SIZE];
            // 创建Minio客户端
            MinioClient minioClient = MinioClient.builder()
                .endpoint(minioPojo.getPath())
                .credentials(minioPojo.getAccessKey(), minioPojo.getSecretKey())
                .build();
            // 创建一个Set来跟踪已经使用的文件名,避免重名
            Set<String> usedFileNames = new HashSet<>();
            // 直接遍历所有附件,不再按任务分组
            for (AttachVO attachVO : attachList) {
                String objectName = attachVO.getName();
                try {
                    // 从MinIO获取文件流
                    InputStream is = minioClient.getObject(
                        GetObjectArgs.builder()
                            .bucket(minioPojo.getBucket())
                            .object(objectName)
                            .build());
                    // 提取文件名(不包含路径)
                    String fileName = objectName.substring(objectName.lastIndexOf("/") + 1);
                    // 处理文件名冲突
                    String uniqueFileName = fileName;
                    int counter = 1;
                    while (usedFileNames.contains(uniqueFileName)) {
                        // 如果文件名已存在,在文件名和扩展名之间添加计数器
                        int dotIndex = fileName.lastIndexOf(".");
                        if (dotIndex > 0) {
                            uniqueFileName = fileName.substring(0, dotIndex) + "_" + counter + fileName.substring(dotIndex);
                        } else {
                            uniqueFileName = fileName + "_" + counter;
                        }
                        counter++;
                    }
                    // 将文件名添加到已使用集合中
                    usedFileNames.add(uniqueFileName);
                    // 创建zip条目(直接放在根目录下)
                    ZipEntry zipEntry = new ZipEntry(uniqueFileName);
                    zos.putNextEntry(zipEntry);
                    // 将文件内容写入zip
                    int length;
                    while ((length = is.read(buffer)) > 0) {
                        zos.write(buffer, 0, length);
                        // 每写入一块数据后刷新,确保数据及时发送到客户端
                        zos.flush();
                    }
                    // 关闭资源
                    zos.closeEntry();
                    is.close();
                } catch (Exception e) {
                    log.error("处理文件 {} 失败: {}", objectName, e.getMessage());
                }
            }
            // 确保所有数据都被写入
            zos.finish();
            zos.flush();
            // 注意:不要关闭ZipOutputStream,因为它会关闭底层的OutputStream
            // 让调用者负责关闭输出流
            return true;
        } catch (Exception e) {
            log.error("创建zip文件失败", e);
            return false;
        }finally {
            if (outputStream != null) {
                try {
                    outputStream.flush();
                } catch (IOException e) {
                    log.error("Error flushing output stream: {}", e.getMessage());
                }
                try {
                    outputStream.close();
                } catch (IOException e) {
                    log.error("Error closing output stream: {}", e.getMessage());
                }
            }
        }
    }
}