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
@@ -1,6 +1,10 @@
server:
port: 8084
gym:
cache:
type: redis
spring:
aop:
proxy-target-class: true
@@ -4,7 +4,7 @@ import cn.novalon.gym.manage.app.ManageApplication;
import cn.novalon.gym.manage.auth.config.DCloudUniverifyConfig;
import cn.novalon.gym.manage.auth.service.PhoneAuthService;
import cn.novalon.gym.manage.auth.service.SmsService;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
import cn.novalon.gym.manage.payment.config.HuifuProperties;
import cn.novalon.gym.manage.payment.service.PaymentNotifyService;
@@ -74,9 +74,9 @@ public abstract class BaseContractTest {
@MockBean
protected ReactiveStringRedisTemplate reactiveStringRedisTemplate;
/** Mock RedisUtil — 阻止 Handler 中的 Redis 操作导致异常 */
/** Mock CacheOperations — 阻止 Handler 中的 Redis 操作导致异常 */
@MockBean
protected RedisUtil redisUtil;
protected CacheOperations cacheOperations;
/** Mock ES Repository — 防止连接 Elasticsearch */
@MockBean
@@ -1,7 +1,7 @@
package cn.novalon.gym.manage.app.integration;
import cn.novalon.gym.manage.app.contract.BaseContractTest;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -26,13 +26,13 @@ import static org.mockito.Mockito.when;
class GroupCourseHandlerIntegrationTest extends BaseContractTest {
@Autowired
private RedisUtil redisUtil;
private CacheOperations cacheOperations;
@BeforeEach
void setUp() {
// Mock Redis 操作,避免 GroupCourseService 中的 Redis 缓存逻辑导致 NPE
when(redisUtil.get(anyString(), any())).thenReturn(Mono.empty());
when(redisUtil.setWithExpire(anyString(), any(), anyLong())).thenReturn(Mono.just(true));
when(cacheOperations.get(anyString(), any())).thenReturn(Mono.empty());
when(cacheOperations.setWithExpire(anyString(), any(), anyLong())).thenReturn(Mono.just(true));
}
@Test
@@ -1,7 +1,7 @@
package cn.novalon.gym.manage.app.integration;
import cn.novalon.gym.manage.app.contract.BaseContractTest;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.member.entity.Member;
import cn.novalon.gym.manage.member.es.entity.MemberES;
import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
@@ -70,10 +70,10 @@ class MemberHandlerIntegrationTest extends BaseContractTest {
memberRepository.deleteAll().block();
when(authUtil.getMemberIdOrThrow(any())).thenReturn(1L);
// Mock Redis 缓存未命中,确保每次查询都走数据库
when(redisUtil.get(anyString(), any())).thenReturn(Mono.empty());
when(redisUtil.setWithExpire(anyString(), any(), anyLong())).thenReturn(Mono.just(true));
when(cacheOperations.get(anyString(), any())).thenReturn(Mono.empty());
when(cacheOperations.setWithExpire(anyString(), any(), anyLong())).thenReturn(Mono.just(true));
// Mock Redis 删除操作(clearMemberCache 调用),防止 NPE 导致 adminUpdateMemberInfo 返回 false
when(redisUtil.delete(anyString())).thenReturn(Mono.just(1L));
when(cacheOperations.delete(anyString())).thenReturn(Mono.just(1L));
// Mock ES 搜索返回空结果,防止因 ES 不可用导致 500
when(memberESRepository.findByMemberNoOrPhoneOrNicknameContaining(anyString(), anyString(), anyString(), any()))
.thenReturn(Flux.empty());