You've already forked FrameTour-BE
feat(watermark): 添加边缘端水印处理功能
- 引入 WatermarkEdgeService 支持边缘端渲染 - 在 WatermarkConfig 中添加边缘端相关配置参数 - 在 WatermarkStage 中实现边缘端处理逻辑和降级机制 - 修改 ImageWatermarkOperatorEnum 的默认输出格式为 jpg - 移除已废弃的 DefaultImageWatermarkOperator 类 - 更新 GoodsServiceImpl 使用边缘端处理水印 - 优化 PuzzleEdgeWorkerIpInterceptor 允许本地回环地址访问 - 修正 PrinterDefaultWatermarkTemplateBuilder 样式常量名称
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
package com.ycwl.basic.image.pipeline.stages;
|
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.image.watermark.enums.ImageWatermarkOperatorEnum;
|
||||||
|
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
@@ -41,4 +43,28 @@ public class WatermarkConfig {
|
|||||||
*/
|
*/
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private final Double scale = 1.0;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.core.PhotoProcessContext;
|
||||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||||
import com.ycwl.basic.image.watermark.ImageWatermarkFactory;
|
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.entity.WatermarkInfo;
|
||||||
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
||||||
import com.ycwl.basic.image.watermark.operator.IOperator;
|
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.AbstractPipelineStage;
|
||||||
import com.ycwl.basic.pipeline.core.StageResult;
|
import com.ycwl.basic.pipeline.core.StageResult;
|
||||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||||
|
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* 水印处理Stage
|
* 水印处理Stage
|
||||||
* 支持三级降级: 配置的水印类型 -> PRINTER_DEFAULT -> 无水印
|
* 支持三级降级: 配置的水印类型 -> PRINTER_DEFAULT -> 无水印
|
||||||
|
* 支持边缘端渲染(可选)
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@StageConfig(
|
@StageConfig(
|
||||||
@@ -127,6 +130,19 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
|
|||||||
File watermarkedFile = context.getTempFileManager()
|
File watermarkedFile = context.getTempFileManager()
|
||||||
.createTempFile("watermark_" + type.getType(), "." + fileExt);
|
.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);
|
WatermarkInfo watermarkInfo = buildWatermarkInfo(context, currentFile, watermarkedFile, type);
|
||||||
|
|
||||||
IOperator operator = ImageWatermarkFactory.get(type);
|
IOperator operator = ImageWatermarkFactory.get(type);
|
||||||
@@ -143,6 +159,46 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
|
|||||||
type.getType(), result.length() / 1024));
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建水印参数
|
* 构建水印参数
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.ycwl.basic.image.watermark;
|
|||||||
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
||||||
import com.ycwl.basic.image.watermark.exception.ImageWatermarkUnsupportedException;
|
import com.ycwl.basic.image.watermark.exception.ImageWatermarkUnsupportedException;
|
||||||
import com.ycwl.basic.image.watermark.operator.IOperator;
|
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.LeicaWatermarkOperator;
|
||||||
import com.ycwl.basic.image.watermark.operator.NormalWatermarkOperator;
|
import com.ycwl.basic.image.watermark.operator.NormalWatermarkOperator;
|
||||||
import com.ycwl.basic.image.watermark.operator.PrinterDefaultWatermarkOperator;
|
import com.ycwl.basic.image.watermark.operator.PrinterDefaultWatermarkOperator;
|
||||||
@@ -18,11 +17,9 @@ public class ImageWatermarkFactory {
|
|||||||
}
|
}
|
||||||
public static IOperator get(ImageWatermarkOperatorEnum type) {
|
public static IOperator get(ImageWatermarkOperatorEnum type) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
case WATERMARK -> new DefaultImageWatermarkOperator();
|
|
||||||
case NORMAL -> new NormalWatermarkOperator();
|
case NORMAL -> new NormalWatermarkOperator();
|
||||||
case LEICA -> new LeicaWatermarkOperator();
|
case LEICA -> new LeicaWatermarkOperator();
|
||||||
case PRINTER_DEFAULT -> new PrinterDefaultWatermarkOperator();
|
case PRINTER_DEFAULT -> new PrinterDefaultWatermarkOperator();
|
||||||
default -> throw new ImageWatermarkUnsupportedException("不支持的类型" + type.name());
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import java.util.Map;
|
|||||||
@Component
|
@Component
|
||||||
public class PrinterDefaultWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuilder {
|
public class PrinterDefaultWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuilder {
|
||||||
|
|
||||||
public static final String STYLE = "printer_default";
|
public static final String STYLE = "pDefault";
|
||||||
|
|
||||||
// 常量配置(与 PrinterDefaultWatermarkOperator 保持一致)
|
// 常量配置(与 PrinterDefaultWatermarkOperator 保持一致)
|
||||||
private static final int OFFSET_Y = 15;
|
private static final int OFFSET_Y = 15;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,9 @@ import lombok.Getter;
|
|||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public enum ImageWatermarkOperatorEnum {
|
public enum ImageWatermarkOperatorEnum {
|
||||||
WATERMARK("defW", "jpg"),
|
LEICA("leica", "jpg"),
|
||||||
LEICA("leica", "png"),
|
NORMAL("normal", "jpg"),
|
||||||
NORMAL("normal", "png"),
|
PRINTER_DEFAULT("pDefault", "jpg");
|
||||||
PRINTER_DEFAULT("pDefault", "png");
|
|
||||||
|
|
||||||
private final String type;
|
private final String type;
|
||||||
private final String preferFileType;
|
private final String preferFileType;
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,6 +35,9 @@ public class PuzzleEdgeWorkerIpInterceptor implements HandlerInterceptor {
|
|||||||
if (Ipv4CidrMatcher.matches(clientIp, properties.getAllowedIpCidr())) {
|
if (Ipv4CidrMatcher.matches(clientIp, properties.getAllowedIpCidr())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (Ipv4CidrMatcher.matches(clientIp, "127.0.0.1/8")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
log.warn("拒绝边缘 Worker 请求: uri={}, ip={}, allowedIpCidr={}",
|
log.warn("拒绝边缘 Worker 请求: uri={}, ip={}, allowedIpCidr={}",
|
||||||
request != null ? request.getRequestURI() : null,
|
request != null ? request.getRequestURI() : null,
|
||||||
|
|||||||
@@ -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.model.pc.source.entity.MemberSourceEntity;
|
||||||
import com.ycwl.basic.repository.MemberRelationRepository;
|
import com.ycwl.basic.repository.MemberRelationRepository;
|
||||||
import com.ycwl.basic.repository.OrderRepository;
|
import com.ycwl.basic.repository.OrderRepository;
|
||||||
|
import com.ycwl.basic.service.pc.FaceService;
|
||||||
import com.ycwl.basic.utils.JacksonUtil;
|
import com.ycwl.basic.utils.JacksonUtil;
|
||||||
import com.ycwl.basic.biz.OrderBiz;
|
import com.ycwl.basic.biz.OrderBiz;
|
||||||
import com.ycwl.basic.constant.StorageConstant;
|
import com.ycwl.basic.constant.StorageConstant;
|
||||||
import com.ycwl.basic.image.watermark.ImageWatermarkFactory;
|
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.entity.WatermarkInfo;
|
||||||
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
||||||
import com.ycwl.basic.image.watermark.exception.ImageWatermarkException;
|
import com.ycwl.basic.image.watermark.exception.ImageWatermarkException;
|
||||||
@@ -107,6 +109,10 @@ public class GoodsServiceImpl implements GoodsService {
|
|||||||
private OrderRepository orderRepository;
|
private OrderRepository orderRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private FaceStatusManager faceStatusManager;
|
private FaceStatusManager faceStatusManager;
|
||||||
|
@Autowired
|
||||||
|
private WatermarkEdgeService watermarkEdgeService;
|
||||||
|
@Autowired
|
||||||
|
private FaceService faceService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<GoodsDetailVO> sourceGoodsList(GoodsReqQuery query) {
|
public List<GoodsDetailVO> sourceGoodsList(GoodsReqQuery query) {
|
||||||
@@ -599,23 +605,16 @@ public class GoodsServiceImpl implements GoodsService {
|
|||||||
} else {
|
} else {
|
||||||
adapter = StorageFactory.use("assets-ext");
|
adapter = StorageFactory.use("assets-ext");
|
||||||
}
|
}
|
||||||
IOperator operator = ImageWatermarkFactory.get(type);
|
|
||||||
List<SourceWatermarkEntity> watermarkEntityList = sourceMapper.listSourceWatermark(list.stream().map(SourceRespVO::getId).collect(Collectors.toList()), face.getId(), type.getType());
|
List<SourceWatermarkEntity> watermarkEntityList = sourceMapper.listSourceWatermark(list.stream().map(SourceRespVO::getId).collect(Collectors.toList()), face.getId(), type.getType());
|
||||||
File qrcode = new File("qrcode_"+face.getId()+".jpg");
|
|
||||||
try {
|
// 边缘端处理:需要二维码和头像 URL
|
||||||
WxMpUtil.generateUnlimitedWXAQRCode(scenicMpConfig.getAppId(), scenicMpConfig.getAppSecret(), "pages/videoSynthesis/from_face", face.getId().toString(), qrcode);
|
String qrcodeUrl = faceService.bindWxaCode(face.getId());
|
||||||
} catch (Exception e) {
|
String faceUrlForEdge = face.getFaceUrl();
|
||||||
log.error("generateWXQRCode error", e);
|
|
||||||
return defaultUrlList;
|
final String finalQrcodeUrl = qrcodeUrl;
|
||||||
}
|
final String finalFaceUrl = faceUrlForEdge;
|
||||||
tmpFile.add(qrcode);
|
final String dtFormat = configManager.getString("watermark_dt_format");
|
||||||
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);
|
|
||||||
}
|
|
||||||
List<GoodsUrlVO> collect = defaultUrlList.stream().peek(item -> {
|
List<GoodsUrlVO> collect = defaultUrlList.stream().peek(item -> {
|
||||||
Optional<SourceWatermarkEntity> any = watermarkEntityList.stream()
|
Optional<SourceWatermarkEntity> any = watermarkEntityList.stream()
|
||||||
.filter(watermark -> watermark.getSourceId().equals(item.getGoodsId()))
|
.filter(watermark -> watermark.getSourceId().equals(item.getGoodsId()))
|
||||||
@@ -623,7 +622,7 @@ public class GoodsServiceImpl implements GoodsService {
|
|||||||
if (any.isPresent()) {
|
if (any.isPresent()) {
|
||||||
item.setUrl(any.get().getWatermarkUrl());
|
item.setUrl(any.get().getWatermarkUrl());
|
||||||
} else {
|
} else {
|
||||||
// 生成
|
// 获取景区文字
|
||||||
String text = configManager.getString("watermark_scenic_text");
|
String text = configManager.getString("watermark_scenic_text");
|
||||||
if (StringUtils.isBlank(text)) {
|
if (StringUtils.isBlank(text)) {
|
||||||
SourceEntity entity = sourceMapper.getEntity(item.getGoodsId());
|
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 dstFile = new File(item.getGoodsId() + ".jpg");
|
||||||
File watermarkedFile = new File(item.getGoodsId() + "_" + type.getType() + "." + type.getPreferFileType());
|
File watermarkedFile = new File(item.getGoodsId() + "_" + type.getType() + "." + type.getPreferFileType());
|
||||||
try {
|
try {
|
||||||
@@ -643,16 +663,28 @@ public class GoodsServiceImpl implements GoodsService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tmpFile.add(dstFile);
|
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();
|
WatermarkInfo info = new WatermarkInfo();
|
||||||
info.setOriginalFile(dstFile);
|
info.setOriginalFile(dstFile);
|
||||||
info.setQrcodeFile(qrcode);
|
info.setQrcodeFile(null);
|
||||||
info.setScenicLine(text);
|
info.setScenicLine(text);
|
||||||
info.setDatetime(item.getCreateTime());
|
info.setDatetime(item.getCreateTime());
|
||||||
info.setFaceFile(faceFile);
|
info.setFaceFile(localFaceFile);
|
||||||
info.setDtFormat(configManager.getString("watermark_dt_format"));
|
info.setDtFormat(dtFormat);
|
||||||
info.setWatermarkedFile(watermarkedFile);
|
info.setWatermarkedFile(watermarkedFile);
|
||||||
try {
|
try {
|
||||||
operator.process(info);
|
IOperator fallbackOperator = ImageWatermarkFactory.get(type);
|
||||||
|
fallbackOperator.process(info);
|
||||||
} catch (ImageWatermarkException e) {
|
} catch (ImageWatermarkException e) {
|
||||||
log.error("process error", e);
|
log.error("process error", e);
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user