/* * Copyright (c) 2018-2028, Chill Zhuang All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * Neither the name of the dreamlu.net developer nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * Author: Chill 庄骞 (smallchill@163.com) */ package org.sxkj.resource.service.impl; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 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; import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.sxkj.common.constant.CacheConstant; 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.*; import org.sxkj.resource.builder.DevicePermissionBuilder; import org.sxkj.resource.builder.OssBuilder; import org.sxkj.resource.dto.WaylineJobInfoQueryParam; import org.sxkj.resource.entity.Attach; import org.sxkj.resource.mapper.AttachMapper; import org.sxkj.resource.model.FileMetadataDTO; import org.sxkj.resource.model.MinioPojo; import org.sxkj.resource.model.PositionDTO; import org.sxkj.resource.param.AttachPageParam; import org.sxkj.resource.service.IAttachService; import org.sxkj.resource.util.MinioFileDownloader; import org.sxkj.resource.vo.*; import org.sxkj.system.cache.SysCache; import org.sxkj.system.feign.ISysClient; import org.sxkj.system.vo.TreeVo; import org.sxkj.tools.feign.ITifConvertClient; 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; import java.util.*; 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; /** * 附件表 服务实现类 * * @author Chill */ @Service @Slf4j public class AttachServiceImpl extends BaseServiceImpl implements IAttachService { /** * 对象存储构建类 */ @Autowired private OssBuilder ossBuilder; @Autowired private ISysClient sysClient; @Autowired private MinioPojo minioPojo; @Autowired private DevicePermissionBuilder permissionBuilder; @Autowired private ITifConvertClient tifConvertClient; @Value("${odm.down.temp.zipBasePath}") private String localSaveDir; @Value("${odm.down.temp.zipPreFix}") private String downPath; @Override public IPage selectAttachPage(IPage page, AttachPageParam attach) { // 创建部门 if (!AuthUtil.isAdministrator()) { List deptList = SysCache.getDeptChildIds(Long.valueOf(AuthUtil.getDeptId())); attach.setDeptList(deptList); } List attachVOS = baseMapper.selectAttachPage(page, attach); return page.setRecords(attachVOS); } @Override public IPage findAttachImages(IPage page, AttachVO attach) { attach.setAreaCode(HeaderUtils.getAreaCodeHeaderAreaCode(attach.getAreaCode())); if (Objects.isNull(attach.getResultTypes())) { // 使用常量定义避免魔法数字 attach.setResultTypes(Arrays.asList(TypeOfOutcome.IMAGE.getType(), TypeOfOutcome.VIDEO.getType(), TypeOfOutcome.AI.getType(), TypeOfOutcome.SURVEY.getType(), TypeOfOutcome.PANORAMA.getType())); } List deptIdList = SysCache.getDeptChildIds(Long.valueOf(AuthUtil.getDeptId())); String permissionCondition = permissionBuilder.buildDataPermissionCondition(AuthUtil.getUserId(), "attach"); List aiImages = baseMapper.findAttachImages(page, attach, deptIdList, permissionCondition, AuthUtil.getUserId()); return page.setRecords(aiImages); } /** * ai数据 * * @param page * @param attach * @return */ @Override public List findAiAttachImages(IPage page, AttachVO attach) { List deptIdList = SysCache.getDeptChildIds(Long.valueOf(AuthUtil.getDeptId())); String permissionCondition = permissionBuilder.buildDataPermissionCondition(AuthUtil.getUserId(), "attach"); if (StringUtils.isNotEmpty(attach.getWayLineJobId())) { attach.setWayLineJobIdList(Arrays.asList(attach.getWayLineJobId().split(","))); } return baseMapper.findAiAttachImages(page, attach, deptIdList, permissionCondition); } @Override public List findAiEventMd5s(List eventRecordIds) { if (CollectionUtils.isEmpty(eventRecordIds)) { return null; } List list = baseMapper.findAllMd5ByAi(eventRecordIds); // 原有的图片 List aiMd5s = baseMapper.findAiMd5s(eventRecordIds); List> allMd5 = aiMd5s.stream() .map(entity -> Arrays.asList(entity.split(","))) .collect(Collectors.toList()); // 原始图片id // List allIds = Streams.flatMap(allMd5); List md5s = Streams.toList(list, entity -> !StringUtils.isEmpty(entity.getMd5()), EventImgMd5VO::getMd5); // List md5Orgs = Streams.toList(list, entity -> !StringUtils.isEmpty(entity.getOriginalMd5()), EventImgMd5VO::getOriginalMd5); // allIds.removeAll(md5Orgs); List all = new ArrayList<>(md5s); // all.addAll(allIds); if (CollectionUtils.isEmpty(all)) { return Arrays.asList("0"); } else { return all; } } @Override public List getDeptAllSize(String startTime, String endTime) { return baseMapper.getDeptAllSize(startTime, endTime); } /** * 保存附件表信息 * * @param attach 附件表信息 * @return */ @Override public Boolean saveAttachInfo(Attach attach) { if (isExist(attach)) { return false; } // 保存 return save(attach); } /** * 判断附件是否已存在 * * @param attach * @return */ private Boolean isExist(Attach attach) { if (null != attach.getId()) { return true; } Boolean flag = false; // 判断是否已入库,根据附件名称判断(重复的不再入库) Integer number = baseMapper.getAttachNumberByParam(attach); if (number > 0) { flag = true; } return flag; } /** * 删除 附件表及对应的附件信息 * * @param ids * @return */ @Override public Boolean removeBatchAndDataByIds(List ids) { boolean flag = false; int i = 0; for (Long id : ids) { Attach attach = getById(id); // 删除数据 boolean remove = removeById(id); if (remove) { i++; attach.setName(attach.getName().replaceAll("cloud-bucket/", "")); // 删除对应的数据 ossBuilder.template().removeFile(attach.getName()); } } if (i == ids.size()) { flag = true; } return flag; } @Override public List getAttachNames(List ids) { if (ids == null || ids.isEmpty()) { return Collections.emptyList(); } // 查询附件列表 List attaches = baseMapper.selectBatchIds(ids); // 处理 name 字段,去除 cloud-bucket 前缀 return attaches.stream().map(attach -> { String name = attach.getName(); if (name != null && name.startsWith("cloud-bucket/")) { return name.substring("cloud-bucket/".length()); } return name; // 如果不匹配,原样返回 }).collect(Collectors.toList()); } /** * @param param * @return */ private List getAttachList(AttachmentDownloadParam param) { // 查询附件列表 List attaches = baseMapper.getAttachList(param); // 处理 name 字段,去除 cloud-bucket 前缀 attaches.stream().forEach(attach -> { String name = attach.getName(); if (name != null && name.startsWith("cloud-bucket/")) { attach.setName(name.substring("cloud-bucket/".length())); } }); return attaches; } /** * 历史正射图片处理 * * @return */ @Override public Boolean hisOrtPicHandler() { // 查询正射的图片信息 List list = baseMapper.getHisOrtPicList(); // 处理正射的图片 for (Attach attach : list) { // 判断图片是否存在,不存在则删除 // 通过路径获取文件 String path = attach.getOriginalName(); File file = new File(path); // 检查文件是否存在 if (!file.exists()) { removeById(attach); log.error("文件 {} 不存在!删除记录", path); continue; } // 存在情况进行图片压缩 // 先判断是否已经压缩过了,如果已经压缩过了,就不再压缩 String smallTargetName = "_small.jpeg"; File smallOutFile = new File(file.getParent(), file.getName().replace(".tif", smallTargetName)); // 检查文件是否存在 if (smallOutFile.exists()) { log.info("文件 {} 存在!不再压缩", path); continue; } // 图片压缩 log.info("开始压缩tif图片,路径:{}", path); Map map = new HashMap<>(1); map.put("ort_path", path); TifConvertResult tifConvertResult = tifConvertClient.tifConvJpeg(map); log.info("tif 图片压缩结果:{}", tifConvertResult); } return null; } /** * 查询媒体属性根据图片,名称 * * @param names * @return */ @Override public Map findMetaDataByName(@Param("names") List names) { if (CollectionUtils.isEmpty(names)) { return Collections.emptyMap(); } String cloudBucket = "cloud-bucket"; names = Streams.toList(names, name -> cloudBucket + name); List metas = baseMapper.findMetaDataByName(names); Map map = Streams.toMap(metas, entity -> entity.getOriginalName().replace(cloudBucket, ""), entity -> entity.getMetadata()); return map; } @Override public void saveMd5() { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .select(Attach::getId, Attach::getLink) .isNull(Attach::getMd5); PaginationUtils.processInPages( baseMapper, // 你的Mapper实例 500, // 每页大小 wrapper, // 固定查询条件 page -> { // 每页数据处理 List attachList = page.getRecords(); // 业务处理逻辑 saveMd5Data(attachList); }); } public void saveMd5Data(List list) { if (CollectionUtils.isEmpty(list)) { return; } for (Attach attach : list) { LambdaUpdateWrapper update = Wrappers.lambdaUpdate() .set(Attach::getMd5, attach.getMd5()) .eq(Attach::getId, attach.getId()); baseMapper.update(update); } } /** * 获取地图上事件位置及数量 * * @param dto 参数 * @return list */ @Override public TreeVo mapAttachEvents(AttachQueryParam dto) { // 所有区域数据 Object regionResultz = RedisOpsUtils.get(CacheConstant.REGION_INFO + dto.getAreaCode()); if (Objects.isNull(regionResultz)) { R regionResult = sysClient.getRegionTreeByPrentCode(dto.getAreaCode()); RedisOpsUtils.setWithExpire(CacheConstant.REGION_INFO + dto.getAreaCode(), regionResult.getData(), TimeUnit.HOURS.toSeconds(1)); regionResultz = regionResult.getData(); } TreeVo data = regionResultz == null ? null : (TreeVo) regionResultz; String areaCode = HeaderUtils.getAreaCode(dto.getAreaCode()); dto.setAreaCode(areaCode); // 查询所有区域的数据 List list = baseMapper.mapAreaCodeAttachStatistics(dto); // 获取附件数据 List attachList = baseMapper.mapAttachList(dto); Map> attachMap = Streams.groupBy(attachList, AttachMapVo::getAreaCode); // 县下面的所有数量 key=区域code,value =数量 Map map = Streams.toMap(list, AttachStatisticsBo::getDictKey, AttachStatisticsBo::getNum); // 县区域下的所有事件数量 TreeVo treeVo = buildTreeVo(data, map, attachMap); return treeVo; } /** * 构建树 * * @param data 区域树 * @param map key=区域code,value=事件数 * @param eventMap key=区域code,value=事件列表 * @return */ public static TreeVo buildTreeVo(TreeVo data, Map map, Map> eventMap) { if (CollectionUtils.isEmpty(map) || data == null) { return null; } String id = data.getId(); // 明细事件列表 List jobEventList = eventMap.get(id); data.setData(jobEventList); List children = data.getChildrens(); // 总数 Integer totalNum = Streams.reduce(children, entity -> NoNullUtils.integer(map.get(entity.getId())), 0, Integer::sum); data.setNumber(Double.valueOf(totalNum)); Integer num = NoNullUtils.integer(map.get(data.getId())); if (num > 0) { data.setNumber(Double.valueOf(num)); } if (!CollectionUtils.isEmpty(children)) { for (TreeVo treeVo : children) { buildTreeVo(treeVo, map, eventMap); } } return data; } @Autowired @Qualifier("downloadTaskExecutor") private ExecutorService downloadTaskExecutor; @Override public AttachInfoVO getAttachInfo(Long id) { AttachInfoVO attachVO = baseMapper.getAttachInfo(id); if (attachVO != null) { // 不为空,获取经纬度 if (!ObjectUtils.isEmpty(attachVO.getMetadata())) { FileMetadataDTO metadata = JSON.parseObject(attachVO.getMetadata().toString(), FileMetadataDTO.class); PositionDTO shootPosition = metadata.getShootPosition(); ValidUtil.assertNull(shootPosition, "媒体数据不存在经纬度!"); Double lat = shootPosition.getLat(); Double lng = shootPosition.getLng(); } } return attachVO; } @Override public List getAttachInfoByJobId(String jobId) { List attachVO = baseMapper.getAttachInfoByJobId(jobId); // 不为空,获取经纬度 attachVO.forEach(item -> { if (!ObjectUtils.isEmpty(item.getMetadata())) { FileMetadataDTO metadata = JSON.parseObject(item.getMetadata().toString(), FileMetadataDTO.class); PositionDTO shootPosition = metadata.getShootPosition(); Double lat = shootPosition.getLat(); Double lng = shootPosition.getLng(); } }); return attachVO; } @Override public int updateFileName(Long id, String nickName) { UpdateWrapper objectUpdateWrapper = new UpdateWrapper<>(); objectUpdateWrapper.set("nick_name", nickName); objectUpdateWrapper.eq("id", id); return baseMapper.update(null, objectUpdateWrapper); } /** * 删除无用的链接地址 */ @Override public void deletedNotExistsUrl() { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .select(Attach::getId, Attach::getLink, Attach::getMd5) .like(Attach::getLink, ".jpeg"); PaginationUtils.processInPages( baseMapper, // 你的Mapper实例 500, // 每页大小 wrapper, // 固定查询条件 page -> { // 每页数据处理 List attachList = page.getRecords(); // 业务处理逻辑 for (Attach attach : attachList) { String link = attach.getLink(); boolean validate = HttpUtils.validateUrl(link); if (!validate) { // 进行删除 baseMapper.update(Wrappers.lambdaUpdate().set(Attach::getIsDeleted, 1).eq(Attach::getId, attach.getId())); // 删除事件里关联数据 baseMapper.deletedEventData(attach.getMd5()); } else { } } }); } /** * 查询成果数量 * * @param jobId * @return */ @Override public Long findResultNumByJobId(String jobId) { return baseMapper.selectCount(Wrappers.lambdaQuery() .eq(Attach::getIsDeleted, 0) .eq(Attach::getPatrolTaskId, jobId).in(Attach::getResultType, AttachResultTypeEnum.getTheAttachmentType())); } @Override /** * 批量删除媒体文件及其关联文件,并软删除数据库记录。 * * @param ids 要删除的记录ID列表 * @return 成功更新的数据库记录条数 */ public int deleteMediaFile(List ids) { if (ids == null || ids.isEmpty()) { log.warn("传入的ID列表为空,无需执行删除操作。"); return 0; } // --- 步骤 1: 查询初始要删除的源文件记录 --- List initialAttaches = baseMapper.selectBatchIds(ids); if (initialAttaches == null || initialAttaches.isEmpty()) { log.warn("根据传入的ID列表 {} 未找到任何数据库记录。", ids); return 0; } final String BUCKET_NAME = minioPojo.getBucket(); // --- 步骤 2: 准备所有待处理的数据列表 --- List minioObjectsToDelete = new ArrayList<>(); // 用于MinIO物理删除 List relatedFileNamesToSearchInDb = new ArrayList<>(); // 用于数据库二次查询 for (Attach attach : initialAttaches) { String fullPath = attach.getName(); // fullPath 是 "bucket/key" // 从完整路径中剥离出 bucket,得到纯粹的 object key String objectKey = fullPath; String prefix = BUCKET_NAME + "/"; if (objectKey.startsWith(prefix)) { objectKey = objectKey.substring(prefix.length()); } // 2.1 添加原始文件的 object key 到MinIO删除列表 minioObjectsToDelete.add(objectKey); // 2.2 根据文件类型,构造其所有关联文件的 key 和 name if ("mp4".equals(attach.getExtension())) { String showKey = objectKey.replaceAll("(\\.[^.]+)$", "_show$1"); String smallJpgKey = objectKey.replaceAll("(\\.[^.]+)$", "_small.jpg"); minioObjectsToDelete.add(showKey); minioObjectsToDelete.add(smallJpgKey); // 将关联文件的完整 name (含bucket) 添加到数据库查询列表 relatedFileNamesToSearchInDb.add(BUCKET_NAME + "/" + showKey); relatedFileNamesToSearchInDb.add(BUCKET_NAME + "/" + smallJpgKey); } else if ("jpeg".equals(attach.getExtension()) || "jpg".equals(attach.getExtension())) { String showKey = objectKey.replaceAll("(\\.[^.]+)$", "_show$1"); String smallKey = objectKey.replaceAll("(\\.[^.]+)$", "_small$1"); String markKey = objectKey.replaceAll("(\\.[^.]+)$", "_mark$1"); minioObjectsToDelete.add(showKey); minioObjectsToDelete.add(smallKey); minioObjectsToDelete.add(markKey); relatedFileNamesToSearchInDb.add(BUCKET_NAME + "/" + showKey); relatedFileNamesToSearchInDb.add(BUCKET_NAME + "/" + smallKey); relatedFileNamesToSearchInDb.add(BUCKET_NAME + "/" + markKey); } } // --- 步骤 3: 执行MinIO物理文件删除 --- log.info("准备从MinIO批量删除 {} 个对象...", minioObjectsToDelete.size()); MinioFileDownloader.deleteObjects( minioPojo.getPath(), minioPojo.getAccessKey(), minioPojo.getSecretKey(), BUCKET_NAME, minioObjectsToDelete ); log.info("MinIO对象删除请求已成功发送。"); // --- 步骤 4: 执行数据库软删除 (包括源文件和所有关联文件) --- // 4.1 初始化最终待更新的ID列表,包含源文件ID List allIdsToUpdate = new ArrayList<>(ids); // 4.2 查询所有关联文件的ID if (!relatedFileNamesToSearchInDb.isEmpty()) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper .select(Attach::getId) .in(Attach::getName, relatedFileNamesToSearchInDb); List relatedAttaches = baseMapper.selectList(queryWrapper); if (relatedAttaches != null && !relatedAttaches.isEmpty()) { List relatedIds = relatedAttaches.stream() .map(Attach::getId) .collect(Collectors.toList()); log.info("查询到 {} 个关联文件的数据库记录ID需要被软删除。", relatedIds.size()); allIdsToUpdate.addAll(relatedIds); } } // 4.3 去重并执行最终的批量更新 List finalIdsToUpdate = allIdsToUpdate.stream().distinct().collect(Collectors.toList()); if (finalIdsToUpdate.isEmpty()) { log.warn("逻辑执行完毕,但没有需要更新的数据库记录。"); return 0; } LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper .set(Attach::getIsDeleted, 1) .in(Attach::getId, finalIdsToUpdate); int updatedRows = baseMapper.update(null, updateWrapper); log.info("成功软删除了 {} 条数据库记录 (包括源文件和所有关联文件)。", updatedRows); return updatedRows; } @Override public List attachTypeStatistics(AttachStatisticsVo attachStatisticsVo) { List deptIdList = SysCache.getDeptChildIds(Long.valueOf(AuthUtil.getDeptId())); String permissionCondition = permissionBuilder.buildDataPermissionCondition(AuthUtil.getUserId(), "attach"); attachStatisticsVo.setAreaCode(HeaderUtils.getAreaCode()); attachStatisticsVo.setResultTypeList(Arrays.asList(3, 10)); List attachTypeStatisticsVOS = baseMapper.attachTypeStatistics(attachStatisticsVo, deptIdList, permissionCondition); AttachTypeStatisticsVO attachTypeStatisticsVOS1 = baseMapper.calculateTheThreeDimensionalArea(attachStatisticsVo, deptIdList, permissionCondition); if (attachTypeStatisticsVOS.size() == 0) { for (int i = 1; i <= 5; i++) { attachTypeStatisticsVOS.add(createDefaultTypeVO(i + "")); } } for (AttachTypeStatisticsVO attachTypeStatisticsVO : attachTypeStatisticsVOS) { if (attachTypeStatisticsVO.getType().equals("3")) { if (attachTypeStatisticsVOS1 != null && attachTypeStatisticsVOS1.getArea() != null) { // 修复NumberFormatException错误,将area值转换为Long时先转换为Double再取整 attachTypeStatisticsVO.setCount(attachTypeStatisticsVOS1.getArea()); } else { // 如果没有三维面积数据,则设置默认值0 attachTypeStatisticsVO.setCount(0d); } } } return attachTypeStatisticsVOS; } @Override public List findAttachInfoByYesterday(int resultType, Date startTime, Date endTime) { // 获取昨天日期范围 LocalDateTime yesterdayStart = LocalDateTime.now().minusDays(1).with(LocalTime.MIN); LocalDateTime yesterdayEnd = LocalDateTime.now().minusDays(1).with(LocalTime.MAX); // 如何开始时间和结束时间有值按传递的参数获取 if (startTime != null && endTime != null) { yesterdayStart = LocalDateTime.ofInstant(startTime.toInstant(), ZoneId.systemDefault()); yesterdayEnd = LocalDateTime.ofInstant(startTime.toInstant(), ZoneId.systemDefault()); } QueryWrapper queryWrapper = new QueryWrapper<>(); // 使用传入的resultType参数 queryWrapper.eq("result_type", resultType); // 使用between queryWrapper.between("create_time", yesterdayStart, yesterdayEnd); return baseMapper.selectList(queryWrapper); } private AttachTypeStatisticsVO createDefaultTypeVO(String type) { AttachTypeStatisticsVO vo = new AttachTypeStatisticsVO(); vo.setType(type); vo.setCount(0d); return vo; } @Override public List getManageAttachTypeStatistics(WaylineJobInfoQueryParam param) { param.setAreaCode(HeaderUtils.getAreaCode()); List deptIdList = SysCache.getDeptChildIds(Long.valueOf(AuthUtil.getDeptId())); String permissionCondition = permissionBuilder.buildDataPermissionCondition(AuthUtil.getUserId(), "attach"); List attachTypeStatisticsVOS = baseMapper.getManageAttachTypeStatistics(param, deptIdList, permissionCondition); // attachTypeStatisticsVOS 统计总数 double total = attachTypeStatisticsVOS.stream().mapToDouble(vo -> vo.getCount()).sum(); List xAxisData = XAxisDataInitializer.initXAxisDataToNow(param.getDateEnum()); List list = new ArrayList<>(); for (int index = 0; index < xAxisData.size(); index++) { String xAxisDatum = xAxisData.get(index); LineColumnDateVo lineColumnDateVo = new LineColumnDateVo(); List dataVoList = attachTypeStatisticsVOS.stream().filter(item -> item.getTimeStr().equals(xAxisDatum)) .map(item -> { ChartDataVo chartDataVo = new ChartDataVo(); chartDataVo.setValue(item.getCount()); chartDataVo.setName(item.getType()); return chartDataVo; }).collect(Collectors.toList()); lineColumnDateVo.setData(dataVoList); lineColumnDateVo.setName(ChartDataVo.formatTime(param.getDateEnum(), xAxisDatum, index)); lineColumnDateVo.setTotal(total); list.add(lineColumnDateVo); } return list; } /** * @param deviceSn * @param timestamp * @param operator * @param workspaceId * @return */ @Override public Boolean deleteAttach(String deviceSn, long timestamp, Long operator, String workspaceId) { return baseMapper.deleteAttach(deviceSn, timestamp, operator, workspaceId); } @Override public List getlotInfoList(Long lotInfoId) { return baseMapper.getlotInfoList(lotInfoId); } /** * 统计三维面积 * * @param deviceSn * @param startDate * @param endDate * @param resultTypes * @return */ @Override public List calculateTheThreeDimensionalArea(String deviceSn, String startDate, String endDate, String resultTypes, String areaCode) { AttachStatisticsVo attachStatisticsVo = new AttachStatisticsVo(); attachStatisticsVo.setDeviceSn(deviceSn); attachStatisticsVo.setStartDate(startDate); attachStatisticsVo.setEndDate(endDate); attachStatisticsVo.setResultTypeList(Func.toIntList(resultTypes)); attachStatisticsVo.setAreaCode(areaCode); // 如果是三维类型,jobtyp = 6 否则就是5 查询倾斜的信息 attachStatisticsVo.setJobType(attachStatisticsVo.getResultTypeList().contains(3) ? 6 : 5); List deptIdList = SysCache.getDeptChildIds(Long.valueOf(AuthUtil.getDeptId())); String permissionCondition = permissionBuilder.buildDataPermissionCondition(AuthUtil.getUserId(), "attach"); List attachTypeStatisticsVOS = baseMapper.calculateTheThreeDimensionalAreaGroupBy(attachStatisticsVo, deptIdList, permissionCondition); return attachTypeStatisticsVOS; } /** * 下载附件 * * @param param 下载参数 * @param outputStream 输出流 * @return * @throws IOException */ @Override public Boolean downloadByByte(String param, OutputStream outputStream) throws IOException { AttachmentDownloadParam attachmentDownloadParam = new AttachmentDownloadParam(); attachmentDownloadParam.setAttachIds(Func.toLongList(param)); // 获取附件列表 List attachList = getAttachList(attachmentDownloadParam); 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 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()); } } } } }