Compare commits

...

4 Commits

Author SHA1 Message Date
56e1081304 refactor(storage): 移除不再使用的人脸存储路径常量
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 删除 StorageConstant 中的 VIID_FACE 常量定义
- 移除 FaceCleaner 中对 VIID_FACE 的引用和相关文件清理逻辑
- 清理相关的导入语句和静态引用
2025-10-15 19:13:48 +08:00
658e741611 feat(printer): 添加图片方向检测与自动旋转功能
- 引入 ImageUtils 工具类处理图片旋转逻辑
- 实现打印前对竖图自动旋转为横图处理
- 完成水印处理后将图片旋转回原始方向-优化临时文件清理逻辑,确保所有中间文件被删除
- 添加图片方向判断方法 isLandscape- 新增图片旋转90度和270度的工具方法
2025-10-15 18:53:28 +08:00
d5cd1924f5 feat(task): 添加视频生成通知防重机制- 新增Redis缓存键VIDEO_NOTIFICATION_CACHE_KEY用于记录通知发送状态
- 设置通知发送间隔为2分钟,防止重复发送
- 在发送通知前检查缓存,若3分钟内已发送则跳过- 发送成功后更新Redis缓存并设置过期时间
- 添加相关日志记录以方便追踪通知发送情况
2025-10-15 18:43:54 +08:00
645afbaf0c feat(printer): 添加打印照片水印处理功能
- 引入图片水印处理相关依赖和工具类
- 实现根据景区配置动态添加水印逻辑
- 支持从配置中读取存储类型和水印类型
- 下载原始图片并应用水印处理
- 将处理后的水印图片上传至指定存储服务
- 打印任务使用水印图片URL替代原始URL
- 增加异常失败时回处理确保水印退到原始图片- 清理处理过程中产生的临时文件
2025-10-15 17:37:26 +08:00
24 changed files with 250 additions and 966 deletions

View File

@@ -5,6 +5,5 @@ public class StorageConstant {
public static final String VIDEO_PIECE_PATH = "source_video";
public static final String PHOTO_PATH = "source_photo";
public static final String PHOTO_WATERMARKED_PATH = "photo_w";
public static final String VIID_FACE = "viid_face";
public static final String USER_FACE = "user_face";
public static final String USER_FACE = "user_face";
}

View File

