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.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),
@@ -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();
}
@@ -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);
}
}
@@ -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 -> {