设备图片

This commit is contained in:
caozehui
2026-07-23 08:39:02 +08:00
parent 23c0855794
commit dde876eb26
12 changed files with 636 additions and 162 deletions

View File

@@ -100,11 +100,26 @@ public class PqDevController extends BaseController {
} }
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DOWNLOAD) @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DOWNLOAD)
@GetMapping("/image/download") @GetMapping("/image/preview")
@ApiOperation("下载设备图片") @ApiOperation("下载设备图片")
@ApiImplicitParam(name = "id", value = "被检设备id", required = true) @ApiImplicitParam(name = "id", value = "被检设备id", required = true)
public void downloadImage(@RequestParam("id") String id, HttpServletResponse response) { public void previewImage(@RequestParam("id") String id,
pqDevService.downloadImage(id, response); @RequestParam("fileName") String fileName,
HttpServletResponse response) {
pqDevService.previewImage(id, fileName, response);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DOWNLOAD)
@GetMapping("/image/download")
@ApiOperation("download device image")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "id", required = true),
@ApiImplicitParam(name = "fileName", value = "fileName", required = true)
})
public void downloadImage(@RequestParam("id") String id,
@RequestParam("fileName") String fileName,
HttpServletResponse response) {
pqDevService.downloadImage(id, fileName, response);
} }
@OperateInfo(operateType = OperateType.DELETE) @OperateInfo(operateType = OperateType.DELETE)

View File

@@ -154,7 +154,7 @@
</select> </select>
<select id="selectByDevId" resultType="com.njcn.gather.device.pojo.vo.PqDevVO"> <select id="selectByDevId" resultType="com.njcn.gather.device.pojo.vo.PqDevVO">
SELECT dev.*, dev_sub.* SELECT <include refid="DevColumnsNoImage"/>, dev_sub.*
FROM pq_dev dev FROM pq_dev dev
left JOIN pq_dev_sub dev_sub ON dev.Id = dev_sub.Dev_Id left JOIN pq_dev_sub dev_sub ON dev.Id = dev_sub.Dev_Id
WHERE dev.Id = #{devId} WHERE dev.Id = #{devId}

View File

@@ -138,7 +138,7 @@ public class PqDevParam {
private Integer importFlag; private Integer importFlag;
@ApiModelProperty("设备图片") @ApiModelProperty("设备图片")
private MultipartFile image; private List<MultipartFile> images;
/** /**
* 更新操作实体 * 更新操作实体
@@ -151,6 +151,9 @@ public class PqDevParam {
@NotBlank(message = DetectionValidMessage.ID_NOT_BLANK) @NotBlank(message = DetectionValidMessage.ID_NOT_BLANK)
@Pattern(regexp = PatternRegex.SYSTEM_ID, message = DetectionValidMessage.ID_FORMAT_ERROR) @Pattern(regexp = PatternRegex.SYSTEM_ID, message = DetectionValidMessage.ID_FORMAT_ERROR)
private String id; private String id;
@ApiModelProperty("deleteImageNames")
private List<String> deleteImageNames;
} }
/** /**

View File

@@ -3,7 +3,6 @@ package com.njcn.gather.device.pojo.po;
import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@@ -185,9 +184,6 @@ public class PqDev extends BaseEntity implements Serializable {
*/ */
private Integer importFlag; private Integer importFlag;
@JsonIgnore
private byte[] image;
/** /**
* 状态0-删除 1-正常 * 状态0-删除 1-正常
*/ */

View File

@@ -0,0 +1,17 @@
package com.njcn.gather.device.pojo.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PqDevImageVO {
private String fileName;
private String previewUrl;
private String downloadUrl;
}

View File

@@ -150,7 +150,5 @@ public class PqDevVO extends PqDev {
private Boolean hasImage; private Boolean hasImage;
private String imageBase64; private List<PqDevImageVO> images;
private String imageContentType;
} }

View File

