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:
+7
-7
@@ -2,7 +2,7 @@ package cn.novalon.gym.manage.coach.scheduler;
|
||||
|
||||
import cn.novalon.gym.manage.coach.enums.ViolationReason;
|
||||
import cn.novalon.gym.manage.coachconfig.service.CoachTimeRuleService;
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.common.cache.CacheOperations;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
|
||||
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity;
|
||||
@@ -35,18 +35,18 @@ public class CoachCourseScheduler {
|
||||
private final GroupCourseDao groupCourseDao;
|
||||
private final GroupCourseBookingDao groupCourseBookingDao;
|
||||
private final DatabaseClient databaseClient;
|
||||
private final RedisUtil redisUtil;
|
||||
private final CacheOperations cacheOperations;
|
||||
private final CoachTimeRuleService timeRuleService;
|
||||
|
||||
public CoachCourseScheduler(GroupCourseDao groupCourseDao,
|
||||
GroupCourseBookingDao groupCourseBookingDao,
|
||||
DatabaseClient databaseClient,
|
||||
RedisUtil redisUtil,
|
||||
CacheOperations cacheOperations,
|
||||
CoachTimeRuleService timeRuleService) {
|
||||
this.groupCourseDao = groupCourseDao;
|
||||
this.groupCourseBookingDao = groupCourseBookingDao;
|
||||
this.databaseClient = databaseClient;
|
||||
this.redisUtil = redisUtil;
|
||||
this.cacheOperations = cacheOperations;
|
||||
this.timeRuleService = timeRuleService;
|
||||
}
|
||||
|
||||
@@ -166,17 +166,17 @@ public class CoachCourseScheduler {
|
||||
/**
|
||||
* 清除统计缓存和团课缓存 —— 调度器触发时,如有课程状态变更则必须及时失效。
|
||||
*
|
||||
* <p>注意:测试环境中 RedisUtil 可能被 Mock 返回 null,需做 null 安全处理。</p>
|
||||
* <p>注意:测试环境中 CacheOperations 可能被 Mock 返回 null,需做 null 安全处理。</p>
|
||||
*/
|
||||
private void invalidateCache() {
|
||||
Mono<Long> statsMono = redisUtil.deleteByPattern("datacount:statistics:*");
|
||||
Mono<Long> statsMono = cacheOperations.deleteByPattern("datacount:statistics:*");
|
||||
if (statsMono != null) {
|
||||
statsMono.subscribe(
|
||||
deleted -> logger.debug("调度器清除统计缓存,已删除 {} 条", deleted),
|
||||
error -> logger.warn("调度器清除统计缓存失败: {}", error.getMessage())
|
||||
);
|
||||
}
|
||||
Mono<Long> courseMono = redisUtil.deleteByPattern("group_course:*");
|
||||
Mono<Long> courseMono = cacheOperations.deleteByPattern("group_course:*");
|
||||
if (courseMono != null) {
|
||||
courseMono.subscribe(
|
||||
deleted -> logger.debug("调度器清除团课缓存,已删除 {} 条", deleted),
|
||||
|
||||
+6
-6
@@ -5,7 +5,7 @@ import cn.novalon.gym.manage.coach.entity.CoachViolationEntity;
|
||||
import cn.novalon.gym.manage.coach.enums.ViolationReason;
|
||||
import cn.novalon.gym.manage.coachconfig.domain.CoachTimeRule;
|
||||
import cn.novalon.gym.manage.coachconfig.service.CoachTimeRuleService;
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.common.cache.CacheOperations;
|
||||
import cn.novalon.gym.manage.common.util.StatusConstants;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
|
||||
@@ -56,7 +56,7 @@ public class CoachCourseService {
|
||||
private final CoachViolationDao violationDao;
|
||||
private final DatabaseClient databaseClient;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final RedisUtil redisUtil;
|
||||
private final CacheOperations cacheOperations;
|
||||
private final CoachTimeRuleService timeRuleService;
|
||||
|
||||
public CoachCourseService(ISysUserRepository userRepository,
|
||||
@@ -69,7 +69,7 @@ public class CoachCourseService {
|
||||
CoachViolationDao violationDao,
|
||||
DatabaseClient databaseClient,
|
||||
PasswordEncoder passwordEncoder,
|
||||
RedisUtil redisUtil,
|
||||
CacheOperations cacheOperations,
|
||||
CoachTimeRuleService timeRuleService) {
|
||||
this.userRepository = userRepository;
|
||||
this.roleRepository = roleRepository;
|
||||
@@ -81,7 +81,7 @@ public class CoachCourseService {
|
||||
this.violationDao = violationDao;
|
||||
this.databaseClient = databaseClient;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.redisUtil = redisUtil;
|
||||
this.cacheOperations = cacheOperations;
|
||||
this.timeRuleService = timeRuleService;
|
||||
}
|
||||
|
||||
@@ -334,8 +334,8 @@ public class CoachCourseService {
|
||||
* 清除统计缓存和团课缓存 —— 课程状态变更后调用,返回 Mono 确保链式执行
|
||||
*/
|
||||
private Mono<Void> invalidateStatisticsCache() {
|
||||
return redisUtil.deleteByPattern("datacount:statistics:*")
|
||||
.then(redisUtil.deleteByPattern("group_course:*"))
|
||||
return cacheOperations.deleteByPattern("datacount:statistics:*")
|
||||
.then(cacheOperations.deleteByPattern("group_course:*"))
|
||||
.doOnNext(count -> {})
|
||||
.then();
|
||||
}
|
||||
|
||||
+15
-15
@@ -3,7 +3,7 @@ package cn.novalon.gym.manage.coach.scheduler;
|
||||
import cn.novalon.gym.manage.coach.enums.ViolationReason;
|
||||
import cn.novalon.gym.manage.coachconfig.domain.CoachTimeRule;
|
||||
import cn.novalon.gym.manage.coachconfig.service.CoachTimeRuleService;
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.common.cache.CacheOperations;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
|
||||
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity;
|
||||
@@ -38,7 +38,7 @@ class CoachCourseSchedulerTest {
|
||||
private DatabaseClient databaseClient;
|
||||
|
||||
@Mock
|
||||
private RedisUtil redisUtil;
|
||||
private CacheOperations cacheOperations;
|
||||
|
||||
@Mock
|
||||
private CoachTimeRuleService timeRuleService;
|
||||
@@ -54,7 +54,7 @@ class CoachCourseSchedulerTest {
|
||||
void setUp() {
|
||||
scheduler = new CoachCourseScheduler(
|
||||
groupCourseDao, groupCourseBookingDao,
|
||||
databaseClient, redisUtil, timeRuleService
|
||||
databaseClient, cacheOperations, timeRuleService
|
||||
);
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ class CoachCourseSchedulerTest {
|
||||
verify(groupCourseDao).findByStatusInAndEndTimeBefore(eq(databaseClient), any(String[].class), any(LocalDateTime.class));
|
||||
|
||||
// 验证未触发缓存清除(count=0,不进入 if 分支)
|
||||
verifyNoInteractions(redisUtil);
|
||||
verifyNoInteractions(cacheOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -403,20 +403,20 @@ class CoachCourseSchedulerTest {
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
// mock RedisUtil 返回非 null 的 Mono
|
||||
when(redisUtil.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
|
||||
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
|
||||
when(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
|
||||
when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
|
||||
|
||||
// 执行
|
||||
scheduler.checkAndProcessCourses();
|
||||
|
||||
// 验证缓存清除被调用
|
||||
verify(redisUtil).deleteByPattern("datacount:statistics:*");
|
||||
verify(redisUtil).deleteByPattern("group_course:*");
|
||||
verify(cacheOperations).deleteByPattern("datacount:statistics:*");
|
||||
verify(cacheOperations).deleteByPattern("group_course:*");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAndProcessCourses_shouldHandleRedisUtilReturningNull() {
|
||||
// 准备:测试 RedisUtil 返回 null 的边界情况
|
||||
void checkAndProcessCourses_shouldHandleCacheOperationsReturningNull() {
|
||||
// 准备:测试 CacheOperations 返回 null 的边界情况
|
||||
LocalDateTime startTime = LocalDateTime.of(2026, 7, 31, 10, 0);
|
||||
LocalDateTime endTime = LocalDateTime.of(2026, 7, 31, 11, 0);
|
||||
|
||||
@@ -440,15 +440,15 @@ class CoachCourseSchedulerTest {
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
// RedisUtil.deleteByPattern 返回 null(模拟 null 安全检查)
|
||||
when(redisUtil.deleteByPattern("datacount:statistics:*")).thenReturn(null);
|
||||
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(null);
|
||||
when(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(null);
|
||||
when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(null);
|
||||
|
||||
// 执行:不应抛出 NPE
|
||||
scheduler.checkAndProcessCourses();
|
||||
|
||||
// 验证:RedisUtil 被调用(即使返回 null)
|
||||
verify(redisUtil).deleteByPattern("datacount:statistics:*");
|
||||
verify(redisUtil).deleteByPattern("group_course:*");
|
||||
verify(cacheOperations).deleteByPattern("datacount:statistics:*");
|
||||
verify(cacheOperations).deleteByPattern("group_course:*");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -465,6 +465,6 @@ class CoachCourseSchedulerTest {
|
||||
// 验证
|
||||
verify(groupCourseDao).findByStatusAndStartTimeBefore(eq(databaseClient), eq("0"), any(LocalDateTime.class));
|
||||
verify(groupCourseDao).findByStatusInAndEndTimeBefore(eq(databaseClient), any(String[].class), any(LocalDateTime.class));
|
||||
verifyNoInteractions(redisUtil);
|
||||
verifyNoInteractions(cacheOperations);
|
||||
}
|
||||
}
|
||||
+17
-17
@@ -4,7 +4,7 @@ import cn.novalon.gym.manage.coach.dao.CoachViolationDao;
|
||||
import cn.novalon.gym.manage.coach.enums.ViolationReason;
|
||||
import cn.novalon.gym.manage.coachconfig.domain.CoachTimeRule;
|
||||
import cn.novalon.gym.manage.coachconfig.service.CoachTimeRuleService;
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.common.cache.CacheOperations;
|
||||
import cn.novalon.gym.manage.common.util.StatusConstants;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
|
||||
@@ -76,7 +76,7 @@ class CoachCourseServiceTest {
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Mock
|
||||
private RedisUtil redisUtil;
|
||||
private CacheOperations cacheOperations;
|
||||
|
||||
@Mock
|
||||
private CoachTimeRuleService timeRuleService;
|
||||
@@ -95,7 +95,7 @@ class CoachCourseServiceTest {
|
||||
groupCourseRepository, bookingRepository,
|
||||
groupCourseDao, groupCourseBookingDao,
|
||||
violationDao, databaseClient,
|
||||
passwordEncoder, redisUtil, timeRuleService
|
||||
passwordEncoder, cacheOperations, timeRuleService
|
||||
);
|
||||
}
|
||||
|
||||
@@ -328,8 +328,8 @@ class CoachCourseServiceTest {
|
||||
updatedUser.setStatus(StatusConstants.DISABLED);
|
||||
when(userRepository.update(any(SysUser.class))).thenReturn(Mono.just(updatedUser));
|
||||
|
||||
when(redisUtil.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
|
||||
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
|
||||
when(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
|
||||
when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
|
||||
|
||||
StepVerifier.create(coachCourseService.disableCoach(COACH_USER_ID))
|
||||
.verifyComplete();
|
||||
@@ -338,15 +338,15 @@ class CoachCourseServiceTest {
|
||||
verify(groupCourseRepository).countByCoachIdAndStatus(COACH_USER_ID, 3L);
|
||||
verify(groupCourseRepository).cancelCoursesByCoachIdExceptStatus(COACH_USER_ID, 3L);
|
||||
verify(userRepository).update(any(SysUser.class));
|
||||
verify(redisUtil).deleteByPattern("datacount:statistics:*");
|
||||
verify(redisUtil).deleteByPattern("group_course:*");
|
||||
verify(cacheOperations).deleteByPattern("datacount:statistics:*");
|
||||
verify(cacheOperations).deleteByPattern("group_course:*");
|
||||
}
|
||||
|
||||
@Test
|
||||
void disableCoach_shouldThrowWhenCoachNotFound() {
|
||||
when(userRepository.findById(COACH_USER_ID)).thenReturn(Mono.empty());
|
||||
// invalidateStatisticsCache() 会在 .then() 参数求值时被调用,需要 stub
|
||||
lenient().when(redisUtil.deleteByPattern(anyString())).thenReturn(Mono.just(1L));
|
||||
lenient().when(cacheOperations.deleteByPattern(anyString())).thenReturn(Mono.just(1L));
|
||||
|
||||
StepVerifier.create(coachCourseService.disableCoach(COACH_USER_ID))
|
||||
.expectErrorMatches(e -> e instanceof RuntimeException
|
||||
@@ -363,7 +363,7 @@ class CoachCourseServiceTest {
|
||||
|
||||
when(groupCourseRepository.countByCoachIdAndStatus(COACH_USER_ID, 3L)).thenReturn(Mono.just(2L));
|
||||
// invalidateStatisticsCache() 会在 .then() 参数求值时被调用,需要 stub
|
||||
lenient().when(redisUtil.deleteByPattern(anyString())).thenReturn(Mono.just(1L));
|
||||
lenient().when(cacheOperations.deleteByPattern(anyString())).thenReturn(Mono.just(1L));
|
||||
|
||||
StepVerifier.create(coachCourseService.disableCoach(COACH_USER_ID))
|
||||
.expectErrorMatches(e -> e instanceof RuntimeException
|
||||
@@ -461,8 +461,8 @@ class CoachCourseServiceTest {
|
||||
|
||||
when(groupCourseDao.updateStartInfo(eq(COURSE_ID), eq("3"), any(LocalDateTime.class), any(LocalDateTime.class))).thenReturn(Mono.just(1));
|
||||
|
||||
when(redisUtil.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
|
||||
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
|
||||
when(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
|
||||
when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
|
||||
|
||||
StepVerifier.create(coachCourseService.startCourse(COURSE_ID, COACH_USER_ID))
|
||||
.assertNext(entity -> {
|
||||
@@ -500,8 +500,8 @@ class CoachCourseServiceTest {
|
||||
|
||||
when(groupCourseDao.updateStartInfo(eq(COURSE_ID), eq("7"), any(LocalDateTime.class), any(LocalDateTime.class))).thenReturn(Mono.just(1));
|
||||
|
||||
when(redisUtil.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
|
||||
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
|
||||
when(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
|
||||
when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
|
||||
|
||||
StepVerifier.create(coachCourseService.startCourse(COURSE_ID, COACH_USER_ID))
|
||||
.assertNext(entity -> {
|
||||
@@ -608,8 +608,8 @@ class CoachCourseServiceTest {
|
||||
|
||||
when(groupCourseDao.updateEndInfo(eq(COURSE_ID), eq("2"), any(LocalDateTime.class), any(LocalDateTime.class))).thenReturn(Mono.just(1));
|
||||
|
||||
when(redisUtil.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
|
||||
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
|
||||
when(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
|
||||
when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
|
||||
|
||||
StepVerifier.create(coachCourseService.endCourse(COURSE_ID, COACH_USER_ID))
|
||||
.assertNext(entity -> {
|
||||
@@ -697,8 +697,8 @@ class CoachCourseServiceTest {
|
||||
|
||||
when(groupCourseDao.updateEndInfo(eq(COURSE_ID), eq("2"), any(LocalDateTime.class), any(LocalDateTime.class))).thenReturn(Mono.just(1));
|
||||
|
||||
when(redisUtil.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
|
||||
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
|
||||
when(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
|
||||
when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
|
||||
|
||||
StepVerifier.create(coachCourseService.endCourse(COURSE_ID, COACH_USER_ID))
|
||||
.assertNext(entity -> {
|
||||
|
||||
Reference in New Issue
Block a user