/*
|
* 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<AttachMapper, Attach> 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<AttachVO> selectAttachPage(IPage<AttachVO> page, AttachPageParam attach) {
|
List<AttachVO> attachVOS = baseMapper.selectAttachPage(page, attach);
|
return page.setRecords(attachVOS);
|
}
|
|
|
@Override
|
public IPage<AttachVO> findAttachImages(IPage<AttachVO> 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<Long> deptIdList = SysCache.getDeptChildIds(Long.valueOf(AuthUtil.getDeptId()));
|
String permissionCondition = permissionBuilder.buildDataPermissionCondition(AuthUtil.getUserId(), "attach");
|
List<AttachVO> aiImages = baseMapper.findAttachImages(page, attach, deptIdList, permissionCondition, AuthUtil.getUserId());
|
return page.setRecords(aiImages);
|
}
|
|
|
/**
|
* ai数据
|
*
|
* @param page
|
* @param attach
|
* @return
|
*/
|
@Override
|
public List<AttachVO> findAiAttachImages(IPage page, AttachVO attach) {
|
List<Long> 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<String> findAiEventMd5s(List<Long> eventRecordIds) {
|
if (CollectionUtils.isEmpty(eventRecordIds)) {
|
return null;
|
}
|
List<EventImgMd5VO> list = baseMapper.findAllMd5ByAi(eventRecordIds);
|
// 原有的图片
|
List<String> aiMd5s = baseMapper.findAiMd5s(eventRecordIds);
|
List<List<String>> allMd5 = aiMd5s.stream()
|
.map(entity -> Arrays.asList(entity.split(",")))
|
.collect(Collectors.toList());
|
// 原始图片id
|
// List<String> allIds = Streams.flatMap(allMd5);
|
List<String> md5s = Streams.toList(list, entity -> !StringUtils.isEmpty(entity.getMd5()), EventImgMd5VO::getMd5);
|
// List<String> md5Orgs = Streams.toList(list, entity -> !StringUtils.isEmpty(entity.getOriginalMd5()), EventImgMd5VO::getOriginalMd5);
|
// allIds.removeAll(md5Orgs);
|
List<String> all = new ArrayList<>(md5s);
|
// all.addAll(allIds);
|
if (CollectionUtils.isEmpty(all)) {
|
return Arrays.asList("0");
|
} else {
|
return all;
|
}
|
}
|
|
|
@Override
|
public List<DeptAndSizeVo> 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<Long> 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<String> getAttachNames(List<Long> ids) {
|
if (ids == null || ids.isEmpty()) {
|
return Collections.emptyList();
|
}
|
|
// 查询附件列表
|
List<Attach> 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<AttachVO> getAttachList(AttachmentDownloadParam param) {
|
// 查询附件列表
|
List<AttachVO> 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<Attach> 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<String, Object> 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<String, Object> findMetaDataByName(@Param("names") List<String> names) {
|
if (CollectionUtils.isEmpty(names)) {
|
return Collections.emptyMap();
|
}
|
String cloudBucket = "cloud-bucket";
|
names = Streams.toList(names, name -> cloudBucket + name);
|
List<Attach> metas = baseMapper.findMetaDataByName(names);
|
Map<String, Object> map = Streams.toMap(metas, entity -> entity.getOriginalName().replace(cloudBucket, ""), entity -> entity.getMetadata());
|
return map;
|
}
|
|
@Override
|
public void saveMd5() {
|
LambdaQueryWrapper<Attach> wrapper = Wrappers.<Attach>lambdaQuery()
|
.select(Attach::getId, Attach::getLink)
|
.isNull(Attach::getMd5);
|
|
PaginationUtils.processInPages(
|
baseMapper, // 你的Mapper实例
|
500, // 每页大小
|
wrapper, // 固定查询条件
|
page -> { // 每页数据处理
|
List<Attach> attachList = page.getRecords();
|
// 业务处理逻辑
|
saveMd5Data(attachList);
|
});
|
|
}
|
|
public void saveMd5Data(List<Attach> list) {
|
if (CollectionUtils.isEmpty(list)) {
|
return;
|
}
|
for (Attach attach : list) {
|
LambdaUpdateWrapper<Attach> update = Wrappers.<Attach>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<TreeVo> 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<AttachStatisticsBo> list = baseMapper.mapAreaCodeAttachStatistics(dto);
|
// 获取附件数据
|
List<AttachMapVo> attachList = baseMapper.mapAttachList(dto);
|
Map<String, List<AttachMapVo>> attachMap = Streams.groupBy(attachList, AttachMapVo::getAreaCode);
|
// 县下面的所有数量 key=区域code,value =数量
|
Map<String, Integer> 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<String, Integer> map, Map<String, List<AttachMapVo>> eventMap) {
|
if (CollectionUtils.isEmpty(map) || data == null) {
|
return null;
|
}
|
String id = data.getId();
|
// 明细事件列表
|
List jobEventList = eventMap.get(id);
|
data.setData(jobEventList);
|
List<TreeVo> 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<AttachInfoVO> getAttachInfoByJobId(String jobId) {
|
List<AttachInfoVO> 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<Attach> objectUpdateWrapper = new UpdateWrapper<>();
|
objectUpdateWrapper.set("nick_name", nickName);
|
objectUpdateWrapper.eq("id", id);
|
return baseMapper.update(null, objectUpdateWrapper);
|
}
|
|
|
/**
|
* 删除无用的链接地址
|
*/
|
@Override
|
public void deletedNotExistsUrl() {
|
LambdaQueryWrapper<Attach> wrapper = Wrappers.<Attach>lambdaQuery()
|
.select(Attach::getId, Attach::getLink, Attach::getMd5)
|
.like(Attach::getLink, ".jpeg");
|
|
PaginationUtils.processInPages(
|
baseMapper, // 你的Mapper实例
|
500, // 每页大小
|
wrapper, // 固定查询条件
|
page -> { // 每页数据处理
|
List<Attach> attachList = page.getRecords();
|
// 业务处理逻辑
|
for (Attach attach : attachList) {
|
String link = attach.getLink();
|
boolean validate = HttpUtils.validateUrl(link);
|
if (!validate) {
|
// 进行删除
|
baseMapper.update(Wrappers.<Attach>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.<Attach>lambdaQuery()
|
.eq(Attach::getIsDeleted, 0)
|
.eq(Attach::getPatrolTaskId, jobId).in(Attach::getResultType, AttachResultTypeEnum.getTheAttachmentType()));
|
}
|
|
@Override
|
/**
|
* 批量删除媒体文件及其关联文件,并软删除数据库记录。
|
*
|
* @param ids 要删除的记录ID列表
|
* @return 成功更新的数据库记录条数
|
*/
|
public int deleteMediaFile(List<Long> ids) {
|
if (ids == null || ids.isEmpty()) {
|
log.warn("传入的ID列表为空,无需执行删除操作。");
|
return 0;
|
}
|
|
// --- 步骤 1: 查询初始要删除的源文件记录 ---
|
List<Attach> initialAttaches = baseMapper.selectBatchIds(ids);
|
if (initialAttaches == null || initialAttaches.isEmpty()) {
|
log.warn("根据传入的ID列表 {} 未找到任何数据库记录。", ids);
|
return 0;
|
}
|
|
final String BUCKET_NAME = minioPojo.getBucket();
|
|
// --- 步骤 2: 准备所有待处理的数据列表 ---
|
List<String> minioObjectsToDelete = new ArrayList<>(); // 用于MinIO物理删除
|
List<String> 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<Long> allIdsToUpdate = new ArrayList<>(ids);
|
|
// 4.2 查询所有关联文件的ID
|
if (!relatedFileNamesToSearchInDb.isEmpty()) {
|
LambdaQueryWrapper<Attach> queryWrapper = new LambdaQueryWrapper<>();
|
queryWrapper
|
.select(Attach::getId)
|
.in(Attach::getName, relatedFileNamesToSearchInDb);
|
|
List<Attach> relatedAttaches = baseMapper.selectList(queryWrapper);
|
|
if (relatedAttaches != null && !relatedAttaches.isEmpty()) {
|
List<Long> relatedIds = relatedAttaches.stream()
|
.map(Attach::getId)
|
.collect(Collectors.toList());
|
log.info("查询到 {} 个关联文件的数据库记录ID需要被软删除。", relatedIds.size());
|
allIdsToUpdate.addAll(relatedIds);
|
}
|
}
|
|
// 4.3 去重并执行最终的批量更新
|
List<Long> finalIdsToUpdate = allIdsToUpdate.stream().distinct().collect(Collectors.toList());
|
if (finalIdsToUpdate.isEmpty()) {
|
log.warn("逻辑执行完毕,但没有需要更新的数据库记录。");
|
return 0;
|
}
|
|
LambdaUpdateWrapper<Attach> 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<AttachTypeStatisticsVO> attachTypeStatistics(AttachStatisticsVo attachStatisticsVo) {
|
|
List<Long> 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<AttachTypeStatisticsVO> 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<Attach> 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<Attach> 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<LineColumnDateVo> getManageAttachTypeStatistics(WaylineJobInfoQueryParam param) {
|
param.setAreaCode(HeaderUtils.getAreaCode());
|
List<Long> deptIdList = SysCache.getDeptChildIds(Long.valueOf(AuthUtil.getDeptId()));
|
String permissionCondition = permissionBuilder.buildDataPermissionCondition(AuthUtil.getUserId(), "attach");
|
List<AttachTypeStatisticsVO> attachTypeStatisticsVOS = baseMapper.getManageAttachTypeStatistics(param, deptIdList, permissionCondition);
|
// attachTypeStatisticsVOS 统计总数
|
double total = attachTypeStatisticsVOS.stream().mapToDouble(vo -> vo.getCount()).sum();
|
List<String> xAxisData = XAxisDataInitializer.initXAxisDataToNow(param.getDateEnum());
|
List<LineColumnDateVo> list = new ArrayList<>();
|
for (int index = 0; index < xAxisData.size(); index++) {
|
String xAxisDatum = xAxisData.get(index);
|
LineColumnDateVo lineColumnDateVo = new LineColumnDateVo();
|
List<ChartDataVo> 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<Attach> getlotInfoList(Long lotInfoId) {
|
return baseMapper.getlotInfoList(lotInfoId);
|
}
|
|
/**
|
* 统计三维面积
|
*
|
* @param deviceSn
|
* @param startDate
|
* @param endDate
|
* @param resultTypes
|
* @return
|
*/
|
@Override
|
public List<AttachTypeStatisticsVO> 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<Long> deptIdList = SysCache.getDeptChildIds(Long.valueOf(AuthUtil.getDeptId()));
|
String permissionCondition = permissionBuilder.buildDataPermissionCondition(AuthUtil.getUserId(), "attach");
|
List<AttachTypeStatisticsVO> attachTypeStatisticsVOS = baseMapper.calculateTheThreeDimensionalAreaGroupBy(attachStatisticsVo, deptIdList, permissionCondition);
|
return attachTypeStatisticsVOS;
|
}
|
|
/**
|
* 下载附件
|
* @param param 下载参数
|
* @param outputStream 输出流
|
* @return
|
* @throws IOException
|
*/
|
@Override
|
public Boolean downloadByByte(AttachmentDownloadParam param, OutputStream outputStream) throws IOException {
|
// 如果附件id集合并且任务id集合参数都为空,就判断起止时间是否都有为空,如果都为空,则返回错误
|
param.setAreaCode(HeaderUtils.formatAreaCode(param.getAreaCode()));
|
if (CollectionUtils.isEmpty(param.getAttachIds())) {
|
// 判断起止时间是否为空
|
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());
|
}
|
}
|
}
|
}
|
|
}
|