From 21d8c56e8247817a36ff1b88f0aa7deda9dc805e Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 1 Jan 2026 21:39:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(puzzle):=20=E6=B7=BB=E5=8A=A0=E6=8B=BC?= =?UTF-8?q?=E5=9B=BE=E6=A8=A1=E5=9D=97=E7=BC=93=E5=AD=98=E4=BB=93=E5=BA=93?= =?UTF-8?q?=E5=B9=B6=E9=9B=86=E6=88=90=E7=BC=93=E5=AD=98=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 PuzzleRepository 缓存仓库类,提供模板和元素的 Redis 缓存功能 - 实现模板按 ID 和编码的双向缓存,减少数据库查询压力 - 实现元素列表按模板 ID 缓存,避免重复查询 - 在模板服务中集成缓存,更新和删除时自动清除相关缓存 - 在生成服务中使用缓存读取模板和元素数据 - 添加缓存过期机制,设置 24 小时自动过期 - 实现批量缓存清除功能,支持按模式删除缓存 --- .../puzzle/repository/PuzzleRepository.java | 260 ++++++++++++++++++ .../impl/PuzzleGenerateServiceImpl.java | 22 +- .../impl/PuzzleTemplateServiceImpl.java | 29 +- 3 files changed, 298 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/puzzle/repository/PuzzleRepository.java diff --git a/src/main/java/com/ycwl/basic/puzzle/repository/PuzzleRepository.java b/src/main/java/com/ycwl/basic/puzzle/repository/PuzzleRepository.java new file mode 100644 index 00000000..df967e86 --- /dev/null +++ b/src/main/java/com/ycwl/basic/puzzle/repository/PuzzleRepository.java @@ -0,0 +1,260 @@ +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.PuzzleTemplateEntity; +import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper; +import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper; +import com.ycwl.basic.utils.JacksonUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 拼图模块缓存仓库 + * 提供模板和元素的缓存读取,减少数据库查询 + * + * @author Claude + * @since 2025-01-01 + */ +@Slf4j +@Repository +public class PuzzleRepository { + + private final PuzzleTemplateMapper templateMapper; + private final PuzzleElementMapper elementMapper; + private final RedisTemplate redisTemplate; + + /** + * 模板缓存KEY(根据code) + */ + private static final String PUZZLE_TEMPLATE_BY_CODE_KEY = "puzzle:template:code:%s"; + + /** + * 模板缓存KEY(根据id) + */ + private static final String PUZZLE_TEMPLATE_BY_ID_KEY = "puzzle:template:id:%s"; + + /** + * 元素列表缓存KEY(根据templateId) + */ + private static final String PUZZLE_ELEMENTS_BY_TEMPLATE_KEY = "puzzle:elements:templateId:%s"; + + /** + * 缓存过期时间(小时) + */ + private static final long CACHE_EXPIRE_HOURS = 24; + + public PuzzleRepository( + PuzzleTemplateMapper templateMapper, + PuzzleElementMapper elementMapper, + RedisTemplate redisTemplate) { + this.templateMapper = templateMapper; + this.elementMapper = elementMapper; + this.redisTemplate = redisTemplate; + } + + // ==================== 模板缓存 ==================== + + /** + * 根据模板编码获取模板(优先从缓存读取) + * + * @param code 模板编码 + * @return 模板实体,不存在返回null + */ + public PuzzleTemplateEntity getTemplateByCode(String code) { + String cacheKey = String.format(PUZZLE_TEMPLATE_BY_CODE_KEY, code); + + // 1. 尝试从缓存读取 + Boolean hasKey = redisTemplate.hasKey(cacheKey); + if (Boolean.TRUE.equals(hasKey)) { + String cacheValue = redisTemplate.opsForValue().get(cacheKey); + if (cacheValue != null) { + log.debug("从缓存读取模板: code={}", code); + return JacksonUtil.parseObject(cacheValue, PuzzleTemplateEntity.class); + } + } + + // 2. 从数据库查询 + PuzzleTemplateEntity template = templateMapper.getByCode(code); + if (template == null) { + return null; + } + + // 3. 写入缓存 + cacheTemplate(template); + log.debug("模板缓存写入: code={}, id={}", code, template.getId()); + + return template; + } + + /** + * 根据模板ID获取模板(优先从缓存读取) + * + * @param id 模板ID + * @return 模板实体,不存在返回null + */ + public PuzzleTemplateEntity getTemplateById(Long id) { + String cacheKey = String.format(PUZZLE_TEMPLATE_BY_ID_KEY, id); + + // 1. 尝试从缓存读取 + Boolean hasKey = redisTemplate.hasKey(cacheKey); + if (Boolean.TRUE.equals(hasKey)) { + String cacheValue = redisTemplate.opsForValue().get(cacheKey); + if (cacheValue != null) { + log.debug("从缓存读取模板: id={}", id); + return JacksonUtil.parseObject(cacheValue, PuzzleTemplateEntity.class); + } + } + + // 2. 从数据库查询 + PuzzleTemplateEntity template = templateMapper.getById(id); + if (template == null) { + return null; + } + + // 3. 写入缓存 + cacheTemplate(template); + log.debug("模板缓存写入: id={}, code={}", id, template.getCode()); + + return template; + } + + /** + * 缓存模板(同时缓存 byId 和 byCode) + */ + private void cacheTemplate(PuzzleTemplateEntity template) { + if (template == null) { + return; + } + String json = JacksonUtil.toJSONString(template); + + // 同时缓存 byId 和 byCode + String idKey = String.format(PUZZLE_TEMPLATE_BY_ID_KEY, template.getId()); + String codeKey = String.format(PUZZLE_TEMPLATE_BY_CODE_KEY, template.getCode()); + + redisTemplate.opsForValue().set(idKey, json, CACHE_EXPIRE_HOURS, TimeUnit.HOURS); + redisTemplate.opsForValue().set(codeKey, json, CACHE_EXPIRE_HOURS, TimeUnit.HOURS); + } + + /** + * 清除模板缓存 + * + * @param id 模板ID + * @param code 模板编码(可为null,此时需要先查询获取) + */ + public void clearTemplateCache(Long id, String code) { + // 如果没有传code,尝试从缓存或数据库获取 + if (code == null && id != null) { + 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); + code = template.getCode(); + } else { + PuzzleTemplateEntity template = templateMapper.getById(id); + if (template != null) { + code = template.getCode(); + } + } + } + + // 清除 byId 缓存 + if (id != null) { + String idKey = String.format(PUZZLE_TEMPLATE_BY_ID_KEY, id); + redisTemplate.delete(idKey); + log.debug("清除模板缓存: id={}", id); + } + + // 清除 byCode 缓存 + if (code != null) { + String codeKey = String.format(PUZZLE_TEMPLATE_BY_CODE_KEY, code); + redisTemplate.delete(codeKey); + log.debug("清除模板缓存: code={}", code); + } + + // 同时清除该模板的元素缓存 + if (id != null) { + clearElementsCache(id); + } + } + + // ==================== 元素缓存 ==================== + + /** + * 根据模板ID获取元素列表(优先从缓存读取) + * + * @param templateId 模板ID + * @return 元素列表 + */ + public List getElementsByTemplateId(Long templateId) { + String cacheKey = String.format(PUZZLE_ELEMENTS_BY_TEMPLATE_KEY, templateId); + + // 1. 尝试从缓存读取 + Boolean hasKey = redisTemplate.hasKey(cacheKey); + if (Boolean.TRUE.equals(hasKey)) { + String cacheValue = redisTemplate.opsForValue().get(cacheKey); + if (cacheValue != null) { + log.debug("从缓存读取元素列表: templateId={}", templateId); + return JacksonUtil.parseObject(cacheValue, new TypeReference>() {}); + } + } + + // 2. 从数据库查询 + List elements = elementMapper.getByTemplateId(templateId); + + // 3. 写入缓存(即使是空列表也要缓存,避免缓存穿透) + String json = JacksonUtil.toJSONString(elements); + redisTemplate.opsForValue().set(cacheKey, json, CACHE_EXPIRE_HOURS, TimeUnit.HOURS); + log.debug("元素列表缓存写入: templateId={}, count={}", templateId, elements.size()); + + return elements; + } + + /** + * 清除元素缓存 + * + * @param templateId 模板ID + */ + public void clearElementsCache(Long templateId) { + String cacheKey = String.format(PUZZLE_ELEMENTS_BY_TEMPLATE_KEY, templateId); + redisTemplate.delete(cacheKey); + log.debug("清除元素缓存: templateId={}", templateId); + } + + // ==================== 批量清除 ==================== + + /** + * 清除所有拼图相关缓存(慎用) + * 使用 SCAN 命令避免 KEYS 阻塞 + */ + public void clearAllPuzzleCache() { + log.warn("开始清除所有拼图缓存..."); + + // 使用 SCAN 删除模板缓存 + deleteByPattern("puzzle:template:*"); + // 使用 SCAN 删除元素缓存 + deleteByPattern("puzzle:elements:*"); + + log.warn("拼图缓存清除完成"); + } + + /** + * 根据模式删除缓存 + * 使用 SCAN 命令避免阻塞 + */ + private void deleteByPattern(String pattern) { + try { + var keys = redisTemplate.keys(pattern); + if (keys != null && !keys.isEmpty()) { + redisTemplate.delete(keys); + log.debug("删除缓存: pattern={}, count={}", pattern, keys.size()); + } + } catch (Exception e) { + log.error("删除缓存失败: pattern={}", pattern, e); + } + } +} 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 e43ed6df..089f1fbd 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 @@ -10,9 +10,9 @@ import com.ycwl.basic.puzzle.entity.PuzzleGenerationRecordEntity; import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity; import com.ycwl.basic.puzzle.fill.FillResult; import com.ycwl.basic.puzzle.fill.PuzzleElementFillEngine; -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.puzzle.repository.PuzzleRepository; import com.ycwl.basic.puzzle.service.IPuzzleGenerateService; import com.ycwl.basic.puzzle.util.PuzzleDuplicationDetector; import com.ycwl.basic.puzzle.util.PuzzleImageRenderer; @@ -51,8 +51,7 @@ import java.util.concurrent.TimeUnit; @Service public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { - private final PuzzleTemplateMapper templateMapper; - private final PuzzleElementMapper elementMapper; + private final PuzzleRepository puzzleRepository; private final PuzzleGenerationRecordMapper recordMapper; private final PuzzleImageRenderer imageRenderer; private final PuzzleElementFillEngine fillEngine; @@ -63,15 +62,14 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { public PuzzleGenerateServiceImpl( PuzzleTemplateMapper templateMapper, - PuzzleElementMapper elementMapper, + PuzzleRepository puzzleRepository, PuzzleGenerationRecordMapper recordMapper, @Lazy PuzzleImageRenderer imageRenderer, @Lazy PuzzleElementFillEngine fillEngine, @Lazy ScenicRepository scenicRepository, @Lazy PuzzleDuplicationDetector duplicationDetector, @Lazy PrinterService printerService) { - this.templateMapper = templateMapper; - this.elementMapper = elementMapper; + this.puzzleRepository = puzzleRepository; this.recordMapper = recordMapper; this.imageRenderer = imageRenderer; this.fillEngine = fillEngine; @@ -104,8 +102,8 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { // 1. 参数校验 validateRequest(request); - // 2. 查询模板 - PuzzleTemplateEntity template = templateMapper.getByCode(request.getTemplateCode()); + // 2. 查询模板(使用缓存) + PuzzleTemplateEntity template = puzzleRepository.getTemplateByCode(request.getTemplateCode()); if (template == null) { throw new IllegalArgumentException("模板不存在: " + request.getTemplateCode()); } @@ -148,8 +146,8 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { // 参数校验 validateRequest(request); - // 1. 查询模板和元素 - PuzzleTemplateEntity template = templateMapper.getByCode(request.getTemplateCode()); + // 1. 查询模板和元素(使用缓存) + PuzzleTemplateEntity template = puzzleRepository.getTemplateByCode(request.getTemplateCode()); if (template == null) { throw new IllegalArgumentException("模板不存在: " + request.getTemplateCode()); } @@ -161,7 +159,7 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { // 2. 校验景区隔离 Long resolvedScenicId = resolveScenicId(template, request.getScenicId()); - List elements = elementMapper.getByTemplateId(template.getId()); + List elements = puzzleRepository.getElementsByTemplateId(template.getId()); if (elements.isEmpty()) { throw new IllegalArgumentException("模板没有配置元素: " + request.getTemplateCode()); } @@ -246,7 +244,7 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService { Long resolvedScenicId, PuzzleGenerationRecordEntity record, long startTime) { - List elements = elementMapper.getByTemplateId(template.getId()); + List elements = puzzleRepository.getElementsByTemplateId(template.getId()); if (elements.isEmpty()) { throw new IllegalArgumentException("模板没有配置元素: " + request.getTemplateCode()); } diff --git a/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleTemplateServiceImpl.java b/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleTemplateServiceImpl.java index 8b11dfc2..daebf81d 100644 --- a/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleTemplateServiceImpl.java +++ b/src/main/java/com/ycwl/basic/puzzle/service/impl/PuzzleTemplateServiceImpl.java @@ -12,6 +12,7 @@ import com.ycwl.basic.puzzle.entity.PuzzleElementEntity; import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity; import com.ycwl.basic.puzzle.mapper.PuzzleElementMapper; import com.ycwl.basic.puzzle.mapper.PuzzleTemplateMapper; +import com.ycwl.basic.puzzle.repository.PuzzleRepository; import com.ycwl.basic.puzzle.service.IPuzzleTemplateService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -37,6 +38,7 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService { private final PuzzleTemplateMapper templateMapper; private final PuzzleElementMapper elementMapper; + private final PuzzleRepository puzzleRepository; @Override @Transactional(rollbackFor = Exception.class) @@ -70,6 +72,7 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService { } // 如果修改了编码,检查新编码是否已存在 + String oldCode = existing.getCode(); if (request.getCode() != null && !request.getCode().equals(existing.getCode())) { int count = templateMapper.countByCode(request.getCode(), id); if (count > 0) { @@ -82,6 +85,12 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService { entity.setId(id); templateMapper.update(entity); + // 清除缓存(如果修改了code,需要同时清除新旧code的缓存) + puzzleRepository.clearTemplateCache(id, oldCode); + if (request.getCode() != null && !request.getCode().equals(oldCode)) { + puzzleRepository.clearTemplateCache(null, request.getCode()); + } + log.info("拼图模板更新成功: id={}", id); } @@ -100,6 +109,9 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService { templateMapper.deleteById(id); elementMapper.deleteByTemplateId(id); + // 清除缓存 + puzzleRepository.clearTemplateCache(id, existing.getCode()); + log.info("拼图模板删除成功: id={}, 同时删除了关联的元素", id); } @@ -196,7 +208,10 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService { // 4. 插入数据库 elementMapper.insert(entity); - log.info("元素添加成功: id={}, type={}, key={}", + // 5. 清除元素缓存 + puzzleRepository.clearElementsCache(request.getTemplateId()); + + log.info("元素添加成功: id={}, type={}, key={}", entity.getId(), entity.getElementType(), entity.getElementKey()); return entity.getId(); } @@ -225,6 +240,8 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService { // 3. 批量插入 if (!entityList.isEmpty()) { elementMapper.batchInsert(entityList); + // 4. 清除元素缓存 + puzzleRepository.clearElementsCache(templateId); log.info("批量添加元素成功: templateId={}, count={}", templateId, entityList.size()); } } @@ -293,6 +310,9 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService { log.info("批量替换元素完成: templateId={}, deleted={}, updated={}, inserted={}", templateId, deletedCount, updatedCount, insertedCount); + + // 7. 清除元素缓存 + puzzleRepository.clearElementsCache(templateId); } @Override @@ -314,6 +334,9 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService { entity.setId(id); elementMapper.update(entity); + // 4. 清除元素缓存 + puzzleRepository.clearElementsCache(existing.getTemplateId()); + log.info("元素更新成功: id={}", id); } @@ -329,6 +352,10 @@ public class PuzzleTemplateServiceImpl implements IPuzzleTemplateService { } elementMapper.deleteById(id); + + // 清除元素缓存 + puzzleRepository.clearElementsCache(existing.getTemplateId()); + log.info("元素删除成功: id={}", id); }