@@ -1,487 +0,0 @@
package com.ycwl.basic.controller.viid;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.thread.ThreadFactoryBuilder;
import cn.hutool.core.util.ObjectUtil;
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.annotation.IgnoreLogReq;
import com.ycwl.basic.annotation.IgnoreToken;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.AddFaceResp;
import com.ycwl.basic.integration.device.service.DeviceIntegrationService;
import com.ycwl.basic.integration.device.dto.device.CreateDeviceRequest;
import com.ycwl.basic.integration.device.dto.device.UpdateDeviceRequest;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceCropConfig;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.model.viid.entity.DeviceIdObject;
import com.ycwl.basic.model.viid.entity.FaceListObject;
import com.ycwl.basic.model.viid.entity.FaceObject;
import com.ycwl.basic.model.viid.entity.FacePositionObject;
import com.ycwl.basic.model.viid.entity.ResponseStatusObject;
import com.ycwl.basic.model.viid.entity.SubImageInfoObject;
import com.ycwl.basic.model.viid.entity.SubImageList;
import com.ycwl.basic.model.viid.entity.SystemTimeObject;
import com.ycwl.basic.model.viid.req.FaceUploadReq;
import com.ycwl.basic.model.viid.req.ImageUploadReq;
import com.ycwl.basic.model.viid.req.KeepaliveReq;
import com.ycwl.basic.model.viid.req.RegisterReq;
import com.ycwl.basic.model.viid.req.UnRegisterReq;
import com.ycwl.basic.model.viid.resp.SystemTimeResp;
import com.ycwl.basic.model.viid.resp.VIIDBaseResp;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.service.task.TaskFaceService;
import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.storage.enums.StorageAcl;
import com.ycwl.basic.storage.utils.StorageUtil;
import com.ycwl.basic.task.DynamicTaskGenerator;
import com.ycwl.basic.utils.ImageUtils;
import com.ycwl.basic.utils.IpUtils;
import com.ycwl.basic.utils.SnowFlakeUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import java.awt.image.RasterFormatException;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static com.ycwl.basic.constant.StorageConstant.PHOTO_PATH;
import static com.ycwl.basic.constant.StorageConstant.VIID_FACE;
@IgnoreToken
@RestController
// 摄像头对接接口
@RequestMapping("/VIID")
@Slf4j
public class ViidController {
@Autowired
private DeviceIntegrationService deviceIntegrationService;
private static final String serverId = "00000000000000000001";
@Autowired
private SourceMapper sourceMapper;
@Autowired
private DeviceRepository deviceRepository;
@Autowired
private TaskFaceService taskFaceService;
private final Map<Long, ThreadPoolExecutor> executors = new ConcurrentHashMap<>();
@Autowired
private ScenicService scenicService;
private ThreadPoolExecutor getExecutor(Long scenicId) {
return executors.computeIfAbsent(scenicId, k -> {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNamePrefix("VIID-" + scenicId + "-t")
.build();
return new ThreadPoolExecutor(
8, 32, 10L, TimeUnit.SECONDS, // 核心2个线程,最大20个线程,空闲60秒回收
new ArrayBlockingQueue<>(1024), // 队列大小从1024降至100
threadFactory,
new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时由调用线程执行,提供背压控制
);
});
}
// region 注册注销基础接口
/**
* 注册接口
*
* @param req 注册的信息
* @param request 请求
* @return 返回
*/
@RequestMapping(value = "/System/Register", method = RequestMethod.POST)
public VIIDBaseResp register(@RequestBody RegisterReq req, HttpServletRequest request) {
DeviceIdObject deviceIdObject = req.getRegisterObject();
log.info("注册的设备信息:{}", deviceIdObject);
// 保存设备注册时间
String deviceId = deviceIdObject.getDeviceId();
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(deviceId);
if (device == null) {
device = new DeviceEntity();
device.setName("未配置设备");
device.setNo(deviceId);
device.setOnline(1);
}
device.setKeepaliveAt(new Date());
device.setIpAddr(IpUtils.getIpAddr(request));
if (device.getId() == null) {
// 通过zt-device服务创建新设备
CreateDeviceRequest createRequest = new CreateDeviceRequest();
createRequest.setName(device.getName());
createRequest.setNo(device.getNo());
createRequest.setType("IPC"); // 默认类型为IPC
createRequest.setIsActive(0);
createRequest.setScenicId(0L);
createRequest.setSort(0);
try {
DeviceV2DTO createdDevice = deviceIntegrationService.createDevice(createRequest);
device.setId(createdDevice.getId());
} catch (Exception e) {
log.warn("创建设备失败,设备编号: {}, 错误: {}", deviceId, e.getMessage());
}
}
return new VIIDBaseResp(
new ResponseStatusObject(serverId, "/VIID/System/Register", "0", "注册成功", sdfTime.format(new Date()))
);
}
/**
* 保活接口
*
* @param req 保活的设备信息
* @param request 请求
* @return 返回
*/
@IgnoreLogReq
@RequestMapping(value = "/System/Keepalive", method = RequestMethod.POST)
public VIIDBaseResp keepalive(@RequestBody KeepaliveReq req, HttpServletRequest request) {
DeviceIdObject keepaliveObject = req.getKeepaliveObject();
// log.info("对方发送的心跳的信息:{}", keepaliveObject);
String deviceId = keepaliveObject.getDeviceId();
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(deviceId);
// 判断设备状态
if (device == null) {
// 不存在设备就注册
device = new DeviceEntity();
device.setName("未配置设备");
device.setNo(deviceId);
device.setOnline(1);
device.setKeepaliveAt(new Date());
device.setIpAddr(IpUtils.getIpAddr(request));
// 通过zt-device服务创建新设备
CreateDeviceRequest createRequest = new CreateDeviceRequest();
createRequest.setName(device.getName());
createRequest.setNo(device.getNo());
createRequest.setType("IPC"); // 默认类型为IPC
createRequest.setIsActive(0);
createRequest.setScenicId(0L);
createRequest.setSort(0);
try {
DeviceV2DTO createdDevice = deviceIntegrationService.createDevice(createRequest);
device.setId(createdDevice.getId());
} catch (Exception e) {
log.warn("创建设备失败,设备编号: {}, 错误: {}", deviceId, e.getMessage());
}
} else {
deviceRepository.updateOnlineStatus(device.getId(), IpUtils.getIpAddr(request), 1, new Date());
}
// log.info("已经解析过的心跳信息:{}", keepaliveObject);
return new VIIDBaseResp(
new ResponseStatusObject(deviceId, "/VIID/System/Keepalive", "0", "保活", sdfTime.format(new Date()))
);
}
/**
* 注销设备
*
* @param req 参数
* @return 返回
*/
@RequestMapping(value = "/System/UnRegister", method = RequestMethod.POST)
public VIIDBaseResp unRegister(@RequestBody UnRegisterReq req, HttpServletRequest request) {
// 获取设备id
DeviceIdObject unRegisterObject = req.getUnRegisterObject();
String deviceId = unRegisterObject.getDeviceId();
log.info("获取的注销的请求参数:{}", unRegisterObject);
// 首先查询该设备是否存在
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(deviceId);
// 判断
if (device != null) {
deviceRepository.updateOnlineStatus(device.getId(), IpUtils.getIpAddr(request), 0, new Date());
}
return new VIIDBaseResp(
new ResponseStatusObject(deviceId, "/VIID/System/UnRegister", "0", "注销成功", sdfTime.format(new Date()))
);
}
/**
* 校时接口
*
* @return 返回
*/
@RequestMapping(value = "/System/Time", method = RequestMethod.GET)
public SystemTimeResp time() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
return new SystemTimeResp(
new SystemTimeObject(serverId, "2", sdf.format(new Date()), TimeZone.getTimeZone("Asia/Shanghai").toString())
);
}
// endregion
@Autowired
private FaceSampleMapper faceSampleMapper;
private final SimpleDateFormat sdfTime = new SimpleDateFormat("yyyyMMddHHmmss");
/**
* 批量新增人脸
*/
@RequestMapping(value = "/Faces", method = RequestMethod.POST)
@IgnoreLogReq
public VIIDBaseResp faces(@RequestBody FaceUploadReq req) {
FaceListObject faceListObject = req.getFaceListObject();
List<FaceObject> faceObject = faceListObject.getFaceObject();
String faceId = null;
// 遍历人脸列表
for (FaceObject face : faceObject) {
// 设置FaceId
faceId = face.getFaceID();
// 获取图片信息
SubImageList subImageList = face.getSubImageList();
// 判断人脸对象中的列表是否为空
String deviceID = face.getDeviceID();
DeviceEntity device = deviceRepository.getDeviceByDeviceNo(deviceID);
if (device == null) {
continue;
}
DeviceConfigManager deviceConfig = deviceRepository.getDeviceConfigManager(device.getId());
DeviceConfigEntity deviceConfigEntity = deviceRepository.getDeviceConfig(device.getId());
if (deviceConfig == null) {
log.warn("设备配置不存在:" + deviceID);
return new VIIDBaseResp(
new ResponseStatusObject(faceId, "/VIID/Faces", "0", "OK", sdfTime.format(new Date()))
);
}
Integer viidMode = deviceConfig.getInteger("viid_mode", 0);
Date shotTime = null;
if (StringUtils.isNotBlank(face.getShotTime())) {
try {
shotTime = sdfTime.parse(face.getShotTime());
} catch (ParseException e) {
log.warn("拍摄时间时间转换失败,使用当前时间。错误entity:{}", face);
}
}
if (shotTime == null) {
if (StringUtils.isNotBlank(face.getFaceAppearTime())) {
try {
shotTime = sdfTime.parse(face.getFaceAppearTime());
} catch (ParseException e) {
log.warn("拍摄时间时间转换失败,使用当前时间。错误entity:{}", face);
}
}
}
if (shotTime == null) {
shotTime = new Date();
} else if (!DateUtil.isSameDay(shotTime, new Date())) {
log.warn("时间不是今天,使用当前时间。错误entity:{}", face);
shotTime = new Date();
}
if (Math.abs(shotTime.getTime() - System.currentTimeMillis()) > 3600 * 1000) {
String jsonString = JacksonUtil.toJSONStringCompat(req);
log.warn("时间差超过1小时。device:{},错误entity:{}", device, jsonString);
}
Long scenicId = device.getScenicId();
if (scenicId == null) {
continue;
}
IStorageAdapter scenicStorageAdapter = scenicService.getScenicStorageAdapter(scenicId);
IFaceBodyAdapter faceBodyAdapter = scenicService.getScenicFaceBodyAdapter(scenicId);
if (faceBodyAdapter == null) {
log.warn("人脸上传适配器不存在:" + scenicId);
continue;
}
FacePositionObject facePosition = new FacePositionObject();
facePosition.setLtY(face.getLeftTopY());
facePosition.setLtX(face.getLeftTopX());
facePosition.setRbY(face.getRightBtmY());
facePosition.setRbX(face.getRightBtmX());
if (ObjectUtil.isNotEmpty(subImageList) && CollUtil.isNotEmpty(subImageList.getSubImageInfoObject())) {
if (viidMode == 0) {
// 遍历每个图片对象
// 先找到type14的图片
List<SubImageInfoObject> type14ImageList = subImageList.getSubImageInfoObject().stream().filter(subImage -> "14".equals(subImage.getType())).toList();
for (SubImageInfoObject subImage : subImageList.getSubImageInfoObject()) {
// base64转换成MultipartFIle
MultipartFile file = ImageUtils.base64ToMultipartFile(subImage.getData());
String ext;
if (subImage.getFileFormat().equalsIgnoreCase("jpeg")) {
ext = "jpg";
} else {
ext = subImage.getFileFormat();
}
IStorageAdapter adapter = StorageFactory.use("faces");
// Type=11 人脸
if (subImage.getType().equals("11")) {
// 上传oss
Long newFaceSampleId = SnowFlakeUtil.getLongId();
if (Integer.valueOf(1).equals(device.getStatus())) {
FaceSampleEntity faceSample = new FaceSampleEntity();
faceSample.setId(newFaceSampleId);
faceSample.setScenicId(scenicId);
faceSample.setDeviceId(device.getId());
faceSample.setStatus(0);
faceSample.setCreateAt(shotTime);
String url = adapter.uploadFile(file, VIID_FACE, UUID.randomUUID() + "." + ext);
faceSample.setFaceUrl(url);
faceSampleMapper.add(faceSample);
ThreadPoolExecutor executor = getExecutor(scenicId);
executor.execute(() -> {
taskFaceService.assureFaceDb(faceBodyAdapter, scenicId.toString());
AddFaceResp addFaceResp;
try {
addFaceResp = faceBodyAdapter.addFace(scenicId.toString(), faceSample.getId().toString(), url, newFaceSampleId.toString());
} catch (Exception e) {
log.error("人脸添加失败:{}", e.getMessage());
return;
}
if (addFaceResp != null) {
faceSample.setScore(addFaceResp.getScore());
faceSampleMapper.updateScore(faceSample.getId(), addFaceResp.getScore());
}
if (Integer.valueOf(1).equals(deviceConfig.getInteger("enable_pre_book"))) {
DynamicTaskGenerator.addTask(faceSample.getId());
}
});
}
for (SubImageInfoObject _subImage : type14ImageList) {
facePosition.setImgHeight(_subImage.getHeight());
facePosition.setImgWidth(_subImage.getWidth());
SourceEntity source = new SourceEntity();
source.setDeviceId(device.getId());
source.setScenicId(device.getScenicId());
source.setFaceSampleId(newFaceSampleId);
source.setCreateTime(shotTime);
source.setType(2);
// 上传oss
MultipartFile _file = ImageUtils.base64ToMultipartFile(_subImage.getData());
ThreadPoolExecutor executor = getExecutor(scenicId);
executor.execute(() -> {
List<DeviceCropConfig> cropConfigs = deviceConfigEntity._getCropConfig();
for (DeviceCropConfig cropConfig : cropConfigs) {
source.setId(SnowFlakeUtil.getLongId());
String filename = StorageUtil.joinPath(PHOTO_PATH, UUID.randomUUID() + "." + ext);
MultipartFile _finalFile = _file;
if (cropConfig.getCropType() == 1) {
// 按固定位置截图
try {
_finalFile = ImageUtils.cropImage(_file, cropConfig.getTargetX(), cropConfig.getTargetY(), cropConfig.getTargetWidth(), cropConfig.getTargetHeight());
} catch (IOException e) {
log.error("裁切图片失败!", e);
} catch (RasterFormatException e) {
log.error("裁切图片出错!", e);
}
} else if (cropConfig.getCropType() == 2) {
// 按人脸位置
try {
int targetX = facePosition.getLtX() - (cropConfig.getTargetWidth() - facePosition.getWidth())/2;
int targetY = facePosition.getLtY() - (cropConfig.getTargetHeight() - facePosition.getHeight())/2;
_finalFile = ImageUtils.cropImage(_file, targetX, targetY, cropConfig.getTargetWidth(), cropConfig.getTargetHeight());
} catch (IOException e) {
log.error("裁切图片失败!", e);
} catch (RasterFormatException e) {
log.error("裁切图片出错!", e);
}
facePosition.setImgHeight(cropConfig.getTargetHeight());
facePosition.setImgWidth(cropConfig.getTargetWidth());
}
String _sourceUrl = scenicStorageAdapter.uploadFile(_finalFile, filename);
scenicStorageAdapter.setAcl(StorageAcl.PUBLIC_READ, filename);
source.setUrl(_sourceUrl);
source.setPosJson(JacksonUtil.toJSONString(facePosition));
sourceMapper.add(source);
}
});
}
log.info("人脸信息及原图{}张入库成功!设备ID:{}", type14ImageList.size(), deviceID);
}
}
} else if (viidMode == 1) {
for (SubImageInfoObject subImage : subImageList.getSubImageInfoObject()) {
// base64转换成MultipartFIle
MultipartFile file = ImageUtils.base64ToMultipartFile(subImage.getData());
String ext = subImage.getFileFormat();
if (ext.equalsIgnoreCase("jpeg")) {
ext = "jpg";
}
IStorageAdapter adapter = StorageFactory.use("faces");
// Type=14 人脸,传™的,有这么传的嘛
if (subImage.getType().equals("14")) {
// 上传oss
if (Integer.valueOf(1).equals(device.getStatus())) {
FaceSampleEntity faceSample = new FaceSampleEntity();
Long newFaceSampleId = SnowFlakeUtil.getLongId();
faceSample.setId(newFaceSampleId);
faceSample.setScenicId(scenicId);
faceSample.setDeviceId(device.getId());
faceSample.setStatus(0);
faceSample.setCreateAt(shotTime);
String url = adapter.uploadFile(file, VIID_FACE, UUID.randomUUID() + "." + ext);
faceSample.setFaceUrl(url);
faceSampleMapper.add(faceSample);
DynamicTaskGenerator.addTask(faceSample.getId());
ThreadPoolExecutor executor = getExecutor(scenicId);
executor.execute(() -> {
taskFaceService.assureFaceDb(faceBodyAdapter, scenicId.toString());
AddFaceResp addFaceResp;
try {
addFaceResp = faceBodyAdapter.addFace(scenicId.toString(), faceSample.getId().toString(), url, newFaceSampleId.toString());
} catch (Exception e) {
log.error("人脸添加失败:{}", e.getMessage());
return;
}
if (addFaceResp != null) {
faceSample.setScore(addFaceResp.getScore());
faceSampleMapper.updateScore(faceSample.getId(), addFaceResp.getScore());
if (Integer.valueOf(1).equals(deviceConfig.getInteger("enable_pre_book"))) {
DynamicTaskGenerator.addTask(faceSample.getId());
}
}
});
log.info("模式1人脸信息入库成功!设备ID:{}", deviceID);
}
}
}
}
}
}
return new VIIDBaseResp(
new ResponseStatusObject(faceId, "/VIID/Faces", "0", "OK", sdfTime.format(new Date()))
);
}
@RequestMapping(value = "/Images", method = RequestMethod.POST)
@IgnoreLogReq
public VIIDBaseResp images(HttpServletRequest request, @RequestBody ImageUploadReq req) throws IOException {
return new VIIDBaseResp(
new ResponseStatusObject("1", "/VIID/Images", "0", "OK", sdfTime.format(new Date()))
);
}
}

