大图合并图片压缩,考试、人员文件压缩包上传

This commit is contained in:
lcj
2025-11-18 19:28:32 +08:00
parent 1ab47ccb57
commit 3f12c15cf8
14 changed files with 718 additions and 179 deletions

View File

@ -336,8 +336,8 @@ ys7:
app-key: 3acf9f1a43dc4209841e0893003db0a2
app-secret: 09e29c70ae1161fbc3ce2030fc09ba2e
job:
capture-enabled: false # 控制是否启用萤石抓拍任务
device-sync-enabled: false # 控制是否同步萤石设备
capture-enabled: true # 控制是否启用萤石抓拍任务
device-sync-enabled: true # 控制是否同步萤石设备
# 斯巴达算法
sparta:
url: http://119.3.204.120:8040

View File

@ -29,6 +29,23 @@
<dependencies>
<!-- TwelveMonkeys ImageIO 扩展 -->
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-core</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-webp</artifactId>
<version>3.12.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.sejda.imageio/webp-imageio -->
<dependency>
<groupId>org.sejda.imageio</groupId>
<artifactId>webp-imageio</artifactId>
<version>0.1.6</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>

View File

@ -0,0 +1,28 @@
package org.dromara.common.domain;
import lombok.Data;
import java.io.InputStream;
/**
* @author lilemy
* @date 2025-11-18 14:38
*/
@Data
public class WebpConverterStreamVo {
/**
* 图片输入流
*/
private InputStream inputStream;
/**
* 图片长度
*/
private long length;
/**
* 图片类型
*/
private String contentType;
}

View File

@ -0,0 +1,187 @@
package org.dromara.common.utils;
import org.dromara.common.domain.WebpConverterStreamVo;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
/**
* @author lilemy
* @date 2025-11-18 14:09
*/
public class WebpConverterUtil {
/**
* 将 PNG 图片转换为 WebP
*
* @param inputFile 输入文件
* @param outputFile 输出文件
* @param quality 压缩质量(0.0 ~ 1.0)
*/
public static void convertPngToWebp(File inputFile, File outputFile, float quality) throws IOException {
// 读取 PNG 图片
BufferedImage image = ImageIO.read(inputFile);
// 获取 WebP writer
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
if (!writers.hasNext()) {
throw new IllegalStateException("未找到 WebP ImageWriter检查依赖是否正确");
}
ImageWriter writer = writers.next();
// 设置压缩质量
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
String[] types = param.getCompressionTypes();
if (types != null && types.length > 0) {
param.setCompressionType(types[0]); // 默认一般是 "Lossy"
}
param.setCompressionQuality(quality); // 0.0 ~ 1.0(越小越压缩)
try (FileImageOutputStream output = new FileImageOutputStream(outputFile)) {
writer.setOutput(output);
writer.write(null, new IIOImage(image, null, null), param);
} finally {
writer.dispose();
}
}
/**
* 从 URL 读取图片并转换为 WebP
*
* @param imageUrl 图片对象存储 URL
* @param outputFile 输出文件
* @param quality WebP 压缩质量 (0.0 ~ 1.0)
*/
public static void convertPngUrlToWebp(String imageUrl, File outputFile, float quality) throws IOException, URISyntaxException {
// 1. 根据 URL 读取图片
BufferedImage image = ImageIO.read(new URI(imageUrl).toURL());
if (image == null) {
throw new IOException("无法从 URL 加载图片URL = " + imageUrl);
}
// 2. 获取 WebP writer
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
if (!writers.hasNext()) {
throw new IllegalStateException("未找到 WebP ImageWriter请检查 TwelveMonkeys 依赖!");
}
ImageWriter writer = writers.next();
// 3. 设置压缩质量
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
String[] types = param.getCompressionTypes();
if (types != null && types.length > 0) {
param.setCompressionType(types[0]); // 默认一般是 "Lossy"
}
param.setCompressionQuality(quality);
// 4. 写出 WebP
try (FileImageOutputStream output = new FileImageOutputStream(outputFile)) {
writer.setOutput(output);
writer.write(null, new IIOImage(image, null, null), param);
} finally {
writer.dispose();
}
}
/**
* 从 URL 加载 PNG/JPG 等图片,压缩分辨率后转换为 WebP并返回 InputStream
*
* @param imageUrl 图片对象存储 URL
* @param quality WebP 压缩质量 (0.0 ~ 1.0)
* @param targetWidth 目标宽度(为 0 表示按高度等比例缩放)
* @param targetHeight 目标高度(为 0 表示按宽度等比例缩放)
*/
public static WebpConverterStreamVo convertUrlToWebpStream(
String imageUrl,
float quality,
int targetWidth,
int targetHeight
) throws IOException, URISyntaxException {
// 1. 加载 URL 图片
BufferedImage original = ImageIO.read(new URI(imageUrl).toURL());
if (original == null) {
throw new IOException("无法从 URL 加载图片: " + imageUrl);
}
// 2. 计算目标宽高(支持等比例缩放)
int width = original.getWidth();
int height = original.getHeight();
if (targetWidth > 0 && targetHeight > 0) {
// 固定分辨率
width = targetWidth;
height = targetHeight;
} else if (targetWidth > 0) {
// 高度自适应
height = original.getHeight() * targetWidth / original.getWidth();
width = targetWidth;
} else if (targetHeight > 0) {
// 宽度自适应
width = original.getWidth() * targetHeight / original.getHeight();
height = targetHeight;
}
// 否则都为 0则保持原图大小
// 3. 按分辨率缩放
Image scaledInstance = original.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage resized = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = resized.createGraphics();
g.drawImage(scaledInstance, 0, 0, null);
g.dispose();
// 4. 获取 WebP writer
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("webp");
if (!writers.hasNext()) {
throw new IllegalStateException("未找到 WebP ImageWriter请确认 TwelveMonkeys 依赖已正确导入!");
}
ImageWriter writer = writers.next();
// 5. 设置压缩参数
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
String[] types = param.getCompressionTypes();
if (types != null && types.length > 0) {
param.setCompressionType(types[0]);
}
param.setCompressionQuality(quality);
// 6. 输出到流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (MemoryCacheImageOutputStream output = new MemoryCacheImageOutputStream(baos)) {
writer.setOutput(output);
writer.write(null, new IIOImage(resized, null, null), param);
} finally {
writer.dispose();
}
// 7. 返回
WebpConverterStreamVo vo = new WebpConverterStreamVo();
vo.setInputStream(new ByteArrayInputStream(baos.toByteArray()));
vo.setLength(baos.size());
vo.setContentType("image/webp");
return vo;
}
public static void main(String[] args) throws Exception {
File input = new File("input.jpeg");
File output = new File("output.webp");
convertPngToWebp(input, output, 0.6f);
System.out.println("转换完成!");
}
}

