From 54cdee333db7873f7700199cd4419681bbe5f73f Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 7 Jan 2026 12:57:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(puzzle):=20=E6=B7=BB=E5=8A=A0=E6=8B=BC?= =?UTF-8?q?=E5=9B=BE=E7=B4=A0=E6=9D=90=E7=89=88=E6=9C=AC=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=87=8D=E5=A4=8D=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 puzzleSourceVersionCache 缓存用于记录拼图素材版本 - 实现 isPuzzleSourceChanged 方法判断素材是否变化 - 添加 markPuzzleSourceVersion 方法标记当前素材版本 - 实现 invalidatePuzzleSourceVersion 方法清除指定人脸缓存 - 在人脸关系变更时自动清除相关拼图素材版本缓存 - 重构 AppPuzzleController 使用 PuzzleRepository 替代直接访问 Mapper - 添加生成记录缓存机制,包括按人脸ID和记录ID的缓存 - 实现素材版本缓存命中时复用历史记录功能 - 优化重复内容检测逻辑,添加缓存标记机制 - 在各种生成流程中添加缓存清除逻辑确保数据一致性 --- .../com/ycwl/basic/biz/FaceStatusManager.java | 88 +++++++++++ .../mobile/AppPuzzleController.java | 17 +-- .../stages/DeleteOldRelationsStage.java | 4 + .../stages/PersistRelationsStage.java | 4 + .../task/PuzzleEdgeRenderTaskService.java | 11 ++ .../mapper/PuzzleGenerationRecordMapper.java | 11 ++ .../puzzle/repository/PuzzleRepository.java | 138 ++++++++++++++++++ .../impl/PuzzleGenerateServiceImpl.java | 78 +++++++++- .../basic/repository/SourceRepository.java | 4 + .../service/pc/impl/FaceServiceImpl.java | 4 + .../FaceMatchingOrchestrator.java | 1 + .../task/impl/TaskFaceServiceImpl.java | 3 + .../mapper/PuzzleGenerationRecordMapper.xml | 11 ++ 13 files changed, 364 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/ycwl/basic/biz/FaceStatusManager.java b/src/main/java/com/ycwl/basic/biz/FaceStatusManager.java index ae600bb8..baf42cfb 100644 --- a/src/main/java/com/ycwl/basic/biz/FaceStatusManager.java +++ b/src/main/java/com/ycwl/basic/biz/FaceStatusManager.java @@ -42,6 +42,13 @@ public class FaceStatusManager { */ private final Cache templateRenderCache; + /** + * 拼图素材版本缓存 + * 键:faceId:puzzleTemplateId -> 当时的图片源数量 + * 用于判断拼图模板的素材是否发生变化,避免重复生成 + */ + private final Cache puzzleSourceVersionCache; + @Autowired private TaskMapper taskMapper; @@ -61,6 +68,11 @@ public class FaceStatusManager { .expireAfterWrite(DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS) .maximumSize(10000) .build(); + + this.puzzleSourceVersionCache = Caffeine.newBuilder() + .expireAfterWrite(DEFAULT_EXPIRE_SECONDS, TimeUnit.SECONDS) + .maximumSize(10000) + .build(); } // ==================== 切片状态相关方法 ==================== @@ -293,4 +305,80 @@ public class FaceStatusManager { log.debug("批量删除模板渲染状态缓存: faceId={}, count={}", faceId, count); } } + + // ==================== 拼图素材版本相关方法 ==================== + + /** + * 标记拼图素材版本(记录当前的图片源数量) + * 在拼图生成成功后调用,用于后续判断素材是否变化 + * + * @param faceId 人脸ID + * @param puzzleTemplateId 拼图模板ID(全局唯一) + * @param sourceCount 当前的图片源数量 + */ + public void markPuzzleSourceVersion(Long faceId, Long puzzleTemplateId, int sourceCount) { + if (faceId == null || puzzleTemplateId == null) { + log.warn("标记拼图素材版本参数为空: faceId={}, puzzleTemplateId={}", faceId, puzzleTemplateId); + return; + } + String key = faceId + ":" + puzzleTemplateId; + puzzleSourceVersionCache.put(key, sourceCount); + log.debug("标记拼图素材版本: faceId={}, puzzleTemplateId={}, sourceCount={}", faceId, puzzleTemplateId, sourceCount); + } + + /** + * 判断拼图素材是否发生变化 + * 通过比较当前的图片源数量与缓存中记录的数量 + * + * @param faceId 人脸ID + * @param puzzleTemplateId 拼图模板ID(全局唯一) + * @param currentSourceCount 当前的图片源数量 + * @return true=素材已变化(需要重新生成),false=素材未变化(可以跳过生成) + */ + public boolean isPuzzleSourceChanged(Long faceId, Long puzzleTemplateId, int currentSourceCount) { + if (faceId == null || puzzleTemplateId == null) { + log.warn("判断拼图素材变化参数为空: faceId={}, puzzleTemplateId={}", faceId, puzzleTemplateId); + return true; // 参数不合法时默认认为有变化 + } + + String key = faceId + ":" + puzzleTemplateId; + Integer cachedCount = puzzleSourceVersionCache.getIfPresent(key); + + if (cachedCount == null) { + // 缓存不存在,认为有变化(首次生成或缓存过期) + log.debug("拼图素材版本缓存不存在,需要生成: faceId={}, puzzleTemplateId={}", faceId, puzzleTemplateId); + return true; + } + + boolean changed = !cachedCount.equals(currentSourceCount); + if (changed) { + log.debug("拼图素材已变化: faceId={}, puzzleTemplateId={}, cachedCount={}, currentCount={}", + faceId, puzzleTemplateId, cachedCount, currentSourceCount); + } else { + log.debug("拼图素材未变化,可跳过生成: faceId={}, puzzleTemplateId={}, sourceCount={}", + faceId, puzzleTemplateId, currentSourceCount); + } + + return changed; + } + + /** + * 使指定人脸的所有拼图素材版本缓存失效 + * 当人脸的图片关联发生变化时调用(如人脸匹配后新增了关联) + * + * @param faceId 人脸ID + */ + public void invalidatePuzzleSourceVersion(Long faceId) { + if (faceId == null) { + return; + } + String prefix = faceId + ":"; + long count = puzzleSourceVersionCache.asMap().keySet().stream() + .filter(key -> key.startsWith(prefix)) + .peek(puzzleSourceVersionCache::invalidate) + .count(); + if (count > 0) { + log.debug("批量使拼图素材版本缓存失效: faceId={}, count={}", faceId, count); + } + } } diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppPuzzleController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppPuzzleController.java index bc1ec823..80d807d6 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/AppPuzzleController.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppPuzzleController.java @@ -1,7 +1,6 @@ package com.ycwl.basic.controller.mobile; import com.ycwl.basic.biz.OrderBiz; -import com.ycwl.basic.constant.SourceType; import com.ycwl.basic.model.mobile.order.IsBuyRespVO; import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO; import com.ycwl.basic.model.pc.face.entity.FaceEntity; @@ -11,7 +10,7 @@ import com.ycwl.basic.pricing.dto.ProductItem; import com.ycwl.basic.pricing.enums.ProductType; import com.ycwl.basic.pricing.service.IPriceCalculationService; import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity; -import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper; +import com.ycwl.basic.puzzle.repository.PuzzleRepository; import com.ycwl.basic.repository.FaceRepository; import com.ycwl.basic.service.printer.PrinterService; import com.ycwl.basic.utils.ApiResponse; @@ -32,7 +31,7 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class AppPuzzleController { - private final PuzzleGenerationRecordMapper recordMapper; + private final PuzzleRepository puzzleRepository; private final FaceRepository faceRepository; private final IPriceCalculationService iPriceCalculationService; private final PrinterService printerService; @@ -46,7 +45,7 @@ public class AppPuzzleController { if (faceId == null) { return ApiResponse.fail("faceId不能为空"); } - int count = recordMapper.countByFaceId(faceId); + int count = puzzleRepository.countRecordsByFaceId(faceId); return ApiResponse.success(count); } @@ -58,7 +57,7 @@ public class AppPuzzleController { if (faceId == null) { return ApiResponse.fail("faceId不能为空"); } - List records = recordMapper.listByFaceId(faceId); + List records = puzzleRepository.getRecordsByFaceId(faceId); List result = records.stream() .map(this::convertToContentPageVO) .collect(Collectors.toList()); @@ -73,7 +72,7 @@ public class AppPuzzleController { if (recordId == null) { return ApiResponse.fail("recordId不能为空"); } - PuzzleGenerationRecordEntity record = recordMapper.getById(recordId); + PuzzleGenerationRecordEntity record = puzzleRepository.getRecordById(recordId); if (record == null) { return ApiResponse.fail("未找到对应的拼图记录"); } @@ -89,7 +88,7 @@ public class AppPuzzleController { if (recordId == null) { return ApiResponse.fail("recordId不能为空"); } - PuzzleGenerationRecordEntity record = recordMapper.getById(recordId); + PuzzleGenerationRecordEntity record = puzzleRepository.getRecordById(recordId); if (record == null) { return ApiResponse.fail("未找到对应的拼图记录"); } @@ -108,7 +107,7 @@ public class AppPuzzleController { if (recordId == null) { return ApiResponse.fail("recordId不能为空"); } - PuzzleGenerationRecordEntity record = recordMapper.getById(recordId); + PuzzleGenerationRecordEntity record = puzzleRepository.getRecordById(recordId); if (record == null) { return ApiResponse.fail("未找到对应的拼图记录"); } @@ -142,7 +141,7 @@ public class AppPuzzleController { } // 查询拼图记录 - PuzzleGenerationRecordEntity record = recordMapper.getById(recordId); + PuzzleGenerationRecordEntity record = puzzleRepository.getRecordById(recordId); if (record == null) { return ApiResponse.fail("未找到对应的拼图记录"); } diff --git a/src/main/java/com/ycwl/basic/face/pipeline/stages/DeleteOldRelationsStage.java b/src/main/java/com/ycwl/basic/face/pipeline/stages/DeleteOldRelationsStage.java index 11b12c66..78f4c439 100644 --- a/src/main/java/com/ycwl/basic/face/pipeline/stages/DeleteOldRelationsStage.java +++ b/src/main/java/com/ycwl/basic/face/pipeline/stages/DeleteOldRelationsStage.java @@ -39,6 +39,9 @@ public class DeleteOldRelationsStage extends AbstractPipelineStage retryRecordIds = new ArrayList<>(); Map failRecordMessages = new HashMap<>(); Map failTaskMessages = new HashMap<>(); // taskId -> errorMessage + Map failRecordFaceIds = new HashMap<>(); // recordId -> faceId,用于缓存清除 synchronized (taskLock) { long now = System.currentTimeMillis(); @@ -303,6 +310,7 @@ public class PuzzleEdgeRenderTaskService { task.setUpdateTime(new Date(now)); if (task.getRecordId() != null) { failRecordMessages.put(task.getRecordId(), errorMessage); + failRecordFaceIds.put(task.getRecordId(), task.getFaceId()); } // 记录需要通知的任务 failTaskMessages.put(task.getId(), errorMessage); @@ -329,6 +337,9 @@ public class PuzzleEdgeRenderTaskService { for (Map.Entry entry : failRecordMessages.entrySet()) { recordMapper.updateFail(entry.getKey(), entry.getValue()); + // 清除生成记录缓存 + Long faceId = failRecordFaceIds.get(entry.getKey()); + puzzleRepository.clearRecordCache(entry.getKey(), faceId); } // 通知等待方任务最终失败 diff --git a/src/main/java/com/ycwl/basic/puzzle/mapper/PuzzleGenerationRecordMapper.java b/src/main/java/com/ycwl/basic/puzzle/mapper/PuzzleGenerationRecordMapper.java index f5ec8bb7..f4666527 100644 --- a/src/main/java/com/ycwl/basic/puzzle/mapper/PuzzleGenerationRecordMapper.java +++ b/src/main/java/com/ycwl/basic/puzzle/mapper/PuzzleGenerationRecordMapper.java @@ -77,4 +77,15 @@ public interface PuzzleGenerationRecordMapper { PuzzleGenerationRecordEntity findByContentHash(@Param("templateId") Long templateId, @Param("contentHash") String contentHash, @Param("scenicId") Long scenicId); + + /** + * 根据人脸ID和模板ID查询最近的成功记录 + * 用于素材版本缓存命中时快速返回历史结果 + * + * @param faceId 人脸ID + * @param templateId 模板ID + * @return 最近的成功记录,如果不存在返回null + */ + PuzzleGenerationRecordEntity findLatestSuccessByFaceAndTemplate(@Param("faceId") Long faceId, + @Param("templateId") Long templateId); } diff --git a/src/main/java/com/ycwl/basic/puzzle/repository/PuzzleRepository.java b/src/main/java/com/ycwl/basic/puzzle/repository/PuzzleRepository.java index df967e86..f4113ad3 100644 --- a/src/main/java/com/ycwl/basic/puzzle/repository/PuzzleRepository.java +++ b/src/main/java/com/ycwl/basic/puzzle/repository/PuzzleRepository.java @@ -2,8 +2,10 @@ package com.ycwl.basic.puzzle.repository; import com.fasterxml.jackson.core.type.TypeReference; 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.PuzzleElementMapper; +import com.ycwl.basic.puzzle.mapper.PuzzleGenerationRecordMapper; import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper; import com.ycwl.basic.utils.JacksonUtil; import lombok.extern.slf4j.Slf4j; @@ -26,6 +28,7 @@ public class PuzzleRepository { private final PuzzleTemplateMapper templateMapper; private final PuzzleElementMapper elementMapper; + private final PuzzleGenerationRecordMapper recordMapper; private final RedisTemplate redisTemplate; /** @@ -43,17 +46,34 @@ public class PuzzleRepository { */ private static final String PUZZLE_ELEMENTS_BY_TEMPLATE_KEY = "puzzle:elements:templateId:%s"; + /** + * 生成记录缓存KEY(根据faceId) + */ + private static final String PUZZLE_RECORDS_BY_FACE_KEY = "puzzle:records:faceId:%s"; + + /** + * 单条生成记录缓存KEY(根据recordId) + */ + private static final String PUZZLE_RECORD_BY_ID_KEY = "puzzle:record:id:%s"; + /** * 缓存过期时间(小时) */ private static final long CACHE_EXPIRE_HOURS = 24; + /** + * 生成记录缓存过期时间(分钟)- 较短,因为可能频繁变化 + */ + private static final long RECORD_CACHE_EXPIRE_MINUTES = 30; + public PuzzleRepository( PuzzleTemplateMapper templateMapper, PuzzleElementMapper elementMapper, + PuzzleGenerationRecordMapper recordMapper, RedisTemplate redisTemplate) { this.templateMapper = templateMapper; this.elementMapper = elementMapper; + this.recordMapper = recordMapper; this.redisTemplate = redisTemplate; } @@ -257,4 +277,122 @@ public class PuzzleRepository { log.error("删除缓存失败: pattern={}", pattern, e); } } + + // ==================== 生成记录缓存 ==================== + + /** + * 根据人脸ID获取生成记录列表(优先从缓存读取) + * + * @param faceId 人脸ID + * @return 生成记录列表 + */ + public List getRecordsByFaceId(Long faceId) { + String cacheKey = String.format(PUZZLE_RECORDS_BY_FACE_KEY, faceId); + + // 1. 尝试从缓存读取 + Boolean hasKey = redisTemplate.hasKey(cacheKey); + if (Boolean.TRUE.equals(hasKey)) { + String cacheValue = redisTemplate.opsForValue().get(cacheKey); + if (cacheValue != null) { + log.debug("从缓存读取生成记录列表: faceId={}", faceId); + return JacksonUtil.parseObject(cacheValue, new TypeReference>() {}); + } + } + + // 2. 从数据库查询 + List records = recordMapper.listByFaceId(faceId); + + // 3. 写入缓存 + String json = JacksonUtil.toJSONString(records); + redisTemplate.opsForValue().set(cacheKey, json, RECORD_CACHE_EXPIRE_MINUTES, TimeUnit.MINUTES); + log.debug("生成记录列表缓存写入: faceId={}, count={}", faceId, records.size()); + + return records; + } + + /** + * 根据人脸ID获取生成记录数量(优先从缓存读取) + * + * @param faceId 人脸ID + * @return 记录数量 + */ + public int countRecordsByFaceId(Long faceId) { + List records = getRecordsByFaceId(faceId); + return records.size(); + } + + /** + * 根据记录ID获取单条生成记录(优先从缓存读取) + * + * @param recordId 记录ID + * @return 生成记录,不存在返回null + */ + public PuzzleGenerationRecordEntity getRecordById(Long recordId) { + String cacheKey = String.format(PUZZLE_RECORD_BY_ID_KEY, recordId); + + // 1. 尝试从缓存读取 + Boolean hasKey = redisTemplate.hasKey(cacheKey); + if (Boolean.TRUE.equals(hasKey)) { + String cacheValue = redisTemplate.opsForValue().get(cacheKey); + if (cacheValue != null) { + log.debug("从缓存读取生成记录: recordId={}", recordId); + return JacksonUtil.parseObject(cacheValue, PuzzleGenerationRecordEntity.class); + } + } + + // 2. 从数据库查询 + PuzzleGenerationRecordEntity record = recordMapper.getById(recordId); + if (record == null) { + return null; + } + + // 3. 写入缓存 + String json = JacksonUtil.toJSONString(record); + redisTemplate.opsForValue().set(cacheKey, json, RECORD_CACHE_EXPIRE_MINUTES, TimeUnit.MINUTES); + log.debug("生成记录缓存写入: recordId={}", recordId); + + return record; + } + + /** + * 清除人脸相关的生成记录缓存 + * 在新拼图生成成功后调用 + * + * @param faceId 人脸ID + */ + public void clearRecordCacheByFace(Long faceId) { + if (faceId == null) { + return; + } + // 清除列表缓存 + String listKey = String.format(PUZZLE_RECORDS_BY_FACE_KEY, faceId); + redisTemplate.delete(listKey); + + log.debug("清除人脸生成记录缓存: faceId={}", faceId); + } + + /** + * 清除单条记录的缓存 + * + * @param recordId 记录ID + */ + public void clearRecordCacheById(Long recordId) { + if (recordId == null) { + return; + } + String cacheKey = String.format(PUZZLE_RECORD_BY_ID_KEY, recordId); + redisTemplate.delete(cacheKey); + log.debug("清除生成记录缓存: recordId={}", recordId); + } + + /** + * 清除单条记录缓存并同时清除关联的人脸缓存 + * + * @param recordId 记录ID + * @param faceId 人脸ID + */ + public void clearRecordCache(Long recordId, Long faceId) { + clearRecordCacheById(recordId); + clearRecordCacheByFace(faceId); + } } diff --git a/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImpl.java b/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImpl.java index aa2333c8..88bb5973 100644 --- a/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImpl.java +++ b/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImpl.java @@ -2,6 +2,7 @@ package com.ycwl.basic.puzzle.service.impl; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; +import com.ycwl.basic.biz.FaceStatusManager; import com.ycwl.basic.model.pc.mp.MpConfigEntity; import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest; import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse; @@ -56,6 +57,7 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { private final PuzzleDuplicationDetector duplicationDetector; private final PrinterService printerService; private final PuzzleEdgeRenderTaskService puzzleEdgeRenderTaskService; + private final FaceStatusManager faceStatusManager; public PuzzleGenerateServiceImpl( PuzzleRepository puzzleRepository, @@ -65,7 +67,8 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { @Lazy ScenicRepository scenicRepository, @Lazy PuzzleDuplicationDetector duplicationDetector, @Lazy PrinterService printerService, - PuzzleEdgeRenderTaskService puzzleEdgeRenderTaskService) { + PuzzleEdgeRenderTaskService puzzleEdgeRenderTaskService, + @Lazy FaceStatusManager faceStatusManager) { this.puzzleRepository = puzzleRepository; this.recordMapper = recordMapper; this.imageRenderer = imageRenderer; @@ -74,6 +77,7 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { this.duplicationDetector = duplicationDetector; this.printerService = printerService; this.puzzleEdgeRenderTaskService = puzzleEdgeRenderTaskService; + this.faceStatusManager = faceStatusManager; } @Override @@ -101,6 +105,32 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { } Long resolvedScenicId = resolveScenicId(template, request.getScenicId()); + // 2.5 素材版本缓存检查(减少数据库查询) + // 如果缓存存在,说明自上次成功生成后素材没有变化,可以直接复用历史记录 + if (request.getFaceId() != null && !faceStatusManager.isPuzzleSourceChanged(request.getFaceId(), template.getId(), 0)) { + PuzzleGenerationRecordEntity cachedRecord = recordMapper.findLatestSuccessByFaceAndTemplate( + request.getFaceId(), template.getId()); + if (cachedRecord != null) { + long duration = System.currentTimeMillis() - startTime; + log.info("素材版本缓存命中,复用历史记录: faceId={}, templateId={}, recordId={}, imageUrl={}, duration={}ms", + request.getFaceId(), template.getId(), cachedRecord.getId(), + cachedRecord.getResultImageUrl(), duration); + return PuzzleGenerateResponse.success( + cachedRecord.getResultImageUrl(), + cachedRecord.getResultFileSize(), + cachedRecord.getResultWidth(), + cachedRecord.getResultHeight(), + (int) duration, + cachedRecord.getId(), + true, + cachedRecord.getId() + ); + } + // 缓存存在但记录被删除,继续执行正常流程 + log.debug("素材版本缓存存在但历史记录不存在,继续正常生成: faceId={}, templateId={}", + request.getFaceId(), template.getId()); + } + // 3. 查询并排序元素 List elements = puzzleRepository.getElementsByTemplateId(template.getId()); if (elements.isEmpty()) { @@ -124,6 +154,10 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { long duration = System.currentTimeMillis() - startTime; log.info("检测到重复内容,复用历史记录: recordId={}, imageUrl={}, duration={}ms", duplicateRecord.getId(), duplicateRecord.getResultImageUrl(), duration); + // 标记素材版本缓存 + if (request.getFaceId() != null) { + faceStatusManager.markPuzzleSourceVersion(request.getFaceId(), template.getId(), 0); + } return PuzzleGenerateResponse.success( duplicateRecord.getResultImageUrl(), duplicateRecord.getResultFileSize(), @@ -141,6 +175,9 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { record.setContentHash(contentHash); recordMapper.insert(record); + // 清除生成记录缓存(新记录插入后列表和数量都会变化) + puzzleRepository.clearRecordCacheByFace(request.getFaceId()); + // 8. 创建边缘渲染任务并等待完成 Long taskId = puzzleEdgeRenderTaskService.createRenderTask( record, @@ -164,6 +201,11 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { log.info("同步拼图边缘渲染完成: recordId={}, taskId={}, imageUrl={}, duration={}ms", record.getId(), taskId, waitResult.getImageUrl(), duration); + // 标记素材版本缓存(成功生成后) + if (request.getFaceId() != null) { + faceStatusManager.markPuzzleSourceVersion(request.getFaceId(), template.getId(), 0); + } + // 重新查询记录获取完整信息(边缘渲染回调已更新) PuzzleGenerationRecordEntity updatedRecord = recordMapper.getById(record.getId()); if (updatedRecord != null && updatedRecord.getResultImageUrl() != null) { @@ -212,6 +254,23 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { } Long resolvedScenicId = resolveScenicId(template, request.getScenicId()); + // 2.5 素材版本缓存检查(减少数据库查询) + // 如果缓存存在,说明自上次成功生成后素材没有变化,可以直接复用历史记录 + if (request.getFaceId() != null && !faceStatusManager.isPuzzleSourceChanged(request.getFaceId(), template.getId(), 0)) { + PuzzleGenerationRecordEntity cachedRecord = recordMapper.findLatestSuccessByFaceAndTemplate( + request.getFaceId(), template.getId()); + if (cachedRecord != null) { + long duration = System.currentTimeMillis() - startTime; + log.info("素材版本缓存命中,复用历史记录: faceId={}, templateId={}, recordId={}, imageUrl={}, duration={}ms", + request.getFaceId(), template.getId(), cachedRecord.getId(), + cachedRecord.getResultImageUrl(), duration); + return cachedRecord.getId(); + } + // 缓存存在但记录被删除,继续执行正常流程 + log.debug("素材版本缓存存在但历史记录不存在,继续正常生成: faceId={}, templateId={}", + request.getFaceId(), template.getId()); + } + // 3. 查询并排序元素 List elements = puzzleRepository.getElementsByTemplateId(template.getId()); if (elements.isEmpty()) { @@ -235,6 +294,10 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { long duration = System.currentTimeMillis() - startTime; log.info("检测到重复内容,复用历史记录: recordId={}, imageUrl={}, duration={}ms", duplicateRecord.getId(), duplicateRecord.getResultImageUrl(), duration); + // 标记素材版本缓存 + if (request.getFaceId() != null) { + faceStatusManager.markPuzzleSourceVersion(request.getFaceId(), template.getId(), 0); + } return duplicateRecord.getId(); } @@ -243,6 +306,9 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { record.setContentHash(contentHash); recordMapper.insert(record); + // 清除生成记录缓存(新记录插入后列表和数量都会变化) + puzzleRepository.clearRecordCacheByFace(request.getFaceId()); + // 8. 创建边缘渲染任务 Long taskId = puzzleEdgeRenderTaskService.createRenderTask( record, @@ -253,6 +319,8 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { request.getQuality() ); + // 异步任务:在回调成功后标记缓存(由边缘渲染服务在成功回调中处理) + // 这里只记录请求信息供后续使用 long duration = System.currentTimeMillis() - startTime; log.info("异步拼图任务已进入边缘渲染队列: recordId={}, taskId={}, templateCode={}, duration={}ms", record.getId(), taskId, request.getTemplateCode(), duration); @@ -331,6 +399,9 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { record.setContentHash(contentHash); recordMapper.insert(record); + // 清除生成记录缓存(新记录插入后列表和数量都会变化) + puzzleRepository.clearRecordCacheByFace(request.getFaceId()); + // 9. 执行核心生成逻辑 return doGenerateInternal(request, template, resolvedScenicId, record, startTime); } @@ -417,6 +488,9 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { (int) duration ); + // 清除生成记录缓存(状态已更新) + puzzleRepository.clearRecordCache(record.getId(), request.getFaceId()); + log.info("拼图生成成功: recordId={}, originalUrl={}, finalUrl={}, duration={}ms", record.getId(), originalImageUrl, finalImageUrl, duration); @@ -450,6 +524,8 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { } catch (Exception e) { log.error("拼图生成失败: templateCode={}", request.getTemplateCode(), e); recordMapper.updateFail(record.getId(), e.getMessage()); + // 清除生成记录缓存(状态已更新) + puzzleRepository.clearRecordCache(record.getId(), request.getFaceId()); throw new RuntimeException("图片生成失败: " + e.getMessage(), e); } } diff --git a/src/main/java/com/ycwl/basic/repository/SourceRepository.java b/src/main/java/com/ycwl/basic/repository/SourceRepository.java index 54e45874..8a0d0698 100644 --- a/src/main/java/com/ycwl/basic/repository/SourceRepository.java +++ b/src/main/java/com/ycwl/basic/repository/SourceRepository.java @@ -59,6 +59,8 @@ public class SourceRepository { @Autowired private MemberRelationRepository memberRelationRepository; @Autowired + private com.ycwl.basic.biz.FaceStatusManager faceStatusManager; + @Autowired private ScenicRepository scenicRepository; public void addSource(SourceEntity source) { @@ -81,6 +83,7 @@ public class SourceRepository { memberSource.setIsBuy(1); sourceMapper.updateRelation(memberSource); memberRelationRepository.clearSCacheByFace(faceId); + faceStatusManager.invalidatePuzzleSourceVersion(faceId); // 如果需要图像处理,对该faceId下的所有type=3的照片进行处理 if (needsImageProcessing) { @@ -229,6 +232,7 @@ public class SourceRepository { memberSource.setIsBuy(0); sourceMapper.updateRelation(memberSource); memberRelationRepository.clearSCacheByFace(faceId); + faceStatusManager.invalidatePuzzleSourceVersion(faceId); } public SourceEntity getSource(Long id) { diff --git a/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java b/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java index 40edcedb..d2bc76c2 100644 --- a/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/pc/impl/FaceServiceImpl.java @@ -207,6 +207,8 @@ public class FaceServiceImpl implements FaceService { private FaceDetectLogAiCamService faceDetectLogAiCamService; @Autowired private OrderRepository orderRepository; + @Autowired + private com.ycwl.basic.biz.FaceStatusManager faceStatusManager; @Override public ApiResponse> pageQuery(FaceReqQuery faceReqQuery) { @@ -994,6 +996,7 @@ public class FaceServiceImpl implements FaceService { sourceMapper.deleteNotBuyFaceRelation(face.getMemberId(), faceId); videoMapper.deleteNotBuyFaceRelations(face.getMemberId(), faceId); memberRelationRepository.clearSCacheByFace(faceId); + faceStatusManager.invalidatePuzzleSourceVersion(faceId); log.debug("人脸旧关系数据删除完成:faceId={}", faceId); List memberSourceEntityList = sourceRelationProcessor.processMemberSources(sampleListIds, face); @@ -1016,6 +1019,7 @@ public class FaceServiceImpl implements FaceService { log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", faceId, memberSourceEntityList.size()); } memberRelationRepository.clearSCacheByFace(faceId); + faceStatusManager.invalidatePuzzleSourceVersion(faceId); taskTaskService.autoCreateTaskByFaceId(faceId); log.info("自定义人脸匹配完成:faceId={}, 匹配样本数={}, 关联源文件数={}, 免费数={}", diff --git a/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java b/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java index bc2bcfb2..49183f78 100644 --- a/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java +++ b/src/main/java/com/ycwl/basic/service/pc/orchestrator/FaceMatchingOrchestrator.java @@ -341,6 +341,7 @@ public class FaceMatchingOrchestrator { // 清除缓存 memberRelationRepository.clearSCacheByFace(faceId); + faceStatusManager.invalidatePuzzleSourceVersion(faceId); } /** diff --git a/src/main/java/com/ycwl/basic/service/task/impl/TaskFaceServiceImpl.java b/src/main/java/com/ycwl/basic/service/task/impl/TaskFaceServiceImpl.java index c2969643..fc24ae7c 100644 --- a/src/main/java/com/ycwl/basic/service/task/impl/TaskFaceServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/task/impl/TaskFaceServiceImpl.java @@ -92,6 +92,8 @@ public class TaskFaceServiceImpl implements TaskFaceService { private ScenicService scenicService; @Autowired private MemberRelationRepository memberRelationRepository; + @Autowired + private com.ycwl.basic.biz.FaceStatusManager faceStatusManager; private IAcsClient getClient() { AliFaceBodyAdapter use = (AliFaceBodyAdapter) FaceBodyFactory.use(); @@ -172,6 +174,7 @@ public class TaskFaceServiceImpl implements TaskFaceService { log.warn("没有有效的关联关系可创建: faceId={}, 原始数量={}", faceId, memberSourceEntityList.size()); } memberRelationRepository.clearSCacheByFace(faceId); + faceStatusManager.invalidatePuzzleSourceVersion(faceId); List faceSampleList = faceRepository.getFaceSampleList(faceId); List faceSampleIds = faceSampleList.stream() .sorted(Comparator.comparing(FaceSampleEntity::getCreateAt).reversed()) diff --git a/src/main/resources/mapper/PuzzleGenerationRecordMapper.xml b/src/main/resources/mapper/PuzzleGenerationRecordMapper.xml index 00949f97..cbc29d03 100644 --- a/src/main/resources/mapper/PuzzleGenerationRecordMapper.xml +++ b/src/main/resources/mapper/PuzzleGenerationRecordMapper.xml @@ -143,4 +143,15 @@ LIMIT 1 + + +