View File

@@ -1,14 +0,0 @@
package com.ycwl.basic.model.viid.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
@Data
public class DeviceIdObject implements Serializable {
@JsonProperty("DeviceID")
private String deviceId;
}

View File

@@ -1,12 +0,0 @@
package com.ycwl.basic.model.viid.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class FaceListObject {
@JsonProperty("FaceObject")
private List<FaceObject> faceObject;
}

View File

@@ -1,169 +0,0 @@
package com.ycwl.basic.model.viid.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class FaceObject {
@JsonProperty("FaceID")
private String FaceID;
@JsonProperty("InfoKind")
private Integer InfoKind;
@JsonProperty("SourceID")
private String SourceID;
@JsonProperty("DeviceID")
private String DeviceID;
@JsonProperty("LeftTopX")
private Integer LeftTopX;
@JsonProperty("LeftTopY")
private Integer LeftTopY;
@JsonProperty("RightBtmX")
private Integer RightBtmX;
@JsonProperty("RightBtmY")
private Integer RightBtmY;
@JsonProperty("IDNumber")
private String IDNumber;
@JsonProperty("Name")
private String Name;
@JsonProperty("UsedName")
private String UsedName;
@JsonProperty("Alias")
private String Alias;
@JsonProperty("AgeUpLimit")
private Integer AgeUpLimit;
@JsonProperty("AgeLowerLimit")
private Integer AgeLowerLimit;
@JsonProperty("EthicCode")
private String EthicCode;
@JsonProperty("NationalityCode")
private String NationalityCode;
@JsonProperty("NativeCityCode")
private String NativeCityCode;
@JsonProperty("ResidenceAdminDivision")
private String ResidenceAdminDivision;
@JsonProperty("ChineseAccentCode")
private String ChineseAccentCode;
@JsonProperty("JobCategory")
private String JobCategory;
@JsonProperty("AccompanyNumber")
private Integer AccompanyNumber;
@JsonProperty("SkinColor")
private String SkinColor;
@JsonProperty("FaceStyle")
private String FaceStyle;
@JsonProperty("FacialFeature")
private String FacialFeature;
@JsonProperty("PhysicalFeature")
private String PhysicalFeature;
@JsonProperty("IsDriver")
private Integer IsDriver;
@JsonProperty("IsForeigner")
private Integer IsForeigner;
@JsonProperty("ImmigrantTypeCode")
private String ImmigrantTypeCode;
@JsonProperty("IsSuspectedTerrorist")
private Integer IsSuspectedTerrorist;
@JsonProperty("SuspectedTerroristNumber")
private String SuspectedTerroristNumber;
@JsonProperty("IsCriminalInvolved")
private Integer IsCriminalInvolved;
@JsonProperty("CriminalInvolvedSpecilisationCode")
private String CriminalInvolvedSpecilisationCode;
@JsonProperty("BodySpeciallMark")
private String BodySpeciallMark;
@JsonProperty("CrimeMethod")
private String CrimeMethod;
@JsonProperty("CrimeCharacterCode")
private String CrimeCharacterCode;
@JsonProperty("EscapedCriminalNumber")
private String EscapedCriminalNumber;
@JsonProperty("IsDetainees")
private Integer IsDetainees;
@JsonProperty("DetentionHouseCode")
private String DetentionHouseCode;
@JsonProperty("DetaineesSpecialIdentity")
private String DetaineesSpecialIdentity;
@JsonProperty("MemberTypeCode")
private String MemberTypeCode;
@JsonProperty("IsVictim")
private String IsVictim;
@JsonProperty("VictimType")
private String VictimType;
@JsonProperty("CorpseConditionCode")
private String CorpseConditionCode;
@JsonProperty("IsSuspiciousPerson")
private String IsSuspiciousPerson;
@JsonProperty("Attitude")
private String Attitude;
@JsonProperty("Similaritydegree")
private String Similaritydegree;
@JsonProperty("EyebrowStyle")
private String EyebrowStyle;
@JsonProperty("NoseStyle")
private String NoseStyle;
@JsonProperty("MustacheStyle")
private String MustacheStyle;
@JsonProperty("LipStyle")
private String LipStyle;
@JsonProperty("WrinklePouch")
private String WrinklePouch;
@JsonProperty("AcneStain")
private String AcneStain;
@JsonProperty("FreckleBirthmark")
private String FreckleBirthmark;
@JsonProperty("ScarDimple")
private String ScarDimple;
@JsonProperty("TabID")
private String TabID;
@JsonProperty("OtherFeature")
private String OtherFeature;
@JsonProperty("Maritalstatus")
private String Maritalstatus;
@JsonProperty("FamilyAddress")
private String FamilyAddress;
@JsonProperty("CollectorOrg")
private String CollectorOrg;
@JsonProperty("CollectorID")
private String CollectorID;
@JsonProperty("DeviceSNNo")
private String DeviceSNNo;
@JsonProperty("APSId")
private String APSId;
@JsonProperty("LocationMarkTime")
private String LocationMarkTime;
@JsonProperty("FaceAppearTime")
private String FaceAppearTime;
@JsonProperty("FaceDisAppearTime")
private String FaceDisAppearTime;
@JsonProperty("ShotTime")
private String ShotTime;
@JsonProperty("IDType")
private String IDType;
@JsonProperty("GenderCode")
private String GenderCode;
@JsonProperty("HairStyle")
private String HairStyle;
@JsonProperty("HairColor")
private String HairColor;
@JsonProperty("RespiratorColor")
private String RespiratorColor;
@JsonProperty("CapStyle")
private String CapStyle;
@JsonProperty("CapColor")
private String CapColor;
@JsonProperty("GlassStyle")
private String GlassStyle;
@JsonProperty("GlassColor")
private String GlassColor;
@JsonProperty("PassportType")
private String PassportType;
@JsonProperty("DetaineesIdentity")
private String DetaineesIdentity;
@JsonProperty("InjuredDegree")
private String InjuredDegree;
@JsonProperty("EntryTime")
private String EntryTime;
@JsonProperty("SubImageList")
private SubImageList subImageList;
}