View File

@ -227,56 +227,8 @@ public class SubConstructionUserFileServiceImpl extends ServiceImpl<SubConstruct
// 3. 解压 zip
destDir = new File(destDirPath);
ZipUtil.unzip(tempZipFile, destDir);
// 4. 遍历最外层文件夹
File[] userFolders = destDir.listFiles();
if (userFolders != null) {
for (File userFolder : userFolders) {
if (userFolder.isDirectory()) {
String userFolderName = userFolder.getName(); // 李四-1905161272755195006
// 5. 解析 userId
long userId = Long.parseLong(userFolderName.substring(userFolderName.lastIndexOf("-") + 1));
// 6. 继续遍历每个用户子文件夹里的 1_合同, 2_体检报告, ...
File[] docFolders = userFolder.listFiles();
if (docFolders != null) {
for (File docFolder : docFolders) {
if (docFolder.isDirectory()) {
String docFolderName = docFolder.getName(); // 1_合同
String[] docParts = docFolderName.split("_");
String fileType = docParts[0];
// 7. 获取该文件夹下所有文件
File[] filesInDocFolder = docFolder.listFiles();
String fileIdStr = null;
if (filesInDocFolder != null) {
// 遍历文件
List<Long> fileIds = new ArrayList<>();
for (File file : filesInDocFolder) {
if (file.isFile()) {
SysOssVo upload = ossService.upload(file);
if (upload != null) {
fileIds.add(upload.getOssId());
}
}
}
// 跳过空文件
if (fileIds.isEmpty()) {
continue;
}
fileIdStr = fileIds.stream()
.map(String::valueOf)
.collect(Collectors.joining(","));
}
// 8. 创建 BusConstructionUserFile 对象
SubConstructionUserFile constructionUserFile = new SubConstructionUserFile();
constructionUserFile.setUserId(userId);
constructionUserFile.setFileType(fileType);
constructionUserFile.setPath(fileIdStr);
constructionUserFileList.add(constructionUserFile);
}
}
}
}
}
}
// 4. 递归扫描目录
scanUserFolders(destDir, constructionUserFileList);
} catch (Exception e) {
throw new ServiceException("文件上传失败", HttpStatus.ERROR);
} finally {
@ -495,4 +447,68 @@ public class SubConstructionUserFileServiceImpl extends ServiceImpl<SubConstruct
}
return resultMap;
}
/**
* 递归扫描指定目录,找到 “姓名-Id” 格式的文件夹并解析
*/
public void scanUserFolders(File root, List<SubConstructionUserFile> resultList) {
if (root == null || !root.exists()) {
return;
}
File[] files = root.listFiles();
if (files == null) return;
for (File file : files) {
if (file.isDirectory()) {
// 判断是否符合 “姓名-数字ID” 格式
if (file.getName().matches(".+-\\d+")) {
// 解析 UserId
long userId = Long.parseLong(file.getName().replaceAll(".*-", ""));
parseUserDocFolder(file, userId, resultList);
} else {
// 目录不是目标,继续递归查找
scanUserFolders(file, resultList);
}
}
}
}
/**
* 解析用户目录下的 1_合同、2_体检报告 等目录
*/
private void parseUserDocFolder(File userFolder, long userId, List<SubConstructionUserFile> resultList) {
File[] docFolders = userFolder.listFiles();
if (docFolders == null) return;
for (File docFolder : docFolders) {
if (!docFolder.isDirectory()) continue;
String folderName = docFolder.getName(); // 例如1_合同
String[] parts = folderName.split("_");
if (parts.length < 2) continue;
String fileType = parts[0];
// 获取所有文件
File[] files = docFolder.listFiles();
if (files == null) continue;
List<Long> fileIds = new ArrayList<>();
for (File item : files) {
if (item.isFile()) {
SysOssVo upload = ossService.upload(item);
if (upload != null) {
fileIds.add(upload.getOssId());
}
}
}
if (fileIds.isEmpty()) continue;
// 生成对象
SubConstructionUserFile u = new SubConstructionUserFile();
u.setUserId(userId);
u.setFileType(fileType);
u.setPath(fileIds.stream().map(String::valueOf).collect(Collectors.joining(",")));
resultList.add(u);
}
}
}

View File

@ -115,6 +115,6 @@ public class DroDroneBigPictureController extends BaseController {
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(droDroneBigPictureService.deleteWithValidByIds(List.of(ids), true));
return toAjax(droDroneBigPictureService.deleteByIds(List.of(ids)));
}
}

View File

@ -48,6 +48,11 @@ public class DroDroneBigPicture extends BaseEntity {
*/
private String smallPic;
/**
* 压缩图片
*/
private String compressPic;
/**
* 大图
*/

View File

@ -54,6 +54,11 @@ public class DroDroneBigPictureBo extends BaseEntity {
*/
private String smallPic;
/**
* 压缩图片
*/
private String compressPic;
/**
* 大图
*/

View File

@ -51,6 +51,17 @@ public class DroDroneBigPictureVo implements Serializable {
@ExcelProperty(value = "任务名称")
private String taskName;
/**
* 压缩图片
*/
private String compressPic;
/**
* 小图片列表 Url
*/
@Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "compressPic")
private String compressPicList;
/**
* 小图片列表
*/

View File

@ -10,6 +10,7 @@ import org.dromara.drone.domain.vo.DroDroneBigPictureVo;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* 无人机大图信息Service接口
@ -69,13 +70,12 @@ public interface IDroDroneBigPictureService extends IService<DroDroneBigPicture>
Boolean createProgressRecognize(Long id);
/**
* 校验并批量删除无人机大图信息信息
* 批量删除无人机大图信息信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @param ids 待删除的主键集合
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
Boolean deleteByIds(Collection<Long> ids);
/**
* 是否合成完成
@ -90,4 +90,30 @@ public interface IDroDroneBigPictureService extends IService<DroDroneBigPicture>
* @param pictureVo 大图信息
*/
DroDroneBigPictureProgressVo isSynthesisCompleted(DroDroneBigPictureVo pictureVo);
/**
* 压缩图片
*
* @param ossIds 图片对象存储Ids
* @param compressPicIds 压缩后图片Ids
* @return 是否成功
*/
Boolean compressPicture(String ossIds, List<Long> compressPicIds);
/**
* 异步添加压缩图片
*
* @param smallPic 压缩图片
* @return 是否成功
*/
CompletableFuture<Boolean> asyncAddCompressPicture(String smallPic, List<Long> compressPicIds);
/**
* 异步更新压缩图片
*
* @param pic 图片
* @param oldPic 旧图片
* @return 是否成功
*/
CompletableFuture<Boolean> asyncUpdateCompressPicture(DroDroneBigPicture pic, DroDroneBigPicture oldPic);
}

View File

@ -1,6 +1,7 @@
package org.dromara.drone.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@ -11,8 +12,12 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.domain.WebpConverterStreamVo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.oss.core.OssClient;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.utils.WebpConverterUtil;
import org.dromara.drone.domain.DroDroneBigPicture;
import org.dromara.drone.domain.bo.DroDroneBigPictureBo;
import org.dromara.drone.domain.bo.DroDroneBigPictureProgressVo;
@ -24,14 +29,21 @@ import org.dromara.manager.dronemanager.vo.DroneImgMergeProgressVo;
import org.dromara.manager.dronemanager.vo.DroneImgMergeUrlVo;
import org.dromara.progress.domain.dto.progressplandetail.PgsProgressPlanDetailAINumberReq;
import org.dromara.progress.service.IPgsProgressPlanDetailService;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.service.ISysOssService;
import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.math.RoundingMode;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/**
* 无人机大图信息Service业务层处理
@ -52,6 +64,13 @@ public class DroDroneBigPictureServiceImpl extends ServiceImpl<DroDroneBigPictur
@Resource
private DroneManager droneManager;
@Resource
private ISysOssService ossService;
@Lazy
@Resource
private IDroDroneBigPictureService self;
/**
* 查询无人机大图信息
*
@ -68,7 +87,10 @@ public class DroDroneBigPictureServiceImpl extends ServiceImpl<DroDroneBigPictur
if (StringUtils.isNotBlank(progressVo.getStatus())) {
pictureVo.setStatus(progressVo.getStatus());
}
pictureVo.setProgress(progressVo.getProgress().multiply(new BigDecimal("100")));
BigDecimal p = progressVo.getProgress()
.multiply(new BigDecimal("100"));
pictureVo.setProgress(p.compareTo(BigDecimal.valueOf(100)) == 0 ?
BigDecimal.valueOf(100) : p.setScale(4, RoundingMode.HALF_UP));
}
} catch (Exception e) {
log.error("查询图片合成进度异常", e);
@ -98,7 +120,10 @@ public class DroDroneBigPictureServiceImpl extends ServiceImpl<DroDroneBigPictur
if (StringUtils.isNotBlank(progressVo.getStatus())) {
pictureVo.setStatus(progressVo.getStatus());
}
pictureVo.setProgress(progressVo.getProgress().multiply(new BigDecimal("100")));
BigDecimal p = progressVo.getProgress()
.multiply(new BigDecimal("100"));
pictureVo.setProgress(p.compareTo(BigDecimal.valueOf(100)) == 0 ?
BigDecimal.valueOf(100) : p.setScale(4, RoundingMode.HALF_UP));
}
} catch (Exception e) {
pictureVo.setStatus("4");
@ -124,7 +149,6 @@ public class DroDroneBigPictureServiceImpl extends ServiceImpl<DroDroneBigPictur
}
private LambdaQueryWrapper<DroDroneBigPicture> buildQueryWrapper(DroDroneBigPictureBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<DroDroneBigPicture> lqw = Wrappers.lambdaQuery();
lqw.orderByDesc(DroDroneBigPicture::getId);
lqw.eq(bo.getProjectId() != null, DroDroneBigPicture::getProjectId, bo.getProjectId());
@ -146,12 +170,33 @@ public class DroDroneBigPictureServiceImpl extends ServiceImpl<DroDroneBigPictur
@Override
public Boolean insertByBo(DroDroneBigPictureBo bo) {
DroDroneBigPicture add = MapstructUtils.convert(bo, DroDroneBigPicture.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
if (add == null) {
return false;
}
return flag;
boolean save = this.save(add);
if (!save) {
throw new ServiceException("新增无人机大图信息失败");
}
// 压缩图片
String smallPic = add.getSmallPic();
if (StringUtils.isNotBlank(smallPic)) {
List<Long> compressPictureIds = new ArrayList<>();
// 异步执行数据同步
self.asyncAddCompressPicture(smallPic, compressPictureIds)
.thenAccept(result -> {
String ossIds = compressPictureIds.stream()
.map(String::valueOf)
.collect(Collectors.joining(","));
DroDroneBigPicture update = new DroDroneBigPicture();
update.setId(add.getId());
update.setCompressPic(ossIds);
this.updateById(update);
}).exceptionally(ex -> {
log.error("无人机大图信息[{}]异步执行压缩图片失败", add.getTaskName(), ex);
return null;
});
}
return true;
}
/**
@ -161,10 +206,26 @@ public class DroDroneBigPictureServiceImpl extends ServiceImpl<DroDroneBigPictur
* @return 是否修改成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(DroDroneBigPictureBo bo) {
DroDroneBigPicture update = MapstructUtils.convert(bo, DroDroneBigPicture.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
if (update == null) {
return false;
}
// 更新数据
boolean b = this.updateById(update);
if (!b) {
throw new ServiceException("修改无人机大图信息失败");
}
// 获取老数据
DroDroneBigPicture old = this.getById(update.getId());
self.asyncUpdateCompressPicture(update, old)
.thenAccept(result -> log.info("无人机大图信息[{}]异步执行压缩图片成功", update.getTaskName()))
.exceptionally(ex -> {
log.error("无人机大图信息[{}]异步执行压缩图片失败", update.getTaskName(), ex);
return null;
});
return true;
}
/**
@ -209,25 +270,54 @@ public class DroDroneBigPictureServiceImpl extends ServiceImpl<DroDroneBigPictur
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(DroDroneBigPicture entity) {
//TODO 做一些数据校验,如唯一约束
}
/**
* 校验并批量删除无人机大图信息信息
* 批量删除无人机大图信息信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @param ids 待删除的主键集合
* @return 是否删除成功
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
//TODO 做一些业务上的校验,判断是否需要校验
@Transactional(rollbackFor = Exception.class)
public Boolean deleteByIds(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return true;
}
return baseMapper.deleteByIds(ids) > 0;
// 获取待删除数据
List<DroDroneBigPicture> list = this.listByIds(ids);
if (CollUtil.isEmpty(list)) {
return true;
}
// 获取所有待删除的文件
Set<Long> smallPics = list.stream()
.map(DroDroneBigPicture::getSmallPic)
.filter(StringUtils::isNotBlank)
.flatMap(str -> Arrays.stream(StringUtils.split(str, ",")))
.filter(StringUtils::isNotBlank)
.map(String::trim)
.map(Long::valueOf)
.collect(Collectors.toSet());
Set<Long> compressPics = list.stream()
.map(DroDroneBigPicture::getCompressPic)
.filter(StringUtils::isNotBlank)
.flatMap(str -> Arrays.stream(StringUtils.split(str, ",")))
.filter(StringUtils::isNotBlank)
.map(String::trim)
.map(Long::valueOf)
.collect(Collectors.toSet());
Set<Long> recognizePics = list.stream()
.map(DroDroneBigPicture::getRecognizePic)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Set<Long> allPics = new HashSet<>();
allPics.addAll(smallPics);
allPics.addAll(compressPics);
allPics.addAll(recognizePics);
if (CollUtil.isNotEmpty(allPics)) {
Boolean b = ossService.deleteWithValidByIds(allPics, false);
if (!b) {
throw new ServiceException("图片删除异常");
}
}
return this.removeBatchByIds(ids);
}
/**
@ -286,4 +376,144 @@ public class DroDroneBigPictureServiceImpl extends ServiceImpl<DroDroneBigPictur
BeanUtils.copyProperties(pictureVo, picture);
return this.isSynthesisCompleted(picture);
}
/**
* 压缩图片
*
* @param ossIds 图片对象存储Ids
* @param compressPicIds 压缩后图片Ids
* @return 是否成功
*/
@Override
public Boolean compressPicture(String ossIds, List<Long> compressPicIds) {
if (StringUtils.isBlank(ossIds)) {
return true;
}
// 获取图片地址
List<Long> ids = StringUtils.splitTo(ossIds, Convert::toLong);
List<SysOssVo> ossVos = ossService.listByIds(ids);
// 根据图片地址压缩图片
final float quality = 0.6f;
for (SysOssVo ossVo : ossVos) {
try {
OssClient ossClient = OssFactory.instance(ossVo.getService());
String filePath = ossClient.getPath(null, ".webp");
// 压缩图片
WebpConverterStreamVo fileStream = WebpConverterUtil.convertUrlToWebpStream(ossVo.getUrl(), quality,
1080, 0);
// 上传图片
SysOssVo upload = ossService.upload(fileStream.getInputStream(),
filePath, fileStream.getContentType(), fileStream.getLength());
compressPicIds.add(upload.getOssId());
} catch (IOException | URISyntaxException e) {
log.error("压缩图片失败", e);
return false;
}
}
return true;
}
/**
* 异步添加压缩图片
*
* @param smallPic 待压缩图片
* @param compressPicIds 压缩后的图片
* @return 是否成功
*/
@Async
@Override
public CompletableFuture<Boolean> asyncAddCompressPicture(String smallPic, List<Long> compressPicIds) {
int maxRetry = 3; // 最大重试次数
long delayMillis = 1000; // 每次重试间隔1秒
for (int attempt = 1; attempt <= maxRetry; attempt++) {
try {
Boolean success = this.compressPicture(smallPic, compressPicIds);
if (Boolean.TRUE.equals(success)) {
return CompletableFuture.completedFuture(true);
}
log.warn("压缩失败,第 {} 次重试...", attempt);
} catch (Exception e) {
log.error("压缩异常,第 {} 次重试error={}", attempt, e.getMessage());
}
// 不是最后一次则等待
if (attempt < maxRetry) {
try {
Thread.sleep(delayMillis);
} catch (InterruptedException ignored) {
}
}
}
log.error("压缩图片最终失败smallPic={}, compressPicIds={}", smallPic, compressPicIds);
return CompletableFuture.completedFuture(false);
}
/**
* 异步更新压缩图片
*
* @param pic 图片
* @param oldPic 旧图片
* @return 是否成功
*/
@Async
@Override
public CompletableFuture<Boolean> asyncUpdateCompressPicture(DroDroneBigPicture pic, DroDroneBigPicture oldPic) {
String smallPic = pic.getSmallPic();
String oldSmallPic = oldPic.getSmallPic();
if (StringUtils.isNotBlank(oldSmallPic) && StringUtils.isNotBlank(smallPic)) {
if (!this.isSameIds(oldSmallPic, smallPic)) {
List<Long> list = StringUtils.splitTo(oldPic.getCompressPic(), Convert::toLong);
Boolean b = ossService.deleteWithValidByIds(list, false);
if (!b) {
throw new ServiceException("图片删除异常");
}
List<Long> compressPictureIds = new ArrayList<>();
b = this.compressPicture(smallPic, compressPictureIds);
if (!b) {
throw new ServiceException("图片压缩异常");
}
String result = compressPictureIds.stream()
.map(String::valueOf)
.collect(Collectors.joining(","));
pic.setCompressPic(result);
}
} else if (StringUtils.isNotBlank(smallPic) && StringUtils.isBlank(oldSmallPic)) {
List<Long> compressPictureIds = new ArrayList<>();
Boolean b = this.compressPicture(smallPic, compressPictureIds);
if (!b) {
throw new ServiceException("图片压缩异常");
}
String result = compressPictureIds.stream()
.map(String::valueOf)
.collect(Collectors.joining(","));
pic.setCompressPic(result);
} else if (StringUtils.isBlank(smallPic) && StringUtils.isNotBlank(oldSmallPic)) {
List<Long> list = StringUtils.splitTo(oldPic.getCompressPic(), Convert::toLong);
Boolean b = ossService.deleteWithValidByIds(list, false);
if (!b) {
throw new ServiceException("图片删除异常");
}
}
// 更新数据
boolean b = this.updateById(pic);
return CompletableFuture.completedFuture(b);
}
/**
* 判断两个Ids是否相同
*
* @param ids1 ids1
* @param ids2 ids2
* @return 是否相同
*/
private boolean isSameIds(String ids1, String ids2) {
Set<String> set1 = Arrays.stream(ids1.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toSet());
Set<String> set2 = Arrays.stream(ids2.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toSet());
return set1.equals(set2);
}
}

