diff --git a/src/main/java/com/ycwl/basic/puzzle/edge/entity/PuzzleEdgeRenderTaskEntity.java b/src/main/java/com/ycwl/basic/puzzle/edge/entity/PuzzleEdgeRenderTaskEntity.java index b258bfe5..ddd26384 100644 --- a/src/main/java/com/ycwl/basic/puzzle/edge/entity/PuzzleEdgeRenderTaskEntity.java +++ b/src/main/java/com/ycwl/basic/puzzle/edge/entity/PuzzleEdgeRenderTaskEntity.java @@ -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; diff --git a/src/main/java/com/ycwl/basic/puzzle/edge/task/PuzzleEdgeRenderTaskService.java b/src/main/java/com/ycwl/basic/puzzle/edge/task/PuzzleEdgeRenderTaskService.java index b892aa11..1c8e5978 100644 --- a/src/main/java/com/ycwl/basic/puzzle/edge/task/PuzzleEdgeRenderTaskService.java +++ b/src/main/java/com/ycwl/basic/puzzle/edge/task/PuzzleEdgeRenderTaskService.java @@ -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 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 sortedElements, + Map 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 payload = new HashMap<>(); + payload.put("recordId", recordId); + payload.put("watermarkType", watermarkType); + + Map 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> elementPayloadList = new ArrayList<>(); + for (PuzzleElementEntity e : sortedElements) { + Map 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 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 之后立即调用此方法