View File

@@ -1,29 +0,0 @@
package com.ycwl.basic.model.viid.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class FacePositionObject {
private Integer imgWidth;
private Integer imgHeight;
private Integer ltX;
private Integer ltY;
private Integer rbX;
private Integer rbY;
@JsonProperty("width")
public Integer getWidth(){
return rbX - ltX;
}
@JsonProperty("height")
public Integer getHeight(){
return rbY - ltY;
}
public Integer centerX(){
return (ltX + rbX) / 2;
}
public Integer centerY(){
return (ltY + rbY) / 2;
}
}

View File

@@ -1,46 +0,0 @@
package com.ycwl.basic.model.viid.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class ImageInfoObject {
@JsonProperty("ContentDescription")
private String contentDescription;
@JsonProperty("EventSort")
private int eventSort;
@JsonProperty("FileFormat")
private String fileFormat;
@JsonProperty("FileSize")
private long fileSize;
@JsonProperty("Height")
private int height;
@JsonProperty("ImageID")
private String imageID;
@JsonProperty("ImageSource")
private String imageSource;
@JsonProperty("InfoKind")
private int infoKind;
@JsonProperty("SecurityLevel")
private String securityLevel;
@JsonProperty("ShotPlaceFullAdress")
private String shotPlaceFullAdress;
@JsonProperty("ShotTime")
private String shotTime;
@JsonProperty("Title")
private String title;
@JsonProperty("Width")
private int width;
}