View File

@ -287,7 +287,7 @@ public class OthYs7DeviceImgServiceImpl extends ServiceImpl<OthYs7DeviceImgMappe
othYs7DeviceImg.setRecType(JSONUtil.toJsonStr(recTypeList));
String targetUrl = null;
try {
RecognizeImageStreamResult imageStreamResult = recognizerManager.drawImageToStream(url, targets);
RecognizeImageStreamResult imageStreamResult = RecognizerManager.drawImageToStream(url, targets);
InputStream inputStream = imageStreamResult.getInputStream();
String contentType = imageStreamResult.getContentType();
long length = imageStreamResult.getLength();

View File

@ -8,7 +8,6 @@ import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
@ -20,10 +19,6 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
@ -284,41 +279,4 @@ public class PgsProgressCategoryController extends BaseController {
return toAjax(pgsProgressCategoryService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 获取进度类别日进度信息
*/
@SaIgnore
@GetMapping("/day/vo")
public R<PgsProgressCategoryDayTotalVo> getDayTotal() {
return R.ok(pgsProgressCategoryService.getProgressCategoryByDay(1897160897167638529L, LocalDate.now()));
}
/**
* 测试
*/
@SaIgnore
@GetMapping("/test")
public void getTest(HttpServletResponse response) throws IOException {
List<List<PgsProgressCategoryEnterTemplateVo>> list = new ArrayList<>();
List<String> sheetNames = new ArrayList<>();
for (int i = 0; i < 10; i++) {
List<PgsProgressCategoryEnterTemplateVo> list1 = new ArrayList<>();
for (int j = 0; j < 5; j++) {
PgsProgressCategoryEnterTemplateVo vo = new PgsProgressCategoryEnterTemplateVo();
vo.setName("测试" + i);
vo.setUnitType("1");
vo.setTotal(BigDecimal.ONE);
vo.setCompleted(BigDecimal.ONE);
vo.setStatus("1");
vo.setUnit("1");
vo.setRemark("测试" + i);
list1.add(vo);
}
sheetNames.add("测试" + i);
list.add(list1);
}
List<Integer> item = List.of(0, 1, 3);
ExcelUtil.exportExcel(list, sheetNames, item, PgsProgressCategoryEnterTemplateVo.class, null, response);
}
}

View File

@ -22,18 +22,20 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.oss.core.OssClient;
import org.dromara.common.oss.exception.OssException;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.utils.IdCardEncryptorUtil;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.project.domain.BusProjectTeamMember;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.dromara.project.domain.BusProjectTeamMember;
import org.dromara.project.service.IBusProjectService;
import org.dromara.project.service.IBusProjectTeamMemberService;
import org.dromara.safety.domain.HseQuestionUserAnswer;
import org.dromara.safety.domain.enums.HseSafetyExamTypeEnum;
import org.dromara.safety.domain.WgzQuestionSavePdf;
import org.dromara.safety.domain.dto.questionuseranswer.*;
import org.dromara.safety.domain.enums.HseSafetyExamTypeEnum;
import org.dromara.safety.domain.vo.questionuseranswer.HseQuestionUserAnswerVo;
import org.dromara.safety.mapper.HseQuestionUserAnswerMapper;
import org.dromara.safety.service.IHseQuestionUserAnswerService;
import org.dromara.safety.service.IWgzQuestionSavePdfService;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.service.ISysOssService;
import org.springframework.beans.BeanUtils;
@ -75,6 +77,12 @@ public class HseQuestionUserAnswerServiceImpl extends ServiceImpl<HseQuestionUse
@Resource
private IBusProjectTeamMemberService projectTeamMemberService;
@Resource
private IdCardEncryptorUtil idCardEncryptorUtil;
@Resource
private IWgzQuestionSavePdfService wgzQuestionSavePdfService;
/**
* 查询用户试卷存储
*
@ -184,50 +192,8 @@ public class HseQuestionUserAnswerServiceImpl extends ServiceImpl<HseQuestionUse
// 5. 解压 zip
destDir = new File(destDirPath);
ZipUtil.unzip(tempZipFile, destDir);
// 4. 遍历最外层文件夹
File[] userFolders = destDir.listFiles();
if (userFolders != null) {
for (File userFolder : userFolders) {
if (userFolder.isDirectory()) {
// 5. 获取文件名,格式为 姓名-身份证-满分-得分-及格分,例如 小明-500106200101011234-100-61-60
String userFolderName = userFolder.getName();
String[] userParts = userFolderName.split("-");
// 6. 继续遍历获取每个用户子文件夹里的文件
File[] docFolders = userFolder.listFiles();
String fileIdStr = null;
if (docFolders != null) {
// 6.1 遍历文件
List<Long> fileIdList = new ArrayList<>();
for (File docFolder : docFolders) {
if (docFolder.isFile()) {
// 6.2 上传文件
SysOssVo upload = ossService.upload(docFolder);
if (upload != null) {
fileIdList.add(upload.getOssId());
}
}
}
// 6.3 跳过空文件
if (fileIdList.isEmpty()) {
continue;
}
fileIdStr = fileIdList.stream()
.map(String::valueOf)
.collect(Collectors.joining(","));
}
// 7. 创建临时对象
HseQuestionUserAnswerUploadTemp temp = new HseQuestionUserAnswerUploadTemp();
temp.setProjectId(projectId);
temp.setUserName(userParts[0]);
temp.setUserIdCard(userParts[1]);
temp.setFullScore(Long.parseLong(userParts[2]));
temp.setScore(Long.parseLong(userParts[3]));
temp.setPassScore(Long.parseLong(userParts[4]));
temp.setFile(fileIdStr);
tempList.add(temp);
}
}
}
// 4. 遍历文件夹
scanFolder(destDir, tempList, projectId);
} catch (Exception e) {
throw new ServiceException("文件上传失败", HttpStatus.ERROR);
} finally {
@ -253,20 +219,29 @@ public class HseQuestionUserAnswerServiceImpl extends ServiceImpl<HseQuestionUse
}
// 8.1 获取用户身份证列表
Set<String> idCardList = tempList.stream().map(HseQuestionUserAnswerUploadTemp::getUserIdCard)
.map(idCard -> idCardEncryptorUtil.encrypt(idCard))
.collect(Collectors.toSet());
// 8.2 根据用户身份证列表查询用户信息
Map<String, List<SubConstructionUser>> userIdMap = constructionUserService.lambdaQuery()
.in(SubConstructionUser::getSfzNumber, idCardList).list()
.stream().collect(Collectors.groupingBy(SubConstructionUser::getSfzNumber));
List<WgzQuestionSavePdf> savePdfList = new ArrayList<>();
// 8.3 遍历临时对象,构造用户试卷存储对象
List<HseQuestionUserAnswer> questionUserAnswerList = tempList.stream().map(temp -> {
HseQuestionUserAnswer questionUserAnswer = new HseQuestionUserAnswer();
WgzQuestionSavePdf savePdf = new WgzQuestionSavePdf();
// 8.4 获取对应用户id
String userIdCard = temp.getUserIdCard();
// 加密
userIdCard = idCardEncryptorUtil.encrypt(userIdCard);
Long userId = null;
if (userIdMap.containsKey(userIdCard)) {
SubConstructionUser constructionUser = userIdMap.get(userIdCard).get(0);
userId = constructionUser.getId();
SubConstructionUser constructionUser = userIdMap.get(userIdCard).getFirst();
userId = constructionUser.getSysUserId();
Long userProjectId = constructionUser.getProjectId() != null ?
constructionUser.getProjectId() : temp.getProjectId();
questionUserAnswer.setProjectId(userProjectId);
savePdf.setProjectId(userProjectId);
}
// 8.5 判断用户是否存在
if (userId == null) {
@ -276,12 +251,19 @@ public class HseQuestionUserAnswerServiceImpl extends ServiceImpl<HseQuestionUse
}
questionUserAnswer.setUserId(userId);
// 8.6 设置其他属性
questionUserAnswer.setProjectId(temp.getProjectId());
questionUserAnswer.setScore(temp.getScore());
String pass = String.format("%s,%s", temp.getPassScore(), temp.getFullScore());
questionUserAnswer.setPass(pass);
questionUserAnswer.setFile(temp.getFile());
questionUserAnswer.setExamType(HseSafetyExamTypeEnum.OFFLINE.getValue());
savePdf.setType(HseSafetyExamTypeEnum.OFFLINE.getValue());
savePdf.setUserId(userId);
savePdf.setPath(temp.getFile());
savePdf.setPass(pass);
savePdf.setTimeOut(0);
savePdf.setTakeTime(0L);
savePdf.setSumScore(Double.valueOf(temp.getScore()));
savePdfList.add(savePdf);
return questionUserAnswer;
}).toList();
// 9. 保存
@ -289,9 +271,83 @@ public class HseQuestionUserAnswerServiceImpl extends ServiceImpl<HseQuestionUse
if (!result) {
throw new ServiceException("数据库操作失败", HttpStatus.ERROR);
}
boolean b = wgzQuestionSavePdfService.saveBatch(savePdfList);
if (!b) {
throw new ServiceException("数据库操作失败", HttpStatus.ERROR);
}
return true;
}
/**
* 扫描文件夹
*
* @param folder 文件夹
* @param tempList 临时对象列表
* @param projectId 项目id
*/
private void scanFolder(File folder, List<HseQuestionUserAnswerUploadTemp> tempList, Long projectId) {
if (!folder.isDirectory()) {
return;
}
// 当前文件夹是否符合格式:姓名-身份证-满分-得分-及格分
String folderName = folder.getName();
String[] userParts = folderName.split("-");
boolean isUserFolder = userParts.length == 5
&& isNumber(userParts[2])
&& isNumber(userParts[3])
&& isNumber(userParts[4])
&& userParts[1].length() == 18;
if (isUserFolder) {
// 找到了匹配的用户文件夹,执行上传逻辑
File[] files = folder.listFiles();
if (files == null) {
return;
}
List<Long> fileIdList = new ArrayList<>();
for (File file : files) {
if (file.isFile()) {
SysOssVo upload = ossService.upload(file);
if (upload != null) {
fileIdList.add(upload.getOssId());
}
}
}
if (fileIdList.isEmpty()) {
return;
}
String fileIdStr = fileIdList.stream()
.map(String::valueOf)
.collect(Collectors.joining(","));
// 创建对象
HseQuestionUserAnswerUploadTemp temp = new HseQuestionUserAnswerUploadTemp();
temp.setProjectId(projectId);
temp.setUserName(userParts[0]);
temp.setUserIdCard(userParts[1]);
temp.setFullScore(Long.parseLong(userParts[2]));
temp.setScore(Long.parseLong(userParts[3]));
temp.setPassScore(Long.parseLong(userParts[4]));
temp.setFile(fileIdStr);
tempList.add(temp);
return; // 关键!匹配后不再递归子目录
}
// 不符合格式 -> 继续扫描子目录
File[] children = folder.listFiles();
if (children != null) {
for (File child : children) {
if (child.isDirectory()) {
scanFolder(child, tempList, projectId);
}
}
}
}
/**
* 判断字符串是否为数字
*/
private boolean isNumber(String value) {
return value != null && value.matches("\\d+");
}
/**
* 修改用户试卷存储
*
@ -488,7 +544,7 @@ public class HseQuestionUserAnswerServiceImpl extends ServiceImpl<HseQuestionUse
List<String> fileUrlList = ossIdList.stream().map(ossId -> {
String url = "";
if (ossMap.containsKey(ossId)) {
url = ossMap.get(ossId).get(0).getUrl();
url = ossMap.get(ossId).getFirst().getUrl();
}
return url;
}).toList();
@ -548,7 +604,7 @@ public class HseQuestionUserAnswerServiceImpl extends ServiceImpl<HseQuestionUse
continue;
}
Long userId = questionUserAnswer.getUserId();
SubConstructionUser constructionUser = userMap.get(userId).get(0);
SubConstructionUser constructionUser = userMap.get(userId).getFirst();
String userFolder = constructionUser.getUserName() + "-" + constructionUser.getId() + "/";
// 写入个人文件夹条目
zos.putNextEntry(new ZipEntry(userFolder));