feat(puzzle): 添加拼图素材版本缓存优化重复生成

- 新增 puzzleSourceVersionCache 缓存用于记录拼图素材版本
- 实现 isPuzzleSourceChanged 方法判断素材是否变化
- 添加 markPuzzleSourceVersion 方法标记当前素材版本
- 实现 invalidatePuzzleSourceVersion 方法清除指定人脸缓存
- 在人脸关系变更时自动清除相关拼图素材版本缓存
- 重构 AppPuzzleController 使用 PuzzleRepository 替代直接访问 Mapper
- 添加生成记录缓存机制,包括按人脸ID和记录ID的缓存
- 实现素材版本缓存命中时复用历史记录功能
- 优化重复内容检测逻辑,添加缓存标记机制
- 在各种生成流程中添加缓存清除逻辑确保数据一致性
This commit is contained in:
2026-01-07 12:57:46 +08:00
parent 286062a81a
commit 54cdee333d
13 changed files with 364 additions and 10 deletions

View File

@@ -218,6 +218,9 @@ public class PuzzleEdgeRenderTaskService {
renderDurationMs
);
// 清除生成记录缓存(状态已更新)
puzzleRepository.clearRecordCache(record.getId(), record.getFaceId());
// 通知等待方任务完成
completeWaitFuture(taskId, TaskWaitResult.success(resultImageUrl));
@@ -258,6 +261,9 @@ public class PuzzleEdgeRenderTaskService {
}
recordMapper.updateFail(task.getRecordId(), errorMessage);
// 清除生成记录缓存(状态已更新)
puzzleRepository.clearRecordCache(task.getRecordId(), task.getFaceId());
// 通知等待方任务失败
completeWaitFuture(taskId, TaskWaitResult.fail(errorMessage));
}
@@ -273,6 +279,7 @@ public class PuzzleEdgeRenderTaskService {
List<Long> retryRecordIds = new ArrayList<>();
Map<Long, String> failRecordMessages = new HashMap<>();
Map<Long, String> failTaskMessages = new HashMap<>(); // taskId -> errorMessage
Map<Long, Long> 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<Long, String> entry : failRecordMessages.entrySet()) {
recordMapper.updateFail(entry.getKey(), entry.getValue());
// 清除生成记录缓存
Long faceId = failRecordFaceIds.get(entry.getKey());
puzzleRepository.clearRecordCache(entry.getKey(), faceId);
}
// 通知等待方任务最终失败

View File

@@ -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);
}

View File

@@ -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<String, String> 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<String, String> 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<PuzzleGenerationRecordEntity> 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<List<PuzzleGenerationRecordEntity>>() {});
}
}
// 2. 从数据库查询
List<PuzzleGenerationRecordEntity> 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<PuzzleGenerationRecordEntity> 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);
}
}

View File

@@ -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<PuzzleElementEntity> 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<PuzzleElementEntity> 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);
}
}