feat(puzzle): 添加水印拼图功能支持

- 在 PuzzleEdgeRenderTaskEntity 中新增 taskType 和 watermarkType 字段
- 添加 TASK_TYPE_PUZZLE 和 TASK_TYPE_WATERMARK 常量定义
- 新增 PuzzleWatermarkMapper 依赖注入
- 实现 handleWatermarkTaskSuccess 方法处理水印拼图任务成功逻辑
- 修改 taskSuccess 方法根据任务类型分别处理原始拼图和水印拼图
- 新增 createWatermarkRenderTask 方法创建水印拼图边缘渲染任务
- 为水印拼图任务添加独立的存储目录和文件命名规则
- 实现水印拼图结果写入 puzzle_watermark 表的功能
This commit is contained in:
2026-01-16 13:56:29 +08:00
parent eba727b446
commit 8d5a10cce1
2 changed files with 184 additions and 9 deletions

View File

@@ -34,6 +34,18 @@ public class PuzzleEdgeRenderTaskEntity {
@TableField("face_id")
private Long faceId;
/**
* 任务类型:PUZZLE-原始拼图 WATERMARK-水印拼图
*/
@TableField("task_type")
private String taskType;
/**
* 水印类型(仅task_type=WATERMARK时有效)
*/
@TableField("watermark_type")
private String watermarkType;
@TableField("content_hash")
private String contentHash;

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.util.StrUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.fasterxml.jackson.core.type.TypeReference;
import com.ycwl.basic.model.pc.puzzle.entity.PuzzleWatermarkEntity;
import com.ycwl.basic.model.task.req.ClientStatusReqVo;
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeRenderTaskDTO;
import com.ycwl.basic.puzzle.edge.dto.PuzzleEdgeTaskFailRequest;
@@ -16,6 +17,7 @@ import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity;
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper;
import com.ycwl.basic.puzzle.mapper.PuzzleWatermarkMapper;
import com.ycwl.basic.puzzle.repository.PuzzleRepository;
import com.ycwl.basic.service.pc.processor.PuzzleRelationProcessor;
import com.ycwl.basic.service.printer.PrinterService;
@@ -55,6 +57,15 @@ public class PuzzleEdgeRenderTaskService {
private static final int STATUS_SUCCESS = 2;
private static final int STATUS_FAIL = 3;
/**
* 任务类型:原始拼图(成功后更新 puzzle_generation_record)
*/
public static final String TASK_TYPE_PUZZLE = "PUZZLE";
/**
* 任务类型:水印拼图(成功后写入 puzzle_watermark)
*/
public static final String TASK_TYPE_WATERMARK = "WATERMARK";
private static final int MAX_SYNC_TASKS = 5;
private static final long LEASE_MILLIS = TimeUnit.SECONDS.toMillis(20);
private static final long UPLOAD_URL_EXPIRE_MILLIS = TimeUnit.HOURS.toMillis(1);
@@ -133,6 +144,7 @@ public class PuzzleEdgeRenderTaskService {
private final ConcurrentHashMap<Long, WaitFutureEntry> waitFutures = new ConcurrentHashMap<>();
private final PuzzleGenerationRecordMapper recordMapper;
private final PuzzleWatermarkMapper puzzleWatermarkMapper;
private final PuzzleRepository puzzleRepository;
private final PrinterService printerService;
private final PuzzleRelationProcessor puzzleRelationProcessor;
@@ -200,15 +212,35 @@ public class PuzzleEdgeRenderTaskService {
throw new IllegalStateException("任务状态更新失败");
}
PuzzleGenerationRecordEntity record = recordMapper.getById(task.getRecordId());
if (record == null) {
log.warn("边缘渲染任务回报成功,但生成记录不存在: taskId={}, recordId={}", taskId, task.getRecordId());
return;
}
IStorageAdapter storage = StorageFactory.use();
String resultImageUrl = storage.getUrl(task.getOriginalObjectKey());
// 根据任务类型决定写入哪个表
String taskType = task.getTaskType();
if (TASK_TYPE_WATERMARK.equals(taskType)) {
// 水印拼图任务:写入 puzzle_watermark 表
handleWatermarkTaskSuccess(task, resultImageUrl);
} else {
// 原始拼图任务(默认):更新 puzzle_generation_record 表
handlePuzzleTaskSuccess(task, req, resultImageUrl);
}
// 通知等待方任务完成
completeWaitFuture(taskId, TaskWaitResult.success(resultImageUrl));
}
/**
* 处理原始拼图任务成功
*/
private void handlePuzzleTaskSuccess(PuzzleEdgeRenderTaskEntity task,
PuzzleEdgeTaskSuccessRequest req,
String resultImageUrl) {
PuzzleGenerationRecordEntity record = recordMapper.getById(task.getRecordId());
if (record == null) {
log.warn("边缘渲染任务回报成功,但生成记录不存在: taskId={}, recordId={}", task.getId(), task.getRecordId());
return;
}
Long resultFileSize = req != null ? req.getResultFileSize() : null;
Integer resultWidth = req != null ? req.getResultWidth() : null;
Integer resultHeight = req != null ? req.getResultHeight() : null;
@@ -234,9 +266,6 @@ public class PuzzleEdgeRenderTaskService {
record.getId()
);
// 通知等待方任务完成
completeWaitFuture(taskId, TaskWaitResult.success(resultImageUrl));
PuzzleTemplateEntity template = puzzleRepository.getTemplateById(task.getTemplateId());
if (template != null && template.getAutoAddPrint() != null && template.getAutoAddPrint() == 1) {
try {
@@ -254,6 +283,26 @@ public class PuzzleEdgeRenderTaskService {
}
}
/**
* 处理水印拼图任务成功
*/
private void handleWatermarkTaskSuccess(PuzzleEdgeRenderTaskEntity task, String resultImageUrl) {
PuzzleWatermarkEntity watermark = new PuzzleWatermarkEntity();
watermark.setRecordId(task.getRecordId());
watermark.setFaceId(task.getFaceId());
watermark.setWatermarkType(task.getWatermarkType());
watermark.setWatermarkUrl(resultImageUrl);
try {
puzzleWatermarkMapper.insert(watermark);
log.info("水印拼图任务成功,已写入puzzle_watermark: taskId={}, recordId={}, watermarkType={}",
task.getId(), task.getRecordId(), task.getWatermarkType());
} catch (Exception e) {
log.error("水印拼图任务成功,但写入puzzle_watermark失败: taskId={}, recordId={}",
task.getId(), task.getRecordId(), e);
}
}
public void taskFail(Long taskId, PuzzleEdgeTaskFailRequest req) {
// IP 验证已在拦截器层完成,此处无需验证 accessKey
Long workerId = DEFAULT_WORKER_ID;
@@ -472,6 +521,8 @@ public class PuzzleEdgeRenderTaskService {
task.setTemplateCode(template.getCode());
task.setScenicId(record.getScenicId());
task.setFaceId(record.getFaceId());
task.setTaskType(TASK_TYPE_PUZZLE);
task.setWatermarkType(null);
task.setContentHash(record.getContentHash());
task.setStatus(STATUS_PENDING);
task.setAttemptCount(0);
@@ -491,6 +542,118 @@ public class PuzzleEdgeRenderTaskService {
return taskId;
}
/**
* 创建水印拼图边缘渲染任务(供中心业务侧调用)
* 成功后将结果写入 puzzle_watermark 表
*
* @param recordId 原始拼图生成记录ID
* @param faceId 人脸ID(可选)
* @param watermarkType 水印类型(如 print、free_download)
* @param template 模板配置
* @param sortedElements 元素列表(按z-index排序)
* @param finalDynamicData 动态数据
* @param outputFormat 输出格式
* @param quality 输出质量
* @return 任务ID
*/
public Long createWatermarkRenderTask(Long recordId,
Long faceId,
String watermarkType,
PuzzleTemplateEntity template,
List<PuzzleElementEntity> sortedElements,
Map<String, String> finalDynamicData,
String outputFormat,
Integer quality) {
if (recordId == null) {
throw new IllegalArgumentException("recordId不能为空");
}
if (StrUtil.isBlank(watermarkType)) {
throw new IllegalArgumentException("watermarkType不能为空");
}
if (template == null || template.getId() == null) {
throw new IllegalArgumentException("template不能为空");
}
if (sortedElements == null) {
sortedElements = List.of();
}
if (finalDynamicData == null) {
finalDynamicData = Map.of();
}
String normalizedFormat = normalizeOutputFormat(outputFormat);
Integer outputQuality = quality != null ? quality : 90;
String ext = "PNG".equals(normalizedFormat) ? "png" : "jpeg";
String fileName = UUID.randomUUID().toString().replace("-", "") + "." + ext;
// 水印拼图使用单独的目录
String originalObjectKey = String.format("puzzle_watermark/%s/%s/%s", template.getCode(), watermarkType, fileName);
Map<String, Object> payload = new HashMap<>();
payload.put("recordId", recordId);
payload.put("watermarkType", watermarkType);
Map<String, Object> templatePayload = new HashMap<>();
templatePayload.put("id", template.getId());
templatePayload.put("code", template.getCode());
templatePayload.put("canvasWidth", template.getCanvasWidth());
templatePayload.put("canvasHeight", template.getCanvasHeight());
templatePayload.put("backgroundType", template.getBackgroundType());
templatePayload.put("backgroundColor", template.getBackgroundColor());
templatePayload.put("backgroundImage", template.getBackgroundImage());
payload.put("template", templatePayload);
List<Map<String, Object>> elementPayloadList = new ArrayList<>();
for (PuzzleElementEntity e : sortedElements) {
Map<String, Object> elementPayload = new HashMap<>();
elementPayload.put("id", e.getId());
elementPayload.put("type", e.getElementType());
elementPayload.put("key", e.getElementKey());
elementPayload.put("name", e.getElementName());
elementPayload.put("x", e.getXPosition());
elementPayload.put("y", e.getYPosition());
elementPayload.put("width", e.getWidth());
elementPayload.put("height", e.getHeight());
elementPayload.put("zIndex", e.getZIndex());
elementPayload.put("rotation", e.getRotation());
elementPayload.put("opacity", e.getOpacity());
elementPayload.put("config", e.getConfig());
elementPayloadList.add(elementPayload);
}
payload.put("elements", elementPayloadList);
payload.put("dynamicData", finalDynamicData);
Map<String, Object> outputPayload = new HashMap<>();
outputPayload.put("format", normalizedFormat);
outputPayload.put("quality", outputQuality);
payload.put("output", outputPayload);
PuzzleEdgeRenderTaskEntity task = new PuzzleEdgeRenderTaskEntity();
task.setRecordId(recordId);
task.setTemplateId(template.getId());
task.setTemplateCode(template.getCode());
task.setScenicId(template.getScenicId());
task.setFaceId(faceId);
task.setTaskType(TASK_TYPE_WATERMARK);
task.setWatermarkType(watermarkType);
task.setContentHash(null); // 水印任务不需要内容哈希去重
task.setStatus(STATUS_PENDING);
task.setAttemptCount(0);
task.setOutputFormat(normalizedFormat);
task.setOutputQuality(outputQuality);
task.setOriginalObjectKey(originalObjectKey);
task.setCroppedObjectKey(null);
task.setPayloadJson(JacksonUtil.toJson(payload));
Long taskId = taskIdSequence.incrementAndGet();
Date now = new Date();
task.setId(taskId);
task.setCreateTime(now);
task.setUpdateTime(now);
taskCache.put(taskId, task);
return taskId;
}
/**
* 注册任务等待,返回用于等待的 CompletableFuture
* 调用方应在 createRenderTask 之后立即调用此方法