@@ -102,7 +102,9 @@ public interface IPqDevService extends IService<PqDev> {
* @param id 设备id * @param id 设备id
* @param response 响应 * @param response 响应
*/ */
void downloadImage(String id, HttpServletResponse response); void previewImage(String id, String fileName, HttpServletResponse response);
void downloadImage(String id, String fileName, HttpServletResponse response);
/** /**
* 获取装置信息和装置下监测点信息 * 获取装置信息和装置下监测点信息

View File

@@ -0,0 +1,288 @@
package com.njcn.gather.device.service;
import com.njcn.gather.device.pojo.vo.PqDevImageVO;
import com.njcn.gather.device.util.PqDevImageSupport;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@Service
public class PqDevImageFileService {
public static final int MAX_IMAGE_COUNT = 5;
private static final Pattern UUID_JPG_FILE_NAME = Pattern.compile(
"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\.jpg$"
);
private static final Pattern DEVICE_ID_PATTERN = Pattern.compile("^[A-Za-z0-9_-]{1,64}$");
private static final String INVALID_IMAGE_NAME = "图片文件名无效";
private static final String INVALID_IMAGE_PATH = "图片路径无效";
private static final String INVALID_DEVICE_ID = "设备ID无效";
private static final String IMAGE_NOT_EXISTS = "设备图片不存在";
private static final String IMAGE_COUNT_EXCEEDED = "设备图片最多上传5张";
private static final String READ_IMAGE_FAILED = "读取设备图片失败";
private static final String PREVIEW_URL = "/pqDev/image/preview?id=%s&fileName=%s";
private static final String DOWNLOAD_URL = "/pqDev/image/download?id=%s&fileName=%s";
private final String imageDir;
public PqDevImageFileService(@Value("${device.image.dir:}") String imageDir) {
this.imageDir = imageDir;
}
public List<PqDevImageVO> listImages(String devId) {
Path deviceDir = deviceDir(devId);
if (!Files.isDirectory(deviceDir)) {
return Collections.emptyList();
}
List<Path> imagePaths = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(deviceDir)) {
for (Path path : stream) {
if (Files.isRegularFile(path) && isValidImageFileName(path.getFileName().toString())) {
imagePaths.add(path);
}
}
} catch (IOException ex) {
throw new IllegalStateException(READ_IMAGE_FAILED, ex);
}
Collections.sort(imagePaths, new Comparator<Path>() {
@Override
public int compare(Path left, Path right) {
int modifiedCompare = Long.compare(lastModified(left), lastModified(right));
if (modifiedCompare != 0) {
return modifiedCompare;
}
return left.getFileName().toString().compareTo(right.getFileName().toString());
}
});
List<PqDevImageVO> images = new ArrayList<>();
for (Path imagePath : imagePaths) {
images.add(buildImageVO(devId, imagePath.getFileName().toString()));
}
return images;
}
public void validateAddCount(List<MultipartFile> newImages) {
if (effectiveNewImageCount(newImages) > MAX_IMAGE_COUNT) {
throw new IllegalArgumentException(IMAGE_COUNT_EXCEEDED);
}
}
public void validateFinalCount(String devId, List<String> deleteImageNames, List<MultipartFile> newImages) {
validateAddCount(newImages);
Set<String> existingNames = currentImageNameSet(devId);
validateExistingImageNames(devId, deleteImageNames);
int finalCount = existingNames.size() - uniqueCount(deleteImageNames) + effectiveNewImageCount(newImages);
if (finalCount > MAX_IMAGE_COUNT) {
throw new IllegalArgumentException(IMAGE_COUNT_EXCEEDED);
}
}
public List<PqDevImageVO> saveImages(String devId, List<MultipartFile> newImages) throws IOException {
validateAddCount(newImages);
if (newImages == null || newImages.isEmpty()) {
return Collections.emptyList();
}
Files.createDirectories(deviceDir(devId));
List<PqDevImageVO> savedImages = new ArrayList<>();
for (MultipartFile image : newImages) {
if (image == null || image.isEmpty()) {
continue;
}
String fileName = UUID.randomUUID().toString().toLowerCase(Locale.ROOT) + ".jpg";
Path path = imagePath(devId, fileName);
writeImage(path, PqDevImageSupport.processUploadToJpeg(image));
savedImages.add(buildImageVO(devId, fileName));
}
return savedImages;
}
public void deleteImages(String devId, List<String> deleteImageNames) throws IOException {
validateExistingImageNames(devId, deleteImageNames);
if (deleteImageNames == null || deleteImageNames.isEmpty()) {
return;
}
Set<String> deletedNames = new HashSet<>();
for (String fileName : deleteImageNames) {
if (deletedNames.add(fileName)) {
Files.delete(imagePath(devId, fileName));
}
}
}
public void validateImageNames(List<String> imageNames) {
if (imageNames == null) {
return;
}
for (String imageName : imageNames) {
if (!isValidImageFileName(imageName)) {
throw new IllegalArgumentException(INVALID_IMAGE_NAME);
}
}
}
public void validateExistingImageNames(String devId, List<String> imageNames) {
validateImageNames(imageNames);
if (imageNames == null || imageNames.isEmpty()) {
return;
}
Set<String> existingNames = currentImageNameSet(devId);
for (String imageName : imageNames) {
if (!existingNames.contains(imageName)) {
throw new IllegalArgumentException(IMAGE_NOT_EXISTS);
}
}
}
public Path imagePath(String devId, String fileName) {
if (!isValidImageFileName(fileName)) {
throw new IllegalArgumentException(INVALID_IMAGE_NAME);
}
Path deviceDir = deviceDir(devId);
Path path = deviceDir.resolve(fileName).normalize();
if (!path.startsWith(deviceDir)) {
throw new IllegalArgumentException(INVALID_IMAGE_PATH);
}
return path;
}
public long imageSize(String devId, String fileName) throws IOException {
Path path = imagePath(devId, fileName);
if (!Files.isRegularFile(path)) {
throw new IllegalArgumentException(IMAGE_NOT_EXISTS);
}
return Files.size(path);
}
public void writeImage(String devId, String fileName, OutputStream outputStream) throws IOException {
Path path = imagePath(devId, fileName);
if (!Files.isRegularFile(path)) {
throw new IllegalArgumentException(IMAGE_NOT_EXISTS);
}
Files.copy(path, outputStream);
}
public void deleteDeviceDirectoryQuietly(String devId) {
Path deviceDir = deviceDir(devId);
if (!Files.exists(deviceDir)) {
return;
}
try (Stream<Path> stream = Files.walk(deviceDir)) {
List<Path> paths = new ArrayList<>();
Iterator<Path> iterator = stream.iterator();
while (iterator.hasNext()) {
paths.add(iterator.next());
}
Collections.sort(paths, new Comparator<Path>() {
@Override
public int compare(Path left, Path right) {
return right.compareTo(left);
}
});
for (Path path : paths) {
Files.deleteIfExists(path);
}
} catch (IOException ignored) {
}
}
private void writeImage(Path imagePath, byte[] imageBytes) throws IOException {
Files.createDirectories(imagePath.getParent());
Files.write(imagePath, imageBytes, StandardOpenOption.CREATE_NEW);
}
private Path rootDir() {
Path root;
if (StringUtils.hasText(imageDir)) {
root = Paths.get(imageDir);
} else {
root = Paths.get(System.getProperty("user.dir"), "data", "device-images");
}
return root.toAbsolutePath().normalize();
}
private Path deviceDir(String devId) {
if (!StringUtils.hasText(devId) || !DEVICE_ID_PATTERN.matcher(devId).matches()) {
throw new IllegalArgumentException(INVALID_DEVICE_ID);
}
Path rootDir = rootDir();
Path deviceDir = rootDir.resolve(devId).normalize();
if (!deviceDir.startsWith(rootDir)) {
throw new IllegalArgumentException(INVALID_IMAGE_PATH);
}
return deviceDir;
}
private PqDevImageVO buildImageVO(String devId, String fileName) {
return new PqDevImageVO(
fileName,
String.format(PREVIEW_URL, devId, fileName),
String.format(DOWNLOAD_URL, devId, fileName)
);
}
private Set<String> currentImageNameSet(String devId) {
Set<String> names = new HashSet<>();
for (PqDevImageVO image : listImages(devId)) {
names.add(image.getFileName());
}
return names;
}
private int effectiveNewImageCount(List<MultipartFile> newImages) {
if (newImages == null || newImages.isEmpty()) {
return 0;
}
int count = 0;
for (MultipartFile image : newImages) {
if (image != null && !image.isEmpty()) {
count++;
}
}
return count;
}
private int uniqueCount(List<String> names) {
if (names == null || names.isEmpty()) {
return 0;
}
return new HashSet<>(names).size();
}
private boolean isValidImageFileName(String fileName) {
return fileName != null && UUID_JPG_FILE_NAME.matcher(fileName).matches();
}
private long lastModified(Path path) {
try {
return Files.getLastModifiedTime(path).toMillis();
} catch (IOException ex) {
return 0L;
}
}
}

View File

@@ -38,7 +38,7 @@ import com.njcn.gather.device.pojo.po.PqDevSub;
import com.njcn.gather.device.pojo.vo.*; import com.njcn.gather.device.pojo.vo.*;
import com.njcn.gather.device.service.IPqDevService; import com.njcn.gather.device.service.IPqDevService;
import com.njcn.gather.device.service.IPqDevSubService; import com.njcn.gather.device.service.IPqDevSubService;
import com.njcn.gather.device.util.PqDevImageSupport; import com.njcn.gather.device.service.PqDevImageFileService;
import com.njcn.gather.monitor.pojo.po.PqMonitor; import com.njcn.gather.monitor.pojo.po.PqMonitor;
import com.njcn.gather.monitor.pojo.vo.PqMonitorExcel; import com.njcn.gather.monitor.pojo.vo.PqMonitorExcel;
import com.njcn.gather.monitor.service.IPqMonitorService; import com.njcn.gather.monitor.service.IPqMonitorService;
@@ -100,6 +100,7 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
private final AdPlanMapper adPlanMapper; private final AdPlanMapper adPlanMapper;
private final IPqDevCheckHistoryService pqDevCheckHistoryService; private final IPqDevCheckHistoryService pqDevCheckHistoryService;
private final RestTemplateUtil restTemplateUtil; private final RestTemplateUtil restTemplateUtil;
private final PqDevImageFileService pqDevImageFileService;
@Override @Override
public Page<PqDevVO> listPqDevs(PqDevParam.QueryParam queryParam) { public Page<PqDevVO> listPqDevs(PqDevParam.QueryParam queryParam) {
@@ -139,10 +140,10 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
pqDevParam.setName(pqDevParam.getName().trim()); pqDevParam.setName(pqDevParam.getName().trim());
pqDevParam.setCreateId(pqDevParam.getCreateId().trim()); pqDevParam.setCreateId(pqDevParam.getCreateId().trim());
this.checkRepeat(pqDevParam, false); this.checkRepeat(pqDevParam, false);
validateAddImages(pqDevParam.getImages());
PqDev pqDev = new PqDev(); PqDev pqDev = new PqDev();
BeanUtil.copyProperties(pqDevParam, pqDev, CopyOptions.create().setIgnoreProperties("image")); BeanUtil.copyProperties(pqDevParam, pqDev, CopyOptions.create().setIgnoreProperties("images"));
fillImage(pqDev, pqDevParam);
String currrentScene = sysTestConfigService.getCurrrentScene(); String currrentScene = sysTestConfigService.getCurrrentScene();
this.checkParams(pqDev, currrentScene); this.checkParams(pqDev, currrentScene);
@@ -170,19 +171,49 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
pqDev.setImportFlag(0); pqDev.setImportFlag(0);
pqDev.setState(DataStateEnum.ENABLE.getCode()); pqDev.setState(DataStateEnum.ENABLE.getCode());
pqDevSubService.save(pqDevSub); pqDevSubService.save(pqDevSub);
return this.save(pqDev); boolean saved = this.save(pqDev);
if (saved) {
saveAddImages(id, pqDevParam.getImages());
}
return saved;
} }
private void fillImage(PqDev pqDev, PqDevParam pqDevParam) { private void validateAddImages(List<MultipartFile> images) {
try { try {
byte[] image = PqDevImageSupport.processUpload(pqDevParam.getImage()); pqDevImageFileService.validateAddCount(images);
if (image != null) { } catch (IllegalArgumentException e) {
pqDev.setImage(image); throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage());
} }
}
private void saveAddImages(String id, List<MultipartFile> images) {
try {
pqDevImageFileService.saveImages(id, images);
} catch (IllegalArgumentException e) {
pqDevImageFileService.deleteDeviceDirectoryQuietly(id);
throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage());
} catch (IOException e) {
pqDevImageFileService.deleteDeviceDirectoryQuietly(id);
throw new BusinessException(CommonResponseEnum.FAIL, "device image save failed");
}
}
private void validateUpdateImages(PqDevParam.UpdateParam updateParam) {
try {
pqDevImageFileService.validateFinalCount(updateParam.getId(), updateParam.getDeleteImageNames(), updateParam.getImages());
} catch (IllegalArgumentException e) {
throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage());
}
}
private void saveUpdateImages(PqDevParam.UpdateParam updateParam) {
try {
pqDevImageFileService.saveImages(updateParam.getId(), updateParam.getImages());
pqDevImageFileService.deleteImages(updateParam.getId(), updateParam.getDeleteImageNames());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage()); throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage());
} catch (IOException e) { } catch (IOException e) {
throw new BusinessException(CommonResponseEnum.FAIL, "图片处理失败"); throw new BusinessException(CommonResponseEnum.FAIL, "device image save failed");
} }
} }
@@ -226,10 +257,10 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
updateParam.setName(updateParam.getName().trim()); updateParam.setName(updateParam.getName().trim());
updateParam.setCreateId(updateParam.getCreateId().trim()); updateParam.setCreateId(updateParam.getCreateId().trim());
this.checkRepeat(updateParam, true); this.checkRepeat(updateParam, true);
validateUpdateImages(updateParam);
PqDev pqDev = new PqDev(); PqDev pqDev = new PqDev();
BeanUtil.copyProperties(updateParam, pqDev, CopyOptions.create().setIgnoreProperties("image")); BeanUtil.copyProperties(updateParam, pqDev, CopyOptions.create().setIgnoreProperties("images", "deleteImageNames"));
fillImage(pqDev, updateParam);
String currrentScene = sysTestConfigService.getCurrrentScene(); String currrentScene = sysTestConfigService.getCurrrentScene();
this.checkParams(pqDev, currrentScene); this.checkParams(pqDev, currrentScene);
@@ -237,7 +268,11 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
if (PatternEnum.CONTRAST.getValue().equals(dictDataService.getDictDataById(updateParam.getPattern()).getCode())) { if (PatternEnum.CONTRAST.getValue().equals(dictDataService.getDictDataById(updateParam.getPattern()).getCode())) {
pqMonitorService.updatePqMonitorByDevId(updateParam.getId(), updateParam.getMonitorList()); pqMonitorService.updatePqMonitorByDevId(updateParam.getId(), updateParam.getMonitorList());
} }
return this.updateById(pqDev); boolean updated = this.updateById(pqDev);
if (updated) {
saveUpdateImages(updateParam);
}
return updated;
} }
@Override @Override
@@ -282,7 +317,13 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
throw new BusinessException(DetectionResponseEnum.DEVICE_DELETE, str.toString()); throw new BusinessException(DetectionResponseEnum.DEVICE_DELETE, str.toString());
} }
pqDevSubService.remove(new QueryWrapper<PqDevSub>().lambda().in(PqDevSub::getDevId, param.getIds())); pqDevSubService.remove(new QueryWrapper<PqDevSub>().lambda().in(PqDevSub::getDevId, param.getIds()));
return this.lambdaUpdate().set(PqDev::getState, DataStateEnum.DELETED.getCode()).in(PqDev::getId, param.getIds()).update(); boolean deleted = this.lambdaUpdate().set(PqDev::getState, DataStateEnum.DELETED.getCode()).in(PqDev::getId, param.getIds()).update();
if (deleted) {
for (String id : param.getIds()) {
pqDevImageFileService.deleteDeviceDirectoryQuietly(id);
}
}
return deleted;
} }
return true; return true;
} }
@@ -399,7 +440,7 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
if (ObjectUtil.isNull(pqDevVO)) { if (ObjectUtil.isNull(pqDevVO)) {
return null; return null;
} }
setImageEchoFields(pqDevVO); setImageListFields(pqDevVO);
if (StrUtil.isNotBlank(pqDevVO.getSeries())) { if (StrUtil.isNotBlank(pqDevVO.getSeries())) {
pqDevVO.setSeries(EncryptionUtil.decoderString(1, pqDevVO.getSeries())); pqDevVO.setSeries(EncryptionUtil.decoderString(1, pqDevVO.getSeries()));
} }
@@ -429,36 +470,38 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
} }
@Override @Override
public void downloadImage(String id, HttpServletResponse response) { public void previewImage(String id, String fileName, HttpServletResponse response) {
PqDev pqDev = this.getById(id); writeDeviceImage(id, fileName, response, "inline");
if (ObjectUtil.isNull(pqDev) || pqDev.getImage() == null || pqDev.getImage().length == 0) {
throw new BusinessException(CommonResponseEnum.FAIL, "设备图片不存在");
}
String contentType = PqDevImageSupport.detectContentType(pqDev.getImage());
String filename = "device-image-" + id + "." + PqDevImageSupport.getExtension(contentType);
response.setContentType(contentType);
response.setContentLength(pqDev.getImage().length);
response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
try {
response.getOutputStream().write(pqDev.getImage());
response.flushBuffer();
} catch (IOException e) {
throw new BusinessException(CommonResponseEnum.FAIL, "设备图片下载失败");
}
} }
private void setImageEchoFields(PqDevVO pqDevVO) { @Override
public void downloadImage(String id, String fileName, HttpServletResponse response) {
writeDeviceImage(id, fileName, response, "attachment");
}
private void writeDeviceImage(String id, String fileName, HttpServletResponse response, String disposition) {
PqDev pqDev = this.getById(id);
if (ObjectUtil.isNull(pqDev)) {
throw new BusinessException(CommonResponseEnum.FAIL, "device not found");
}
try { try {
PqDevImageSupport.ImageEchoPayload payload = PqDevImageSupport.buildEchoPayload(pqDevVO.getImage()); response.setContentType("image/jpeg");
pqDevVO.setHasImage(payload.isHasImage()); response.setContentLength((int) pqDevImageFileService.imageSize(id, fileName));
pqDevVO.setImageBase64(payload.getImageBase64()); response.setHeader("Content-Disposition", disposition + "; filename=\"" + fileName + "\"");
pqDevVO.setImageContentType(payload.getImageContentType()); pqDevImageFileService.writeImage(id, fileName, response.getOutputStream());
pqDevVO.setImage(null); response.flushBuffer();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage()); throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage());
} catch (IOException e) {
throw new BusinessException(CommonResponseEnum.FAIL, "device image output failed");
} }
} }
private void setImageListFields(PqDevVO pqDevVO) {
List<PqDevImageVO> images = pqDevImageFileService.listImages(pqDevVO.getId());
pqDevVO.setImages(images);
pqDevVO.setHasImage(CollUtil.isNotEmpty(images));
}
/** /**
* 获取查询条件wrapper * 获取查询条件wrapper

View File

@@ -1,7 +1,5 @@
package com.njcn.gather.device.util; package com.njcn.gather.device.util;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.imageio.IIOImage; import javax.imageio.IIOImage;
@@ -9,35 +7,53 @@ import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter; import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.ImageOutputStream;
import java.awt.Color;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import java.util.Iterator; import java.util.Iterator;
import java.util.Locale; import java.util.Locale;
public final class PqDevImageSupport { public final class PqDevImageSupport {
private static final long MAX_FILE_SIZE = 10L * 1024L * 1024L; public static final long MAX_FILE_SIZE = 10L * 1024L * 1024L;
private static final int MAX_EDGE = 1600; public static final int MAX_EDGE = 1600;
private static final String INVALID_IMAGE_CONTENT = "图片内容无效";
private static final String UNSUPPORTED_IMAGE_TYPE = "仅支持 JPG、JPEG、PNG 图片";
private static final String IMAGE_TOO_LARGE = "图片大小不能超过 10MB";
private static final float JPEG_QUALITY = 0.82f; private static final float JPEG_QUALITY = 0.82f;
private PqDevImageSupport() { private PqDevImageSupport() {
} }
public static ImageEchoPayload buildEchoPayload(byte[] imageBytes) { public static byte[] processUploadToJpeg(MultipartFile image) throws IOException {
if (imageBytes == null || imageBytes.length == 0) { if (image == null || image.isEmpty()) {
return new ImageEchoPayload(false, null, null); throw new IllegalArgumentException(INVALID_IMAGE_CONTENT);
} }
return new ImageEchoPayload(true, Base64.getEncoder().encodeToString(imageBytes), detectContentType(imageBytes)); if (image.getSize() > MAX_FILE_SIZE) {
throw new IllegalArgumentException(IMAGE_TOO_LARGE);
}
if (!hasAllowedDeclaredType(image)) {
throw new IllegalArgumentException(UNSUPPORTED_IMAGE_TYPE);
}
byte[] originalBytes = image.getBytes();
detectContentType(originalBytes);
BufferedImage source = ImageIO.read(new ByteArrayInputStream(originalBytes));
if (source == null) {
throw new IllegalArgumentException(INVALID_IMAGE_CONTENT);
}
return writeJpeg(scaleIfNeeded(source));
} }
public static String detectContentType(byte[] imageBytes) { public static String detectContentType(byte[] imageBytes) {
if (imageBytes == null || imageBytes.length < 4) { if (imageBytes == null || imageBytes.length < 4) {
throw new IllegalArgumentException("图片内容无效"); throw new IllegalArgumentException(INVALID_IMAGE_CONTENT);
} }
if (isPng(imageBytes)) { if (isPng(imageBytes)) {
return "image/png"; return "image/png";
@@ -45,47 +61,31 @@ public final class PqDevImageSupport {
if (isJpeg(imageBytes)) { if (isJpeg(imageBytes)) {
return "image/jpeg"; return "image/jpeg";
} }
throw new IllegalArgumentException("仅支持 JPG、PNG 图片"); throw new IllegalArgumentException(UNSUPPORTED_IMAGE_TYPE);
}
public static byte[] processUpload(MultipartFile image) throws IOException {
if (image == null || image.isEmpty()) {
return null;
}
if (image.getSize() > MAX_FILE_SIZE) {
throw new IllegalArgumentException("图片大小不能超过 10MB");
} }
private static boolean hasAllowedDeclaredType(MultipartFile image) {
String normalizedName = normalize(image.getOriginalFilename()); String normalizedName = normalize(image.getOriginalFilename());
String normalizedContentType = normalize(image.getContentType()); String normalizedContentType = normalize(image.getContentType());
boolean allowedType = normalizedContentType.equals("image/jpeg") boolean hasContentType = !normalizedContentType.isEmpty();
|| normalizedContentType.equals("image/png") boolean hasKnownExtension = normalizedName.endsWith(".jpg")
|| normalizedName.endsWith(".jpg") || normalizedName.endsWith(".jpeg")
|| normalizedName.endsWith(".png")
|| normalizedName.matches(".*\\.[^.]+$");
boolean contentTypeAllowed = normalizedContentType.equals("image/jpeg")
|| normalizedContentType.equals("image/jpg")
|| normalizedContentType.equals("image/png");
boolean extensionAllowed = normalizedName.endsWith(".jpg")
|| normalizedName.endsWith(".jpeg") || normalizedName.endsWith(".jpeg")
|| normalizedName.endsWith(".png"); || normalizedName.endsWith(".png");
if (!allowedType) {
throw new IllegalArgumentException("仅支持 JPG、PNG 图片");
}
byte[] originalBytes = image.getBytes(); if (hasContentType && !contentTypeAllowed) {
String contentType = detectContentType(originalBytes); return false;
BufferedImage source;
try (InputStream inputStream = image.getInputStream()) {
source = ImageIO.read(inputStream);
} }
if (source == null) { if (hasKnownExtension && !extensionAllowed) {
throw new IllegalArgumentException("图片内容无效"); return false;
} }
return contentTypeAllowed || extensionAllowed;
BufferedImage target = scaleIfNeeded(source);
if ("image/png".equals(contentType)) {
return writePng(target);
}
return writeJpeg(target);
}
public static String getExtension(String contentType) {
return "image/png".equals(contentType) ? "png" : "jpg";
} }
private static BufferedImage scaleIfNeeded(BufferedImage source) { private static BufferedImage scaleIfNeeded(BufferedImage source) {
@@ -99,8 +99,7 @@ public final class PqDevImageSupport {
double scale = (double) MAX_EDGE / max; double scale = (double) MAX_EDGE / max;
int targetWidth = Math.max(1, (int) Math.round(width * scale)); int targetWidth = Math.max(1, (int) Math.round(width * scale));
int targetHeight = Math.max(1, (int) Math.round(height * scale)); int targetHeight = Math.max(1, (int) Math.round(height * scale));
int type = source.getType() == BufferedImage.TYPE_CUSTOM ? BufferedImage.TYPE_INT_ARGB : source.getType(); BufferedImage target = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_ARGB);
BufferedImage target = new BufferedImage(targetWidth, targetHeight, type);
Graphics2D graphics = target.createGraphics(); Graphics2D graphics = target.createGraphics();
try { try {
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
@@ -113,16 +112,12 @@ public final class PqDevImageSupport {
return target; return target;
} }
private static byte[] writePng(BufferedImage image) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "png", outputStream);
return outputStream.toByteArray();
}
private static byte[] writeJpeg(BufferedImage image) throws IOException { private static byte[] writeJpeg(BufferedImage image) throws IOException {
BufferedImage rgbImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); BufferedImage rgbImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = rgbImage.createGraphics(); Graphics2D graphics = rgbImage.createGraphics();
try { try {
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
graphics.drawImage(image, 0, 0, null); graphics.drawImage(image, 0, 0, null);
} finally { } finally {
graphics.dispose(); graphics.dispose();
@@ -130,7 +125,7 @@ public final class PqDevImageSupport {
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg"); Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
if (!writers.hasNext()) { if (!writers.hasNext()) {
throw new IOException("未找到 JPG 图片编码器"); throw new IOException("JPG image writer not found");
} }
ImageWriter writer = writers.next(); ImageWriter writer = writers.next();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
@@ -170,12 +165,4 @@ public final class PqDevImageSupport {
private static String normalize(String value) { private static String normalize(String value) {
return value == null ? "" : value.toLowerCase(Locale.ROOT); return value == null ? "" : value.toLowerCase(Locale.ROOT);
} }
@Data
@AllArgsConstructor
public static class ImageEchoPayload {
private boolean hasImage;
private String imageBase64;
private String imageContentType;
}
} }

View File

@@ -1,55 +1,203 @@
package com.njcn.gather.device.util; package com.njcn.gather.device.util;
import org.junit.Test; import org.junit.Test;
import org.springframework.mock.web.MockMultipartFile;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.Color; import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public class PqDevImageSupportTest { public class PqDevImageSupportTest {
@Test private static final String INVALID_IMAGE_CONTENT = "图片内容无效";
public void buildEchoPayloadReturnsEmptyFieldsWhenImageMissing() { private static final String UNSUPPORTED_IMAGE_TYPE = "仅支持 JPG、JPEG、PNG 图片";
PqDevImageSupport.ImageEchoPayload payload = PqDevImageSupport.buildEchoPayload(null); private static final String IMAGE_TOO_LARGE = "图片大小不能超过 10MB";
assertFalse(payload.isHasImage()); @Test
assertNull(payload.getImageBase64()); public void processUploadConvertsPngToJpeg() throws Exception {
assertNull(payload.getImageContentType()); byte[] pngBytes = createImageBytes("png", 16, 10, BufferedImage.TYPE_INT_ARGB);
MockMultipartFile image = new MockMultipartFile("image", "test.png", "image/png", pngBytes);
byte[] result = PqDevImageSupport.processUploadToJpeg(image);
assertEquals("image/jpeg", PqDevImageSupport.detectContentType(result));
BufferedImage decoded = ImageIO.read(new ByteArrayInputStream(result));
assertNotNull(decoded);
assertEquals(16, decoded.getWidth());
assertEquals(10, decoded.getHeight());
} }
@Test @Test
public void buildEchoPayloadDetectsPngAndCreatesBase64() throws Exception { public void processUploadConvertsTransparentPngToWhiteJpegBackground() throws Exception {
byte[] imageBytes = createImageBytes("png"); byte[] pngBytes = createTransparentImageBytes();
MockMultipartFile image = new MockMultipartFile("image", "transparent.png", "image/png", pngBytes);
PqDevImageSupport.ImageEchoPayload payload = PqDevImageSupport.buildEchoPayload(imageBytes); byte[] result = PqDevImageSupport.processUploadToJpeg(image);
assertTrue(payload.isHasImage()); BufferedImage decoded = ImageIO.read(new ByteArrayInputStream(result));
assertEquals("image/png", payload.getImageContentType()); assertNotNull(decoded);
assertNotNull(payload.getImageBase64()); Color background = new Color(decoded.getRGB(0, 0));
assertFalse(payload.getImageBase64().isEmpty()); assertTrue(background.getRed() >= 245);
assertTrue(background.getGreen() >= 245);
assertTrue(background.getBlue() >= 245);
} }
@Test(expected = IllegalArgumentException.class) @Test
public void processUploadKeepsJpegReadable() throws Exception {
byte[] jpegBytes = createImageBytes("jpg", 12, 8, BufferedImage.TYPE_INT_RGB);
MockMultipartFile image = new MockMultipartFile("image", "test.jpeg", "image/jpeg", jpegBytes);
byte[] result = PqDevImageSupport.processUploadToJpeg(image);
assertEquals("image/jpeg", PqDevImageSupport.detectContentType(result));
BufferedImage decoded = ImageIO.read(new ByteArrayInputStream(result));
assertNotNull(decoded);
assertEquals(12, decoded.getWidth());
assertEquals(8, decoded.getHeight());
}
@Test
public void processUploadRejectsInvalidBytes() throws Exception {
MockMultipartFile image = new MockMultipartFile("image", "test.png", "image/png", new byte[]{1, 2, 3});
assertIllegalArgumentMessage(INVALID_IMAGE_CONTENT, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadRejectsNullImage() {
assertIllegalArgumentMessage(INVALID_IMAGE_CONTENT, () -> PqDevImageSupport.processUploadToJpeg(null));
}
@Test
public void processUploadRejectsEmptyImage() {
MockMultipartFile image = new MockMultipartFile("image", "empty.png", "image/png", new byte[0]);
assertIllegalArgumentMessage(INVALID_IMAGE_CONTENT, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadRejectsOversizedImage() {
MockMultipartFile image = new MockMultipartFile(
"image",
"large.jpg",
"image/jpeg",
new byte[(int) PqDevImageSupport.MAX_FILE_SIZE + 1]
);
assertIllegalArgumentMessage(IMAGE_TOO_LARGE, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadRejectsUnsupportedType() throws Exception {
byte[] pngBytes = createImageBytes("png", 2, 2, BufferedImage.TYPE_INT_RGB);
MockMultipartFile image = new MockMultipartFile("image", "test.gif", "image/gif", pngBytes);
assertIllegalArgumentMessage(UNSUPPORTED_IMAGE_TYPE, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadRejectsUnsupportedContentTypeEvenWithAllowedFileName() throws Exception {
byte[] pngBytes = createImageBytes("png", 2, 2, BufferedImage.TYPE_INT_RGB);
MockMultipartFile image = new MockMultipartFile("image", "test.png", "image/gif", pngBytes);
assertIllegalArgumentMessage(UNSUPPORTED_IMAGE_TYPE, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadRejectsUnsupportedFileNameEvenWithAllowedContentType() throws Exception {
byte[] pngBytes = createImageBytes("png", 2, 2, BufferedImage.TYPE_INT_RGB);
MockMultipartFile image = new MockMultipartFile("image", "test.gif", "image/png", pngBytes);
assertIllegalArgumentMessage(UNSUPPORTED_IMAGE_TYPE, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadRejectsUnsupportedImageBytes() throws Exception {
byte[] gifBytes = new byte[]{0x47, 0x49, 0x46, 0x38, 0x39, 0x61};
MockMultipartFile image = new MockMultipartFile("image", "test.png", "image/png", gifBytes);
assertIllegalArgumentMessage(UNSUPPORTED_IMAGE_TYPE, () -> PqDevImageSupport.processUploadToJpeg(image));
}
@Test
public void processUploadScalesLargeImage() throws Exception {
byte[] jpegBytes = createImageBytes("jpg", 3200, 1200, BufferedImage.TYPE_INT_RGB);
MockMultipartFile image = new MockMultipartFile("image", "large.jpg", "image/jpeg", jpegBytes);
byte[] result = PqDevImageSupport.processUploadToJpeg(image);
BufferedImage decoded = ImageIO.read(new ByteArrayInputStream(result));
assertNotNull(decoded);
assertEquals(PqDevImageSupport.MAX_EDGE, Math.max(decoded.getWidth(), decoded.getHeight()));
assertEquals(1600, decoded.getWidth());
assertEquals(600, decoded.getHeight());
}
@Test
public void detectContentTypeRejectsInvalidImageBytes() { public void detectContentTypeRejectsInvalidImageBytes() {
PqDevImageSupport.detectContentType(new byte[]{1, 2, 3, 4}); assertIllegalArgumentMessage(INVALID_IMAGE_CONTENT, () -> PqDevImageSupport.detectContentType(new byte[]{1, 2, 3}));
} }
private byte[] createImageBytes(String format) throws Exception { @Test
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB); public void detectContentTypeRejectsUnsupportedImageBytes() {
image.setRGB(0, 0, Color.RED.getRGB()); assertIllegalArgumentMessage(UNSUPPORTED_IMAGE_TYPE, () -> PqDevImageSupport.detectContentType(new byte[]{1, 2, 3, 4}));
image.setRGB(1, 0, Color.GREEN.getRGB()); }
image.setRGB(0, 1, Color.BLUE.getRGB());
image.setRGB(1, 1, Color.WHITE.getRGB()); private byte[] createImageBytes(String format, int width, int height, int type) throws Exception {
BufferedImage image = new BufferedImage(width, height, type);
Graphics2D graphics = image.createGraphics();
try {
graphics.setColor(new Color(255, 0, 0, type == BufferedImage.TYPE_INT_ARGB ? 120 : 255));
graphics.fillRect(0, 0, width, height);
graphics.setColor(Color.BLUE);
graphics.fillRect(width / 2, height / 2, Math.max(1, width / 2), Math.max(1, height / 2));
} finally {
graphics.dispose();
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, format, outputStream); assertTrue(ImageIO.write(image, format, outputStream));
return outputStream.toByteArray(); return outputStream.toByteArray();
} }
private byte[] createTransparentImageBytes() throws Exception {
BufferedImage image = new BufferedImage(4, 4, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
try {
graphics.setComposite(java.awt.AlphaComposite.Clear);
graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
graphics.setComposite(java.awt.AlphaComposite.SrcOver);
graphics.setColor(new Color(0, 0, 255, 128));
graphics.fillRect(2, 2, 2, 2);
} finally {
graphics.dispose();
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
assertTrue(ImageIO.write(image, "png", outputStream));
return outputStream.toByteArray();
}
private void assertIllegalArgumentMessage(String expectedMessage, ThrowingRunnable runnable) {
try {
runnable.run();
} catch (IllegalArgumentException ex) {
assertEquals(expectedMessage, ex.getMessage());
return;
} catch (Exception ex) {
throw new AssertionError("Expected IllegalArgumentException", ex);
}
throw new AssertionError("Expected IllegalArgumentException");
}
private interface ThrowingRunnable {
void run() throws Exception;
}
} }

View File

@@ -52,12 +52,7 @@ socket:
device: device:
ip: 127.0.0.1 ip: 127.0.0.1
port: 61000 port: 61000
# source:
# ip: 192.168.1.121
# port: 10086
# device:
# ip: 192.168.1.121
# port: 61000
webSocket: webSocket:
port: 7778 port: 7778
@@ -66,33 +61,15 @@ webSocket:
sntp: sntp:
port: 123 port: 123
#Dip:
# 暂态前时间s
# fPreTime: 2f
#写入时间s
# fRampIn: 0.001f
#写出时间s
# fRampOut: 0.001f
# 暂态后时间s
# fAfterTime: 3f
#Flicker:
# waveFluType: CPM
# waveType: SQU
# fDutyCycle: 50f
#log:
# homeDir: D:\logs
# commonLevel: info
report: report:
# template: D:\template # template: D:\template
# reportDir: D:\report # reportDir: D:\report
dateFormat: yyyy年MM月dd日 dateFormat: yyyy年MM月dd日
#data: device:
# homeDir: D:\data image:
#resource: dir: D:\device-images
# videoDir: ${data.homeDir}\resources\videos
qr: qr:
cloud: http://pqmcc.com:18082/api/file cloud: http://pqmcc.com:18082/api/file
dev: dev: