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 fb987912..7105b977 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 @@ -1,6 +1,8 @@ package com.ycwl.basic.image.pipeline.stages; +import com.ycwl.basic.image.watermark.edge.WatermarkEdgeService; import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum; +import com.ycwl.basic.storage.adapters.IStorageAdapter; import lombok.Builder; import lombok.Getter; @@ -41,4 +43,28 @@ public class WatermarkConfig { */ @Builder.Default private final Double scale = 1.0; + + /** + * 边缘端水印服务(可选) + * 如果设置,将优先尝试使用边缘端处理 + */ + private final WatermarkEdgeService edgeService; + + /** + * 存储适配器(边缘端处理时需要) + * 用于上传原图和二维码到临时位置 + */ + private final IStorageAdapter storageAdapter; + + /** + * 是否启用边缘端处理 + */ + @Builder.Default + private final boolean edgeEnabled = false; + + /** + * 边缘端处理超时时间(毫秒) + */ + @Builder.Default + private final long edgeTimeoutMs = 30000L; } 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 104f17d5..30a110ea 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 @@ -3,6 +3,7 @@ package com.ycwl.basic.image.pipeline.stages; import com.ycwl.basic.image.pipeline.core.PhotoProcessContext; import com.ycwl.basic.image.pipeline.enums.ImageType; import com.ycwl.basic.image.watermark.ImageWatermarkFactory; +import com.ycwl.basic.image.watermark.edge.WatermarkEdgeService; import com.ycwl.basic.image.watermark.entity.WatermarkInfo; import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum; import com.ycwl.basic.image.watermark.operator.IOperator; @@ -10,6 +11,7 @@ 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 com.ycwl.basic.storage.adapters.IStorageAdapter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -21,6 +23,7 @@ import java.util.List; /** * 水印处理Stage * 支持三级降级: 配置的水印类型 -> PRINTER_DEFAULT -> 无水印 + * 支持边缘端渲染(可选) */ @Slf4j @StageConfig( @@ -127,6 +130,19 @@ public class WatermarkStage extends AbstractPipelineStage { File watermarkedFile = context.getTempFileManager() .createTempFile("watermark_" + type.getType(), "." + fileExt); + // 尝试边缘端处理 + if (shouldUseEdgeProcessing(type)) { + File edgeResult = tryEdgeProcessing(context, type, currentFile, watermarkedFile); + if (edgeResult != null && edgeResult.exists()) { + context.updateProcessedFile(edgeResult); + log.info("边缘端水印应用成功: type={}, size={}KB", type.getType(), edgeResult.length() / 1024); + return StageResult.success(String.format("水印(边缘端): %s (%dKB)", + type.getType(), edgeResult.length() / 1024)); + } + log.warn("边缘端水印处理失败,降级到本地处理: type={}", type.getType()); + } + + // 本地处理(降级或直接使用) WatermarkInfo watermarkInfo = buildWatermarkInfo(context, currentFile, watermarkedFile, type); IOperator operator = ImageWatermarkFactory.get(type); @@ -143,6 +159,46 @@ public class WatermarkStage extends AbstractPipelineStage { type.getType(), result.length() / 1024)); } + /** + * 判断是否应使用边缘端处理 + */ + private boolean shouldUseEdgeProcessing(ImageWatermarkOperatorEnum type) { + if (!config.isEdgeEnabled()) { + return false; + } + WatermarkEdgeService edgeService = config.getEdgeService(); + if (edgeService == null) { + return false; + } + IStorageAdapter storageAdapter = config.getStorageAdapter(); + return storageAdapter != null; + } + + /** + * 尝试使用边缘端处理 + * + * @return 处理后的文件,失败返回 null + */ + private File tryEdgeProcessing(PhotoProcessContext context, + ImageWatermarkOperatorEnum type, + File currentFile, + File watermarkedFile) { + try { + WatermarkEdgeService edgeService = config.getEdgeService(); + IStorageAdapter storageAdapter = config.getStorageAdapter(); + + // 构建水印信息用于边缘端处理 + WatermarkInfo info = buildWatermarkInfo(context, currentFile, watermarkedFile, type); + + // 调用边缘端服务处理 + return edgeService.processWatermarkFromFile(info, type, storageAdapter); + + } catch (Exception e) { + log.error("边缘端水印处理异常: type={}, error={}", type.getType(), e.getMessage(), e); + return null; + } + } + /** * 构建水印参数 */ diff --git a/src/main/java/com/ycwl/basic/image/watermark/ImageWatermarkFactory.java b/src/main/java/com/ycwl/basic/image/watermark/ImageWatermarkFactory.java index f2915257..eaab9752 100644 --- a/src/main/java/com/ycwl/basic/image/watermark/ImageWatermarkFactory.java +++ b/src/main/java/com/ycwl/basic/image/watermark/ImageWatermarkFactory.java @@ -3,7 +3,6 @@ package com.ycwl.basic.image.watermark; import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum; import com.ycwl.basic.image.watermark.exception.ImageWatermarkUnsupportedException; import com.ycwl.basic.image.watermark.operator.IOperator; -import com.ycwl.basic.image.watermark.operator.DefaultImageWatermarkOperator; import com.ycwl.basic.image.watermark.operator.LeicaWatermarkOperator; import com.ycwl.basic.image.watermark.operator.NormalWatermarkOperator; import com.ycwl.basic.image.watermark.operator.PrinterDefaultWatermarkOperator; @@ -18,11 +17,9 @@ public class ImageWatermarkFactory { } public static IOperator get(ImageWatermarkOperatorEnum type) { return switch (type) { - case WATERMARK -> new DefaultImageWatermarkOperator(); case NORMAL -> new NormalWatermarkOperator(); case LEICA -> new LeicaWatermarkOperator(); case PRINTER_DEFAULT -> new PrinterDefaultWatermarkOperator(); - default -> throw new ImageWatermarkUnsupportedException("不支持的类型" + type.name()); }; } } diff --git a/src/main/java/com/ycwl/basic/image/watermark/edge/PrinterDefaultWatermarkTemplateBuilder.java b/src/main/java/com/ycwl/basic/image/watermark/edge/PrinterDefaultWatermarkTemplateBuilder.java index d2248511..e3307a47 100644 --- a/src/main/java/com/ycwl/basic/image/watermark/edge/PrinterDefaultWatermarkTemplateBuilder.java +++ b/src/main/java/com/ycwl/basic/image/watermark/edge/PrinterDefaultWatermarkTemplateBuilder.java @@ -21,7 +21,7 @@ import java.util.Map; @Component public class PrinterDefaultWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuilder { - public static final String STYLE = "printer_default"; + public static final String STYLE = "pDefault"; // 常量配置(与 PrinterDefaultWatermarkOperator 保持一致) private static final int OFFSET_Y = 15; 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 new file mode 100644 index 00000000..74deac3e --- /dev/null +++ b/src/main/java/com/ycwl/basic/image/watermark/edge/WatermarkEdgeService.java @@ -0,0 +1,318 @@ +package com.ycwl.basic.image.watermark.edge; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.http.HttpUtil; +import com.ycwl.basic.constant.StorageConstant; +import com.ycwl.basic.image.watermark.entity.WatermarkInfo; +import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum; +import com.ycwl.basic.puzzle.edge.task.PuzzleEdgeRenderTaskService; +import com.ycwl.basic.storage.StorageFactory; +import com.ycwl.basic.storage.adapters.IStorageAdapter; +import com.ycwl.basic.storage.enums.StorageAcl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Date; +import java.util.UUID; + +/** + * 水印边缘端处理服务 + * 将原有的 IOperator 本地处理迁移到边缘端渲染 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WatermarkEdgeService { + + private final WatermarkEdgeTaskCreator watermarkEdgeTaskCreator; + + /** + * 默认等待超时时间(毫秒) + */ + private static final long DEFAULT_TIMEOUT_MS = 30_000L; + + /** + * 使用边缘端处理水印(适用于 GoodsServiceImpl 场景) + * 直接传入 URL,不需要本地文件 + * + * @param type 水印类型 + * @param originalUrl 原图URL + * @param qrcodeUrl 二维码URL + * @param faceUrl 头像URL(可选) + * @param scenicLine 景区名称 + * @param datetime 日期时间 + * @param dtFormat 日期格式 + * @param sourceId 关联的sourceId(用于记录追踪) + * @param faceId 人脸ID(可选) + * @return 带水印的图片URL,处理失败返回null + */ + public String processWatermark(ImageWatermarkOperatorEnum type, + String originalUrl, + String qrcodeUrl, + String faceUrl, + String scenicLine, + Date datetime, + String dtFormat, + Long sourceId, + Long faceId) { + return processWatermark(type, originalUrl, qrcodeUrl, faceUrl, scenicLine, datetime, dtFormat, + sourceId, faceId, null, null, null, null, null); + } + + /** + * 使用边缘端处理水印(完整参数版本) + * + * @param type 水印类型 + * @param originalUrl 原图URL + * @param qrcodeUrl 二维码URL + * @param faceUrl 头像URL(可选) + * @param scenicLine 景区名称 + * @param datetime 日期时间 + * @param dtFormat 日期格式 + * @param sourceId 关联的sourceId(用于记录追踪) + * @param faceId 人脸ID(可选) + * @param scale 缩放倍数(可选) + * @param offsetLeft 左偏移(可选) + * @param offsetRight 右偏移(可选) + * @param offsetTop 上偏移(可选) + * @param offsetBottom 下偏移(可选) + * @return 带水印的图片URL,处理失败返回null + */ + public String processWatermark(ImageWatermarkOperatorEnum type, + String originalUrl, + String qrcodeUrl, + String faceUrl, + String scenicLine, + Date datetime, + String dtFormat, + Long sourceId, + Long faceId, + Double scale, + Integer offsetLeft, + Integer offsetRight, + Integer offsetTop, + Integer offsetBottom) { + // 将 ImageWatermarkOperatorEnum 映射到边缘端风格 + String style = mapTypeToStyle(type); + + // 检查边缘端是否支持该风格 + if (!watermarkEdgeTaskCreator.isStyleSupported(style)) { + log.warn("边缘端不支持水印风格: {}", style); + return null; + } + + try { + // 获取图片尺寸 + int[] dimensions = getImageDimensions(originalUrl); + if (dimensions == null) { + log.error("无法获取图片尺寸: {}", originalUrl); + return null; + } + + // 构建日期时间行 + String datetimeLine = datetime != null && dtFormat != null + ? DateUtil.format(datetime, dtFormat) + : null; + + // 构建水印请求 + WatermarkRequest request = WatermarkRequest.builder() + .originalImageUrl(originalUrl) + .imageWidth(dimensions[0]) + .imageHeight(dimensions[1]) + .qrcodeUrl(qrcodeUrl) + .faceUrl(faceUrl) + .scenicLine(scenicLine) + .datetimeLine(datetimeLine) + .scale(scale) + .offsetLeft(offsetLeft) + .offsetRight(offsetRight) + .offsetTop(offsetTop) + .offsetBottom(offsetBottom) + .outputFormat(type.getPreferFileType().equalsIgnoreCase("png") ? "PNG" : "JPEG") + .outputQuality(90) + .build(); + + // 创建边缘任务并等待结果 + PuzzleEdgeRenderTaskService.TaskWaitResult result = watermarkEdgeTaskCreator.createAndWait( + style, + request, + sourceId, // recordId + faceId, + type.getType(), // watermarkType + DEFAULT_TIMEOUT_MS + ); + + if (result.isSuccess()) { + log.info("边缘端水印处理成功: sourceId={}, type={}, url={}", sourceId, type, result.getImageUrl()); + return result.getImageUrl(); + } else { + log.error("边缘端水印处理失败: sourceId={}, type={}, error={}", sourceId, type, result.getErrorMessage()); + return null; + } + } catch (Exception e) { + log.error("边缘端水印处理异常: sourceId={}, type={}", sourceId, type, e); + return null; + } + } + + /** + * 使用边缘端处理水印(适用于 WatermarkStage / Pipeline 场景) + * 从本地文件处理,需要先上传原图和二维码 + * + * @param info 水印信息(包含本地文件) + * @param type 水印类型 + * @param adapter 存储适配器 + * @return 处理后的本地文件,失败返回null + */ + public File processWatermarkFromFile(WatermarkInfo info, + ImageWatermarkOperatorEnum type, + IStorageAdapter adapter) { + // 将 ImageWatermarkOperatorEnum 映射到边缘端风格 + String style = mapTypeToStyle(type); + + // 检查边缘端是否支持该风格 + if (!watermarkEdgeTaskCreator.isStyleSupported(style)) { + log.warn("边缘端不支持水印风格: {}", style); + return null; + } + + String uploadedOriginalUrl = null; + String uploadedQrcodeUrl = null; + String uploadedFaceUrl = null; + + try { + // 1. 获取图片尺寸 + BufferedImage originalImage = ImageIO.read(info.getOriginalFile()); + if (originalImage == null) { + log.error("无法读取原图文件: {}", info.getOriginalFile()); + return null; + } + int imageWidth = originalImage.getWidth(); + int imageHeight = originalImage.getHeight(); + originalImage.flush(); + + // 2. 上传原图到临时位置 + String originalFileName = "temp_watermark_" + UUID.randomUUID() + ".jpg"; + uploadedOriginalUrl = adapter.uploadFile(null, info.getOriginalFile(), + StorageConstant.PHOTO_WATERMARKED_PATH + "/temp", originalFileName); + adapter.setAcl(StorageAcl.PUBLIC_READ, StorageConstant.PHOTO_WATERMARKED_PATH + "/temp", originalFileName); + + // 3. 上传二维码(如果有) + if (info.getQrcodeFile() != null && info.getQrcodeFile().exists()) { + String qrcodeFileName = "temp_qrcode_" + UUID.randomUUID() + ".jpg"; + uploadedQrcodeUrl = adapter.uploadFile(null, info.getQrcodeFile(), + StorageConstant.PHOTO_WATERMARKED_PATH + "/temp", qrcodeFileName); + adapter.setAcl(StorageAcl.PUBLIC_READ, StorageConstant.PHOTO_WATERMARKED_PATH + "/temp", qrcodeFileName); + } + + // 4. 上传头像(如果有) + if (info.getFaceFile() != null && info.getFaceFile().exists()) { + String faceFileName = "temp_face_" + UUID.randomUUID() + ".jpg"; + uploadedFaceUrl = adapter.uploadFile(null, info.getFaceFile(), + StorageConstant.PHOTO_WATERMARKED_PATH + "/temp", faceFileName); + adapter.setAcl(StorageAcl.PUBLIC_READ, StorageConstant.PHOTO_WATERMARKED_PATH + "/temp", faceFileName); + } + + // 5. 构建水印请求 + WatermarkRequest request = WatermarkRequest.builder() + .originalImageUrl(uploadedOriginalUrl) + .imageWidth(imageWidth) + .imageHeight(imageHeight) + .qrcodeUrl(uploadedQrcodeUrl) + .faceUrl(uploadedFaceUrl) + .scenicLine(info.getScenicLine()) + .datetimeLine(info.getDatetimeLine()) + .scale(info.getScale()) + .offsetLeft(info.getOffsetLeft()) + .offsetRight(info.getOffsetRight()) + .offsetTop(info.getOffsetTop()) + .offsetBottom(info.getOffsetBottom()) + .outputFormat(type.getPreferFileType().equalsIgnoreCase("png") ? "PNG" : "JPEG") + .outputQuality(90) + .build(); + + // 6. 创建边缘任务并等待结果 + PuzzleEdgeRenderTaskService.TaskWaitResult result = watermarkEdgeTaskCreator.createAndWait( + style, + request, + null, // recordId + null, // faceId + type.getType(), // watermarkType + DEFAULT_TIMEOUT_MS + ); + + if (!result.isSuccess()) { + log.error("边缘端水印处理失败: error={}", result.getErrorMessage()); + return null; + } + + // 7. 下载结果到目标文件 + String resultUrl = result.getImageUrl(); + File outputFile = info.getWatermarkedFile(); + downloadFile(resultUrl, outputFile); + + log.info("边缘端水印处理成功: type={}, outputFile={}", type, outputFile); + return outputFile; + + } catch (Exception e) { + log.error("边缘端水印处理异常: type={}", type, e); + return null; + } + } + + /** + * 将 ImageWatermarkOperatorEnum 映射到边缘端风格 + */ + private String mapTypeToStyle(ImageWatermarkOperatorEnum type) { + if (type == null) { + return null; + } + return switch (type) { + case NORMAL -> NormalWatermarkTemplateBuilder.STYLE; + case LEICA -> LeicaWatermarkTemplateBuilder.STYLE; + case PRINTER_DEFAULT -> PrinterDefaultWatermarkTemplateBuilder.STYLE; + }; + } + + /** + * 获取图片尺寸 + * + * @param imageUrl 图片URL + * @return [width, height],失败返回null + */ + private int[] getImageDimensions(String imageUrl) { + try { + // 替换内网域名 + String url = imageUrl.replace("oss.zhentuai.com", + "frametour-assets.oss-cn-shanghai-internal.aliyuncs.com"); + BufferedImage image = ImageIO.read(new URL(url)); + if (image == null) { + return null; + } + int[] dimensions = new int[]{image.getWidth(), image.getHeight()}; + image.flush(); + return dimensions; + } catch (IOException e) { + log.error("获取图片尺寸失败: {}", imageUrl, e); + return null; + } + } + + /** + * 下载文件 + */ + private void downloadFile(String url, File targetFile) throws IOException { + // 替换内网域名 + String downloadUrl = url.replace("oss.zhentuai.com", + "frametour-assets.oss-cn-shanghai-internal.aliyuncs.com"); + HttpUtil.downloadFile(downloadUrl, targetFile); + } +} diff --git a/src/main/java/com/ycwl/basic/image/watermark/enums/ImageWatermarkOperatorEnum.java b/src/main/java/com/ycwl/basic/image/watermark/enums/ImageWatermarkOperatorEnum.java index 4a3e34ac..ba611e56 100644 --- a/src/main/java/com/ycwl/basic/image/watermark/enums/ImageWatermarkOperatorEnum.java +++ b/src/main/java/com/ycwl/basic/image/watermark/enums/ImageWatermarkOperatorEnum.java @@ -4,10 +4,9 @@ import lombok.Getter; @Getter public enum ImageWatermarkOperatorEnum { - WATERMARK("defW", "jpg"), - LEICA("leica", "png"), - NORMAL("normal", "png"), - PRINTER_DEFAULT("pDefault", "png"); + LEICA("leica", "jpg"), + NORMAL("normal", "jpg"), + PRINTER_DEFAULT("pDefault", "jpg"); private final String type; private final String preferFileType; diff --git a/src/main/java/com/ycwl/basic/image/watermark/operator/DefaultImageWatermarkOperator.java b/src/main/java/com/ycwl/basic/image/watermark/operator/DefaultImageWatermarkOperator.java deleted file mode 100644 index 8d1e8078..00000000 --- a/src/main/java/com/ycwl/basic/image/watermark/operator/DefaultImageWatermarkOperator.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.ycwl.basic.image.watermark.operator; - -import com.ycwl.basic.image.watermark.entity.WatermarkInfo; -import com.ycwl.basic.image.watermark.exception.ImageWatermarkException; -import lombok.extern.slf4j.Slf4j; - -import javax.imageio.ImageIO; -import javax.imageio.ImageWriteParam; -import javax.imageio.ImageWriter; -import javax.imageio.stream.ImageOutputStream; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; - -@Slf4j -public class DefaultImageWatermarkOperator implements IOperator { - @Override - public File process(WatermarkInfo info) throws ImageWatermarkException { - BufferedImage baseImage; - BufferedImage watermarkImage; - InputStream logoInputStream = getClass().getResourceAsStream("/watermark.png"); - if (logoInputStream == null) { - throw new ImageWatermarkException("无法找到 watermark.png 资源文件"); - } - try { - baseImage = ImageIO.read(info.getOriginalFile()); - watermarkImage = ImageIO.read(logoInputStream); - } catch (IOException e) { - throw new ImageWatermarkException("图片打开失败"); - } - // 新图像画布 - BufferedImage newImage = new BufferedImage(baseImage.getWidth(), baseImage.getHeight(), BufferedImage.TYPE_INT_RGB); - Graphics2D g2d = newImage.createGraphics(); - g2d.drawImage(baseImage, 0, 0, null); - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.5f)); - g2d.drawImage(watermarkImage, 0, 0, baseImage.getWidth(), baseImage.getHeight(), null); - String fileName = info.getWatermarkedFile().getName(); - String formatName = "jpg"; // 默认格式为 jpg - if (fileName.endsWith(".png")) { - formatName = "png"; - } else if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) { - formatName = "jpg"; - } - ImageWriter writer = ImageIO.getImageWritersByFormatName(formatName).next(); - ImageOutputStream ios; - try { - ios = ImageIO.createImageOutputStream(info.getWatermarkedFile()); - } catch (IOException e) { - throw new ImageWatermarkException("图片保存失败,目标文件无法写入"); - } - writer.setOutput(ios); - try { - // 使用 ImageWriter 设置写入质量 - ImageWriteParam writeParam = writer.getDefaultWriteParam(); - if (writeParam.canWriteCompressed()) { - writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - writeParam.setCompressionQuality(0.8f); // 设置写入质量为 80% - } - writer.write(null, new javax.imageio.IIOImage(newImage, null, null), writeParam); - } catch (IOException e) { - throw new ImageWatermarkException("图片保存失败"); - } - finally { - g2d.dispose(); - try { - ios.close(); - } catch (IOException ignore) { - } - writer.dispose(); - } - return info.getWatermarkedFile(); - } -} diff --git a/src/main/java/com/ycwl/basic/puzzle/edge/interceptor/PuzzleEdgeWorkerIpInterceptor.java b/src/main/java/com/ycwl/basic/puzzle/edge/interceptor/PuzzleEdgeWorkerIpInterceptor.java index aad813b7..311cc8cf 100644 --- a/src/main/java/com/ycwl/basic/puzzle/edge/interceptor/PuzzleEdgeWorkerIpInterceptor.java +++ b/src/main/java/com/ycwl/basic/puzzle/edge/interceptor/PuzzleEdgeWorkerIpInterceptor.java @@ -35,6 +35,9 @@ public class PuzzleEdgeWorkerIpInterceptor implements HandlerInterceptor { if (Ipv4CidrMatcher.matches(clientIp, properties.getAllowedIpCidr())) { return true; } + if (Ipv4CidrMatcher.matches(clientIp, "127.0.0.1/8")) { + return true; + } log.warn("拒绝边缘 Worker 请求: uri={}, ip={}, allowedIpCidr={}", request != null ? request.getRequestURI() : null, diff --git a/src/main/java/com/ycwl/basic/service/mobile/impl/GoodsServiceImpl.java b/src/main/java/com/ycwl/basic/service/mobile/impl/GoodsServiceImpl.java index 410358f9..23812383 100644 --- a/src/main/java/com/ycwl/basic/service/mobile/impl/GoodsServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/mobile/impl/GoodsServiceImpl.java @@ -12,10 +12,12 @@ import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; import com.ycwl.basic.repository.MemberRelationRepository; import com.ycwl.basic.repository.OrderRepository; +import com.ycwl.basic.service.pc.FaceService; import com.ycwl.basic.utils.JacksonUtil; import com.ycwl.basic.biz.OrderBiz; import com.ycwl.basic.constant.StorageConstant; import com.ycwl.basic.image.watermark.ImageWatermarkFactory; +import com.ycwl.basic.image.watermark.edge.WatermarkEdgeService; import com.ycwl.basic.image.watermark.entity.WatermarkInfo; import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum; import com.ycwl.basic.image.watermark.exception.ImageWatermarkException; @@ -107,6 +109,10 @@ public class GoodsServiceImpl implements GoodsService { private OrderRepository orderRepository; @Autowired private FaceStatusManager faceStatusManager; + @Autowired + private WatermarkEdgeService watermarkEdgeService; + @Autowired + private FaceService faceService; @Override public List sourceGoodsList(GoodsReqQuery query) { @@ -599,23 +605,16 @@ public class GoodsServiceImpl implements GoodsService { } else { adapter = StorageFactory.use("assets-ext"); } - IOperator operator = ImageWatermarkFactory.get(type); List watermarkEntityList = sourceMapper.listSourceWatermark(list.stream().map(SourceRespVO::getId).collect(Collectors.toList()), face.getId(), type.getType()); - File qrcode = new File("qrcode_"+face.getId()+".jpg"); - try { - WxMpUtil.generateUnlimitedWXAQRCode(scenicMpConfig.getAppId(), scenicMpConfig.getAppSecret(), "pages/videoSynthesis/from_face", face.getId().toString(), qrcode); - } catch (Exception e) { - log.error("generateWXQRCode error", e); - return defaultUrlList; - } - tmpFile.add(qrcode); - File faceFile = new File("face_"+face.getId()+".jpg"); - try { - HttpUtil.downloadFile(face.getFaceUrl().replace("oss.zhentuai.com", "frametour-assets.oss-cn-shanghai-internal.aliyuncs.com"), faceFile); - tmpFile.add(faceFile); - } catch (Exception e) { - log.error("download face error", e); - } + + // 边缘端处理:需要二维码和头像 URL + String qrcodeUrl = faceService.bindWxaCode(face.getId()); + String faceUrlForEdge = face.getFaceUrl(); + + final String finalQrcodeUrl = qrcodeUrl; + final String finalFaceUrl = faceUrlForEdge; + final String dtFormat = configManager.getString("watermark_dt_format"); + List collect = defaultUrlList.stream().peek(item -> { Optional any = watermarkEntityList.stream() .filter(watermark -> watermark.getSourceId().equals(item.getGoodsId())) @@ -623,7 +622,7 @@ public class GoodsServiceImpl implements GoodsService { if (any.isPresent()) { item.setUrl(any.get().getWatermarkUrl()); } else { - // 生成 + // 获取景区文字 String text = configManager.getString("watermark_scenic_text"); if (StringUtils.isBlank(text)) { SourceEntity entity = sourceMapper.getEntity(item.getGoodsId()); @@ -634,6 +633,27 @@ public class GoodsServiceImpl implements GoodsService { } } } + + // 尝试边缘端处理 + String resultUrl = watermarkEdgeService.processWatermark( + type, + item.getUrl(), + finalQrcodeUrl, + finalFaceUrl, + text, + item.getCreateTime(), + dtFormat, + item.getGoodsId(), + face.getId() + ); + if (resultUrl != null) { + sourceMapper.addSourceWatermark(item.getGoodsId(), face.getId(), type.getType(), resultUrl); + item.setUrl(resultUrl); + return; + } + log.warn("边缘端水印处理失败,降级到本地处理: sourceId={}", item.getGoodsId()); + + // 本地处理(边缘端处理失败时的降级) File dstFile = new File(item.getGoodsId() + ".jpg"); File watermarkedFile = new File(item.getGoodsId() + "_" + type.getType() + "." + type.getPreferFileType()); try { @@ -643,16 +663,28 @@ public class GoodsServiceImpl implements GoodsService { return; } tmpFile.add(dstFile); + + // 降级处理时下载头像文件 + File localFaceFile = new File("face_fallback_" + face.getId() + ".jpg"); + try { + HttpUtil.downloadFile(face.getFaceUrl().replace("oss.zhentuai.com", "frametour-assets.oss-cn-shanghai-internal.aliyuncs.com"), localFaceFile); + tmpFile.add(localFaceFile); + } catch (Exception e) { + log.error("download face for fallback error", e); + localFaceFile = null; + } + WatermarkInfo info = new WatermarkInfo(); info.setOriginalFile(dstFile); - info.setQrcodeFile(qrcode); + info.setQrcodeFile(null); info.setScenicLine(text); info.setDatetime(item.getCreateTime()); - info.setFaceFile(faceFile); - info.setDtFormat(configManager.getString("watermark_dt_format")); + info.setFaceFile(localFaceFile); + info.setDtFormat(dtFormat); info.setWatermarkedFile(watermarkedFile); try { - operator.process(info); + IOperator fallbackOperator = ImageWatermarkFactory.get(type); + fallbackOperator.process(info); } catch (ImageWatermarkException e) { log.error("process error", e); return;