diff --git a/src/main/java/com/ycwl/basic/image/pipeline/stages/ImageResizeStage.java b/src/main/java/com/ycwl/basic/image/pipeline/stages/ImageResizeStage.java new file mode 100644 index 00000000..ad535c6c --- /dev/null +++ b/src/main/java/com/ycwl/basic/image/pipeline/stages/ImageResizeStage.java @@ -0,0 +1,126 @@ +package com.ycwl.basic.image.pipeline.stages; + +import com.ycwl.basic.image.pipeline.core.PhotoProcessContext; +import com.ycwl.basic.pipeline.annotation.StageConfig; +import com.ycwl.basic.pipeline.core.AbstractPipelineStage; +import com.ycwl.basic.pipeline.core.StageResult; +import com.ycwl.basic.pipeline.enums.StageOptionalMode; +import lombok.extern.slf4j.Slf4j; + +import javax.imageio.ImageIO; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.io.File; + +/** + * 图像缩放Stage + * 支持按比例放大或缩小图片 + */ +@Slf4j +@StageConfig( + stageId = "image_resize", + optionalMode = StageOptionalMode.SUPPORT, + description = "图像缩放处理", + defaultEnabled = true +) +public class ImageResizeStage extends AbstractPipelineStage { + + private final double scaleFactor; + + /** + * 构造函数 + * @param scaleFactor 缩放比例(例如: 1.5表示放大1.5倍, 0.333表示缩小到1/3) + */ + public ImageResizeStage(double scaleFactor) { + if (scaleFactor <= 0) { + throw new IllegalArgumentException("scaleFactor must be positive"); + } + this.scaleFactor = scaleFactor; + } + + @Override + public String getName() { + return "ImageResizeStage"; + } + + @Override + protected StageResult doExecute(PhotoProcessContext context) { + File currentFile = context.getCurrentFile(); + if (currentFile == null || !currentFile.exists()) { + return StageResult.skipped("当前文件不存在"); + } + + BufferedImage originalImage = null; + BufferedImage resizedImage = null; + + try { + log.debug("开始图像缩放处理: file={}, scaleFactor={}", currentFile.getName(), scaleFactor); + + // 读取原图 + originalImage = ImageIO.read(currentFile); + if (originalImage == null) { + return StageResult.failed("无法读取图片文件"); + } + + int originalWidth = originalImage.getWidth(); + int originalHeight = originalImage.getHeight(); + + // 计算新尺寸 + int newWidth = (int) Math.round(originalWidth * scaleFactor); + int newHeight = (int) Math.round(originalHeight * scaleFactor); + + // 检查尺寸是否合理 + if (newWidth <= 0 || newHeight <= 0) { + return StageResult.failed("缩放后尺寸无效: " + newWidth + "x" + newHeight); + } + + // 创建缩放后的图像 + resizedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = resizedImage.createGraphics(); + + try { + // 设置高质量渲染选项 + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // 执行缩放 + g2d.drawImage(originalImage, 0, 0, newWidth, newHeight, null); + } finally { + g2d.dispose(); + } + + // 保存缩放后的图片 + File resizedFile = context.getTempFileManager().createTempFile("resized", ".jpg"); + ImageIO.write(resizedImage, "jpg", resizedFile); + + if (!resizedFile.exists() || resizedFile.length() == 0) { + return StageResult.failed("缩放后图片保存失败"); + } + + // 更新处理后的文件 + context.updateProcessedFile(resizedFile); + + log.info("图像缩放完成: {}x{} -> {}x{} (比例: {})", + originalWidth, originalHeight, + newWidth, newHeight, + scaleFactor); + + return StageResult.success(String.format("缩放完成 (%dx%d -> %dx%d)", + originalWidth, originalHeight, newWidth, newHeight)); + + } catch (Exception e) { + log.error("图像缩放失败: {}", e.getMessage(), e); + return StageResult.failed("缩放失败: " + e.getMessage(), e); + } finally { + // 释放图像资源 + if (originalImage != null) { + originalImage.flush(); + } + if (resizedImage != null) { + resizedImage.flush(); + } + } + } +} diff --git a/src/main/java/com/ycwl/basic/image/pipeline/stages/UpdateMemberPrintStage.java b/src/main/java/com/ycwl/basic/image/pipeline/stages/UpdateMemberPrintStage.java new file mode 100644 index 00000000..8fce6748 --- /dev/null +++ b/src/main/java/com/ycwl/basic/image/pipeline/stages/UpdateMemberPrintStage.java @@ -0,0 +1,82 @@ +package com.ycwl.basic.image.pipeline.stages; + +import com.ycwl.basic.image.pipeline.core.PhotoProcessContext; +import com.ycwl.basic.mapper.PrinterMapper; +import com.ycwl.basic.pipeline.annotation.StageConfig; +import com.ycwl.basic.pipeline.core.AbstractPipelineStage; +import com.ycwl.basic.pipeline.core.StageResult; +import com.ycwl.basic.pipeline.enums.StageOptionalMode; +import lombok.extern.slf4j.Slf4j; + +/** + * 更新MemberPrint记录Stage + * 用于更新member_print表中的cropUrl字段 + */ +@Slf4j +@StageConfig( + stageId = "update_member_print", + optionalMode = StageOptionalMode.UNSUPPORT, + description = "更新MemberPrint记录", + defaultEnabled = true +) +public class UpdateMemberPrintStage extends AbstractPipelineStage { + + private final PrinterMapper printerMapper; + private final Integer memberPrintId; + private final Long memberId; + private final Long scenicId; + + /** + * 构造函数 + * @param printerMapper PrinterMapper实例 + * @param memberPrintId MemberPrint记录ID + * @param memberId 用户ID + * @param scenicId 景区ID + */ + public UpdateMemberPrintStage(PrinterMapper printerMapper, Integer memberPrintId, Long memberId, Long scenicId) { + this.printerMapper = printerMapper; + this.memberPrintId = memberPrintId; + this.memberId = memberId; + this.scenicId = scenicId; + } + + @Override + public String getName() { + return "UpdateMemberPrintStage"; + } + + @Override + protected StageResult doExecute(PhotoProcessContext context) { + String resultUrl = context.getResultUrl(); + + if (resultUrl == null || resultUrl.trim().isEmpty()) { + return StageResult.skipped("结果URL为空,跳过更新"); + } + + if (memberPrintId == null || memberId == null || scenicId == null) { + log.warn("MemberPrint更新参数不完整: memberPrintId={}, memberId={}, scenicId={}", + memberPrintId, memberId, scenicId); + return StageResult.skipped("更新参数不完整"); + } + + try { + log.debug("开始更新MemberPrint记录: id={}, newCropUrl={}", memberPrintId, resultUrl); + + // 更新cropUrl字段 + int rows = printerMapper.setPhotoCropped(memberId, scenicId, memberPrintId.longValue(), resultUrl, null); + + if (rows > 0) { + log.info("MemberPrint记录更新成功: id={}, cropUrl已更新", memberPrintId); + return StageResult.success("更新成功"); + } else { + log.warn("MemberPrint记录更新失败: 可能记录不存在, id={}", memberPrintId); + return StageResult.degraded("更新失败,记录可能不存在"); + } + + } catch (Exception e) { + log.error("更新MemberPrint记录异常: id={}", memberPrintId, e); + // 更新失败不影响整个流程,使用降级状态 + return StageResult.degraded("更新异常: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/ycwl/basic/service/printer/impl/PrinterServiceImpl.java b/src/main/java/com/ycwl/basic/service/printer/impl/PrinterServiceImpl.java index 67e7c0c0..2484401f 100644 --- a/src/main/java/com/ycwl/basic/service/printer/impl/PrinterServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/printer/impl/PrinterServiceImpl.java @@ -351,6 +351,8 @@ public class PrinterServiceImpl implements PrinterService { log.info("照片裁剪成功: memberId={}, scenicId={}, 原图={}, 裁剪后={}, 尺寸={}x{}", memberId, scenicId, url, cropUrl, printWidth, printHeight); + String crop = JacksonUtil.toJSONString(new Crop(270)); + entity.setCrop(crop); } finally { // 清理临时文件 if (croppedFile != null && croppedFile.exists()) { @@ -471,6 +473,34 @@ public class PrinterServiceImpl implements PrinterService { request.setProducts(productItems); + // 检查是否存在type=3的source记录,存在才自动发券 + boolean hasType3Source = userPhotoList.stream() + .filter(item -> item.getSourceId() != null && item.getSourceId() > 0) + .anyMatch(item -> { + try { + SourceEntity source = sourceMapper.getEntity(item.getSourceId()); + return source != null && Integer.valueOf(3).equals(source.getType()); + } catch (Exception e) { + log.warn("查询source失败: sourceId={}, error={}", item.getSourceId(), e.getMessage()); + return false; + } + }); + + if (hasType3Source) { + if (normalCount > 0) { + try { + autoCouponService.autoGrantCoupon( + memberId, + faceId, + scenicId, + ProductType.PHOTO_PRINT + ); + } catch (Exception e) { + log.warn("自动发券失败,不影响下单流程: memberId={}, faceId={}, scenicId={}, error={}", + memberId, faceId, scenicId, e.getMessage()); + } + } + } if (mobileCount > 0) { try { autoCouponService.autoGrantCoupon( @@ -484,19 +514,6 @@ public class PrinterServiceImpl implements PrinterService { memberId, faceId, scenicId, e.getMessage()); } } - if (normalCount > 0) { - try { - autoCouponService.autoGrantCoupon( - memberId, - faceId, - scenicId, - ProductType.PHOTO_PRINT - ); - } catch (Exception e) { - log.warn("自动发券失败,不影响下单流程: memberId={}, faceId={}, scenicId={}, error={}", - memberId, faceId, scenicId, e.getMessage()); - } - } request.setAutoUseCoupon(true); request.setPreviewOnly(true); // 仅查询价格,不实际使用优惠 @@ -795,7 +812,6 @@ public class PrinterServiceImpl implements PrinterService { .addStage(new ImageOrientationStage()) .addStage(new ConditionalRotateStage()) .addStage(new ImageEnhanceStage(config)) // 通过setStageState方法设置是否启用 - .addStage(new ImageResizeStage(2.0 / 3.0)) // 缩小2/3倍(回到原分辨率) .addStage(new WatermarkStage(watermarkConfig)) .addStage(new RestoreOrientationStage()) .addStage(new UploadStage()) @@ -823,6 +839,18 @@ public class PrinterServiceImpl implements PrinterService { * @return 处理后的URL,失败返回原URL */ private String processPhotoWithPipeline(MemberPrintResp item, Long scenicId, File qrCodeFile) { + return processPhotoWithPipeline(item, scenicId, qrCodeFile, null); + } + + /** + * 使用管线处理照片(支持增强选项) + * @param item 打印项 + * @param scenicId 景区ID + * @param qrCodeFile 二维码文件 + * @param needEnhance 是否需要图像增强(null 或 false 表示不增强) + * @return 处理后的URL,失败返回原URL + */ + private String processPhotoWithPipeline(MemberPrintResp item, Long scenicId, File qrCodeFile, Boolean needEnhance) { PrinterOrderItem orderItem = PrinterOrderItem.fromMemberPrintResp(item); PhotoProcessContext context = PhotoProcessContext.fromPrinterOrderItem(orderItem, scenicId); @@ -835,6 +863,11 @@ public class PrinterServiceImpl implements PrinterService { // 设置管线场景为图片打印 context.setScene(PipelineScene.IMAGE_PRINT); + // 处理图像增强选项 + if (needEnhance != null && needEnhance) { + context.setStageState("image_enhance", true); + } + // 根据sourceId判断图片来源和source类型 SourceEntity source = null; if (item.getSourceId() != null && item.getSourceId() > 0) { @@ -861,13 +894,13 @@ public class PrinterServiceImpl implements PrinterService { bceConfig.setSecretKey("dYatXReVriPeiktTjUblhfubpcmYfuMk"); // 准备水印配置 - WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile); + WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile, 3.0); // 准备存储适配器 prepareStorageAdapter(context); context.enableStage("image_sr"); context.enableStage("image_enhance"); - // 构建特殊管线: 放大1.5倍 -> 超分(2倍) -> 增强 -> 更新MemberPrint -> 缩小3倍 -> 水印 -> 上传 + // 构建特殊管线: 超分(2倍) -> 增强 -> 更新MemberPrint -> 缩小2倍 -> 水印 -> 上传 pipeline = new PipelineBuilder("Type3Pipeline") .addStage(new DownloadStage()) // 1. 下载图片 .addStage(new ImageOrientationStage()) // 2. 检测方向 @@ -877,7 +910,6 @@ public class PrinterServiceImpl implements PrinterService { .addStage(new UploadStage()) // 6. 上传(用于更新MemberPrint) .addStage(new UpdateMemberPrintStage(printerMapper, // 7. 更新MemberPrint的cropUrl item.getId(), item.getMemberId(), scenicId)) - .addStage(new ImageResizeStage(4.0 / 3.0)) // 8. 缩小3倍(回到原分辨率) .addStage(new WatermarkStage(watermarkConfig)) // 9. 添加水印 .addStage(new RestoreOrientationStage()) // 10. 恢复方向 .addStage(new UploadStage()) // 11. 最终上传 @@ -886,7 +918,7 @@ public class PrinterServiceImpl implements PrinterService { } else if (context.getImageType() == ImageType.NORMAL_PHOTO) { // 普通照片处理流程 - WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile); + WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile, 1.5); prepareStorageAdapter(context); pipeline = createNormalPhotoPipeline(watermarkConfig); } else { @@ -924,7 +956,7 @@ public class PrinterServiceImpl implements PrinterService { * @param qrCodeFile 二维码文件 * @return WatermarkConfig */ - private WatermarkConfig prepareWatermarkConfig(PhotoProcessContext context, File qrCodeFile) { + private WatermarkConfig prepareWatermarkConfig(PhotoProcessContext context, File qrCodeFile, Double scale) { ScenicConfigManager scenicConfig = context.getScenicConfigManager(); if (scenicConfig == null) { log.warn("scenicConfigManager未设置,返回空水印配置"); @@ -945,6 +977,7 @@ public class PrinterServiceImpl implements PrinterService { .scenicText(scenicText) .dateFormat(dateFormat) .qrcodeFile(qrCodeFile) + .scale(scale) .build(); } @@ -994,53 +1027,55 @@ public class PrinterServiceImpl implements PrinterService { } catch (Exception e) { throw new RuntimeException(e); } - userPhotoListByOrderId.forEach(item -> { - PrinterEntity printer = printerMapper.getById(item.getPrinterId()); + Thread.ofVirtual().start(() -> { + userPhotoListByOrderId.forEach(item -> { + PrinterEntity printer = printerMapper.getById(item.getPrinterId()); - // 使用管线处理照片 - String printUrl = processPhotoWithPipeline(item, item.getScenicId(), qrCodeFile); + // 使用管线处理照片 + String printUrl = processPhotoWithPipeline(item, item.getScenicId(), qrCodeFile); - // 根据数量创建多个打印任务 - Integer quantity = item.getQuantity(); - if (quantity == null || quantity <= 0) { - quantity = 1; // 默认至少打印1张 - } + // 根据数量创建多个打印任务 + Integer quantity = item.getQuantity(); + if (quantity == null || quantity <= 0) { + quantity = 1; // 默认至少打印1张 + } - for (int i = 0; i < quantity; i++) { - // 获取打印机名称(支持轮询) - String selectedPrinter = getNextPrinter(printer); + for (int i = 0; i < quantity; i++) { + // 获取打印机名称(支持轮询) + String selectedPrinter = getNextPrinter(printer); - // 根据景区配置决定任务初始状态 - ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(order.getScenicId()); - Boolean purchaseNeedReview = scenicConfig.getBoolean("printer_manual_approve"); - int initialStatus = (purchaseNeedReview != null && purchaseNeedReview) - ? TASK_STATUS_PENDING_REVIEW - : TASK_STATUS_PENDING; + // 根据景区配置决定任务初始状态 + ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(order.getScenicId()); + Boolean purchaseNeedReview = scenicConfig.getBoolean("printer_manual_approve"); + int initialStatus = (purchaseNeedReview != null && purchaseNeedReview) + ? TASK_STATUS_PENDING_REVIEW + : TASK_STATUS_PENDING; - PrintTaskEntity task = new PrintTaskEntity(); - task.setPrinterId(printer.getId()); - task.setPrinterName(selectedPrinter); - task.setMpId(item.getId()); - task.setPaper(printer.getPreferPaper()); - task.setStatus(initialStatus); - task.setUrl(printUrl); - task.setHeight(printer.getPreferH()); - task.setWidth(printer.getPreferW()); - task.setCreateTime(new Date()); - task.setUpdateTime(new Date()); - printTaskMapper.insertTask(task); + PrintTaskEntity task = new PrintTaskEntity(); + task.setPrinterId(printer.getId()); + task.setPrinterName(selectedPrinter); + task.setMpId(item.getId()); + task.setPaper(printer.getPreferPaper()); + task.setStatus(initialStatus); + task.setUrl(printUrl); + task.setHeight(printer.getPreferH()); + task.setWidth(printer.getPreferW()); + task.setCreateTime(new Date()); + task.setUpdateTime(new Date()); + printTaskMapper.insertTask(task); - // ========== WebSocket 推送任务 ========== - // 只推送立即可处理的任务(status=0),待审核任务(status=4)等审核通过后再推送 - if (initialStatus == TASK_STATUS_PENDING) { - try { - taskPushService.pushTaskToPrinter(printer.getId(), task.getId()); - } catch (Exception e) { - log.error("推送任务失败: printerId={}, taskId={}", printer.getId(), task.getId(), e); - // 推送失败不影响任务创建,任务会通过 HTTP 轮询获取 + // ========== WebSocket 推送任务 ========== + // 只推送立即可处理的任务(status=0),待审核任务(status=4)等审核通过后再推送 + if (initialStatus == TASK_STATUS_PENDING) { + try { + taskPushService.pushTaskToPrinter(printer.getId(), task.getId()); + } catch (Exception e) { + log.error("推送任务失败: printerId={}, taskId={}", printer.getId(), task.getId(), e); + // 推送失败不影响任务创建,任务会通过 HTTP 轮询获取 + } } } - } + }); }); } @@ -1475,58 +1510,15 @@ public class PrinterServiceImpl implements PrinterService { needEnhance = false; // 默认不增强 } - // 3.1 创建图片处理上下文 - PrinterOrderItem orderItem = PrinterOrderItem.fromMemberPrintResp(memberPrint); - PhotoProcessContext context = PhotoProcessContext.fromPrinterOrderItem(orderItem, memberPrint.getScenicId()); - context.setStageState("image_enhance", needEnhance); // 通过setStageState方法设置是否启用 - - // 3.2 设置景区配置和场景 - ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(memberPrint.getScenicId()); - context.setScenicConfigManager(scenicConfig); - context.setScene(PipelineScene.IMAGE_PRINT); - - // 3.3 判断图片来源 - if (memberPrint.getSourceId() != null && memberPrint.getSourceId() > 0) { - context.setSource(ImageSource.IPC); - } else if (memberPrint.getSourceId() == null) { - context.setSource(ImageSource.PHONE); - } else { - context.setSource(ImageSource.UNKNOWN); - } - - // 3.4 构建管线(关键:条件性添加 ImageEnhanceStage) - Pipeline pipeline; - String newPrintUrl = null; - + // 3.1 使用管线处理照片(复用 processPhotoWithPipeline) + String newPrintUrl; try { - if (context.getImageType() == ImageType.NORMAL_PHOTO) { - // 准备水印配置(重打印需要二维码) - WatermarkConfig watermarkConfig = prepareWatermarkConfig(context, qrCodeFile); - prepareStorageAdapter(context); - - // 创建管线,条件性添加增强 Stage - pipeline = createNormalPhotoPipeline(watermarkConfig); - } else { - // 拼图 - prepareStorageAdapter(context); - pipeline = createPuzzlePipeline(); - } - - // 3.5 执行管线 - boolean success = pipeline.execute(context); - if (success && context.getResultUrl() != null) { - newPrintUrl = context.getResultUrl(); - log.info("handleReprint: 照片重新处理成功, taskId={}, mpId={}, enhance={}, newUrl={}", - id, mpId, needEnhance, newPrintUrl); - } else { - log.warn("handleReprint: 照片重新处理失败, taskId={}, 使用原图", id); - newPrintUrl = memberPrint.getCropUrl(); // 使用原裁剪图 - } + newPrintUrl = processPhotoWithPipeline(memberPrint, memberPrint.getScenicId(), qrCodeFile, needEnhance); + log.info("handleReprint: 照片重新处理成功, taskId={}, mpId={}, enhance={}, newUrl={}", + id, mpId, needEnhance, newPrintUrl); } catch (Exception e) { log.error("handleReprint: 照片重新处理异常, taskId={}, 使用原图", id, e); newPrintUrl = memberPrint.getCropUrl(); - } finally { - context.cleanup(); } // 4. 更新打印任务