View File

@@ -1,12 +0,0 @@
package com.ycwl.basic.model.viid.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class ImageListObject {
@JsonProperty("Image")
private List<ImageObject> imageObject;
}

View File

@@ -1,16 +0,0 @@
package com.ycwl.basic.model.viid.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class ImageObject {
@JsonProperty("Data")
private String data;
@JsonProperty("FaceList")
private FaceListObject faceListObject;
@JsonProperty("ImageInfo")
private ImageInfoObject imageInfoObject;
}

View File

@@ -1,21 +0,0 @@
package com.ycwl.basic.model.viid.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ResponseStatusObject {
@JsonProperty("Id")
private String id;
@JsonProperty("RequestURL")
private String requestUrl;
@JsonProperty("StatusCode")
private String statusCode;
@JsonProperty("StatusString")
private String statusString;
@JsonProperty("LocalTime")
private String localTime;
}

View File

@@ -1,28 +0,0 @@
package com.ycwl.basic.model.viid.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class SubImageInfoObject {
@JsonProperty("ImageID")
private String ImageID;
@JsonProperty("EventSort")
private Integer EventSort;
@JsonProperty("DeviceID")
private String DeviceID;
@JsonProperty("StoragePath")
private String StoragePath;
@JsonProperty("Type")
private String Type;
@JsonProperty("FileFormat")
private String FileFormat;
@JsonProperty("Width")
private Integer Width;
@JsonProperty("Height")
private Integer Height;
@JsonProperty("ShotTime")
private String ShotTime;
@JsonProperty("Data")
private String Data;
}

