feat(puzzle): 添加景区模板列表缓存功能

- 新增景区模板列表缓存KEY常量PUZZLE_TEMPLATES_BY_SCENIC_KEY
- 在清除模板缓存时同步清除对应景区的模板列表缓存
- 实现listTemplateByScenic方法根据景区ID获取启用模板列表并缓存
- 实现clearTemplateByScenicCache方法清除景区模板列表缓存
- 重构人脸匹配编排器使用新的缓存方法替代原有数据库查询
- 移除过期的redisTemplate依赖
This commit is contained in:
2026-01-07 15:03:34 +08:00
parent 917668da0c
commit 3291371dd7
2 changed files with 86 additions and 12 deletions

View File

@@ -51,6 +51,11 @@ public class PuzzleRepository {
*/
private static final String PUZZLE_RECORDS_BY_FACE_KEY = "puzzle:records:faceId:%s";
/**
* 景区模板列表缓存KEY(根据scenicId)
*/
private static final String PUZZLE_TEMPLATES_BY_SCENIC_KEY = "puzzle:templates:scenicId:%s";
/**
* 单条生成记录缓存KEY(根据recordId)
*/
@@ -167,6 +172,8 @@ public class PuzzleRepository {
* @param code 模板编码(可为null,此时需要先查询获取)
*/
public void clearTemplateCache(Long id, String code) {
Long scenicId = null;
// 如果没有传code,尝试从缓存或数据库获取
if (code == null && id != null) {
String idKey = String.format(PUZZLE_TEMPLATE_BY_ID_KEY, id);
@@ -174,10 +181,25 @@ public class PuzzleRepository {
if (cacheValue != null) {
PuzzleTemplateEntity template = JacksonUtil.parseObject(cacheValue, PuzzleTemplateEntity.class);
code = template.getCode();
scenicId = template.getScenicId();
} else {
PuzzleTemplateEntity template = templateMapper.getById(id);
if (template != null) {
code = template.getCode();
scenicId = template.getScenicId();
}
}
} else if (id != null) {
// 有 code 但需要获取 scenicId
String idKey = String.format(PUZZLE_TEMPLATE_BY_ID_KEY, id);
String cacheValue = redisTemplate.opsForValue().get(idKey);
if (cacheValue != null) {
PuzzleTemplateEntity template = JacksonUtil.parseObject(cacheValue, PuzzleTemplateEntity.class);
scenicId = template.getScenicId();
} else {
PuzzleTemplateEntity template = templateMapper.getById(id);
if (template != null) {
scenicId = template.getScenicId();
}
}
}
@@ -200,6 +222,11 @@ public class PuzzleRepository {
if (id != null) {
clearElementsCache(id);
}
// 清除景区模板列表缓存(确保列表数据一致性)
if (scenicId != null) {
clearTemplateByScenicCache(scenicId);
}
}
// ==================== 元素缓存 ====================
@@ -245,6 +272,58 @@ public class PuzzleRepository {
log.debug("清除元素缓存: templateId={}", templateId);
}
// ==================== 景区模板列表缓存 ====================
/**
* 根据景区ID获取启用的模板列表(优先从缓存读取)
* 用于人脸匹配后的批量拼图生成场景
*
* @param scenicId 景区ID
* @return 启用状态的模板列表
*/
public List<PuzzleTemplateEntity> listTemplateByScenic(Long scenicId) {
if (scenicId == null) {
log.warn("景区ID为空,跳过缓存查询");
return templateMapper.list(null, null, 1);
}
String cacheKey = String.format(PUZZLE_TEMPLATES_BY_SCENIC_KEY, scenicId);
// 1. 尝试从缓存读取
Boolean hasKey = redisTemplate.hasKey(cacheKey);
if (Boolean.TRUE.equals(hasKey)) {
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
if (cacheValue != null) {
log.debug("从缓存读取景区模板列表: scenicId={}", scenicId);
return JacksonUtil.parseObject(cacheValue, new TypeReference<List<PuzzleTemplateEntity>>() {});
}
}
// 2. 从数据库查询(只查启用状态 status=1)
List<PuzzleTemplateEntity> templates = templateMapper.list(scenicId, null, 1);
// 3. 写入缓存(即使是空列表也缓存,避免缓存穿透)
String json = JacksonUtil.toJSONString(templates);
redisTemplate.opsForValue().set(cacheKey, json, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
log.debug("景区模板列表缓存写入: scenicId={}, count={}", scenicId, templates.size());
return templates;
}
/**
* 清除景区模板列表缓存
*
* @param scenicId 景区ID
*/
public void clearTemplateByScenicCache(Long scenicId) {
if (scenicId == null) {
return;
}
String cacheKey = String.format(PUZZLE_TEMPLATES_BY_SCENIC_KEY, scenicId);
redisTemplate.delete(cacheKey);
log.debug("清除景区模板列表缓存: scenicId={}", scenicId);
}
// ==================== 批量清除 ====================
/**
@@ -258,6 +337,8 @@ public class PuzzleRepository {
deleteByPattern("puzzle:template:*");
// 使用 SCAN 删除元素缓存
deleteByPattern("puzzle:elements:*");
// 使用 SCAN 删除景区模板列表缓存
deleteByPattern("puzzle:templates:*");
log.warn("拼图缓存清除完成");
}

View File

@@ -6,6 +6,8 @@ import com.ycwl.basic.integration.scenic.dto.scenic.ScenicV2DTO;
import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest;
import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse;
import com.ycwl.basic.puzzle.dto.PuzzleTemplateDTO;
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
import com.ycwl.basic.puzzle.repository.PuzzleRepository;
import com.ycwl.basic.puzzle.service.IPuzzleGenerateService;
import com.ycwl.basic.puzzle.service.IPuzzleTemplateService;
@@ -102,9 +104,9 @@ public class FaceMatchingOrchestrator {
@Autowired
private IPuzzleGenerateService puzzleGenerateService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private FaceStatusManager faceStatusManager;
@Autowired
private PuzzleRepository puzzleRepository;
/**
* 编排人脸匹配的完整流程
@@ -364,17 +366,8 @@ public class FaceMatchingOrchestrator {
* @return
*/
private void asyncGeneratePuzzleTemplate(Long scenicId, Long faceId, Long memberId, String scene) {
if (redisTemplate.hasKey("puzzle_generated:face:" + faceId)) {
return;
}
redisTemplate.opsForValue().set(
"puzzle_generated:face:" + faceId,
"1",
60 * 10, TimeUnit.SECONDS);
// 查询该景区所有启用状态的拼图模板
List<PuzzleTemplateDTO> templateList = puzzleTemplateService.listTemplates(
scenicId, null, 1); // 查询启用状态的模板
List<PuzzleTemplateEntity> templateList = puzzleRepository.listTemplateByScenic(scenicId); // 查询启用状态的模板
if (templateList == null || templateList.isEmpty()) {
log.debug("景区不存在启用的拼图模板,跳过生成: scenicId={}", scenicId);