You've already forked FrameTour-BE
feat(printer): 引入照片处理管线机制
- 新增Crop和PrinterOrderItem模型用于封装裁剪信息和打印订单项 - 实现基于Pipeline模式的照片处理流程,支持普通照片和拼图处理 - 添加多个处理阶段:下载、方向检测、条件旋转、水印、恢复方向、上传和清理 - 创建PipelineBuilder用于动态构建处理管线 - 实现抽象Stage基类和具体Stage实现类 - 添加Stage执行结果管理和异常处理机制 - 优化照片处理逻辑,使用管线替代原有复杂的嵌套处理代码 - 支持通过景区配置管理水印类型、存储适配器等参数 - 提供临时文件管理工具确保处理过程中文件及时清理 - 增强日志记录和错误处理能力,提升系统可维护性
This commit is contained in:
@@ -1,15 +1,21 @@
|
||||
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.pipeline.core.PhotoProcessContext;
|
||||
import com.ycwl.basic.image.pipeline.core.Pipeline;
|
||||
import com.ycwl.basic.image.pipeline.core.PipelineBuilder;
|
||||
import com.ycwl.basic.image.pipeline.stages.CleanupStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.ConditionalRotateStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.DownloadStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.ImageOrientationStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.PuzzleBorderStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.RestoreOrientationStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.UploadStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.WatermarkStage;
|
||||
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
||||
import com.ycwl.basic.image.watermark.operator.IOperator;
|
||||
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||
import com.ycwl.basic.mapper.FaceMapper;
|
||||
@@ -19,6 +25,7 @@ import com.ycwl.basic.mapper.OrderMapper;
|
||||
import com.ycwl.basic.mapper.PrintTaskMapper;
|
||||
import com.ycwl.basic.mapper.PrinterMapper;
|
||||
import com.ycwl.basic.mapper.SourceMapper;
|
||||
import com.ycwl.basic.model.PrinterOrderItem;
|
||||
import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
|
||||
import com.ycwl.basic.model.mobile.order.PriceObj;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
@@ -55,7 +62,6 @@ import com.ycwl.basic.service.pc.FaceService;
|
||||
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;
|
||||
@@ -731,6 +737,118 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
private static final int TASK_STATUS_PENDING_REVIEW = 4; // 待审核
|
||||
private final Lock syncTaskLock = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* 创建普通照片处理管线
|
||||
*/
|
||||
private Pipeline<PhotoProcessContext> createNormalPhotoPipeline() {
|
||||
return new PipelineBuilder<PhotoProcessContext>("NormalPhotoPipeline")
|
||||
.addStage(new DownloadStage())
|
||||
.addStage(new ImageOrientationStage())
|
||||
.addStage(new ConditionalRotateStage())
|
||||
.addStage(new WatermarkStage())
|
||||
.addStage(new RestoreOrientationStage())
|
||||
.addStage(new UploadStage())
|
||||
.addStage(new CleanupStage())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建拼图处理管线
|
||||
*/
|
||||
private Pipeline<PhotoProcessContext> createPuzzlePipeline() {
|
||||
return new PipelineBuilder<PhotoProcessContext>("PuzzlePipeline")
|
||||
.addStage(new DownloadStage())
|
||||
.addStage(new PuzzleBorderStage())
|
||||
.addStage(new UploadStage())
|
||||
.addStage(new CleanupStage())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用管线处理照片
|
||||
* @param item 打印项
|
||||
* @param scenicId 景区ID
|
||||
* @param qrCodeFile 二维码文件
|
||||
* @return 处理后的URL,失败返回原URL
|
||||
*/
|
||||
private String processPhotoWithPipeline(MemberPrintResp item, Long scenicId, File qrCodeFile) {
|
||||
PrinterOrderItem orderItem = PrinterOrderItem.fromMemberPrintResp(item);
|
||||
|
||||
PhotoProcessContext context = new PhotoProcessContext(orderItem, scenicId);
|
||||
context.setQrcodeFile(qrCodeFile);
|
||||
|
||||
try {
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(scenicId);
|
||||
|
||||
if (context.isNormalPhoto()) {
|
||||
prepareNormalPhotoContext(context, scenicConfig);
|
||||
} else if (context.isPuzzle()) {
|
||||
prepareStorageAdapter(context, scenicConfig);
|
||||
}
|
||||
|
||||
Pipeline<PhotoProcessContext> pipeline = context.isPuzzle()
|
||||
? createPuzzlePipeline()
|
||||
: createNormalPhotoPipeline();
|
||||
|
||||
boolean success = pipeline.execute(context);
|
||||
|
||||
if (success && context.getResultUrl() != null) {
|
||||
log.info("照片处理成功: photoId={}, type={}, url={}",
|
||||
item.getId(),
|
||||
context.isPuzzle() ? "拼图" : "普通照片",
|
||||
context.getResultUrl());
|
||||
return context.getResultUrl();
|
||||
} else {
|
||||
log.warn("照片处理失败,使用原图: photoId={}", item.getId());
|
||||
return item.getCropUrl();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("照片处理异常,使用原图: photoId={}", item.getId(), e);
|
||||
return item.getCropUrl();
|
||||
} finally {
|
||||
context.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备普通照片的Context配置
|
||||
*/
|
||||
private void prepareNormalPhotoContext(PhotoProcessContext context, ScenicConfigManager scenicConfig) {
|
||||
String printWatermarkType = scenicConfig.getString("print_watermark_type");
|
||||
if (StringUtils.isNotBlank(printWatermarkType)) {
|
||||
ImageWatermarkOperatorEnum watermarkType = ImageWatermarkOperatorEnum.getByCode(printWatermarkType);
|
||||
context.setWatermarkType(watermarkType);
|
||||
}
|
||||
|
||||
String scenicText = scenicConfig.getString("print_watermark_scenic_text", "");
|
||||
context.setScenicText(scenicText);
|
||||
|
||||
String dateFormat = scenicConfig.getString("print_watermark_dt_format", "yyyy.MM.dd");
|
||||
context.setDateFormat(dateFormat);
|
||||
|
||||
prepareStorageAdapter(context, scenicConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备存储适配器
|
||||
*/
|
||||
private void prepareStorageAdapter(PhotoProcessContext context, ScenicConfigManager scenicConfig) {
|
||||
try {
|
||||
String storeType = scenicConfig.getString("store_type");
|
||||
if (storeType != null) {
|
||||
IStorageAdapter adapter = StorageFactory.get(storeType);
|
||||
String storeConfigJson = scenicConfig.getString("store_config_json");
|
||||
if (StringUtils.isNotBlank(storeConfigJson)) {
|
||||
adapter.loadConfig(JacksonUtil.parseObject(storeConfigJson, Map.class));
|
||||
}
|
||||
context.setStorageAdapter(adapter);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("准备存储适配器失败,将使用默认存储: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserIsBuyItem(Long memberId, Long id, Long orderId) {
|
||||
if (redisTemplate.opsForValue().get(USER_PHOTO_LIST_TO_PRINTER + memberId + ":" + orderId) != null) {
|
||||
@@ -753,166 +871,9 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
}
|
||||
userPhotoListByOrderId.forEach(item -> {
|
||||
PrinterEntity printer = printerMapper.getById(item.getPrinterId());
|
||||
// 水印处理逻辑(仅当sourceId不为空时执行)
|
||||
String printUrl = item.getCropUrl();
|
||||
if (item.getSourceId() != null && item.getSourceId() > 0) {
|
||||
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);
|
||||
|
||||
// 生成唯一的处理标识符,避免多线程环境下的文件冲突
|
||||
String processId = item.getId() + "_" + UUID.randomUUID().toString();
|
||||
// 下载原图
|
||||
File originalFile = new File("print_" + processId + ".jpg");
|
||||
File watermarkedFile = new File("print_" + processId + "_" + watermarkType.getType() + "." + watermarkType.getPreferFileType());
|
||||
File rotatedOriginalFile = null;
|
||||
File rotatedWatermarkedFile = null;
|
||||
boolean needRotation = false;
|
||||
|
||||
try {
|
||||
HttpUtil.downloadFile(item.getCropUrl(), originalFile);
|
||||
WatermarkInfo watermarkInfo = new WatermarkInfo();
|
||||
|
||||
// 判断图片方向并处理旋转
|
||||
boolean isLandscape = false;
|
||||
try {
|
||||
Integer rotate = JacksonUtil.getInt(item.getCrop(), "rotation");
|
||||
if (rotate != null) {
|
||||
isLandscape = rotate % 180 == 0;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
if (!isLandscape) {
|
||||
// 竖图需要旋转为横图
|
||||
needRotation = true;
|
||||
rotatedOriginalFile = new File("print_" + processId + "_rotated.jpg");
|
||||
ImageUtils.rotateImage90(originalFile, rotatedOriginalFile);
|
||||
log.info("竖图已旋转为横图,照片ID: {}", item.getId());
|
||||
watermarkInfo.setOffsetLeft(40);
|
||||
}
|
||||
|
||||
// 处理水印
|
||||
watermarkInfo.setScenicLine(scenicConfig.getString("print_watermark_scenic_text", ""));
|
||||
watermarkInfo.setOriginalFile(needRotation ? rotatedOriginalFile : originalFile);
|
||||
watermarkInfo.setWatermarkedFile(watermarkedFile);
|
||||
watermarkInfo.setQrcodeFile(qrCodeFile);
|
||||
watermarkInfo.setDatetime(new Date());
|
||||
watermarkInfo.setDtFormat(scenicConfig.getString("print_watermark_dt_format", "yyyy.MM.dd"));
|
||||
|
||||
operator.process(watermarkInfo);
|
||||
|
||||
// 如果之前旋转了,需要将水印图片旋转回去
|
||||
if (needRotation) {
|
||||
rotatedWatermarkedFile = new File("print_" + processId + "_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);
|
||||
}
|
||||
} else if (item.getSourceId() != null && item.getSourceId() == 0) {
|
||||
// 拼图:添加白边框并向上偏移以避免打印机偏移
|
||||
try {
|
||||
// 生成唯一的处理标识符,避免多线程环境下的文件冲突
|
||||
String processId = item.getId() + "_" + UUID.randomUUID().toString();
|
||||
File originalFile = new File("puzzle_" + processId + ".png");
|
||||
File processedFile = new File("puzzle_" + processId + "_processed.png");
|
||||
|
||||
try {
|
||||
// 下载原图
|
||||
HttpUtil.downloadFile(item.getCropUrl(), originalFile);
|
||||
|
||||
// 添加白边框(左右20px,上下30px)并向上偏移15px
|
||||
ImageUtils.addBorderAndShiftUp(originalFile, processedFile, 20, 30, 15);
|
||||
|
||||
// 上传处理后的图片
|
||||
IStorageAdapter adapter;
|
||||
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(item.getScenicId());
|
||||
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");
|
||||
}
|
||||
|
||||
String processedUrl = adapter.uploadFile(null, processedFile, StorageConstant.PHOTO_WATERMARKED_PATH, processedFile.getName());
|
||||
adapter.setAcl(StorageAcl.PUBLIC_READ, StorageConstant.PHOTO_WATERMARKED_PATH, processedFile.getName());
|
||||
|
||||
printUrl = processedUrl;
|
||||
log.info("拼图照片添加白边框并向上偏移成功,照片ID: {}, 新URL: {}", item.getId(), processedUrl);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("拼图照片处理失败,使用原始照片进行打印。照片ID: {}", item.getId(), e);
|
||||
} finally {
|
||||
// 清理临时文件
|
||||
if (originalFile != null && originalFile.exists()) {
|
||||
originalFile.delete();
|
||||
}
|
||||
if (processedFile != null && processedFile.exists()) {
|
||||
processedFile.delete();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("拼图照片处理失败,使用原始照片进行打印。照片ID: {}", item.getId(), e);
|
||||
}
|
||||
}
|
||||
// 使用管线处理照片
|
||||
String printUrl = processPhotoWithPipeline(item, item.getScenicId(), qrCodeFile);
|
||||
|
||||
// 根据数量创建多个打印任务
|
||||
Integer quantity = item.getQuantity();
|
||||
|
||||
Reference in New Issue
Block a user