View File

@@ -1,13 +0,0 @@
package com.ycwl.basic.model.viid.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class SubImageList {
@JsonProperty("SubImageInfoObject")
private List<SubImageInfoObject> subImageInfoObject;
}

View File

@@ -1,18 +0,0 @@
package com.ycwl.basic.model.viid.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class SystemTimeObject {
@JsonProperty("VIIDServerID")
private String viidServerId;
@JsonProperty("TimeMode")
private String timeMode;
@JsonProperty("LocalTime")
private String localTime;
@JsonProperty("TimeZone")
private String timezone;
}

View File

@@ -1,11 +0,0 @@
package com.ycwl.basic.model.viid.req;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ycwl.basic.model.viid.entity.FaceListObject;
import lombok.Data;
@Data
public class FaceUploadReq {
@JsonProperty("FaceListObject")
private FaceListObject faceListObject;
}

View File

@@ -1,11 +0,0 @@
package com.ycwl.basic.model.viid.req;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ycwl.basic.model.viid.entity.ImageListObject;
import lombok.Data;
@Data
public class ImageUploadReq {
@JsonProperty("ImageListObject")
private ImageListObject imageListObject;
}

View File

@@ -1,11 +0,0 @@
package com.ycwl.basic.model.viid.req;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ycwl.basic.model.viid.entity.DeviceIdObject;
import lombok.Data;
@Data
public class KeepaliveReq {
@JsonProperty("KeepaliveObject")
private DeviceIdObject keepaliveObject;
}

View File

@@ -1,11 +0,0 @@
package com.ycwl.basic.model.viid.req;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ycwl.basic.model.viid.entity.DeviceIdObject;
import lombok.Data;
@Data
public class RegisterReq {
@JsonProperty("RegisterObject")
private DeviceIdObject registerObject;
}

View File

@@ -1,12 +0,0 @@
package com.ycwl.basic.model.viid.req;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ycwl.basic.model.viid.entity.DeviceIdObject;
import lombok.Data;
@Data
public class UnRegisterReq {
@JsonProperty("UnRegisterObject")
private DeviceIdObject unRegisterObject;
}

View File

@@ -1,13 +0,0 @@
package com.ycwl.basic.model.viid.resp;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ycwl.basic.model.viid.entity.SystemTimeObject;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class SystemTimeResp {
@JsonProperty("SystemTimeObject")
private SystemTimeObject systemTimeObject;
}

View File

@@ -1,15 +0,0 @@
package com.ycwl.basic.model.viid.resp;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ycwl.basic.model.viid.entity.ResponseStatusObject;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class VIIDBaseResp {
@JsonProperty("ResponseStatusObject")
private ResponseStatusObject responseStatusObject;
}

View File

