refactor(cache): 缓存操作接口化,支持 Redis/Caffeine 双实现切换

将系统所有缓存操作从 `RedisUtil` 迁移至 `CacheOperations` 接口,
通过 `gym.cache.type` 配置选择 Redis 或 Caffeine 实现。

变更内容:
- 新增 CacheOperations 接口,定义 get/set/delete/hasKey/expire/deleteByPattern 等标准方法
- 新增 RedisCacheOperations 实现(基于 ReactiveRedisTemplate,默认实现)
- 新增 CaffeineCacheOperations 实现(基于 Caffeine 本地缓存)
- 新增 CacheProperties 配置类,支持 Caffeine 容量/过期等参数配置
- 新增 CacheAutoConfiguration 自动装配,通过 AutoConfiguration.imports 注册
- 全系统 17 个业务服务 + 18 个测试文件从 RedisUtil 迁移至 CacheOperations
- manage-gateway 配置 caffeine 实现(无需 Redis),manage-app 配置 redis 实现
- 分布式锁等 Redis 独有特性保持直接使用 ReactiveRedisTemplate

测试:202 通过,0 失败,0 错误
This commit was merged in pull request #56.
This commit is contained in:
2026-08-02 18:26:15 +08:00
parent eb33755f23
commit 4ba4af2f53
45 changed files with 798 additions and 476 deletions
@@ -2,7 +2,7 @@
package cn.novalon.gym.manage.groupcourse.handler;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto;
@@ -30,16 +30,16 @@ public class GroupCourseHandler {
private static final Logger logger = LoggerFactory.getLogger(GroupCourseHandler.class);
private final IGroupCourseService groupCourseService;
private final Validator validator;
private final RedisUtil redisUtil;
private final CacheOperations cacheOperations;
private final ObjectMapper objectMapper;
public GroupCourseHandler(IGroupCourseService groupCourseService,
Validator validator,
RedisUtil redisUtil,
CacheOperations cacheOperations,
ObjectMapper objectMapper){
this.groupCourseService = groupCourseService;
this.validator = validator;
this.redisUtil = redisUtil;
this.cacheOperations = cacheOperations;
this.objectMapper = objectMapper;
}
@@ -258,7 +258,7 @@ public class GroupCourseHandler {
return ServerResponse.badRequest().bodyValue(error);
}
return redisUtil.get(key)
return cacheOperations.get(key)
.map(cachedValue -> {
Map<String, Object> result = new HashMap<>();
if (cachedValue != null) {
@@ -2,7 +2,7 @@ package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository;
@@ -24,21 +24,21 @@ public class CourseLabelService implements ICourseLabelService {
private final ICourseLabelRepository courseLabelRepository;
private final IGroupCourseRepository groupCourseRepository;
private final RedisUtil redisUtil;
private final CacheOperations cacheOperations;
public CourseLabelService(ICourseLabelRepository courseLabelRepository,
IGroupCourseRepository groupCourseRepository,
RedisUtil redisUtil) {
CacheOperations cacheOperations) {
this.courseLabelRepository = courseLabelRepository;
this.groupCourseRepository = groupCourseRepository;
this.redisUtil = redisUtil;
this.cacheOperations = cacheOperations;
}
private Mono<Void> invalidateGroupCourseDetailCache(Long typeId) {
return groupCourseRepository.findByCourseType(typeId)
.flatMap(course -> {
String cacheKey = CACHE_KEY_DETAIL_PREFIX + course.getId();
return redisUtil.delete(cacheKey)
return cacheOperations.delete(cacheKey)
.doOnSuccess(deleted -> logger.debug("清除团课详情缓存 - courseId={}", course.getId()));
})
.then();
@@ -1,8 +1,7 @@
package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
@@ -13,9 +12,11 @@ import java.time.Duration;
/**
* 团课Redis缓存服务
*
* 负责团课信息的缓存管理和分布式锁实现
*
* <p>
* 缓存操作委托给 {@link CacheOperations} 接口,支持通过配置切换 Redis / Caffeine 实现
* 分布式锁操作仍直接使用 {@link ReactiveRedisTemplate}(锁为 Redis 独有特性)。
* </p>
*
* @author 张翔
* @date 2026-06-01
*/
@@ -33,13 +34,13 @@ public class GroupCourseRedisService {
// 锁过期时间(30秒)
private static final Duration LOCK_EXPIRE_TIME = Duration.ofSeconds(30);
private final CacheOperations cacheOperations;
private final ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
private final ObjectMapper objectMapper;
public GroupCourseRedisService(ReactiveRedisTemplate<String, Object> reactiveRedisTemplate,
ObjectMapper objectMapper) {
public GroupCourseRedisService(CacheOperations cacheOperations,
ReactiveRedisTemplate<String, Object> reactiveRedisTemplate) {
this.cacheOperations = cacheOperations;
this.reactiveRedisTemplate = reactiveRedisTemplate;
this.objectMapper = objectMapper;
}
/**
@@ -61,16 +62,9 @@ public class GroupCourseRedisService {
*/
public Mono<Void> cacheCourse(GroupCourse course) {
String key = getCourseCacheKey(course.getId());
try {
String value = objectMapper.writeValueAsString(course);
return reactiveRedisTemplate.opsForValue()
.set(key, value, CACHE_EXPIRE_TIME)
.doOnSuccess(result -> logger.debug("团课信息已缓存:courseId={}", course.getId()))
.then();
} catch (JsonProcessingException e) {
logger.error("序列化团课信息失败:courseId={}", course.getId(), e);
return Mono.error(e);
}
return cacheOperations.setWithExpire(key, course, CACHE_EXPIRE_TIME.toSeconds())
.doOnSuccess(result -> logger.debug("团课信息已缓存:courseId={}", course.getId()))
.then();
}
/**
@@ -78,19 +72,8 @@ public class GroupCourseRedisService {
*/
public Mono<GroupCourse> getCachedCourse(Long courseId) {
String key = getCourseCacheKey(courseId);
return reactiveRedisTemplate.opsForValue()
.get(key)
.cast(String.class)
.flatMap(value -> {
try {
GroupCourse course = objectMapper.readValue(value, GroupCourse.class);
logger.debug("从缓存获取团课信息:courseId={}", courseId);
return Mono.just(course);
} catch (JsonProcessingException e) {
logger.error("反序列化团课信息失败:courseId={}", courseId, e);
return Mono.empty();
}
})
return cacheOperations.get(key, GroupCourse.class)
.doOnNext(course -> logger.debug("从缓存获取团课信息:courseId={}", courseId))
.switchIfEmpty(Mono.fromRunnable(() -> logger.debug("缓存中未找到团课信息:courseId={}", courseId)));
}
@@ -99,15 +82,16 @@ public class GroupCourseRedisService {
*/
public Mono<Void> invalidateCourseCache(Long courseId) {
String key = getCourseCacheKey(courseId);
return reactiveRedisTemplate.delete(key)
return cacheOperations.delete(key)
.doOnSuccess(result -> logger.debug("团课缓存已删除:courseId={}", courseId))
.then();
}
/**
* 获取分布式锁
*
* @param courseId 课程ID
* <p>注意:分布式锁为 Redis 独有特性,始终保持直接使用 RedisTemplate。</p>
*
* @param courseId 课程ID
* @param requestId 请求ID(用于锁的唯一性校验)
* @return 是否获取成功
*/
@@ -126,8 +110,9 @@ public class GroupCourseRedisService {
/**
* 释放分布式锁
*
* @param courseId 课程ID
* <p>注意:分布式锁为 Redis 独有特性,始终保持直接使用 RedisTemplate。</p>
*
* @param courseId 课程ID
* @param requestId 请求ID(用于锁的唯一性校验)
* @return 是否释放成功
*/
@@ -142,11 +127,11 @@ public class GroupCourseRedisService {
.map(deleted -> deleted > 0)
.doOnSuccess(result -> logger.debug("释放预约锁成功:courseId={}, requestId={}", courseId, requestId));
} else {
logger.warn("锁归属校验失败:courseId={}, expectedRequestId={}, actualRequestId={}",
logger.warn("锁归属校验失败:courseId={}, expectedRequestId={}, actualRequestId={}",
courseId, requestId, storedRequestId);
return Mono.just(false);
}
})
.defaultIfEmpty(false);
}
}
}
@@ -1,9 +1,9 @@
package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
@@ -53,7 +53,7 @@ public class GroupCourseService implements IGroupCourseService {
private final ICourseLabelRepository courseLabelRepository;
private final IMemberCardRecordService memberCardRecordService;
private final MemberCardRepository memberCardRepository;
private final RedisUtil redisUtil;
private final CacheOperations cacheOperations;
private final ObjectMapper objectMapper;
private final GroupCourseStateMachine stateMachine;
private final DatabaseClient databaseClient;
@@ -73,7 +73,7 @@ public class GroupCourseService implements IGroupCourseService {
ICourseLabelRepository courseLabelRepository,
IMemberCardRecordService memberCardRecordService,
MemberCardRepository memberCardRepository,
RedisUtil redisUtil,
CacheOperations cacheOperations,
ObjectMapper objectMapper,
GroupCourseStateMachine stateMachine,
DatabaseClient databaseClient,
@@ -85,7 +85,7 @@ public class GroupCourseService implements IGroupCourseService {
this.courseLabelRepository = courseLabelRepository;
this.memberCardRecordService = memberCardRecordService;
this.memberCardRepository = memberCardRepository;
this.redisUtil = redisUtil;
this.cacheOperations = cacheOperations;
this.objectMapper = objectMapper;
this.stateMachine = stateMachine;
this.databaseClient = databaseClient;
@@ -97,7 +97,7 @@ public class GroupCourseService implements IGroupCourseService {
public Mono<GroupCourseDetail> findDetailById(Long id) {
String cacheKey = CACHE_KEY_DETAIL_PREFIX + id;
Mono<String> cachedMono = redisUtil.get(cacheKey, String.class);
Mono<String> cachedMono = cacheOperations.get(cacheKey, String.class);
return cachedMono
.flatMap(cachedJson -> {
if (cachedJson != null && !cachedJson.isEmpty()) {
@@ -107,7 +107,7 @@ public class GroupCourseService implements IGroupCourseService {
return Mono.<GroupCourseDetail>just(detail);
} catch (JsonProcessingException e) {
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage());
return redisUtil.delete(cacheKey).then(Mono.<GroupCourseDetail>empty());
return cacheOperations.delete(cacheKey).then(Mono.<GroupCourseDetail>empty());
}
}
return Mono.<GroupCourseDetail>empty();
@@ -141,7 +141,7 @@ public class GroupCourseService implements IGroupCourseService {
.flatMap(detail -> {
try {
String jsonData = objectMapper.writeValueAsString(detail);
return redisUtil.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS)
return cacheOperations.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS)
.thenReturn(detail)
.doOnSuccess(d -> logger.debug("缓存已设置 - findDetailById: id={}", id));
} catch (JsonProcessingException e) {
@@ -233,7 +233,7 @@ public class GroupCourseService implements IGroupCourseService {
public Mono<GroupCourse> findById(Long id) {
String cacheKey = CACHE_KEY_ID_PREFIX + id;
Mono<String> cachedMono = redisUtil.get(cacheKey, String.class);
Mono<String> cachedMono = cacheOperations.get(cacheKey, String.class);
return cachedMono
.flatMap(cachedJson -> {
if (cachedJson != null && !cachedJson.isEmpty()) {
@@ -243,7 +243,7 @@ public class GroupCourseService implements IGroupCourseService {
return Mono.<GroupCourse>just(groupCourse);
} catch (JsonProcessingException e) {
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage());
return redisUtil.delete(cacheKey).then(Mono.<GroupCourse>empty());
return cacheOperations.delete(cacheKey).then(Mono.<GroupCourse>empty());
}
}
return Mono.<GroupCourse>empty();
@@ -253,7 +253,7 @@ public class GroupCourseService implements IGroupCourseService {
.flatMap(groupCourse -> {
try {
String jsonData = objectMapper.writeValueAsString(groupCourse);
return redisUtil.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS)
return cacheOperations.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS)
.thenReturn(groupCourse)
.doOnSuccess(gc -> logger.debug("缓存已设置 - findById: id={}", id));
} catch (JsonProcessingException e) {
@@ -332,7 +332,7 @@ public class GroupCourseService implements IGroupCourseService {
String cacheKey = CACHE_KEY_PREFIX + page + ":" + size + ":" + includeDeleted + ":" + sort + ":" + order + ":" + keyword + ":" + status;
Mono<String> cachedMono = redisUtil.get(cacheKey, String.class);
Mono<String> cachedMono = cacheOperations.get(cacheKey, String.class);
return cachedMono
.flatMap(cachedJson -> {
if (cachedJson != null && !cachedJson.isEmpty()) {
@@ -343,7 +343,7 @@ public class GroupCourseService implements IGroupCourseService {
return Mono.<PageResponse<GroupCourse>>just(pageResponse);
} catch (JsonProcessingException e) {
logger.warn("缓存解析失败,删除缓存 - key: {}, error: {}", cacheKey, e.getMessage());
return redisUtil.delete(cacheKey).then(Mono.<PageResponse<GroupCourse>>empty());
return cacheOperations.delete(cacheKey).then(Mono.<PageResponse<GroupCourse>>empty());
}
}
return Mono.<PageResponse<GroupCourse>>empty();
@@ -361,7 +361,7 @@ public class GroupCourseService implements IGroupCourseService {
return resultMono.flatMap(pageResponse -> {
try {
String jsonData = objectMapper.writeValueAsString(pageResponse);
return redisUtil.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS)
return cacheOperations.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS)
.thenReturn(pageResponse)
.doOnSuccess(pr -> logger.debug("缓存已设置 - findByPage: key={}", cacheKey));
} catch (JsonProcessingException e) {
@@ -739,10 +739,10 @@ public class GroupCourseService implements IGroupCourseService {
}
private Mono<Void> clearCache() {
return redisUtil.deleteByPattern(CACHE_KEY_PREFIX + "*")
.then(redisUtil.deleteByPattern(CACHE_KEY_ID_PREFIX + "*"))
.then(redisUtil.deleteByPattern(CACHE_KEY_DETAIL_PREFIX + "*"))
.then(redisUtil.deleteByPattern("datacount:statistics:*"))
return cacheOperations.deleteByPattern(CACHE_KEY_PREFIX + "*")
.then(cacheOperations.deleteByPattern(CACHE_KEY_ID_PREFIX + "*"))
.then(cacheOperations.deleteByPattern(CACHE_KEY_DETAIL_PREFIX + "*"))
.then(cacheOperations.deleteByPattern("datacount:statistics:*"))
.then();
}
@@ -1,6 +1,6 @@
package cn.novalon.gym.manage.groupcourse.handler;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
@@ -35,7 +35,7 @@ class GroupCourseHandlerTest {
private Validator validator;
@Mock
private RedisUtil redisUtil;
private CacheOperations cacheOperations;
@Mock
private ObjectMapper objectMapper;
@@ -44,7 +44,7 @@ class GroupCourseHandlerTest {
@BeforeEach
void setUp() {
handler = new GroupCourseHandler(groupCourseService, validator, redisUtil, objectMapper);
handler = new GroupCourseHandler(groupCourseService, validator, cacheOperations, objectMapper);
}
// ==================== getAllGroupCourse ====================
@@ -2,7 +2,7 @@ package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository;
@@ -38,13 +38,13 @@ class CourseLabelServiceTest {
private IGroupCourseRepository groupCourseRepository;
@Mock
private RedisUtil redisUtil;
private CacheOperations cacheOperations;
private CourseLabelService courseLabelService;
@BeforeEach
void setUp() {
courseLabelService = new CourseLabelService(courseLabelRepository, groupCourseRepository, redisUtil);
courseLabelService = new CourseLabelService(courseLabelRepository, groupCourseRepository, cacheOperations);
}
// ==================== findById ====================
@@ -287,16 +287,16 @@ class CourseLabelServiceTest {
GroupCourse course2 = new GroupCourse();
course2.setId(102L);
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.just(course1, course2));
when(redisUtil.delete("group_course:detail:101")).thenReturn(Mono.just(1L));
when(redisUtil.delete("group_course:detail:102")).thenReturn(Mono.just(1L));
when(cacheOperations.delete("group_course:detail:101")).thenReturn(Mono.just(1L));
when(cacheOperations.delete("group_course:detail:102")).thenReturn(Mono.just(1L));
StepVerifier.create(courseLabelService.addLabelsToType(1L, List.of(1L, 2L)))
.verifyComplete();
verify(courseLabelRepository).addLabelsToType(1L, List.of(1L, 2L));
verify(groupCourseRepository).findByCourseType(1L);
verify(redisUtil).delete("group_course:detail:101");
verify(redisUtil).delete("group_course:detail:102");
verify(cacheOperations).delete("group_course:detail:101");
verify(cacheOperations).delete("group_course:detail:102");
}
@Test
@@ -309,7 +309,7 @@ class CourseLabelServiceTest {
verify(courseLabelRepository).addLabelsToType(1L, List.of(1L));
verify(groupCourseRepository).findByCourseType(1L);
verify(redisUtil, never()).delete(anyString());
verify(cacheOperations, never()).delete(anyString());
}
// ==================== removeLabelFromType ====================
@@ -321,14 +321,14 @@ class CourseLabelServiceTest {
GroupCourse course = new GroupCourse();
course.setId(101L);
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.just(course));
when(redisUtil.delete("group_course:detail:101")).thenReturn(Mono.just(1L));
when(cacheOperations.delete("group_course:detail:101")).thenReturn(Mono.just(1L));
StepVerifier.create(courseLabelService.removeLabelFromType(1L, 1L))
.verifyComplete();
verify(courseLabelRepository).removeLabelFromType(1L, 1L);
verify(groupCourseRepository).findByCourseType(1L);
verify(redisUtil).delete("group_course:detail:101");
verify(cacheOperations).delete("group_course:detail:101");
}
@Test
@@ -340,7 +340,7 @@ class CourseLabelServiceTest {
.verifyComplete();
verify(courseLabelRepository).removeLabelFromType(1L, 1L);
verify(redisUtil, never()).delete(anyString());
verify(cacheOperations, never()).delete(anyString());
}
// ==================== clearLabelsFromType ====================
@@ -354,16 +354,16 @@ class CourseLabelServiceTest {
GroupCourse course2 = new GroupCourse();
course2.setId(102L);
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.just(course1, course2));
when(redisUtil.delete("group_course:detail:101")).thenReturn(Mono.just(1L));
when(redisUtil.delete("group_course:detail:102")).thenReturn(Mono.just(1L));
when(cacheOperations.delete("group_course:detail:101")).thenReturn(Mono.just(1L));
when(cacheOperations.delete("group_course:detail:102")).thenReturn(Mono.just(1L));
StepVerifier.create(courseLabelService.clearLabelsFromType(1L))
.verifyComplete();
verify(courseLabelRepository).clearLabelsFromType(1L);
verify(groupCourseRepository).findByCourseType(1L);
verify(redisUtil).delete("group_course:detail:101");
verify(redisUtil).delete("group_course:detail:102");
verify(cacheOperations).delete("group_course:detail:101");
verify(cacheOperations).delete("group_course:detail:102");
}
@Test
@@ -375,7 +375,7 @@ class CourseLabelServiceTest {
.verifyComplete();
verify(courseLabelRepository).clearLabelsFromType(1L);
verify(redisUtil, never()).delete(anyString());
verify(cacheOperations, never()).delete(anyString());
}
// ==================== findByPage ====================
@@ -1,7 +1,7 @@
package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -21,14 +21,15 @@ import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class GroupCourseRedisServiceTest {
@Mock
private CacheOperations cacheOperations;
@Mock
private ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
@Mock
private ReactiveValueOperations<String, Object> reactiveValueOps;
private ObjectMapper objectMapper = new ObjectMapper();
private GroupCourseRedisService service;
private GroupCourse testCourse;
@@ -36,7 +37,7 @@ class GroupCourseRedisServiceTest {
@BeforeEach
void setUp() {
lenient().when(reactiveRedisTemplate.opsForValue()).thenReturn(reactiveValueOps);
service = new GroupCourseRedisService(reactiveRedisTemplate, objectMapper);
service = new GroupCourseRedisService(cacheOperations, reactiveRedisTemplate);
testCourse = new GroupCourse();
testCourse.setId(1L);
@@ -47,20 +48,20 @@ class GroupCourseRedisServiceTest {
// ==================== cacheCourse ====================
@Test
void cacheCourse_shouldSerializeAndSetInRedis() {
when(reactiveValueOps.set(eq("group_course:1"), anyString(), eq(Duration.ofMinutes(5))))
void cacheCourse_shouldSetInCache() {
when(cacheOperations.setWithExpire(eq("group_course:1"), eq(testCourse), eq(300L)))
.thenReturn(Mono.just(true));
StepVerifier.create(service.cacheCourse(testCourse))
.verifyComplete();
verify(reactiveValueOps).set(eq("group_course:1"), anyString(), eq(Duration.ofMinutes(5)));
verify(cacheOperations).setWithExpire(eq("group_course:1"), eq(testCourse), eq(300L));
}
@Test
void cacheCourse_shouldErrorWhenRedisFails() {
when(reactiveValueOps.set(eq("group_course:1"), anyString(), eq(Duration.ofMinutes(5))))
.thenReturn(Mono.error(new RuntimeException("Redis error")));
void cacheCourse_shouldErrorWhenCacheFails() {
when(cacheOperations.setWithExpire(eq("group_course:1"), eq(testCourse), eq(300L)))
.thenReturn(Mono.error(new RuntimeException("Cache error")));
StepVerifier.create(service.cacheCourse(testCourse))
.expectError(RuntimeException.class)
@@ -70,9 +71,9 @@ class GroupCourseRedisServiceTest {
// ==================== getCachedCourse ====================
@Test
void getCachedCourse_shouldReturnCachedCourseWhenFound() throws Exception {
String json = objectMapper.writeValueAsString(testCourse);
when(reactiveValueOps.get("group_course:1")).thenReturn(Mono.just(json));
void getCachedCourse_shouldReturnCachedCourseWhenFound() {
when(cacheOperations.get(eq("group_course:1"), eq(GroupCourse.class)))
.thenReturn(Mono.just(testCourse));
StepVerifier.create(service.getCachedCourse(1L))
.assertNext(course -> {
@@ -84,16 +85,8 @@ class GroupCourseRedisServiceTest {
@Test
void getCachedCourse_shouldReturnEmptyWhenCacheMiss() {
when(reactiveValueOps.get("group_course:1")).thenReturn(Mono.empty());
StepVerifier.create(service.getCachedCourse(1L))
.verifyComplete();
}
@Test
void getCachedCourse_shouldReturnEmptyWhenDeserializationFails() {
String invalidJson = "invalid json";
when(reactiveValueOps.get("group_course:1")).thenReturn(Mono.just(invalidJson));
when(cacheOperations.get(eq("group_course:1"), eq(GroupCourse.class)))
.thenReturn(Mono.empty());
StepVerifier.create(service.getCachedCourse(1L))
.verifyComplete();
@@ -102,13 +95,13 @@ class GroupCourseRedisServiceTest {
// ==================== invalidateCourseCache ====================
@Test
void invalidateCourseCache_shouldDeleteFromRedis() {
when(reactiveRedisTemplate.delete("group_course:1")).thenReturn(Mono.just(1L));
void invalidateCourseCache_shouldDeleteFromCache() {
when(cacheOperations.delete("group_course:1")).thenReturn(Mono.just(1L));
StepVerifier.create(service.invalidateCourseCache(1L))
.verifyComplete();
verify(reactiveRedisTemplate).delete("group_course:1");
verify(cacheOperations).delete("group_course:1");
}
// ==================== acquireLock ====================
@@ -2,7 +2,7 @@ package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.file.core.domain.SysFile;
import cn.novalon.gym.manage.file.core.service.ISysFileService;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
@@ -63,7 +63,7 @@ class GroupCourseServiceTest {
@Mock
private MemberCardRepository memberCardRepository;
@Mock
private RedisUtil redisUtil;
private CacheOperations cacheOperations;
@Mock
private GroupCourseStateMachine stateMachine;
@Mock
@@ -90,7 +90,7 @@ class GroupCourseServiceTest {
groupCourseService = new GroupCourseService(
groupCourseRepository, bookingRepository, groupCourseTypeRepository,
courseLabelRepository, memberCardRecordService, memberCardRepository,
redisUtil, objectMapper, stateMachine, databaseClient, fileService, sysUserRepository
cacheOperations, objectMapper, stateMachine, databaseClient, fileService, sysUserRepository
);
testCourse = new GroupCourse();
@@ -136,10 +136,10 @@ class GroupCourseServiceTest {
}
private void mockClearCache() {
when(redisUtil.deleteByPattern("group_course:page:*")).thenReturn(Mono.just(0L));
when(redisUtil.deleteByPattern("group_course:id:*")).thenReturn(Mono.just(0L));
when(redisUtil.deleteByPattern("group_course:detail:*")).thenReturn(Mono.just(0L));
when(redisUtil.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(0L));
when(cacheOperations.deleteByPattern("group_course:page:*")).thenReturn(Mono.just(0L));
when(cacheOperations.deleteByPattern("group_course:id:*")).thenReturn(Mono.just(0L));
when(cacheOperations.deleteByPattern("group_course:detail:*")).thenReturn(Mono.just(0L));
when(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(0L));
}
private void mockEnrichCurrentMembers() {
@@ -157,7 +157,7 @@ class GroupCourseServiceTest {
detail.setCoachName("张教练");
String json = objectMapper.writeValueAsString(detail);
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.just(json));
when(cacheOperations.get(cacheKey, String.class)).thenReturn(Mono.just(json));
// switchIfEmpty 的 Mono 参数被急切求值,需要 mock 以避免 NPE
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
@@ -169,7 +169,7 @@ class GroupCourseServiceTest {
})
.verifyComplete();
verify(redisUtil).get(cacheKey, String.class);
verify(cacheOperations).get(cacheKey, String.class);
// switchIfEmpty 的 Mono 参数被急切求值,findByIdAndDeletedAtIsNull 会被调用但不会订阅
}
@@ -177,13 +177,13 @@ class GroupCourseServiceTest {
void findDetailById_cacheMissWithType_shouldBuildAndCacheDetail() throws Exception {
String cacheKey = "group_course:detail:1";
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(cacheOperations.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(groupCourseTypeRepository.findById(100L)).thenReturn(Mono.just(testType));
when(courseLabelRepository.findByTypeId(100L)).thenReturn(Flux.just(testLabel));
when(sysUserRepository.findByIdIncludingDeleted(10L)).thenReturn(Mono.just(testCoach));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(8L));
when(redisUtil.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
when(cacheOperations.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
StepVerifier.create(groupCourseService.findDetailById(1L))
.assertNext(result -> {
@@ -198,13 +198,13 @@ class GroupCourseServiceTest {
})
.verifyComplete();
verify(redisUtil).get(cacheKey, String.class);
verify(cacheOperations).get(cacheKey, String.class);
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L);
verify(groupCourseTypeRepository).findById(100L);
verify(courseLabelRepository).findByTypeId(100L);
verify(sysUserRepository).findByIdIncludingDeleted(10L);
verify(bookingRepository).countValidBookings(1L);
verify(redisUtil).setWithExpire(eq(cacheKey), anyString(), eq(300L));
verify(cacheOperations).setWithExpire(eq(cacheKey), anyString(), eq(300L));
}
@Test
@@ -212,11 +212,11 @@ class GroupCourseServiceTest {
String cacheKey = "group_course:detail:1";
testCourse.setCourseType(null);
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(cacheOperations.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(sysUserRepository.findByIdIncludingDeleted(10L)).thenReturn(Mono.just(testCoach));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(3L));
when(redisUtil.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
when(cacheOperations.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
StepVerifier.create(groupCourseService.findDetailById(1L))
.assertNext(result -> {
@@ -236,7 +236,7 @@ class GroupCourseServiceTest {
void findDetailById_cacheMissNotFound_shouldReturnEmpty() {
String cacheKey = "group_course:detail:1";
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(cacheOperations.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.findDetailById(1L))
@@ -250,15 +250,15 @@ class GroupCourseServiceTest {
void findDetailById_cacheParseError_shouldDeleteCacheAndFallback() throws Exception {
String cacheKey = "group_course:detail:1";
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.just("invalid-json"));
when(redisUtil.delete(cacheKey)).thenReturn(Mono.just(1L));
when(cacheOperations.get(cacheKey, String.class)).thenReturn(Mono.just("invalid-json"));
when(cacheOperations.delete(cacheKey)).thenReturn(Mono.just(1L));
// After cache delete, switchIfEmpty kicks in
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(groupCourseTypeRepository.findById(100L)).thenReturn(Mono.just(testType));
when(courseLabelRepository.findByTypeId(100L)).thenReturn(Flux.just(testLabel));
when(sysUserRepository.findByIdIncludingDeleted(10L)).thenReturn(Mono.just(testCoach));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L));
when(redisUtil.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
when(cacheOperations.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
StepVerifier.create(groupCourseService.findDetailById(1L))
.assertNext(result -> {
@@ -267,7 +267,7 @@ class GroupCourseServiceTest {
})
.verifyComplete();
verify(redisUtil).delete(cacheKey);
verify(cacheOperations).delete(cacheKey);
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L);
}
@@ -278,7 +278,7 @@ class GroupCourseServiceTest {
String cacheKey = "group_course:id:1";
String json = objectMapper.writeValueAsString(testCourse);
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.just(json));
when(cacheOperations.get(cacheKey, String.class)).thenReturn(Mono.just(json));
// switchIfEmpty 的 Mono 参数被急切求值,需要 mock 以避免 NPE
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L));
@@ -290,7 +290,7 @@ class GroupCourseServiceTest {
})
.verifyComplete();
verify(redisUtil).get(cacheKey, String.class);
verify(cacheOperations).get(cacheKey, String.class);
verify(bookingRepository).countValidBookings(1L);
// switchIfEmpty 的 Mono 参数被急切求值,findByIdAndDeletedAtIsNull 会被调用但不会订阅
}
@@ -299,9 +299,9 @@ class GroupCourseServiceTest {
void findById_cacheMiss_shouldQueryDbAndCache() throws Exception {
String cacheKey = "group_course:id:1";
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(cacheOperations.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(redisUtil.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
when(cacheOperations.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L));
StepVerifier.create(groupCourseService.findById(1L))
@@ -312,7 +312,7 @@ class GroupCourseServiceTest {
.verifyComplete();
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L);
verify(redisUtil).setWithExpire(eq(cacheKey), anyString(), eq(300L));
verify(cacheOperations).setWithExpire(eq(cacheKey), anyString(), eq(300L));
verify(bookingRepository).countValidBookings(1L);
}
@@ -320,7 +320,7 @@ class GroupCourseServiceTest {
void findById_cacheMissNotFound_shouldReturnEmpty() {
String cacheKey = "group_course:id:1";
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(cacheOperations.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.findById(1L))
@@ -383,7 +383,7 @@ class GroupCourseServiceTest {
String cacheKey = "group_course:page:0:10:false:id:asc::";
String json = objectMapper.writeValueAsString(pageResponse);
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.just(json));
when(cacheOperations.get(cacheKey, String.class)).thenReturn(Mono.just(json));
StepVerifier.create(groupCourseService.findByPage(pageRequest, false))
.assertNext(result -> {
@@ -393,7 +393,7 @@ class GroupCourseServiceTest {
})
.verifyComplete();
verify(redisUtil).get(cacheKey, String.class);
verify(cacheOperations).get(cacheKey, String.class);
verifyNoInteractions(groupCourseRepository);
}
@@ -409,9 +409,9 @@ class GroupCourseServiceTest {
String cacheKey = "group_course:page:0:10:false:id:asc::";
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(cacheOperations.get(cacheKey, String.class)).thenReturn(Mono.empty());
when(groupCourseRepository.findByPageAndNotDeleted(pageRequest)).thenReturn(Mono.just(pageResponse));
when(redisUtil.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
when(cacheOperations.setWithExpire(eq(cacheKey), anyString(), eq(300L))).thenReturn(Mono.just(true));
StepVerifier.create(groupCourseService.findByPage(pageRequest, false))
.assertNext(result -> {
@@ -421,7 +421,7 @@ class GroupCourseServiceTest {
.verifyComplete();
verify(groupCourseRepository).findByPageAndNotDeleted(pageRequest);
verify(redisUtil).setWithExpire(eq(cacheKey), anyString(), eq(300L));
verify(cacheOperations).setWithExpire(eq(cacheKey), anyString(), eq(300L));
}
// ==================== create ====================