diff --git a/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkConfig.java b/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkConfig.java index 7105b977..f67bd084 100644 --- a/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkConfig.java +++ b/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkConfig.java @@ -60,11 +60,11 @@ public class WatermarkConfig { * 是否启用边缘端处理 */ @Builder.Default - private final boolean edgeEnabled = false; + private final boolean edgeEnabled = true; /** * 边缘端处理超时时间(毫秒) */ @Builder.Default - private final long edgeTimeoutMs = 30000L; + private final long edgeTimeoutMs = 10_000L; } diff --git a/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkStage.java b/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkStage.java index 30a110ea..c9d82778 100644 --- a/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkStage.java +++ b/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkStage.java @@ -190,8 +190,8 @@ public class WatermarkStage extends AbstractPipelineStage { // 构建水印信息用于边缘端处理 WatermarkInfo info = buildWatermarkInfo(context, currentFile, watermarkedFile, type); - // 调用边缘端服务处理 - return edgeService.processWatermarkFromFile(info, type, storageAdapter); + // 调用边缘端服务处理,传递 processId 作为 recordId + return edgeService.processWatermarkFromFile(info, type, storageAdapter, context.getProcessId()); } catch (Exception e) { log.error("边缘端水印处理异常: type={}, error={}", type.getType(), e.getMessage(), e); diff --git a/src/main/java/com/ycwl/basic/image/watermark/edge/LeicaWatermarkTemplateBuilder.java b/src/main/java/com/ycwl/basic/image/watermark/edge/LeicaWatermarkTemplateBuilder.java index 38f71543..fb1ed949 100644 --- a/src/main/java/com/ycwl/basic/image/watermark/edge/LeicaWatermarkTemplateBuilder.java +++ b/src/main/java/com/ycwl/basic/image/watermark/edge/LeicaWatermarkTemplateBuilder.java @@ -11,7 +11,7 @@ import java.util.Map; * 徕卡风格水印模板构建器 * 对应 LeicaWatermarkOperator * - * 布局说明: + * 布局说明(百分比基于1920x1080量化,精度0.5%): * - 画布大小 = 原图大小(不扩展) * - 原图收缩放在画布上半部分,底部留出空间 * - 底部白色区域左侧:帧途 Logo + "帧途" 文字 @@ -22,20 +22,32 @@ public class LeicaWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuil public static final String STYLE = "leica"; - // 常量配置(与 LeicaWatermarkOperator 保持一致) - private static final int EXTRA_BOTTOM_PX = 140; - private static final int LOGO_SIZE = 50; - private static final int LOGO_EXTRA_BORDER = 20; - private static final int LOGO_FONT_SIZE = 38; + // 百分比常量配置(基于1920x1080量化,精度0.5%) + /** 底部额外区域占高度百分比 */ + private static final double EXTRA_BOTTOM_PERCENT = 0.13; // 13% + /** Logo大小占高度百分比 */ + private static final double LOGO_SIZE_PERCENT = 0.045; // 4.5% + /** Logo额外边距占高度百分比 */ + private static final double LOGO_EXTRA_BORDER_PERCENT = 0.02; // 2% + /** Logo字体大小占高度百分比 */ + private static final double LOGO_FONT_SIZE_PERCENT = 0.035; // 3.5% + /** 二维码大小占高度百分比 */ + private static final double QRCODE_SIZE_PERCENT = 0.11; // 11% + /** 二维码X偏移占宽度百分比 */ + private static final double QRCODE_OFFSET_X_PERCENT = 0.005; // 0.5% + /** 二维码Y偏移占高度百分比 */ + private static final double QRCODE_OFFSET_Y_PERCENT = 0.02; // 2% + /** 左右边距占宽度百分比 */ + private static final double OFFSET_X_PERCENT = 0.04; // 4% + /** 上下边距占高度百分比 */ + private static final double OFFSET_Y_PERCENT = 0.03; // 3% + /** 景区名字体大小占高度百分比 */ + private static final double SCENIC_FONT_SIZE_PERCENT = 0.03; // 3% + /** 日期时间字体大小占高度百分比 */ + private static final double DATETIME_FONT_SIZE_PERCENT = 0.025; // 2.5% + private static final String LOGO_TEXT_COLOR = "#333333"; - private static final int QRCODE_SIZE = 120; - private static final int QRCODE_OFFSET_X = 5; - private static final int QRCODE_OFFSET_Y = 20; - private static final int OFFSET_X = 80; - private static final int OFFSET_Y = 30; - private static final int SCENIC_FONT_SIZE = 32; private static final String SCENIC_COLOR = "#333333"; - private static final int DATETIME_FONT_SIZE = 28; private static final String DATETIME_COLOR = "#999999"; /** @@ -53,12 +65,25 @@ public class LeicaWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuil int imageWidth = request.getImageWidth(); int imageHeight = request.getImageHeight(); + // 根据百分比计算实际像素值 + int extraBottom = (int) (imageHeight * EXTRA_BOTTOM_PERCENT); + int logoSize = (int) (imageHeight * LOGO_SIZE_PERCENT); + int logoExtraBorder = (int) (imageHeight * LOGO_EXTRA_BORDER_PERCENT); + int logoFontSize = (int) (imageHeight * LOGO_FONT_SIZE_PERCENT); + int qrcodeSize = (int) (imageHeight * QRCODE_SIZE_PERCENT); + int qrcodeOffsetX = (int) (imageWidth * QRCODE_OFFSET_X_PERCENT); + int qrcodeOffsetY = (int) (imageHeight * QRCODE_OFFSET_Y_PERCENT); + int offsetX = (int) (imageWidth * OFFSET_X_PERCENT); + int offsetY = (int) (imageHeight * OFFSET_Y_PERCENT); + int scenicFontSize = (int) (imageHeight * SCENIC_FONT_SIZE_PERCENT); + int datetimeFontSize = (int) (imageHeight * DATETIME_FONT_SIZE_PERCENT); + // 画布大小 = 原图大小(不扩展) int canvasWidth = imageWidth; int canvasHeight = imageHeight; // 原图收缩后的区域高度 - int shrunkImageHeight = imageHeight - EXTRA_BOTTOM_PX; + int shrunkImageHeight = imageHeight - extraBottom; // 底部区域起始 Y 坐标 int bottomAreaY = shrunkImageHeight; @@ -84,47 +109,44 @@ public class LeicaWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuil dynamicData.put("originalImage", request.getOriginalImageUrl()); // 2. Logo 元素(底部左侧) - int logoY = bottomAreaY + OFFSET_Y + LOGO_EXTRA_BORDER; + int logoY = bottomAreaY + offsetY + logoExtraBorder; PuzzleElementEntity logoElement = createImageElement( "logo", "Logo", - OFFSET_X, logoY - 12, - LOGO_SIZE, LOGO_SIZE, 10, + offsetX, logoY - (int)(logoSize * 0.24), + logoSize, logoSize, 10, FIT_MODE_CONTAIN, null, null ); elements.add(logoElement); dynamicData.put("logo", LOGO_URL); // 3. "帧途" 文字(Logo 右边) - int logoTextX = OFFSET_X + LOGO_SIZE + 5; - int logoTextY = bottomAreaY + OFFSET_Y + LOGO_EXTRA_BORDER; + int logoTextX = offsetX + logoSize + (int)(imageWidth * 0.005); + int logoTextY = bottomAreaY + offsetY + logoExtraBorder; PuzzleElementEntity logoTextElement = createTextElement( "logoText", "帧途文字", logoTextX, logoTextY, - 100, LOGO_SIZE, 10, - "PingFang SC", LOGO_FONT_SIZE, LOGO_TEXT_COLOR, + (int)(imageWidth * 0.05), logoSize, 10, + "PingFang SC", logoFontSize, LOGO_TEXT_COLOR, "NORMAL", TEXT_ALIGN_LEFT ); elements.add(logoTextElement); dynamicData.put("logoText", "帧途"); // 4. 计算右侧区域位置 - int qrcodeWidth = QRCODE_SIZE; - int qrcodeHeight = QRCODE_SIZE; - // 估算文字宽度(使用景区名和日期的较大者) int estimatedTextWidth = Math.max( - (request.getScenicLine() != null ? request.getScenicLine().length() : 0) * SCENIC_FONT_SIZE / 2, - (request.getDatetimeLine() != null ? request.getDatetimeLine().length() : 0) * DATETIME_FONT_SIZE / 2 + (request.getScenicLine() != null ? request.getScenicLine().length() : 0) * scenicFontSize / 2, + (request.getDatetimeLine() != null ? request.getDatetimeLine().length() : 0) * datetimeFontSize / 2 ); - int qrcodeX = canvasWidth - OFFSET_X - qrcodeWidth - QRCODE_OFFSET_X - estimatedTextWidth; - int qrcodeY = bottomAreaY + OFFSET_Y - QRCODE_OFFSET_Y; + int qrcodeX = canvasWidth - offsetX - qrcodeSize - qrcodeOffsetX - estimatedTextWidth; + int qrcodeY = bottomAreaY + offsetY - qrcodeOffsetY; // 5. 二维码元素 PuzzleElementEntity qrcodeElement = createImageElement( "qrcode", "二维码", qrcodeX, qrcodeY, - qrcodeWidth, qrcodeHeight, 10, + qrcodeSize, qrcodeSize, 10, FIT_MODE_CONTAIN, null, null ); elements.add(qrcodeElement); @@ -132,9 +154,9 @@ public class LeicaWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuil // 6. 头像元素(二维码中央,可选) if (request.getFaceUrl() != null && !request.getFaceUrl().isEmpty()) { - int avatarDiameter = (int) (qrcodeHeight * 0.45); - int avatarX = qrcodeX + (qrcodeWidth - avatarDiameter) / 2; - int avatarY = qrcodeY + (qrcodeHeight - avatarDiameter) / 2; + int avatarDiameter = (int) (qrcodeSize * 0.45); + int avatarX = qrcodeX + (qrcodeSize - avatarDiameter) / 2; + int avatarY = qrcodeY + (qrcodeSize - avatarDiameter) / 2; PuzzleElementEntity faceElement = createCircleImageElement( "face", "头像", @@ -146,29 +168,29 @@ public class LeicaWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuil } // 7. 计算文字位置(与二维码垂直居中) - int qrcodeCenter = qrcodeY + qrcodeHeight / 2; - int totalTextHeight = SCENIC_FONT_SIZE + DATETIME_FONT_SIZE + 10; + int qrcodeCenter = qrcodeY + qrcodeSize / 2; + int totalTextHeight = scenicFontSize + datetimeFontSize + (int)(imageHeight * 0.01); int textY = qrcodeCenter - totalTextHeight / 2; - int textX = canvasWidth - OFFSET_X - estimatedTextWidth; + int textX = canvasWidth - offsetX - estimatedTextWidth; // 8. 景区名文字 PuzzleElementEntity scenicTextElement = createTextElement( "scenicLine", "景区名", textX, textY, - estimatedTextWidth, SCENIC_FONT_SIZE + 10, 30, - "PingFang SC", SCENIC_FONT_SIZE, SCENIC_COLOR, + estimatedTextWidth, scenicFontSize + (int)(imageHeight * 0.01), 30, + "PingFang SC", scenicFontSize, SCENIC_COLOR, "NORMAL", TEXT_ALIGN_LEFT ); elements.add(scenicTextElement); dynamicData.put("scenicLine", request.getScenicLine() != null ? request.getScenicLine() : ""); // 9. 日期时间文字 - int datetimeY = textY + SCENIC_FONT_SIZE + 5; + int datetimeY = textY + scenicFontSize + (int)(imageHeight * 0.005); PuzzleElementEntity datetimeTextElement = createTextElement( "datetimeLine", "日期时间", textX, datetimeY, - estimatedTextWidth, DATETIME_FONT_SIZE + 10, 30, - "PingFang SC", DATETIME_FONT_SIZE, DATETIME_COLOR, + estimatedTextWidth, datetimeFontSize + (int)(imageHeight * 0.01), 30, + "PingFang SC", datetimeFontSize, DATETIME_COLOR, "NORMAL", TEXT_ALIGN_LEFT ); elements.add(datetimeTextElement); diff --git a/src/main/java/com/ycwl/basic/image/watermark/edge/WatermarkEdgeService.java b/src/main/java/com/ycwl/basic/image/watermark/edge/WatermarkEdgeService.java index 74deac3e..1ed3781f 100644 --- a/src/main/java/com/ycwl/basic/image/watermark/edge/WatermarkEdgeService.java +++ b/src/main/java/com/ycwl/basic/image/watermark/edge/WatermarkEdgeService.java @@ -170,11 +170,13 @@ public class WatermarkEdgeService { * @param info 水印信息(包含本地文件) * @param type 水印类型 * @param adapter 存储适配器 + * @param recordId 记录ID(用于边缘端任务追踪,不能为空) * @return 处理后的本地文件,失败返回null */ public File processWatermarkFromFile(WatermarkInfo info, ImageWatermarkOperatorEnum type, - IStorageAdapter adapter) { + IStorageAdapter adapter, + String recordId) { // 将 ImageWatermarkOperatorEnum 映射到边缘端风格 String style = mapTypeToStyle(type); @@ -239,18 +241,27 @@ public class WatermarkEdgeService { .outputQuality(90) .build(); - // 6. 创建边缘任务并等待结果 + // 6. 创建边缘任务并等待结果(使用传入的 recordId) + // recordId 转换为 Long,如果无法转换则使用哈希值 + Long recordIdLong; + try { + recordIdLong = Long.parseLong(recordId); + } catch (NumberFormatException e) { + // 如果 recordId 不是数字(如 UUID),使用其哈希值的绝对值 + recordIdLong = (long) Math.abs(recordId.hashCode()); + } + PuzzleEdgeRenderTaskService.TaskWaitResult result = watermarkEdgeTaskCreator.createAndWait( style, request, - null, // recordId + recordIdLong, // recordId null, // faceId type.getType(), // watermarkType DEFAULT_TIMEOUT_MS ); if (!result.isSuccess()) { - log.error("边缘端水印处理失败: error={}", result.getErrorMessage()); + log.error("边缘端水印处理失败: recordId={}, error={}", recordId, result.getErrorMessage()); return null; } @@ -259,11 +270,11 @@ public class WatermarkEdgeService { File outputFile = info.getWatermarkedFile(); downloadFile(resultUrl, outputFile); - log.info("边缘端水印处理成功: type={}, outputFile={}", type, outputFile); + log.info("边缘端水印处理成功: recordId={}, type={}, outputFile={}", recordId, type, outputFile); return outputFile; } catch (Exception e) { - log.error("边缘端水印处理异常: type={}", type, e); + log.error("边缘端水印处理异常: recordId={}, type={}", recordId, type, e); return null; } } 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 d4e022fe..63947f05 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 @@ -22,6 +22,7 @@ import com.ycwl.basic.image.pipeline.stages.UpdateMemberPrintStage; import com.ycwl.basic.image.pipeline.stages.UploadStage; import com.ycwl.basic.image.pipeline.stages.WatermarkConfig; import com.ycwl.basic.image.pipeline.stages.WatermarkStage; +import com.ycwl.basic.image.watermark.edge.WatermarkEdgeService; import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum; import com.ycwl.basic.integration.common.manager.DeviceConfigManager; import com.ycwl.basic.integration.common.manager.ScenicConfigManager; @@ -159,6 +160,9 @@ public class PrinterServiceImpl implements PrinterService { ); @Autowired private SourceRepository sourceRepository; + @Autowired + @Lazy + private WatermarkEdgeService watermarkEdgeService; @Override public List listByScenicId(Long scenicId) { @@ -1052,6 +1056,9 @@ public class PrinterServiceImpl implements PrinterService { .watermarkType(watermarkType) .scenicText(scenicText) .dateFormat(dateFormat) + .edgeService(watermarkEdgeService) + .storageAdapter(StorageFactory.use()) + .edgeEnabled(true) .qrcodeFile(qrCodeFile) .scale(scale) .build();