@@ -1,9 +1,17 @@
package com.ycwl.basic.service.printer.impl;
import cn.hutool.http.HttpUtil;
import com.ycwl.basic.biz.OrderBiz;
import com.ycwl.basic.constant.NumberConstant;
import com.ycwl.basic.constant.StorageConstant;
import com.ycwl.basic.enums.OrderStateEnum;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.image.watermark.ImageWatermarkFactory;
import com.ycwl.basic.image.watermark.entity.WatermarkInfo;
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
import com.ycwl.basic.image.watermark.exception.ImageWatermarkException;
import com.ycwl.basic.image.watermark.operator.IOperator;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.mapper.MemberMapper;
import com.ycwl.basic.mapper.OrderMapper;
import com.ycwl.basic.mapper.PrintTaskMapper;
@@ -31,9 +39,15 @@ import com.ycwl.basic.model.printer.req.WorkerAuthReqVo;
import com.ycwl.basic.model.printer.resp.PrintTaskResp;
import com.ycwl.basic.model.wx.WXPayOrderReqVO;
import com.ycwl.basic.repository.PriceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.mobile.WxPayService;
import com.ycwl.basic.service.printer.PrinterService;
import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.storage.enums.StorageAcl;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.ImageUtils;
import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.utils.SnowFlakeUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -43,6 +57,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.io.File;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
@@ -75,6 +90,8 @@ public class PrinterServiceImpl implements PrinterService {
private PrintTaskMapper printTaskMapper;
@Autowired
private IPriceCalculationService priceCalculationService;
@Autowired
private ScenicRepository scenicRepository;
@Override
public List<PrinterResp> listByScenicId(Long scenicId) {
@@ -421,10 +438,108 @@ public class PrinterServiceImpl implements PrinterService {
List<MemberPrintResp> userPhotoListByOrderId = getUserPhotoListByOrderId(orderId);
userPhotoListByOrderId.forEach(item -> {
PrinterEntity printer = printerMapper.getById(item.getPrinterId());
// 水印处理逻辑
String printUrl = item.getCropUrl();
try {
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(item.getScenicId());
String printWatermarkType = scenicConfig.getString("print_watermark_type");
if (StringUtils.isNotBlank(printWatermarkType)) {
ImageWatermarkOperatorEnum watermarkType = ImageWatermarkOperatorEnum.getByCode(printWatermarkType);
if (watermarkType != null) {
// 准备存储适配器
IStorageAdapter adapter;
String storeType = scenicConfig.getString("store_type");
if (storeType != null) {
adapter = StorageFactory.get(storeType);
String storeConfigJson = scenicConfig.getString("store_config_json");
if (StringUtils.isNotBlank(storeConfigJson)) {
adapter.loadConfig(JacksonUtil.parseObject(storeConfigJson, Map.class));
}
} else {
adapter = StorageFactory.use("assets-ext");
}
// 准备水印处理器
IOperator operator = ImageWatermarkFactory.get(watermarkType);
// 下载原图
File originalFile = new File("print_" + item.getId() + ".jpg");
File watermarkedFile = new File("print_" + item.getId() + "_" + watermarkType.getType() + "." + watermarkType.getPreferFileType());
File rotatedOriginalFile = null;
File rotatedWatermarkedFile = null;
boolean needRotation = false;
try {
HttpUtil.downloadFile(item.getCropUrl().replace("oss.zhentuai.com", "frametour-assets.oss-cn-shanghai-internal.aliyuncs.com"), originalFile);
// 判断图片方向并处理旋转
boolean isLandscape = ImageUtils.isLandscape(originalFile);
log.info("打印照片方向检测,照片ID: {}, 是否为横图: {}", item.getId(), isLandscape);
if (!isLandscape) {
// 竖图需要旋转为横图
needRotation = true;
rotatedOriginalFile = new File("print_" + item.getId() + "_rotated.jpg");
ImageUtils.rotateImage90(originalFile, rotatedOriginalFile);
log.info("竖图已旋转为横图,照片ID: {}", item.getId());
}
// 处理水印
WatermarkInfo watermarkInfo = new WatermarkInfo();
watermarkInfo.setOriginalFile(needRotation ? rotatedOriginalFile : originalFile);
watermarkInfo.setWatermarkedFile(watermarkedFile);
operator.process(watermarkInfo);
// 如果之前旋转了,需要将水印图片旋转回去
if (needRotation) {
rotatedWatermarkedFile = new File("print_" + item.getId() + "_final_" + watermarkType.getType() + "." + watermarkType.getPreferFileType());
ImageUtils.rotateImage270(watermarkedFile, rotatedWatermarkedFile);
log.info("水印图片已旋转回竖图,照片ID: {}", item.getId());
// 删除中间的横图水印文件
if (watermarkedFile.exists()) {
watermarkedFile.delete();
}
// 将最终的竖图水印文件赋值给watermarkedFile
watermarkedFile = rotatedWatermarkedFile;
}
// 上传水印图片
String watermarkedUrl = adapter.uploadFile(null, watermarkedFile, StorageConstant.PHOTO_WATERMARKED_PATH, watermarkedFile.getName());
adapter.setAcl(StorageAcl.PUBLIC_READ, StorageConstant.PHOTO_WATERMARKED_PATH, watermarkedFile.getName());
printUrl = watermarkedUrl;
log.info("水印处理成功,打印照片ID: {}, 水印URL: {}", item.getId(), watermarkedUrl);
} catch (Exception e) {
log.error("水印处理失败,使用原始照片进行打印。照片ID: {}", item.getId(), e);
} finally {
// 清理临时文件
if (originalFile != null && originalFile.exists()) {
originalFile.delete();
}
if (rotatedOriginalFile != null && rotatedOriginalFile.exists()) {
rotatedOriginalFile.delete();
}
if (watermarkedFile != null && watermarkedFile.exists()) {
watermarkedFile.delete();
}
if (rotatedWatermarkedFile != null && rotatedWatermarkedFile.exists()) {
rotatedWatermarkedFile.delete();
}
}
}
}
} catch (Exception e) {
log.error("获取景区配置失败,使用原始照片进行打印。景区ID: {}, 照片ID: {}", item.getScenicId(), item.getId(), e);
}
PrintTaskEntity task = new PrintTaskEntity();
task.setPrinterId(printer.getId());
task.setStatus(0);
task.setUrl(item.getCropUrl());
task.setUrl(printUrl);
task.setHeight(printer.getPreferH());
task.setWidth(printer.getPreferW());
task.setCreateTime(new Date());

View File

@@ -81,7 +81,9 @@ import java.util.stream.Collectors;
@Service
public class TaskTaskServiceImpl implements TaskService {
private static final String WORKER_SELF_HOSTED_CACHE_KEY = "worker_self_hosted_scenic:%s";
private static final String VIDEO_NOTIFICATION_CACHE_KEY = "video_notification_member:%s";
private static final int CACHE_EXPIRE_MINUTES = 3;
private static final int NOTIFICATION_CACHE_EXPIRE_MINUTES = 2;
@Autowired
private TaskMapper taskMapper;
@Autowired
@@ -621,6 +623,15 @@ public class TaskTaskServiceImpl implements TaskService {
@Override
public void sendVideoGeneratedServiceNotification(Long taskId, Long memberId) {
// 检查Redis中该memberId是否在3分钟内已发送过通知
String notificationCacheKey = String.format(VIDEO_NOTIFICATION_CACHE_KEY, memberId);
String cachedValue = redisTemplate.opsForValue().get(notificationCacheKey);
if (cachedValue != null) {
log.info("memberId:{} 在3分钟内已发送过通知,跳过本次发送", memberId);
return;
}
MemberVideoEntity item = videoMapper.queryRelationByMemberTask(memberId, taskId);
MemberRespVO member = memberMapper.getById(memberId);
String openId = member.getOpenId();
@@ -673,6 +684,10 @@ public class TaskTaskServiceImpl implements TaskService {
msg.setSendReason("视频生成通知");
msg.setSendBiz("视频生成");
ztMessageProducerService.send(msg);
// 发送成功后,设置Redis缓存,2分钟过期
redisTemplate.opsForValue().set(notificationCacheKey, String.valueOf(System.currentTimeMillis()), NOTIFICATION_CACHE_EXPIRE_MINUTES, TimeUnit.MINUTES);
log.debug("memberId:{} 通知发送成功,已设置{}分钟缓存", memberId, NOTIFICATION_CACHE_EXPIRE_MINUTES);
}
}

View File

@@ -38,7 +38,6 @@ import java.util.List;
import java.util.Objects;
import static com.ycwl.basic.constant.FaceConstant.USER_FACE_DB_NAME;
import static com.ycwl.basic.constant.StorageConstant.VIID_FACE;
@Component
@EnableScheduling
@@ -229,19 +228,8 @@ public class FaceCleaner {
log.info("开始清理人脸文件");
List<FaceSampleRespVO> list = faceSampleMapper.list(new FaceSampleReqQuery());
IStorageAdapter adapter = StorageFactory.use("faces");
List<StorageFileObject> fileObjectList = adapter.listDir(VIID_FACE);
fileObjectList.parallelStream().forEach(fileObject -> {
if (fileObject.getModifyTime() != null) {
// 如果是一天以内修改的,则跳过
if (DateUtil.between(fileObject.getModifyTime(), new Date(), DateUnit.DAY) < 1) {
return;
}
}
if(list.parallelStream().noneMatch(faceSampleRespVO -> faceSampleRespVO.getFaceUrl().contains(fileObject.getName()))){
log.info("删除人脸文件:{}", fileObject);
adapter.deleteFile(fileObject.getFullPath());
}
});
// VIID相关功能已移除,不再清理VIID_FACE目录
log.info("VIID人脸文件清理功能已移除");
}
public void cleanSourceOss() {
log.info("开始清理源视频素材文件");

View File

@@ -5,6 +5,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -77,6 +79,120 @@ public class ImageUtils {
}
}
/**
* 判断图片是否为横图(宽度大于高度)
*
* @param file 图片文件
* @return true表示横图,false表示竖图
* @throws IOException 读取文件失败
*/
public static boolean isLandscape(File file) throws IOException {
BufferedImage image = null;
try {
image = ImageIO.read(file);
if (image == null) {
throw new IOException("无法读取图片文件: " + file.getPath());
}
return image.getWidth() > image.getHeight();
} finally {
if (image != null) {
image.flush();
}
}
}
/**
* 旋转图片90度(顺时针)
*
* @param sourceFile 源图片文件
* @param targetFile 目标图片文件
* @throws IOException 读取或写入文件失败
*/
public static void rotateImage90(File sourceFile, File targetFile) throws IOException {
BufferedImage sourceImage = null;
BufferedImage rotatedImage = null;
try {
sourceImage = ImageIO.read(sourceFile);
if (sourceImage == null) {
throw new IOException("无法读取图片文件: " + sourceFile.getPath());
}
int width = sourceImage.getWidth();
int height = sourceImage.getHeight();
// 创建旋转后的图片(宽高互换)
rotatedImage = new BufferedImage(height, width, sourceImage.getType());
Graphics2D g2d = rotatedImage.createGraphics();
// 设置旋转变换
AffineTransform transform = new AffineTransform();
transform.translate(height / 2.0, width / 2.0);
transform.rotate(Math.PI / 2);
transform.translate(-width / 2.0, -height / 2.0);
g2d.setTransform(transform);
g2d.drawImage(sourceImage, 0, 0, null);
g2d.dispose();
// 保存旋转后的图片
ImageIO.write(rotatedImage, "jpg", targetFile);
log.info("图片旋转成功,原始尺寸: {}x{}, 旋转后尺寸: {}x{}", width, height, height, width);
} finally {
if (sourceImage != null) {
sourceImage.flush();
}
if (rotatedImage != null) {
rotatedImage.flush();
}
}
}
/**
* 旋转图片270度(逆时针90度)
*
* @param sourceFile 源图片文件
* @param targetFile 目标图片文件
* @throws IOException 读取或写入文件失败
*/
public static void rotateImage270(File sourceFile, File targetFile) throws IOException {
BufferedImage sourceImage = null;
BufferedImage rotatedImage = null;
try {
sourceImage = ImageIO.read(sourceFile);
if (sourceImage == null) {
throw new IOException("无法读取图片文件: " + sourceFile.getPath());
}
int width = sourceImage.getWidth();
int height = sourceImage.getHeight();
// 创建旋转后的图片(宽高互换)
rotatedImage = new BufferedImage(height, width, sourceImage.getType());
Graphics2D g2d = rotatedImage.createGraphics();
// 设置旋转变换
AffineTransform transform = new AffineTransform();
transform.translate(height / 2.0, width / 2.0);
transform.rotate(-Math.PI / 2);
transform.translate(-width / 2.0, -height / 2.0);
g2d.setTransform(transform);
g2d.drawImage(sourceImage, 0, 0, null);
g2d.dispose();
// 保存旋转后的图片
ImageIO.write(rotatedImage, "jpg", targetFile);
log.info("图片旋转成功,原始尺寸: {}x{}, 旋转后尺寸: {}x{}", width, height, height, width);
} finally {
if (sourceImage != null) {
sourceImage.flush();
}
if (rotatedImage != null) {
rotatedImage.flush();
}
}
}
public static class Base64DecodedMultipartFile implements MultipartFile {
private final byte[] imgContent;