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
@@ -14,7 +14,7 @@ import cn.novalon.gym.manage.checkIn.vo.SignInRecordVO;
import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO;
import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler;
import cn.novalon.gym.manage.common.constant.RedisKeyConstants;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService;
@@ -44,7 +44,7 @@ import java.util.Map;
public class CheckServiceImpl implements ICheckInService {
private final QRCodeConfig qrCodeConfig;
private final RedisUtil redisUtil;
private final CacheOperations cacheOperations;
private final MemberCardRecordRepository memberCardRecordRepository;
private final MemberCardRepository memberCardRepository;
private final SignInRecordRepository signInRecordRepository;
@@ -63,7 +63,7 @@ public class CheckServiceImpl implements ICheckInService {
redisMap.put("isUsed", false);
redisMap.put("memberId", memberId);
return redisUtil.setWithExpire(
return cacheOperations.setWithExpire(
RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(),
redisMap,
getSecondsUntilEndOfDay()
@@ -87,7 +87,7 @@ public class CheckServiceImpl implements ICheckInService {
MyWebSocketHandler.sendFailure(qrContent, "您已经在" + checkInTime + "完成签到,请勿重复签到");
return Mono.error(new RuntimeException("您已经在" + checkInTime + "完成签到,请勿重复签到"));
})
.then(Mono.defer(() -> redisUtil.get(key)))
.then(Mono.defer(() -> cacheOperations.get(key)))
.flatMap(cachedObj -> {
if (cachedObj != null) {
Map<String, Object> map;
@@ -157,11 +157,11 @@ public class CheckServiceImpl implements ICheckInService {
redisMap.put("checkInTime", now.format(DATE_FORMATTER));
return saveSignInRecord(memberId, null, null)
.then(redisUtil.set(RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(), redisMap))
.then(cacheOperations.set(RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(), redisMap))
// 清除统计缓存和课程缓存,确保管理端/教练端立即反映最新数据
.then(Mono.defer(() ->
redisUtil.deleteByPattern("datacount:statistics:*")
.then(Mono.defer(() -> redisUtil.deleteByPattern("group_course:*")))
cacheOperations.deleteByPattern("datacount:statistics:*")
.then(Mono.defer(() -> cacheOperations.deleteByPattern("group_course:*")))
.doOnSuccess(v -> log.info("已清除统计缓存和课程缓存, memberId: {}", memberId))
.then()
))
@@ -10,7 +10,7 @@ import cn.novalon.gym.manage.checkIn.vo.QRCodeVo;
import cn.novalon.gym.manage.checkIn.vo.SignInRecordVO;
import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO;
import cn.novalon.gym.manage.common.constant.RedisKeyConstants;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.member.entity.MemberCard;
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
@@ -44,7 +44,7 @@ class CheckInModuleTest {
private QRCodeConfig qrCodeConfig;
@Mock
private RedisUtil redisUtil;
private CacheOperations cacheOperations;
@Mock
private MemberCardRecordRepository memberCardRecordRepository;
@@ -75,7 +75,7 @@ class CheckInModuleTest {
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
checkService = new CheckServiceImpl(qrCodeConfig, redisUtil, memberCardRecordRepository,
checkService = new CheckServiceImpl(qrCodeConfig, cacheOperations, memberCardRecordRepository,
memberCardRepository, signInRecordRepository, groupCourseBookingService,
groupCourseBookingRepository);
@@ -103,7 +103,7 @@ class CheckInModuleTest {
void testGetQRCode() {
when(memberCardRecordRepository.findActiveCardsByMemberId(1L))
.thenReturn(Flux.just(mockMemberCardRecord));
when(redisUtil.setWithExpire(any(String.class), any(Map.class), any(Long.class)))
when(cacheOperations.setWithExpire(any(String.class), any(Map.class), any(Long.class)))
.thenReturn(Mono.just(true));
Mono<QRCodeVo> result = checkService.getQRCode(1L);
@@ -130,12 +130,12 @@ class CheckInModuleTest {
String key = RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now();
when(redisUtil.get(eq(key))).thenReturn(Mono.just(qrData));
when(redisUtil.deleteByPattern(any(String.class))).thenReturn(Mono.empty());
when(cacheOperations.get(eq(key))).thenReturn(Mono.just(qrData));
when(cacheOperations.deleteByPattern(any(String.class))).thenReturn(Mono.empty());
when(memberCardRecordRepository.findById(1L)).thenReturn(Mono.just(mockMemberCardRecord));
when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(mockMemberCard));
when(signInRecordRepository.save(any(SignInRecord.class))).thenReturn(Mono.just(mockSignInRecord));
when(redisUtil.set(any(String.class), any(Map.class))).thenReturn(Mono.just(true));
when(cacheOperations.set(any(String.class), any(Map.class))).thenReturn(Mono.just(true));
when(groupCourseBookingService.getBookingsByMemberId(memberId)).thenReturn(Flux.empty());
when(signInRecordRepository.findByMemberIdAndDate(eq(memberId), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty());
@@ -259,7 +259,7 @@ class CheckInModuleTest {
void testCheckIn_QRCodeInvalid() {
Long memberId = 1L;
String key = RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now();
when(redisUtil.get(eq(key))).thenReturn(Mono.just(new HashMap<>()));
when(cacheOperations.get(eq(key))).thenReturn(Mono.just(new HashMap<>()));
when(signInRecordRepository.findByMemberIdAndDate(eq(memberId), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty());
@@ -275,7 +275,7 @@ class CheckInModuleTest {
void testCheckIn_QRCodeNotFound() {
Long memberId = 1L;
String key = RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now();
when(redisUtil.get(eq(key))).thenReturn(Mono.empty());
when(cacheOperations.get(eq(key))).thenReturn(Mono.empty());
when(signInRecordRepository.findByMemberIdAndDate(eq(memberId), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty());
@@ -8,7 +8,7 @@ import cn.novalon.gym.manage.checkIn.vo.QRCodeVo;
import cn.novalon.gym.manage.checkIn.vo.SignInRecordVO;
import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO;
import cn.novalon.gym.manage.common.constant.RedisKeyConstants;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService;
@@ -40,7 +40,7 @@ class CheckServiceImplTest {
private QRCodeConfig qrCodeConfig;
@Mock
private RedisUtil redisUtil;
private CacheOperations cacheOperations;
@Mock
private MemberCardRecordRepository memberCardRecordRepository;
@@ -70,7 +70,7 @@ class CheckServiceImplTest {
void getQRCode_shouldReturnQRCodeVo() {
when(qrCodeConfig.getWidth()).thenReturn(300);
when(qrCodeConfig.getHeight()).thenReturn(300);
when(redisUtil.setWithExpire(anyString(), any(Map.class), anyLong()))
when(cacheOperations.setWithExpire(anyString(), any(Map.class), anyLong()))
.thenReturn(Mono.just(true));
Mono<QRCodeVo> result = checkService.getQRCode(MEMBER_ID);
@@ -100,7 +100,7 @@ class CheckServiceImplTest {
// Redis 中有有效二维码数据
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
Map<String, Object> qrData = buildQrData(false);
when(redisUtil.get(eq(key))).thenReturn(Mono.just(qrData));
when(cacheOperations.get(eq(key))).thenReturn(Mono.just(qrData));
// 无预约
when(groupCourseBookingService.getBookingsByMemberId(MEMBER_ID)).thenReturn(Flux.empty());
@@ -110,10 +110,10 @@ class CheckServiceImplTest {
.thenReturn(Mono.just(createMockSignInRecord()));
// 更新缓存
when(redisUtil.set(anyString(), any(Map.class))).thenReturn(Mono.just(true));
when(cacheOperations.set(anyString(), any(Map.class))).thenReturn(Mono.just(true));
// 清除缓存
when(redisUtil.deleteByPattern(anyString())).thenReturn(Mono.empty());
when(cacheOperations.deleteByPattern(anyString())).thenReturn(Mono.empty());
Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
@@ -122,8 +122,8 @@ class CheckServiceImplTest {
.verifyComplete();
verify(signInRecordRepository).save(any(SignInRecord.class));
verify(redisUtil).set(anyString(), any(Map.class));
verify(redisUtil, times(2)).deleteByPattern(anyString());
verify(cacheOperations).set(anyString(), any(Map.class));
verify(cacheOperations, times(2)).deleteByPattern(anyString());
}
@Test
@@ -136,7 +136,7 @@ class CheckServiceImplTest {
// Redis 中有有效二维码数据
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
Map<String, Object> qrData = buildQrData(false);
when(redisUtil.get(eq(key))).thenReturn(Mono.just(qrData));
when(cacheOperations.get(eq(key))).thenReturn(Mono.just(qrData));
// 有有效预约
GroupCourseBooking booking = createValidBooking();
@@ -148,10 +148,10 @@ class CheckServiceImplTest {
.thenReturn(Mono.just(createMockSignInRecord()));
// 更新缓存
when(redisUtil.set(anyString(), any(Map.class))).thenReturn(Mono.just(true));
when(cacheOperations.set(anyString(), any(Map.class))).thenReturn(Mono.just(true));
// 清除缓存
when(redisUtil.deleteByPattern(anyString())).thenReturn(Mono.empty());
when(cacheOperations.deleteByPattern(anyString())).thenReturn(Mono.empty());
Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
@@ -172,7 +172,7 @@ class CheckServiceImplTest {
// Redis 中缓存数据为 JSON 字符串格式
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
String jsonData = "{\"qrContent\":\"" + QR_CONTENT + "\",\"isUsed\":false,\"memberId\":" + MEMBER_ID + "}";
when(redisUtil.get(eq(key))).thenReturn(Mono.just(jsonData));
when(cacheOperations.get(eq(key))).thenReturn(Mono.just(jsonData));
// 无预约
when(groupCourseBookingService.getBookingsByMemberId(MEMBER_ID)).thenReturn(Flux.empty());
@@ -182,10 +182,10 @@ class CheckServiceImplTest {
.thenReturn(Mono.just(createMockSignInRecord()));
// 更新缓存
when(redisUtil.set(anyString(), any(Map.class))).thenReturn(Mono.just(true));
when(cacheOperations.set(anyString(), any(Map.class))).thenReturn(Mono.just(true));
// 清除缓存
when(redisUtil.deleteByPattern(anyString())).thenReturn(Mono.empty());
when(cacheOperations.deleteByPattern(anyString())).thenReturn(Mono.empty());
Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
@@ -217,7 +217,7 @@ class CheckServiceImplTest {
&& e.getMessage().contains("请勿重复签到"))
.verify();
verify(redisUtil, never()).get(anyString());
verify(cacheOperations, never()).get(anyString());
}
@Test
@@ -231,7 +231,7 @@ class CheckServiceImplTest {
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
Map<String, Object> qrData = buildQrData(true);
qrData.put("checkInTime", "2026-07-31 10:00:00");
when(redisUtil.get(eq(key))).thenReturn(Mono.just(qrData));
when(cacheOperations.get(eq(key))).thenReturn(Mono.just(qrData));
Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
@@ -252,7 +252,7 @@ class CheckServiceImplTest {
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
Map<String, Object> qrData = buildQrData(false);
qrData.put("qrContent", "different-qr-content");
when(redisUtil.get(eq(key))).thenReturn(Mono.just(qrData));
when(cacheOperations.get(eq(key))).thenReturn(Mono.just(qrData));
Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
@@ -271,7 +271,7 @@ class CheckServiceImplTest {
// Redis 中无数据
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
when(redisUtil.get(eq(key))).thenReturn(Mono.empty());
when(cacheOperations.get(eq(key))).thenReturn(Mono.empty());
Mono<String> result = checkService.checkIn(MEMBER_ID, "not-exist");
@@ -289,7 +289,7 @@ class CheckServiceImplTest {
// Redis 返回非 Map 非 String 的数据
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
when(redisUtil.get(eq(key))).thenReturn(Mono.just(12345));
when(cacheOperations.get(eq(key))).thenReturn(Mono.just(12345));
Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);