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
@@ -3,7 +3,7 @@ package cn.novalon.gym.manage.auth.service.impl;
import cn.novalon.gym.manage.auth.config.SmsProperties; import cn.novalon.gym.manage.auth.config.SmsProperties;
import cn.novalon.gym.manage.auth.service.SmsService; import cn.novalon.gym.manage.auth.service.SmsService;
import cn.novalon.gym.manage.common.constant.RedisKeyConstants; 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 com.aliyuncs.CommonRequest; import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse; import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.DefaultAcsClient;
@@ -31,7 +31,7 @@ public class SmsServiceImpl implements SmsService {
private static final long CODE_EXPIRE_SECONDS = 300; private static final long CODE_EXPIRE_SECONDS = 300;
private final SmsProperties smsProperties; private final SmsProperties smsProperties;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@Override @Override
@@ -40,7 +40,7 @@ public class SmsServiceImpl implements SmsService {
String rateLimitKey = RedisKeyConstants.SMS_CODE + phone + ":rate_limit"; String rateLimitKey = RedisKeyConstants.SMS_CODE + phone + ":rate_limit";
return redisUtil.get(rateLimitKey, Long.class) return cacheOperations.get(rateLimitKey, Long.class)
.defaultIfEmpty(0L) .defaultIfEmpty(0L)
.flatMap(lastSendTime -> { .flatMap(lastSendTime -> {
long currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC); long currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
@@ -97,7 +97,7 @@ public class SmsServiceImpl implements SmsService {
if (verifyCodeNode != null) { if (verifyCodeNode != null) {
String verifyCode = verifyCodeNode.asText(); String verifyCode = verifyCodeNode.asText();
String smsCodeKey = RedisKeyConstants.SMS_CODE + phone; String smsCodeKey = RedisKeyConstants.SMS_CODE + phone;
redisUtil.setWithExpire(smsCodeKey, verifyCode, CODE_EXPIRE_SECONDS).subscribe(); cacheOperations.setWithExpire(smsCodeKey, verifyCode, CODE_EXPIRE_SECONDS).subscribe();
log.info("验证码已存入Redis, phone: {}, key: {}, expire: {}秒", phone, smsCodeKey, CODE_EXPIRE_SECONDS); log.info("验证码已存入Redis, phone: {}, key: {}, expire: {}秒", phone, smsCodeKey, CODE_EXPIRE_SECONDS);
} else { } else {
log.warn("响应中未找到Model.VerifyCode字段, 原始响应: {}", responseData); log.warn("响应中未找到Model.VerifyCode字段, 原始响应: {}", responseData);
@@ -106,7 +106,7 @@ public class SmsServiceImpl implements SmsService {
log.warn("响应中未找到Model字段, 原始响应: {}", responseData); log.warn("响应中未找到Model字段, 原始响应: {}", responseData);
} }
redisUtil.setWithExpire(rateLimitKey, currentTime, SEND_INTERVAL_SECONDS).subscribe(); cacheOperations.setWithExpire(rateLimitKey, currentTime, SEND_INTERVAL_SECONDS).subscribe();
return true; return true;
} }
@@ -180,7 +180,7 @@ public class SmsServiceImpl implements SmsService {
public Mono<String> getVerificationCode(String phone) { public Mono<String> getVerificationCode(String phone) {
try { try {
String cacheKey = RedisKeyConstants.SMS_CODE + phone; String cacheKey = RedisKeyConstants.SMS_CODE + phone;
return redisUtil.get(cacheKey, String.class); return cacheOperations.get(cacheKey, String.class);
} catch (Exception e) { } catch (Exception e) {
log.error("获取验证码异常, phone: {}", phone, e); log.error("获取验证码异常, phone: {}", phone, e);
return Mono.empty(); return Mono.empty();
@@ -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.vo.SignInStatsVO;
import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler; import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler;
import cn.novalon.gym.manage.common.constant.RedisKeyConstants; 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.domain.GroupCourseBooking;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService; import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService;
@@ -44,7 +44,7 @@ import java.util.Map;
public class CheckServiceImpl implements ICheckInService { public class CheckServiceImpl implements ICheckInService {
private final QRCodeConfig qrCodeConfig; private final QRCodeConfig qrCodeConfig;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private final MemberCardRecordRepository memberCardRecordRepository; private final MemberCardRecordRepository memberCardRecordRepository;
private final MemberCardRepository memberCardRepository; private final MemberCardRepository memberCardRepository;
private final SignInRecordRepository signInRecordRepository; private final SignInRecordRepository signInRecordRepository;
@@ -63,7 +63,7 @@ public class CheckServiceImpl implements ICheckInService {
redisMap.put("isUsed", false); redisMap.put("isUsed", false);
redisMap.put("memberId", memberId); redisMap.put("memberId", memberId);
return redisUtil.setWithExpire( return cacheOperations.setWithExpire(
RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(), RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(),
redisMap, redisMap,
getSecondsUntilEndOfDay() getSecondsUntilEndOfDay()
@@ -87,7 +87,7 @@ public class CheckServiceImpl implements ICheckInService {
MyWebSocketHandler.sendFailure(qrContent, "您已经在" + checkInTime + "完成签到,请勿重复签到"); MyWebSocketHandler.sendFailure(qrContent, "您已经在" + checkInTime + "完成签到,请勿重复签到");
return Mono.error(new RuntimeException("您已经在" + checkInTime + "完成签到,请勿重复签到")); return Mono.error(new RuntimeException("您已经在" + checkInTime + "完成签到,请勿重复签到"));
}) })
.then(Mono.defer(() -> redisUtil.get(key))) .then(Mono.defer(() -> cacheOperations.get(key)))
.flatMap(cachedObj -> { .flatMap(cachedObj -> {
if (cachedObj != null) { if (cachedObj != null) {
Map<String, Object> map; Map<String, Object> map;
@@ -157,11 +157,11 @@ public class CheckServiceImpl implements ICheckInService {
redisMap.put("checkInTime", now.format(DATE_FORMATTER)); redisMap.put("checkInTime", now.format(DATE_FORMATTER));
return saveSignInRecord(memberId, null, null) 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(() -> .then(Mono.defer(() ->
redisUtil.deleteByPattern("datacount:statistics:*") cacheOperations.deleteByPattern("datacount:statistics:*")
.then(Mono.defer(() -> redisUtil.deleteByPattern("group_course:*"))) .then(Mono.defer(() -> cacheOperations.deleteByPattern("group_course:*")))
.doOnSuccess(v -> log.info("已清除统计缓存和课程缓存, memberId: {}", memberId)) .doOnSuccess(v -> log.info("已清除统计缓存和课程缓存, memberId: {}", memberId))
.then() .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.SignInRecordVO;
import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO; import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO;
import cn.novalon.gym.manage.common.constant.RedisKeyConstants; 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.MemberCard;
import cn.novalon.gym.manage.member.entity.MemberCardRecord; import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository; import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
@@ -44,7 +44,7 @@ class CheckInModuleTest {
private QRCodeConfig qrCodeConfig; private QRCodeConfig qrCodeConfig;
@Mock @Mock
private RedisUtil redisUtil; private CacheOperations cacheOperations;
@Mock @Mock
private MemberCardRecordRepository memberCardRecordRepository; private MemberCardRecordRepository memberCardRecordRepository;
@@ -75,7 +75,7 @@ class CheckInModuleTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
MockitoAnnotations.openMocks(this); MockitoAnnotations.openMocks(this);
checkService = new CheckServiceImpl(qrCodeConfig, redisUtil, memberCardRecordRepository, checkService = new CheckServiceImpl(qrCodeConfig, cacheOperations, memberCardRecordRepository,
memberCardRepository, signInRecordRepository, groupCourseBookingService, memberCardRepository, signInRecordRepository, groupCourseBookingService,
groupCourseBookingRepository); groupCourseBookingRepository);
@@ -103,7 +103,7 @@ class CheckInModuleTest {
void testGetQRCode() { void testGetQRCode() {
when(memberCardRecordRepository.findActiveCardsByMemberId(1L)) when(memberCardRecordRepository.findActiveCardsByMemberId(1L))
.thenReturn(Flux.just(mockMemberCardRecord)); .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)); .thenReturn(Mono.just(true));
Mono<QRCodeVo> result = checkService.getQRCode(1L); Mono<QRCodeVo> result = checkService.getQRCode(1L);
@@ -130,12 +130,12 @@ class CheckInModuleTest {
String key = RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(); String key = RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now();
when(redisUtil.get(eq(key))).thenReturn(Mono.just(qrData)); when(cacheOperations.get(eq(key))).thenReturn(Mono.just(qrData));
when(redisUtil.deleteByPattern(any(String.class))).thenReturn(Mono.empty()); when(cacheOperations.deleteByPattern(any(String.class))).thenReturn(Mono.empty());
when(memberCardRecordRepository.findById(1L)).thenReturn(Mono.just(mockMemberCardRecord)); when(memberCardRecordRepository.findById(1L)).thenReturn(Mono.just(mockMemberCardRecord));
when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(mockMemberCard)); when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(mockMemberCard));
when(signInRecordRepository.save(any(SignInRecord.class))).thenReturn(Mono.just(mockSignInRecord)); 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(groupCourseBookingService.getBookingsByMemberId(memberId)).thenReturn(Flux.empty());
when(signInRecordRepository.findByMemberIdAndDate(eq(memberId), any(LocalDateTime.class), any(LocalDateTime.class))) when(signInRecordRepository.findByMemberIdAndDate(eq(memberId), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty()); .thenReturn(Mono.empty());
@@ -259,7 +259,7 @@ class CheckInModuleTest {
void testCheckIn_QRCodeInvalid() { void testCheckIn_QRCodeInvalid() {
Long memberId = 1L; Long memberId = 1L;
String key = RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(); 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))) when(signInRecordRepository.findByMemberIdAndDate(eq(memberId), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty()); .thenReturn(Mono.empty());
@@ -275,7 +275,7 @@ class CheckInModuleTest {
void testCheckIn_QRCodeNotFound() { void testCheckIn_QRCodeNotFound() {
Long memberId = 1L; Long memberId = 1L;
String key = RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(); 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))) when(signInRecordRepository.findByMemberIdAndDate(eq(memberId), any(LocalDateTime.class), any(LocalDateTime.class)))
.thenReturn(Mono.empty()); .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.SignInRecordVO;
import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO; import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO;
import cn.novalon.gym.manage.common.constant.RedisKeyConstants; 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.domain.GroupCourseBooking;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository; import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService; import cn.novalon.gym.manage.groupcourse.service.IGroupCourseBookingService;
@@ -40,7 +40,7 @@ class CheckServiceImplTest {
private QRCodeConfig qrCodeConfig; private QRCodeConfig qrCodeConfig;
@Mock @Mock
private RedisUtil redisUtil; private CacheOperations cacheOperations;
@Mock @Mock
private MemberCardRecordRepository memberCardRecordRepository; private MemberCardRecordRepository memberCardRecordRepository;
@@ -70,7 +70,7 @@ class CheckServiceImplTest {
void getQRCode_shouldReturnQRCodeVo() { void getQRCode_shouldReturnQRCodeVo() {
when(qrCodeConfig.getWidth()).thenReturn(300); when(qrCodeConfig.getWidth()).thenReturn(300);
when(qrCodeConfig.getHeight()).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)); .thenReturn(Mono.just(true));
Mono<QRCodeVo> result = checkService.getQRCode(MEMBER_ID); Mono<QRCodeVo> result = checkService.getQRCode(MEMBER_ID);
@@ -100,7 +100,7 @@ class CheckServiceImplTest {
// Redis 中有有效二维码数据 // Redis 中有有效二维码数据
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now(); String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
Map<String, Object> qrData = buildQrData(false); 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()); when(groupCourseBookingService.getBookingsByMemberId(MEMBER_ID)).thenReturn(Flux.empty());
@@ -110,10 +110,10 @@ class CheckServiceImplTest {
.thenReturn(Mono.just(createMockSignInRecord())); .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); Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
@@ -122,8 +122,8 @@ class CheckServiceImplTest {
.verifyComplete(); .verifyComplete();
verify(signInRecordRepository).save(any(SignInRecord.class)); verify(signInRecordRepository).save(any(SignInRecord.class));
verify(redisUtil).set(anyString(), any(Map.class)); verify(cacheOperations).set(anyString(), any(Map.class));
verify(redisUtil, times(2)).deleteByPattern(anyString()); verify(cacheOperations, times(2)).deleteByPattern(anyString());
} }
@Test @Test
@@ -136,7 +136,7 @@ class CheckServiceImplTest {
// Redis 中有有效二维码数据 // Redis 中有有效二维码数据
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now(); String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
Map<String, Object> qrData = buildQrData(false); 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(); GroupCourseBooking booking = createValidBooking();
@@ -148,10 +148,10 @@ class CheckServiceImplTest {
.thenReturn(Mono.just(createMockSignInRecord())); .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); Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
@@ -172,7 +172,7 @@ class CheckServiceImplTest {
// Redis 中缓存数据为 JSON 字符串格式 // Redis 中缓存数据为 JSON 字符串格式
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now(); String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
String jsonData = "{\"qrContent\":\"" + QR_CONTENT + "\",\"isUsed\":false,\"memberId\":" + MEMBER_ID + "}"; 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()); when(groupCourseBookingService.getBookingsByMemberId(MEMBER_ID)).thenReturn(Flux.empty());
@@ -182,10 +182,10 @@ class CheckServiceImplTest {
.thenReturn(Mono.just(createMockSignInRecord())); .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); Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
@@ -217,7 +217,7 @@ class CheckServiceImplTest {
&& e.getMessage().contains("请勿重复签到")) && e.getMessage().contains("请勿重复签到"))
.verify(); .verify();
verify(redisUtil, never()).get(anyString()); verify(cacheOperations, never()).get(anyString());
} }
@Test @Test
@@ -231,7 +231,7 @@ class CheckServiceImplTest {
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now(); String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
Map<String, Object> qrData = buildQrData(true); Map<String, Object> qrData = buildQrData(true);
qrData.put("checkInTime", "2026-07-31 10:00:00"); 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); 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(); String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now();
Map<String, Object> qrData = buildQrData(false); Map<String, Object> qrData = buildQrData(false);
qrData.put("qrContent", "different-qr-content"); 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); Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
@@ -271,7 +271,7 @@ class CheckServiceImplTest {
// Redis 中无数据 // Redis 中无数据
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now(); 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"); Mono<String> result = checkService.checkIn(MEMBER_ID, "not-exist");
@@ -289,7 +289,7 @@ class CheckServiceImplTest {
// Redis 返回非 Map 非 String 的数据 // Redis 返回非 Map 非 String 的数据
String key = RedisKeyConstants.QRCODE_USER_DAILY + MEMBER_ID + LocalDate.now(); 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); Mono<String> result = checkService.checkIn(MEMBER_ID, QR_CONTENT);
@@ -3,7 +3,7 @@ package cn.novalon.gym.manage.coachconfig.service;
import cn.novalon.gym.manage.coachconfig.domain.CoachTimeRule; import cn.novalon.gym.manage.coachconfig.domain.CoachTimeRule;
import cn.novalon.gym.manage.coachconfig.repository.ICoachTimeRuleRepository; import cn.novalon.gym.manage.coachconfig.repository.ICoachTimeRuleRepository;
import cn.novalon.gym.manage.common.constant.RedisKeyConstants; 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 org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -43,11 +43,11 @@ public class CoachTimeRuleService {
private static final long CACHE_TTL_SECONDS = 300; private static final long CACHE_TTL_SECONDS = 300;
private final ICoachTimeRuleRepository repository; private final ICoachTimeRuleRepository repository;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
public CoachTimeRuleService(ICoachTimeRuleRepository repository, RedisUtil redisUtil) { public CoachTimeRuleService(ICoachTimeRuleRepository repository, CacheOperations cacheOperations) {
this.repository = repository; this.repository = repository;
this.redisUtil = redisUtil; this.cacheOperations = cacheOperations;
} }
// ==================== 规则匹配 ==================== // ==================== 规则匹配 ====================
@@ -118,7 +118,7 @@ public class CoachTimeRuleService {
* 获取所有启用规则(带 Redis 缓存) * 获取所有启用规则(带 Redis 缓存)
*/ */
private Flux<CoachTimeRule> getActiveRules() { private Flux<CoachTimeRule> getActiveRules() {
return redisUtil.get(RedisKeyConstants.COACH_TIME_RULES) return cacheOperations.get(RedisKeyConstants.COACH_TIME_RULES)
.flatMapMany(cached -> { .flatMapMany(cached -> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<CoachTimeRule> list = (List<CoachTimeRule>) cached; List<CoachTimeRule> list = (List<CoachTimeRule>) cached;
@@ -130,7 +130,7 @@ public class CoachTimeRuleService {
return repository.findByStatusAndDeletedAtIsNull("1") return repository.findByStatusAndDeletedAtIsNull("1")
.collectList() .collectList()
.flatMapMany(list -> { .flatMapMany(list -> {
redisUtil.setWithExpire(RedisKeyConstants.COACH_TIME_RULES, list, CACHE_TTL_SECONDS) cacheOperations.setWithExpire(RedisKeyConstants.COACH_TIME_RULES, list, CACHE_TTL_SECONDS)
.subscribe( .subscribe(
ok -> logger.debug("教练时间规则已写入 Redis 缓存,共 {} 条", list.size()), ok -> logger.debug("教练时间规则已写入 Redis 缓存,共 {} 条", list.size()),
err -> logger.warn("教练时间规则写入 Redis 缓存失败: {}", err.getMessage()) err -> logger.warn("教练时间规则写入 Redis 缓存失败: {}", err.getMessage())
@@ -144,7 +144,7 @@ public class CoachTimeRuleService {
* 写操作后清除缓存(热更新入口) * 写操作后清除缓存(热更新入口)
*/ */
private void invalidateCache() { private void invalidateCache() {
redisUtil.delete(RedisKeyConstants.COACH_TIME_RULES) cacheOperations.delete(RedisKeyConstants.COACH_TIME_RULES)
.subscribe( .subscribe(
count -> logger.info("教练时间规则缓存已清除"), count -> logger.info("教练时间规则缓存已清除"),
err -> logger.warn("教练时间规则缓存清除失败: {}", err.getMessage()) err -> logger.warn("教练时间规则缓存清除失败: {}", err.getMessage())
@@ -2,7 +2,7 @@ package cn.novalon.gym.manage.coach.scheduler;
import cn.novalon.gym.manage.coach.enums.ViolationReason; import cn.novalon.gym.manage.coach.enums.ViolationReason;
import cn.novalon.gym.manage.coachconfig.service.CoachTimeRuleService; 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.GroupCourseBookingDao;
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao; import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity; import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity;
@@ -35,18 +35,18 @@ public class CoachCourseScheduler {
private final GroupCourseDao groupCourseDao; private final GroupCourseDao groupCourseDao;
private final GroupCourseBookingDao groupCourseBookingDao; private final GroupCourseBookingDao groupCourseBookingDao;
private final DatabaseClient databaseClient; private final DatabaseClient databaseClient;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private final CoachTimeRuleService timeRuleService; private final CoachTimeRuleService timeRuleService;
public CoachCourseScheduler(GroupCourseDao groupCourseDao, public CoachCourseScheduler(GroupCourseDao groupCourseDao,
GroupCourseBookingDao groupCourseBookingDao, GroupCourseBookingDao groupCourseBookingDao,
DatabaseClient databaseClient, DatabaseClient databaseClient,
RedisUtil redisUtil, CacheOperations cacheOperations,
CoachTimeRuleService timeRuleService) { CoachTimeRuleService timeRuleService) {
this.groupCourseDao = groupCourseDao; this.groupCourseDao = groupCourseDao;
this.groupCourseBookingDao = groupCourseBookingDao; this.groupCourseBookingDao = groupCourseBookingDao;
this.databaseClient = databaseClient; this.databaseClient = databaseClient;
this.redisUtil = redisUtil; this.cacheOperations = cacheOperations;
this.timeRuleService = timeRuleService; 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() { private void invalidateCache() {
Mono<Long> statsMono = redisUtil.deleteByPattern("datacount:statistics:*"); Mono<Long> statsMono = cacheOperations.deleteByPattern("datacount:statistics:*");
if (statsMono != null) { if (statsMono != null) {
statsMono.subscribe( statsMono.subscribe(
deleted -> logger.debug("调度器清除统计缓存,已删除 {} 条", deleted), deleted -> logger.debug("调度器清除统计缓存,已删除 {} 条", deleted),
error -> logger.warn("调度器清除统计缓存失败: {}", error.getMessage()) error -> logger.warn("调度器清除统计缓存失败: {}", error.getMessage())
); );
} }
Mono<Long> courseMono = redisUtil.deleteByPattern("group_course:*"); Mono<Long> courseMono = cacheOperations.deleteByPattern("group_course:*");
if (courseMono != null) { if (courseMono != null) {
courseMono.subscribe( courseMono.subscribe(
deleted -> logger.debug("调度器清除团课缓存,已删除 {} 条", deleted), 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.coach.enums.ViolationReason;
import cn.novalon.gym.manage.coachconfig.domain.CoachTimeRule; import cn.novalon.gym.manage.coachconfig.domain.CoachTimeRule;
import cn.novalon.gym.manage.coachconfig.service.CoachTimeRuleService; 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.common.util.StatusConstants;
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao; import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao;
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao; import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
@@ -56,7 +56,7 @@ public class CoachCourseService {
private final CoachViolationDao violationDao; private final CoachViolationDao violationDao;
private final DatabaseClient databaseClient; private final DatabaseClient databaseClient;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private final CoachTimeRuleService timeRuleService; private final CoachTimeRuleService timeRuleService;
public CoachCourseService(ISysUserRepository userRepository, public CoachCourseService(ISysUserRepository userRepository,
@@ -69,7 +69,7 @@ public class CoachCourseService {
CoachViolationDao violationDao, CoachViolationDao violationDao,
DatabaseClient databaseClient, DatabaseClient databaseClient,
PasswordEncoder passwordEncoder, PasswordEncoder passwordEncoder,
RedisUtil redisUtil, CacheOperations cacheOperations,
CoachTimeRuleService timeRuleService) { CoachTimeRuleService timeRuleService) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.roleRepository = roleRepository; this.roleRepository = roleRepository;
@@ -81,7 +81,7 @@ public class CoachCourseService {
this.violationDao = violationDao; this.violationDao = violationDao;
this.databaseClient = databaseClient; this.databaseClient = databaseClient;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
this.redisUtil = redisUtil; this.cacheOperations = cacheOperations;
this.timeRuleService = timeRuleService; this.timeRuleService = timeRuleService;
} }
@@ -334,8 +334,8 @@ public class CoachCourseService {
* 清除统计缓存和团课缓存 —— 课程状态变更后调用,返回 Mono 确保链式执行 * 清除统计缓存和团课缓存 —— 课程状态变更后调用,返回 Mono 确保链式执行
*/ */
private Mono<Void> invalidateStatisticsCache() { private Mono<Void> invalidateStatisticsCache() {
return redisUtil.deleteByPattern("datacount:statistics:*") return cacheOperations.deleteByPattern("datacount:statistics:*")
.then(redisUtil.deleteByPattern("group_course:*")) .then(cacheOperations.deleteByPattern("group_course:*"))
.doOnNext(count -> {}) .doOnNext(count -> {})
.then(); .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.coach.enums.ViolationReason;
import cn.novalon.gym.manage.coachconfig.domain.CoachTimeRule; import cn.novalon.gym.manage.coachconfig.domain.CoachTimeRule;
import cn.novalon.gym.manage.coachconfig.service.CoachTimeRuleService; 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.GroupCourseBookingDao;
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao; import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity; import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity;
@@ -38,7 +38,7 @@ class CoachCourseSchedulerTest {
private DatabaseClient databaseClient; private DatabaseClient databaseClient;
@Mock @Mock
private RedisUtil redisUtil; private CacheOperations cacheOperations;
@Mock @Mock
private CoachTimeRuleService timeRuleService; private CoachTimeRuleService timeRuleService;
@@ -54,7 +54,7 @@ class CoachCourseSchedulerTest {
void setUp() { void setUp() {
scheduler = new CoachCourseScheduler( scheduler = new CoachCourseScheduler(
groupCourseDao, groupCourseBookingDao, 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)); verify(groupCourseDao).findByStatusInAndEndTimeBefore(eq(databaseClient), any(String[].class), any(LocalDateTime.class));
// 验证未触发缓存清除(count=0,不进入 if 分支) // 验证未触发缓存清除(count=0,不进入 if 分支)
verifyNoInteractions(redisUtil); verifyNoInteractions(cacheOperations);
} }
@Test @Test
@@ -403,20 +403,20 @@ class CoachCourseSchedulerTest {
.thenReturn(Flux.empty()); .thenReturn(Flux.empty());
// mock RedisUtil 返回非 null 的 Mono // mock RedisUtil 返回非 null 的 Mono
when(redisUtil.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L)); when(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L)); when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
// 执行 // 执行
scheduler.checkAndProcessCourses(); scheduler.checkAndProcessCourses();
// 验证缓存清除被调用 // 验证缓存清除被调用
verify(redisUtil).deleteByPattern("datacount:statistics:*"); verify(cacheOperations).deleteByPattern("datacount:statistics:*");
verify(redisUtil).deleteByPattern("group_course:*"); verify(cacheOperations).deleteByPattern("group_course:*");
} }
@Test @Test
void checkAndProcessCourses_shouldHandleRedisUtilReturningNull() { void checkAndProcessCourses_shouldHandleCacheOperationsReturningNull() {
// 准备:测试 RedisUtil 返回 null 的边界情况 // 准备:测试 CacheOperations 返回 null 的边界情况
LocalDateTime startTime = LocalDateTime.of(2026, 7, 31, 10, 0); LocalDateTime startTime = LocalDateTime.of(2026, 7, 31, 10, 0);
LocalDateTime endTime = LocalDateTime.of(2026, 7, 31, 11, 0); LocalDateTime endTime = LocalDateTime.of(2026, 7, 31, 11, 0);
@@ -440,15 +440,15 @@ class CoachCourseSchedulerTest {
.thenReturn(Flux.empty()); .thenReturn(Flux.empty());
// RedisUtil.deleteByPattern 返回 null(模拟 null 安全检查) // RedisUtil.deleteByPattern 返回 null(模拟 null 安全检查)
when(redisUtil.deleteByPattern("datacount:statistics:*")).thenReturn(null); when(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(null);
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(null); when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(null);
// 执行:不应抛出 NPE // 执行:不应抛出 NPE
scheduler.checkAndProcessCourses(); scheduler.checkAndProcessCourses();
// 验证:RedisUtil 被调用(即使返回 null // 验证:RedisUtil 被调用(即使返回 null
verify(redisUtil).deleteByPattern("datacount:statistics:*"); verify(cacheOperations).deleteByPattern("datacount:statistics:*");
verify(redisUtil).deleteByPattern("group_course:*"); verify(cacheOperations).deleteByPattern("group_course:*");
} }
@Test @Test
@@ -465,6 +465,6 @@ class CoachCourseSchedulerTest {
// 验证 // 验证
verify(groupCourseDao).findByStatusAndStartTimeBefore(eq(databaseClient), eq("0"), any(LocalDateTime.class)); verify(groupCourseDao).findByStatusAndStartTimeBefore(eq(databaseClient), eq("0"), any(LocalDateTime.class));
verify(groupCourseDao).findByStatusInAndEndTimeBefore(eq(databaseClient), any(String[].class), 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.coach.enums.ViolationReason;
import cn.novalon.gym.manage.coachconfig.domain.CoachTimeRule; import cn.novalon.gym.manage.coachconfig.domain.CoachTimeRule;
import cn.novalon.gym.manage.coachconfig.service.CoachTimeRuleService; 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.common.util.StatusConstants;
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao; import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao;
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao; import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
@@ -76,7 +76,7 @@ class CoachCourseServiceTest {
private PasswordEncoder passwordEncoder; private PasswordEncoder passwordEncoder;
@Mock @Mock
private RedisUtil redisUtil; private CacheOperations cacheOperations;
@Mock @Mock
private CoachTimeRuleService timeRuleService; private CoachTimeRuleService timeRuleService;
@@ -95,7 +95,7 @@ class CoachCourseServiceTest {
groupCourseRepository, bookingRepository, groupCourseRepository, bookingRepository,
groupCourseDao, groupCourseBookingDao, groupCourseDao, groupCourseBookingDao,
violationDao, databaseClient, violationDao, databaseClient,
passwordEncoder, redisUtil, timeRuleService passwordEncoder, cacheOperations, timeRuleService
); );
} }
@@ -328,8 +328,8 @@ class CoachCourseServiceTest {
updatedUser.setStatus(StatusConstants.DISABLED); updatedUser.setStatus(StatusConstants.DISABLED);
when(userRepository.update(any(SysUser.class))).thenReturn(Mono.just(updatedUser)); when(userRepository.update(any(SysUser.class))).thenReturn(Mono.just(updatedUser));
when(redisUtil.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L)); when(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L)); when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
StepVerifier.create(coachCourseService.disableCoach(COACH_USER_ID)) StepVerifier.create(coachCourseService.disableCoach(COACH_USER_ID))
.verifyComplete(); .verifyComplete();
@@ -338,15 +338,15 @@ class CoachCourseServiceTest {
verify(groupCourseRepository).countByCoachIdAndStatus(COACH_USER_ID, 3L); verify(groupCourseRepository).countByCoachIdAndStatus(COACH_USER_ID, 3L);
verify(groupCourseRepository).cancelCoursesByCoachIdExceptStatus(COACH_USER_ID, 3L); verify(groupCourseRepository).cancelCoursesByCoachIdExceptStatus(COACH_USER_ID, 3L);
verify(userRepository).update(any(SysUser.class)); verify(userRepository).update(any(SysUser.class));
verify(redisUtil).deleteByPattern("datacount:statistics:*"); verify(cacheOperations).deleteByPattern("datacount:statistics:*");
verify(redisUtil).deleteByPattern("group_course:*"); verify(cacheOperations).deleteByPattern("group_course:*");
} }
@Test @Test
void disableCoach_shouldThrowWhenCoachNotFound() { void disableCoach_shouldThrowWhenCoachNotFound() {
when(userRepository.findById(COACH_USER_ID)).thenReturn(Mono.empty()); when(userRepository.findById(COACH_USER_ID)).thenReturn(Mono.empty());
// invalidateStatisticsCache() 会在 .then() 参数求值时被调用,需要 stub // 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)) StepVerifier.create(coachCourseService.disableCoach(COACH_USER_ID))
.expectErrorMatches(e -> e instanceof RuntimeException .expectErrorMatches(e -> e instanceof RuntimeException
@@ -363,7 +363,7 @@ class CoachCourseServiceTest {
when(groupCourseRepository.countByCoachIdAndStatus(COACH_USER_ID, 3L)).thenReturn(Mono.just(2L)); when(groupCourseRepository.countByCoachIdAndStatus(COACH_USER_ID, 3L)).thenReturn(Mono.just(2L));
// invalidateStatisticsCache() 会在 .then() 参数求值时被调用,需要 stub // 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)) StepVerifier.create(coachCourseService.disableCoach(COACH_USER_ID))
.expectErrorMatches(e -> e instanceof RuntimeException .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(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(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L)); when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
StepVerifier.create(coachCourseService.startCourse(COURSE_ID, COACH_USER_ID)) StepVerifier.create(coachCourseService.startCourse(COURSE_ID, COACH_USER_ID))
.assertNext(entity -> { .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(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(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L)); when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
StepVerifier.create(coachCourseService.startCourse(COURSE_ID, COACH_USER_ID)) StepVerifier.create(coachCourseService.startCourse(COURSE_ID, COACH_USER_ID))
.assertNext(entity -> { .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(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(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L)); when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
StepVerifier.create(coachCourseService.endCourse(COURSE_ID, COACH_USER_ID)) StepVerifier.create(coachCourseService.endCourse(COURSE_ID, COACH_USER_ID))
.assertNext(entity -> { .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(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(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(1L));
when(redisUtil.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L)); when(cacheOperations.deleteByPattern("group_course:*")).thenReturn(Mono.just(1L));
StepVerifier.create(coachCourseService.endCourse(COURSE_ID, COACH_USER_ID)) StepVerifier.create(coachCourseService.endCourse(COURSE_ID, COACH_USER_ID))
.assertNext(entity -> { .assertNext(entity -> {
@@ -1,5 +1,6 @@
package cn.novalon.gym.manage.datacount.scheduler; package cn.novalon.gym.manage.datacount.scheduler;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.datacount.domain.DataStatistics; import cn.novalon.gym.manage.datacount.domain.DataStatistics;
import cn.novalon.gym.manage.datacount.service.IDataStatisticsService; import cn.novalon.gym.manage.datacount.service.IDataStatisticsService;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -98,7 +99,7 @@ public class DataStatisticsScheduler {
// 清理Redis中的旧统计数据 // 清理Redis中的旧统计数据
String pattern = "datacount:statistics:*:" + cutoffDate.toString(); String pattern = "datacount:statistics:*:" + cutoffDate.toString();
cn.novalon.gym.manage.common.util.RedisUtil redisUtil = null; CacheOperations cacheOperations = null;
try { try {
// 这里可以通过注入的service来清理,但当前实现使用Redis缓存30天自动过期 // 这里可以通过注入的service来清理,但当前实现使用Redis缓存30天自动过期
log.info("Old statistics cleanup completed, cutoff date: {}", cutoffDate); log.info("Old statistics cleanup completed, cutoff date: {}", cutoffDate);
@@ -1,7 +1,7 @@
package cn.novalon.gym.manage.datacount.service.impl; package cn.novalon.gym.manage.datacount.service.impl;
import cn.novalon.gym.manage.checkIn.entity.SignInRecord; import cn.novalon.gym.manage.checkIn.entity.SignInRecord;
import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.datacount.dao.DataStatisticsDao; import cn.novalon.gym.manage.datacount.dao.DataStatisticsDao;
import cn.novalon.gym.manage.datacount.domain.*; import cn.novalon.gym.manage.datacount.domain.*;
import cn.novalon.gym.manage.datacount.service.IDataStatisticsService; import cn.novalon.gym.manage.datacount.service.IDataStatisticsService;
@@ -46,7 +46,7 @@ public class DataStatisticsServiceImpl implements IDataStatisticsService {
private DataStatisticsDao dataStatisticsDao; private DataStatisticsDao dataStatisticsDao;
@Autowired @Autowired
private RedisUtil redisUtil; private CacheOperations cacheOperations;
@Autowired @Autowired
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
@@ -223,7 +223,7 @@ public class DataStatisticsServiceImpl implements IDataStatisticsService {
public reactor.core.publisher.Flux<DataStatistics> queryHistoricalStatistics(StatisticsQuery query) { public reactor.core.publisher.Flux<DataStatistics> queryHistoricalStatistics(StatisticsQuery query) {
// 历史统计数据查询(从Redis缓存中获取) // 历史统计数据查询(从Redis缓存中获取)
String cacheKey = buildCacheKey(query); String cacheKey = buildCacheKey(query);
return redisUtil.get(cacheKey, String.class) return cacheOperations.get(cacheKey, String.class)
.flatMapMany(json -> { .flatMapMany(json -> {
try { try {
java.util.List<DataStatistics> stats = objectMapper.readValue(json, java.util.List<DataStatistics> stats = objectMapper.readValue(json,
@@ -270,9 +270,9 @@ public class DataStatisticsServiceImpl implements IDataStatisticsService {
String signInJson = objectMapper.writeValueAsString(signInStats); String signInJson = objectMapper.writeValueAsString(signInStats);
return reactor.core.publisher.Flux.merge( return reactor.core.publisher.Flux.merge(
redisUtil.setWithExpire(memberKey, memberJson, Duration.ofDays(30).getSeconds()), cacheOperations.setWithExpire(memberKey, memberJson, Duration.ofDays(30).getSeconds()),
redisUtil.setWithExpire(bookingKey, bookingJson, Duration.ofDays(30).getSeconds()), cacheOperations.setWithExpire(bookingKey, bookingJson, Duration.ofDays(30).getSeconds()),
redisUtil.setWithExpire(signInKey, signInJson, Duration.ofDays(30).getSeconds()) cacheOperations.setWithExpire(signInKey, signInJson, Duration.ofDays(30).getSeconds())
).then(); ).then();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to serialize statistics data", e); log.error("Failed to serialize statistics data", e);
@@ -445,13 +445,13 @@ public class DataStatisticsServiceImpl implements IDataStatisticsService {
public Mono<StatisticsSummary> getStatisticsSummaryWithCache(StatisticsQuery query) { public Mono<StatisticsSummary> getStatisticsSummaryWithCache(StatisticsQuery query) {
String cacheKey = buildCacheKey(query); String cacheKey = buildCacheKey(query);
return redisUtil.get(cacheKey, StatisticsSummary.class) return cacheOperations.get(cacheKey, StatisticsSummary.class)
.switchIfEmpty( .switchIfEmpty(
getStatisticsSummary(query) getStatisticsSummary(query)
.flatMap(summary -> { .flatMap(summary -> {
try { try {
String json = objectMapper.writeValueAsString(summary); String json = objectMapper.writeValueAsString(summary);
return redisUtil.setWithExpire(cacheKey, json, cacheExpireSeconds) return cacheOperations.setWithExpire(cacheKey, json, cacheExpireSeconds)
.thenReturn(summary); .thenReturn(summary);
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to serialize statistics summary", e); log.error("Failed to serialize statistics summary", e);
@@ -2,7 +2,7 @@
package cn.novalon.gym.manage.groupcourse.handler; package cn.novalon.gym.manage.groupcourse.handler;
import cn.novalon.gym.manage.common.dto.PageRequest; 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.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail; import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
import cn.novalon.gym.manage.groupcourse.dto.GroupCourseQueryDto; 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 static final Logger logger = LoggerFactory.getLogger(GroupCourseHandler.class);
private final IGroupCourseService groupCourseService; private final IGroupCourseService groupCourseService;
private final Validator validator; private final Validator validator;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
public GroupCourseHandler(IGroupCourseService groupCourseService, public GroupCourseHandler(IGroupCourseService groupCourseService,
Validator validator, Validator validator,
RedisUtil redisUtil, CacheOperations cacheOperations,
ObjectMapper objectMapper){ ObjectMapper objectMapper){
this.groupCourseService = groupCourseService; this.groupCourseService = groupCourseService;
this.validator = validator; this.validator = validator;
this.redisUtil = redisUtil; this.cacheOperations = cacheOperations;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
} }
@@ -258,7 +258,7 @@ public class GroupCourseHandler {
return ServerResponse.badRequest().bodyValue(error); return ServerResponse.badRequest().bodyValue(error);
} }
return redisUtil.get(key) return cacheOperations.get(key)
.map(cachedValue -> { .map(cachedValue -> {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
if (cachedValue != null) { 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.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse; 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.CourseLabel;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository; import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository;
@@ -24,21 +24,21 @@ public class CourseLabelService implements ICourseLabelService {
private final ICourseLabelRepository courseLabelRepository; private final ICourseLabelRepository courseLabelRepository;
private final IGroupCourseRepository groupCourseRepository; private final IGroupCourseRepository groupCourseRepository;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
public CourseLabelService(ICourseLabelRepository courseLabelRepository, public CourseLabelService(ICourseLabelRepository courseLabelRepository,
IGroupCourseRepository groupCourseRepository, IGroupCourseRepository groupCourseRepository,
RedisUtil redisUtil) { CacheOperations cacheOperations) {
this.courseLabelRepository = courseLabelRepository; this.courseLabelRepository = courseLabelRepository;
this.groupCourseRepository = groupCourseRepository; this.groupCourseRepository = groupCourseRepository;
this.redisUtil = redisUtil; this.cacheOperations = cacheOperations;
} }
private Mono<Void> invalidateGroupCourseDetailCache(Long typeId) { private Mono<Void> invalidateGroupCourseDetailCache(Long typeId) {
return groupCourseRepository.findByCourseType(typeId) return groupCourseRepository.findByCourseType(typeId)
.flatMap(course -> { .flatMap(course -> {
String cacheKey = CACHE_KEY_DETAIL_PREFIX + course.getId(); String cacheKey = CACHE_KEY_DETAIL_PREFIX + course.getId();
return redisUtil.delete(cacheKey) return cacheOperations.delete(cacheKey)
.doOnSuccess(deleted -> logger.debug("清除团课详情缓存 - courseId={}", course.getId())); .doOnSuccess(deleted -> logger.debug("清除团课详情缓存 - courseId={}", course.getId()));
}) })
.then(); .then();
@@ -1,8 +1,7 @@
package cn.novalon.gym.manage.groupcourse.service.impl; 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 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate; import org.springframework.data.redis.core.ReactiveRedisTemplate;
@@ -13,8 +12,10 @@ import java.time.Duration;
/** /**
* 团课Redis缓存服务 * 团课Redis缓存服务
* * <p>
* 负责团课信息的缓存管理和分布式锁实现 * 缓存操作委托给 {@link CacheOperations} 接口,支持通过配置切换 Redis / Caffeine 实现
* 分布式锁操作仍直接使用 {@link ReactiveRedisTemplate}(锁为 Redis 独有特性)。
* </p>
* *
* @author 张翔 * @author 张翔
* @date 2026-06-01 * @date 2026-06-01
@@ -33,13 +34,13 @@ public class GroupCourseRedisService {
// 锁过期时间(30秒) // 锁过期时间(30秒)
private static final Duration LOCK_EXPIRE_TIME = Duration.ofSeconds(30); private static final Duration LOCK_EXPIRE_TIME = Duration.ofSeconds(30);
private final CacheOperations cacheOperations;
private final ReactiveRedisTemplate<String, Object> reactiveRedisTemplate; private final ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
private final ObjectMapper objectMapper;
public GroupCourseRedisService(ReactiveRedisTemplate<String, Object> reactiveRedisTemplate, public GroupCourseRedisService(CacheOperations cacheOperations,
ObjectMapper objectMapper) { ReactiveRedisTemplate<String, Object> reactiveRedisTemplate) {
this.cacheOperations = cacheOperations;
this.reactiveRedisTemplate = reactiveRedisTemplate; this.reactiveRedisTemplate = reactiveRedisTemplate;
this.objectMapper = objectMapper;
} }
/** /**
@@ -61,16 +62,9 @@ public class GroupCourseRedisService {
*/ */
public Mono<Void> cacheCourse(GroupCourse course) { public Mono<Void> cacheCourse(GroupCourse course) {
String key = getCourseCacheKey(course.getId()); String key = getCourseCacheKey(course.getId());
try { return cacheOperations.setWithExpire(key, course, CACHE_EXPIRE_TIME.toSeconds())
String value = objectMapper.writeValueAsString(course);
return reactiveRedisTemplate.opsForValue()
.set(key, value, CACHE_EXPIRE_TIME)
.doOnSuccess(result -> logger.debug("团课信息已缓存:courseId={}", course.getId())) .doOnSuccess(result -> logger.debug("团课信息已缓存:courseId={}", course.getId()))
.then(); .then();
} catch (JsonProcessingException e) {
logger.error("序列化团课信息失败:courseId={}", course.getId(), e);
return Mono.error(e);
}
} }
/** /**
@@ -78,19 +72,8 @@ public class GroupCourseRedisService {
*/ */
public Mono<GroupCourse> getCachedCourse(Long courseId) { public Mono<GroupCourse> getCachedCourse(Long courseId) {
String key = getCourseCacheKey(courseId); String key = getCourseCacheKey(courseId);
return reactiveRedisTemplate.opsForValue() return cacheOperations.get(key, GroupCourse.class)
.get(key) .doOnNext(course -> logger.debug("从缓存获取团课信息:courseId={}", courseId))
.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();
}
})
.switchIfEmpty(Mono.fromRunnable(() -> logger.debug("缓存中未找到团课信息:courseId={}", courseId))); .switchIfEmpty(Mono.fromRunnable(() -> logger.debug("缓存中未找到团课信息:courseId={}", courseId)));
} }
@@ -99,13 +82,14 @@ public class GroupCourseRedisService {
*/ */
public Mono<Void> invalidateCourseCache(Long courseId) { public Mono<Void> invalidateCourseCache(Long courseId) {
String key = getCourseCacheKey(courseId); String key = getCourseCacheKey(courseId);
return reactiveRedisTemplate.delete(key) return cacheOperations.delete(key)
.doOnSuccess(result -> logger.debug("团课缓存已删除:courseId={}", courseId)) .doOnSuccess(result -> logger.debug("团课缓存已删除:courseId={}", courseId))
.then(); .then();
} }
/** /**
* 获取分布式锁 * 获取分布式锁
* <p>注意:分布式锁为 Redis 独有特性,始终保持直接使用 RedisTemplate。</p>
* *
* @param courseId 课程ID * @param courseId 课程ID
* @param requestId 请求ID(用于锁的唯一性校验) * @param requestId 请求ID(用于锁的唯一性校验)
@@ -126,6 +110,7 @@ public class GroupCourseRedisService {
/** /**
* 释放分布式锁 * 释放分布式锁
* <p>注意:分布式锁为 Redis 独有特性,始终保持直接使用 RedisTemplate。</p>
* *
* @param courseId 课程ID * @param courseId 课程ID
* @param requestId 请求ID(用于锁的唯一性校验) * @param requestId 请求ID(用于锁的唯一性校验)
@@ -1,9 +1,9 @@
package cn.novalon.gym.manage.groupcourse.service.impl; 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.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse; 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.CourseLabel;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking; import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
@@ -53,7 +53,7 @@ public class GroupCourseService implements IGroupCourseService {
private final ICourseLabelRepository courseLabelRepository; private final ICourseLabelRepository courseLabelRepository;
private final IMemberCardRecordService memberCardRecordService; private final IMemberCardRecordService memberCardRecordService;
private final MemberCardRepository memberCardRepository; private final MemberCardRepository memberCardRepository;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final GroupCourseStateMachine stateMachine; private final GroupCourseStateMachine stateMachine;
private final DatabaseClient databaseClient; private final DatabaseClient databaseClient;
@@ -73,7 +73,7 @@ public class GroupCourseService implements IGroupCourseService {
ICourseLabelRepository courseLabelRepository, ICourseLabelRepository courseLabelRepository,
IMemberCardRecordService memberCardRecordService, IMemberCardRecordService memberCardRecordService,
MemberCardRepository memberCardRepository, MemberCardRepository memberCardRepository,
RedisUtil redisUtil, CacheOperations cacheOperations,
ObjectMapper objectMapper, ObjectMapper objectMapper,
GroupCourseStateMachine stateMachine, GroupCourseStateMachine stateMachine,
DatabaseClient databaseClient, DatabaseClient databaseClient,
@@ -85,7 +85,7 @@ public class GroupCourseService implements IGroupCourseService {
this.courseLabelRepository = courseLabelRepository; this.courseLabelRepository = courseLabelRepository;
this.memberCardRecordService = memberCardRecordService; this.memberCardRecordService = memberCardRecordService;
this.memberCardRepository = memberCardRepository; this.memberCardRepository = memberCardRepository;
this.redisUtil = redisUtil; this.cacheOperations = cacheOperations;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.stateMachine = stateMachine; this.stateMachine = stateMachine;
this.databaseClient = databaseClient; this.databaseClient = databaseClient;
@@ -97,7 +97,7 @@ public class GroupCourseService implements IGroupCourseService {
public Mono<GroupCourseDetail> findDetailById(Long id) { public Mono<GroupCourseDetail> findDetailById(Long id) {
String cacheKey = CACHE_KEY_DETAIL_PREFIX + 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 return cachedMono
.flatMap(cachedJson -> { .flatMap(cachedJson -> {
if (cachedJson != null && !cachedJson.isEmpty()) { if (cachedJson != null && !cachedJson.isEmpty()) {
@@ -107,7 +107,7 @@ public class GroupCourseService implements IGroupCourseService {
return Mono.<GroupCourseDetail>just(detail); return Mono.<GroupCourseDetail>just(detail);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage()); 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(); return Mono.<GroupCourseDetail>empty();
@@ -141,7 +141,7 @@ public class GroupCourseService implements IGroupCourseService {
.flatMap(detail -> { .flatMap(detail -> {
try { try {
String jsonData = objectMapper.writeValueAsString(detail); String jsonData = objectMapper.writeValueAsString(detail);
return redisUtil.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS) return cacheOperations.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS)
.thenReturn(detail) .thenReturn(detail)
.doOnSuccess(d -> logger.debug("缓存已设置 - findDetailById: id={}", id)); .doOnSuccess(d -> logger.debug("缓存已设置 - findDetailById: id={}", id));
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
@@ -233,7 +233,7 @@ public class GroupCourseService implements IGroupCourseService {
public Mono<GroupCourse> findById(Long id) { public Mono<GroupCourse> findById(Long id) {
String cacheKey = CACHE_KEY_ID_PREFIX + 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 return cachedMono
.flatMap(cachedJson -> { .flatMap(cachedJson -> {
if (cachedJson != null && !cachedJson.isEmpty()) { if (cachedJson != null && !cachedJson.isEmpty()) {
@@ -243,7 +243,7 @@ public class GroupCourseService implements IGroupCourseService {
return Mono.<GroupCourse>just(groupCourse); return Mono.<GroupCourse>just(groupCourse);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage()); 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(); return Mono.<GroupCourse>empty();
@@ -253,7 +253,7 @@ public class GroupCourseService implements IGroupCourseService {
.flatMap(groupCourse -> { .flatMap(groupCourse -> {
try { try {
String jsonData = objectMapper.writeValueAsString(groupCourse); String jsonData = objectMapper.writeValueAsString(groupCourse);
return redisUtil.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS) return cacheOperations.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS)
.thenReturn(groupCourse) .thenReturn(groupCourse)
.doOnSuccess(gc -> logger.debug("缓存已设置 - findById: id={}", id)); .doOnSuccess(gc -> logger.debug("缓存已设置 - findById: id={}", id));
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
@@ -332,7 +332,7 @@ public class GroupCourseService implements IGroupCourseService {
String cacheKey = CACHE_KEY_PREFIX + page + ":" + size + ":" + includeDeleted + ":" + sort + ":" + order + ":" + keyword + ":" + status; 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 return cachedMono
.flatMap(cachedJson -> { .flatMap(cachedJson -> {
if (cachedJson != null && !cachedJson.isEmpty()) { if (cachedJson != null && !cachedJson.isEmpty()) {
@@ -343,7 +343,7 @@ public class GroupCourseService implements IGroupCourseService {
return Mono.<PageResponse<GroupCourse>>just(pageResponse); return Mono.<PageResponse<GroupCourse>>just(pageResponse);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
logger.warn("缓存解析失败,删除缓存 - key: {}, error: {}", cacheKey, e.getMessage()); 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(); return Mono.<PageResponse<GroupCourse>>empty();
@@ -361,7 +361,7 @@ public class GroupCourseService implements IGroupCourseService {
return resultMono.flatMap(pageResponse -> { return resultMono.flatMap(pageResponse -> {
try { try {
String jsonData = objectMapper.writeValueAsString(pageResponse); String jsonData = objectMapper.writeValueAsString(pageResponse);
return redisUtil.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS) return cacheOperations.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS)
.thenReturn(pageResponse) .thenReturn(pageResponse)
.doOnSuccess(pr -> logger.debug("缓存已设置 - findByPage: key={}", cacheKey)); .doOnSuccess(pr -> logger.debug("缓存已设置 - findByPage: key={}", cacheKey));
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
@@ -739,10 +739,10 @@ public class GroupCourseService implements IGroupCourseService {
} }
private Mono<Void> clearCache() { private Mono<Void> clearCache() {
return redisUtil.deleteByPattern(CACHE_KEY_PREFIX + "*") return cacheOperations.deleteByPattern(CACHE_KEY_PREFIX + "*")
.then(redisUtil.deleteByPattern(CACHE_KEY_ID_PREFIX + "*")) .then(cacheOperations.deleteByPattern(CACHE_KEY_ID_PREFIX + "*"))
.then(redisUtil.deleteByPattern(CACHE_KEY_DETAIL_PREFIX + "*")) .then(cacheOperations.deleteByPattern(CACHE_KEY_DETAIL_PREFIX + "*"))
.then(redisUtil.deleteByPattern("datacount:statistics:*")) .then(cacheOperations.deleteByPattern("datacount:statistics:*"))
.then(); .then();
} }
@@ -1,6 +1,6 @@
package cn.novalon.gym.manage.groupcourse.handler; 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.GroupCourse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail; import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService; import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
@@ -35,7 +35,7 @@ class GroupCourseHandlerTest {
private Validator validator; private Validator validator;
@Mock @Mock
private RedisUtil redisUtil; private CacheOperations cacheOperations;
@Mock @Mock
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
@@ -44,7 +44,7 @@ class GroupCourseHandlerTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
handler = new GroupCourseHandler(groupCourseService, validator, redisUtil, objectMapper); handler = new GroupCourseHandler(groupCourseService, validator, cacheOperations, objectMapper);
} }
// ==================== getAllGroupCourse ==================== // ==================== 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.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse; 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.CourseLabel;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository; import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository;
@@ -38,13 +38,13 @@ class CourseLabelServiceTest {
private IGroupCourseRepository groupCourseRepository; private IGroupCourseRepository groupCourseRepository;
@Mock @Mock
private RedisUtil redisUtil; private CacheOperations cacheOperations;
private CourseLabelService courseLabelService; private CourseLabelService courseLabelService;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
courseLabelService = new CourseLabelService(courseLabelRepository, groupCourseRepository, redisUtil); courseLabelService = new CourseLabelService(courseLabelRepository, groupCourseRepository, cacheOperations);
} }
// ==================== findById ==================== // ==================== findById ====================
@@ -287,16 +287,16 @@ class CourseLabelServiceTest {
GroupCourse course2 = new GroupCourse(); GroupCourse course2 = new GroupCourse();
course2.setId(102L); course2.setId(102L);
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.just(course1, course2)); when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.just(course1, course2));
when(redisUtil.delete("group_course:detail:101")).thenReturn(Mono.just(1L)); when(cacheOperations.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:102")).thenReturn(Mono.just(1L));
StepVerifier.create(courseLabelService.addLabelsToType(1L, List.of(1L, 2L))) StepVerifier.create(courseLabelService.addLabelsToType(1L, List.of(1L, 2L)))
.verifyComplete(); .verifyComplete();
verify(courseLabelRepository).addLabelsToType(1L, List.of(1L, 2L)); verify(courseLabelRepository).addLabelsToType(1L, List.of(1L, 2L));
verify(groupCourseRepository).findByCourseType(1L); verify(groupCourseRepository).findByCourseType(1L);
verify(redisUtil).delete("group_course:detail:101"); verify(cacheOperations).delete("group_course:detail:101");
verify(redisUtil).delete("group_course:detail:102"); verify(cacheOperations).delete("group_course:detail:102");
} }
@Test @Test
@@ -309,7 +309,7 @@ class CourseLabelServiceTest {
verify(courseLabelRepository).addLabelsToType(1L, List.of(1L)); verify(courseLabelRepository).addLabelsToType(1L, List.of(1L));
verify(groupCourseRepository).findByCourseType(1L); verify(groupCourseRepository).findByCourseType(1L);
verify(redisUtil, never()).delete(anyString()); verify(cacheOperations, never()).delete(anyString());
} }
// ==================== removeLabelFromType ==================== // ==================== removeLabelFromType ====================
@@ -321,14 +321,14 @@ class CourseLabelServiceTest {
GroupCourse course = new GroupCourse(); GroupCourse course = new GroupCourse();
course.setId(101L); course.setId(101L);
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.just(course)); 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)) StepVerifier.create(courseLabelService.removeLabelFromType(1L, 1L))
.verifyComplete(); .verifyComplete();
verify(courseLabelRepository).removeLabelFromType(1L, 1L); verify(courseLabelRepository).removeLabelFromType(1L, 1L);
verify(groupCourseRepository).findByCourseType(1L); verify(groupCourseRepository).findByCourseType(1L);
verify(redisUtil).delete("group_course:detail:101"); verify(cacheOperations).delete("group_course:detail:101");
} }
@Test @Test
@@ -340,7 +340,7 @@ class CourseLabelServiceTest {
.verifyComplete(); .verifyComplete();
verify(courseLabelRepository).removeLabelFromType(1L, 1L); verify(courseLabelRepository).removeLabelFromType(1L, 1L);
verify(redisUtil, never()).delete(anyString()); verify(cacheOperations, never()).delete(anyString());
} }
// ==================== clearLabelsFromType ==================== // ==================== clearLabelsFromType ====================
@@ -354,16 +354,16 @@ class CourseLabelServiceTest {
GroupCourse course2 = new GroupCourse(); GroupCourse course2 = new GroupCourse();
course2.setId(102L); course2.setId(102L);
when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.just(course1, course2)); when(groupCourseRepository.findByCourseType(1L)).thenReturn(Flux.just(course1, course2));
when(redisUtil.delete("group_course:detail:101")).thenReturn(Mono.just(1L)); when(cacheOperations.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:102")).thenReturn(Mono.just(1L));
StepVerifier.create(courseLabelService.clearLabelsFromType(1L)) StepVerifier.create(courseLabelService.clearLabelsFromType(1L))
.verifyComplete(); .verifyComplete();
verify(courseLabelRepository).clearLabelsFromType(1L); verify(courseLabelRepository).clearLabelsFromType(1L);
verify(groupCourseRepository).findByCourseType(1L); verify(groupCourseRepository).findByCourseType(1L);
verify(redisUtil).delete("group_course:detail:101"); verify(cacheOperations).delete("group_course:detail:101");
verify(redisUtil).delete("group_course:detail:102"); verify(cacheOperations).delete("group_course:detail:102");
} }
@Test @Test
@@ -375,7 +375,7 @@ class CourseLabelServiceTest {
.verifyComplete(); .verifyComplete();
verify(courseLabelRepository).clearLabelsFromType(1L); verify(courseLabelRepository).clearLabelsFromType(1L);
verify(redisUtil, never()).delete(anyString()); verify(cacheOperations, never()).delete(anyString());
} }
// ==================== findByPage ==================== // ==================== findByPage ====================
@@ -1,7 +1,7 @@
package cn.novalon.gym.manage.groupcourse.service.impl; 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 cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -21,14 +21,15 @@ import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class GroupCourseRedisServiceTest { class GroupCourseRedisServiceTest {
@Mock
private CacheOperations cacheOperations;
@Mock @Mock
private ReactiveRedisTemplate<String, Object> reactiveRedisTemplate; private ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
@Mock @Mock
private ReactiveValueOperations<String, Object> reactiveValueOps; private ReactiveValueOperations<String, Object> reactiveValueOps;
private ObjectMapper objectMapper = new ObjectMapper();
private GroupCourseRedisService service; private GroupCourseRedisService service;
private GroupCourse testCourse; private GroupCourse testCourse;
@@ -36,7 +37,7 @@ class GroupCourseRedisServiceTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
lenient().when(reactiveRedisTemplate.opsForValue()).thenReturn(reactiveValueOps); lenient().when(reactiveRedisTemplate.opsForValue()).thenReturn(reactiveValueOps);
service = new GroupCourseRedisService(reactiveRedisTemplate, objectMapper); service = new GroupCourseRedisService(cacheOperations, reactiveRedisTemplate);
testCourse = new GroupCourse(); testCourse = new GroupCourse();
testCourse.setId(1L); testCourse.setId(1L);
@@ -47,20 +48,20 @@ class GroupCourseRedisServiceTest {
// ==================== cacheCourse ==================== // ==================== cacheCourse ====================
@Test @Test
void cacheCourse_shouldSerializeAndSetInRedis() { void cacheCourse_shouldSetInCache() {
when(reactiveValueOps.set(eq("group_course:1"), anyString(), eq(Duration.ofMinutes(5)))) when(cacheOperations.setWithExpire(eq("group_course:1"), eq(testCourse), eq(300L)))
.thenReturn(Mono.just(true)); .thenReturn(Mono.just(true));
StepVerifier.create(service.cacheCourse(testCourse)) StepVerifier.create(service.cacheCourse(testCourse))
.verifyComplete(); .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 @Test
void cacheCourse_shouldErrorWhenRedisFails() { void cacheCourse_shouldErrorWhenCacheFails() {
when(reactiveValueOps.set(eq("group_course:1"), anyString(), eq(Duration.ofMinutes(5)))) when(cacheOperations.setWithExpire(eq("group_course:1"), eq(testCourse), eq(300L)))
.thenReturn(Mono.error(new RuntimeException("Redis error"))); .thenReturn(Mono.error(new RuntimeException("Cache error")));
StepVerifier.create(service.cacheCourse(testCourse)) StepVerifier.create(service.cacheCourse(testCourse))
.expectError(RuntimeException.class) .expectError(RuntimeException.class)
@@ -70,9 +71,9 @@ class GroupCourseRedisServiceTest {
// ==================== getCachedCourse ==================== // ==================== getCachedCourse ====================
@Test @Test
void getCachedCourse_shouldReturnCachedCourseWhenFound() throws Exception { void getCachedCourse_shouldReturnCachedCourseWhenFound() {
String json = objectMapper.writeValueAsString(testCourse); when(cacheOperations.get(eq("group_course:1"), eq(GroupCourse.class)))
when(reactiveValueOps.get("group_course:1")).thenReturn(Mono.just(json)); .thenReturn(Mono.just(testCourse));
StepVerifier.create(service.getCachedCourse(1L)) StepVerifier.create(service.getCachedCourse(1L))
.assertNext(course -> { .assertNext(course -> {
@@ -84,16 +85,8 @@ class GroupCourseRedisServiceTest {
@Test @Test
void getCachedCourse_shouldReturnEmptyWhenCacheMiss() { void getCachedCourse_shouldReturnEmptyWhenCacheMiss() {
when(reactiveValueOps.get("group_course:1")).thenReturn(Mono.empty()); when(cacheOperations.get(eq("group_course:1"), eq(GroupCourse.class)))
.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));
StepVerifier.create(service.getCachedCourse(1L)) StepVerifier.create(service.getCachedCourse(1L))
.verifyComplete(); .verifyComplete();
@@ -102,13 +95,13 @@ class GroupCourseRedisServiceTest {
// ==================== invalidateCourseCache ==================== // ==================== invalidateCourseCache ====================
@Test @Test
void invalidateCourseCache_shouldDeleteFromRedis() { void invalidateCourseCache_shouldDeleteFromCache() {
when(reactiveRedisTemplate.delete("group_course:1")).thenReturn(Mono.just(1L)); when(cacheOperations.delete("group_course:1")).thenReturn(Mono.just(1L));
StepVerifier.create(service.invalidateCourseCache(1L)) StepVerifier.create(service.invalidateCourseCache(1L))
.verifyComplete(); .verifyComplete();
verify(reactiveRedisTemplate).delete("group_course:1"); verify(cacheOperations).delete("group_course:1");
} }
// ==================== acquireLock ==================== // ==================== 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.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse; 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.domain.SysFile;
import cn.novalon.gym.manage.file.core.service.ISysFileService; import cn.novalon.gym.manage.file.core.service.ISysFileService;
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel; import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
@@ -63,7 +63,7 @@ class GroupCourseServiceTest {
@Mock @Mock
private MemberCardRepository memberCardRepository; private MemberCardRepository memberCardRepository;
@Mock @Mock
private RedisUtil redisUtil; private CacheOperations cacheOperations;
@Mock @Mock
private GroupCourseStateMachine stateMachine; private GroupCourseStateMachine stateMachine;
@Mock @Mock
@@ -90,7 +90,7 @@ class GroupCourseServiceTest {
groupCourseService = new GroupCourseService( groupCourseService = new GroupCourseService(
groupCourseRepository, bookingRepository, groupCourseTypeRepository, groupCourseRepository, bookingRepository, groupCourseTypeRepository,
courseLabelRepository, memberCardRecordService, memberCardRepository, courseLabelRepository, memberCardRecordService, memberCardRepository,
redisUtil, objectMapper, stateMachine, databaseClient, fileService, sysUserRepository cacheOperations, objectMapper, stateMachine, databaseClient, fileService, sysUserRepository
); );
testCourse = new GroupCourse(); testCourse = new GroupCourse();
@@ -136,10 +136,10 @@ class GroupCourseServiceTest {
} }
private void mockClearCache() { private void mockClearCache() {
when(redisUtil.deleteByPattern("group_course:page:*")).thenReturn(Mono.just(0L)); when(cacheOperations.deleteByPattern("group_course:page:*")).thenReturn(Mono.just(0L));
when(redisUtil.deleteByPattern("group_course:id:*")).thenReturn(Mono.just(0L)); when(cacheOperations.deleteByPattern("group_course:id:*")).thenReturn(Mono.just(0L));
when(redisUtil.deleteByPattern("group_course:detail:*")).thenReturn(Mono.just(0L)); when(cacheOperations.deleteByPattern("group_course:detail:*")).thenReturn(Mono.just(0L));
when(redisUtil.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(0L)); when(cacheOperations.deleteByPattern("datacount:statistics:*")).thenReturn(Mono.just(0L));
} }
private void mockEnrichCurrentMembers() { private void mockEnrichCurrentMembers() {
@@ -157,7 +157,7 @@ class GroupCourseServiceTest {
detail.setCoachName("张教练"); detail.setCoachName("张教练");
String json = objectMapper.writeValueAsString(detail); 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 // switchIfEmpty 的 Mono 参数被急切求值,需要 mock 以避免 NPE
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty()); when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
@@ -169,7 +169,7 @@ class GroupCourseServiceTest {
}) })
.verifyComplete(); .verifyComplete();
verify(redisUtil).get(cacheKey, String.class); verify(cacheOperations).get(cacheKey, String.class);
// switchIfEmpty 的 Mono 参数被急切求值,findByIdAndDeletedAtIsNull 会被调用但不会订阅 // switchIfEmpty 的 Mono 参数被急切求值,findByIdAndDeletedAtIsNull 会被调用但不会订阅
} }
@@ -177,13 +177,13 @@ class GroupCourseServiceTest {
void findDetailById_cacheMissWithType_shouldBuildAndCacheDetail() throws Exception { void findDetailById_cacheMissWithType_shouldBuildAndCacheDetail() throws Exception {
String cacheKey = "group_course:detail:1"; 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(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(groupCourseTypeRepository.findById(100L)).thenReturn(Mono.just(testType)); when(groupCourseTypeRepository.findById(100L)).thenReturn(Mono.just(testType));
when(courseLabelRepository.findByTypeId(100L)).thenReturn(Flux.just(testLabel)); when(courseLabelRepository.findByTypeId(100L)).thenReturn(Flux.just(testLabel));
when(sysUserRepository.findByIdIncludingDeleted(10L)).thenReturn(Mono.just(testCoach)); when(sysUserRepository.findByIdIncludingDeleted(10L)).thenReturn(Mono.just(testCoach));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(8L)); 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)) StepVerifier.create(groupCourseService.findDetailById(1L))
.assertNext(result -> { .assertNext(result -> {
@@ -198,13 +198,13 @@ class GroupCourseServiceTest {
}) })
.verifyComplete(); .verifyComplete();
verify(redisUtil).get(cacheKey, String.class); verify(cacheOperations).get(cacheKey, String.class);
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L); verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L);
verify(groupCourseTypeRepository).findById(100L); verify(groupCourseTypeRepository).findById(100L);
verify(courseLabelRepository).findByTypeId(100L); verify(courseLabelRepository).findByTypeId(100L);
verify(sysUserRepository).findByIdIncludingDeleted(10L); verify(sysUserRepository).findByIdIncludingDeleted(10L);
verify(bookingRepository).countValidBookings(1L); verify(bookingRepository).countValidBookings(1L);
verify(redisUtil).setWithExpire(eq(cacheKey), anyString(), eq(300L)); verify(cacheOperations).setWithExpire(eq(cacheKey), anyString(), eq(300L));
} }
@Test @Test
@@ -212,11 +212,11 @@ class GroupCourseServiceTest {
String cacheKey = "group_course:detail:1"; String cacheKey = "group_course:detail:1";
testCourse.setCourseType(null); 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(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(sysUserRepository.findByIdIncludingDeleted(10L)).thenReturn(Mono.just(testCoach)); when(sysUserRepository.findByIdIncludingDeleted(10L)).thenReturn(Mono.just(testCoach));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(3L)); 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)) StepVerifier.create(groupCourseService.findDetailById(1L))
.assertNext(result -> { .assertNext(result -> {
@@ -236,7 +236,7 @@ class GroupCourseServiceTest {
void findDetailById_cacheMissNotFound_shouldReturnEmpty() { void findDetailById_cacheMissNotFound_shouldReturnEmpty() {
String cacheKey = "group_course:detail:1"; 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()); when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.findDetailById(1L)) StepVerifier.create(groupCourseService.findDetailById(1L))
@@ -250,15 +250,15 @@ class GroupCourseServiceTest {
void findDetailById_cacheParseError_shouldDeleteCacheAndFallback() throws Exception { void findDetailById_cacheParseError_shouldDeleteCacheAndFallback() throws Exception {
String cacheKey = "group_course:detail:1"; String cacheKey = "group_course:detail:1";
when(redisUtil.get(cacheKey, String.class)).thenReturn(Mono.just("invalid-json")); when(cacheOperations.get(cacheKey, String.class)).thenReturn(Mono.just("invalid-json"));
when(redisUtil.delete(cacheKey)).thenReturn(Mono.just(1L)); when(cacheOperations.delete(cacheKey)).thenReturn(Mono.just(1L));
// After cache delete, switchIfEmpty kicks in // After cache delete, switchIfEmpty kicks in
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse)); when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.just(testCourse));
when(groupCourseTypeRepository.findById(100L)).thenReturn(Mono.just(testType)); when(groupCourseTypeRepository.findById(100L)).thenReturn(Mono.just(testType));
when(courseLabelRepository.findByTypeId(100L)).thenReturn(Flux.just(testLabel)); when(courseLabelRepository.findByTypeId(100L)).thenReturn(Flux.just(testLabel));
when(sysUserRepository.findByIdIncludingDeleted(10L)).thenReturn(Mono.just(testCoach)); when(sysUserRepository.findByIdIncludingDeleted(10L)).thenReturn(Mono.just(testCoach));
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L)); 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)) StepVerifier.create(groupCourseService.findDetailById(1L))
.assertNext(result -> { .assertNext(result -> {
@@ -267,7 +267,7 @@ class GroupCourseServiceTest {
}) })
.verifyComplete(); .verifyComplete();
verify(redisUtil).delete(cacheKey); verify(cacheOperations).delete(cacheKey);
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L); verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L);
} }
@@ -278,7 +278,7 @@ class GroupCourseServiceTest {
String cacheKey = "group_course:id:1"; String cacheKey = "group_course:id:1";
String json = objectMapper.writeValueAsString(testCourse); 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 // switchIfEmpty 的 Mono 参数被急切求值,需要 mock 以避免 NPE
when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty()); when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L)); when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L));
@@ -290,7 +290,7 @@ class GroupCourseServiceTest {
}) })
.verifyComplete(); .verifyComplete();
verify(redisUtil).get(cacheKey, String.class); verify(cacheOperations).get(cacheKey, String.class);
verify(bookingRepository).countValidBookings(1L); verify(bookingRepository).countValidBookings(1L);
// switchIfEmpty 的 Mono 参数被急切求值,findByIdAndDeletedAtIsNull 会被调用但不会订阅 // switchIfEmpty 的 Mono 参数被急切求值,findByIdAndDeletedAtIsNull 会被调用但不会订阅
} }
@@ -299,9 +299,9 @@ class GroupCourseServiceTest {
void findById_cacheMiss_shouldQueryDbAndCache() throws Exception { void findById_cacheMiss_shouldQueryDbAndCache() throws Exception {
String cacheKey = "group_course:id:1"; 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(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)); when(bookingRepository.countValidBookings(1L)).thenReturn(Mono.just(5L));
StepVerifier.create(groupCourseService.findById(1L)) StepVerifier.create(groupCourseService.findById(1L))
@@ -312,7 +312,7 @@ class GroupCourseServiceTest {
.verifyComplete(); .verifyComplete();
verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L); verify(groupCourseRepository).findByIdAndDeletedAtIsNull(1L);
verify(redisUtil).setWithExpire(eq(cacheKey), anyString(), eq(300L)); verify(cacheOperations).setWithExpire(eq(cacheKey), anyString(), eq(300L));
verify(bookingRepository).countValidBookings(1L); verify(bookingRepository).countValidBookings(1L);
} }
@@ -320,7 +320,7 @@ class GroupCourseServiceTest {
void findById_cacheMissNotFound_shouldReturnEmpty() { void findById_cacheMissNotFound_shouldReturnEmpty() {
String cacheKey = "group_course:id:1"; 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()); when(groupCourseRepository.findByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
StepVerifier.create(groupCourseService.findById(1L)) StepVerifier.create(groupCourseService.findById(1L))
@@ -383,7 +383,7 @@ class GroupCourseServiceTest {
String cacheKey = "group_course:page:0:10:false:id:asc::"; String cacheKey = "group_course:page:0:10:false:id:asc::";
String json = objectMapper.writeValueAsString(pageResponse); 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)) StepVerifier.create(groupCourseService.findByPage(pageRequest, false))
.assertNext(result -> { .assertNext(result -> {
@@ -393,7 +393,7 @@ class GroupCourseServiceTest {
}) })
.verifyComplete(); .verifyComplete();
verify(redisUtil).get(cacheKey, String.class); verify(cacheOperations).get(cacheKey, String.class);
verifyNoInteractions(groupCourseRepository); verifyNoInteractions(groupCourseRepository);
} }
@@ -409,9 +409,9 @@ class GroupCourseServiceTest {
String cacheKey = "group_course:page:0:10:false:id:asc::"; 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(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)) StepVerifier.create(groupCourseService.findByPage(pageRequest, false))
.assertNext(result -> { .assertNext(result -> {
@@ -421,7 +421,7 @@ class GroupCourseServiceTest {
.verifyComplete(); .verifyComplete();
verify(groupCourseRepository).findByPageAndNotDeleted(pageRequest); verify(groupCourseRepository).findByPageAndNotDeleted(pageRequest);
verify(redisUtil).setWithExpire(eq(cacheKey), anyString(), eq(300L)); verify(cacheOperations).setWithExpire(eq(cacheKey), anyString(), eq(300L));
} }
// ==================== create ==================== // ==================== create ====================
@@ -6,7 +6,7 @@ import cn.novalon.gym.manage.member.enums.MemberCardRecordStatus;
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository; import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
import cn.novalon.gym.manage.member.repository.MemberCardRepository; import cn.novalon.gym.manage.member.repository.MemberCardRepository;
import cn.novalon.gym.manage.member.service.IMemberCardRecordService; import cn.novalon.gym.manage.member.service.IMemberCardRecordService;
import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.common.cache.CacheOperations;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -29,30 +29,30 @@ import java.util.stream.Collectors;
public class MemberCardRecordServiceImpl implements IMemberCardRecordService { public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
private final MemberCardRecordRepository memberCardRecordRepository; private final MemberCardRecordRepository memberCardRecordRepository;
private final MemberCardRepository memberCardRepository; private final MemberCardRepository memberCardRepository;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private static final String MEMBER_CARD_RECORD_CACHE_PREFIX = "member:card:record:"; private static final String MEMBER_CARD_RECORD_CACHE_PREFIX = "member:card:record:";
private static final long CACHE_EXPIRE_SECONDS = 300; private static final long CACHE_EXPIRE_SECONDS = 300;
public MemberCardRecordServiceImpl(MemberCardRecordRepository memberCardRecordRepository, public MemberCardRecordServiceImpl(MemberCardRecordRepository memberCardRecordRepository,
MemberCardRepository memberCardRepository, MemberCardRepository memberCardRepository,
RedisUtil redisUtil) { CacheOperations cacheOperations) {
this.memberCardRecordRepository = memberCardRecordRepository; this.memberCardRecordRepository = memberCardRecordRepository;
this.memberCardRepository = memberCardRepository; this.memberCardRepository = memberCardRepository;
this.redisUtil = redisUtil; this.cacheOperations = cacheOperations;
} }
@Override @Override
public Mono<MemberCardRecord> findById(Long recordId) { public Mono<MemberCardRecord> findById(Long recordId) {
String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId; String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId;
return redisUtil.get(cacheKey) return cacheOperations.get(cacheKey)
.filter(cached -> cached instanceof MemberCardRecord) .filter(cached -> cached instanceof MemberCardRecord)
.map(cached -> (MemberCardRecord) cached) .map(cached -> (MemberCardRecord) cached)
.switchIfEmpty(Mono.defer(() -> .switchIfEmpty(Mono.defer(() ->
memberCardRecordRepository.findById(recordId) memberCardRecordRepository.findById(recordId)
.doOnSuccess(record -> { .doOnSuccess(record -> {
if (record != null) { if (record != null) {
redisUtil.setWithExpire(cacheKey, record, CACHE_EXPIRE_SECONDS); cacheOperations.setWithExpire(cacheKey, record, CACHE_EXPIRE_SECONDS);
} }
}) })
)); ));
@@ -255,7 +255,7 @@ public class MemberCardRecordServiceImpl implements IMemberCardRecordService {
private void clearRecordCache(Long recordId) { private void clearRecordCache(Long recordId) {
String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId; String cacheKey = MEMBER_CARD_RECORD_CACHE_PREFIX + recordId;
redisUtil.delete(cacheKey); cacheOperations.delete(cacheKey);
log.debug("清除会员卡记录缓存, recordId: {}", recordId); log.debug("清除会员卡记录缓存, recordId: {}", recordId);
} }
} }
@@ -1,6 +1,6 @@
package cn.novalon.gym.manage.member.service.impl; package cn.novalon.gym.manage.member.service.impl;
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.MemberCard;
import cn.novalon.gym.manage.member.entity.MemberCardRecord; import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.entity.MemberCardTransaction; import cn.novalon.gym.manage.member.entity.MemberCardTransaction;
@@ -40,7 +40,7 @@ public class MemberCardServiceImpl implements IMemberCardService {
private final DistributedLockService distributedLockService; private final DistributedLockService distributedLockService;
private final ExpirationReminderService expirationReminderService; private final ExpirationReminderService expirationReminderService;
private final RefundSagaHandler refundSagaHandler; private final RefundSagaHandler refundSagaHandler;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private static final String MEMBER_CARD_CACHE_PREFIX = "member:card:"; private static final String MEMBER_CARD_CACHE_PREFIX = "member:card:";
private static final long CACHE_EXPIRE_SECONDS = 300; private static final long CACHE_EXPIRE_SECONDS = 300;
@@ -53,7 +53,7 @@ public class MemberCardServiceImpl implements IMemberCardService {
DistributedLockService distributedLockService, DistributedLockService distributedLockService,
ExpirationReminderService expirationReminderService, ExpirationReminderService expirationReminderService,
RefundSagaHandler refundSagaHandler, RefundSagaHandler refundSagaHandler,
RedisUtil redisUtil) { CacheOperations cacheOperations) {
this.memberCardRepository = memberCardRepository; this.memberCardRepository = memberCardRepository;
this.recordRepository = recordRepository; this.recordRepository = recordRepository;
this.transactionService = transactionService; this.transactionService = transactionService;
@@ -61,13 +61,13 @@ public class MemberCardServiceImpl implements IMemberCardService {
this.distributedLockService = distributedLockService; this.distributedLockService = distributedLockService;
this.expirationReminderService = expirationReminderService; this.expirationReminderService = expirationReminderService;
this.refundSagaHandler = refundSagaHandler; this.refundSagaHandler = refundSagaHandler;
this.redisUtil = redisUtil; this.cacheOperations = cacheOperations;
} }
@Override @Override
public Mono<MemberCard> findByMemberCardIdAndDeletedAtIsNull(Long memberCardId) { public Mono<MemberCard> findByMemberCardIdAndDeletedAtIsNull(Long memberCardId) {
String cacheKey = MEMBER_CARD_CACHE_PREFIX + memberCardId; String cacheKey = MEMBER_CARD_CACHE_PREFIX + memberCardId;
Object cached = redisUtil.get(cacheKey); Object cached = cacheOperations.get(cacheKey);
if (cached != null && cached instanceof MemberCard) { if (cached != null && cached instanceof MemberCard) {
log.debug("从缓存获取会员卡信息, memberCardId: {}", memberCardId); log.debug("从缓存获取会员卡信息, memberCardId: {}", memberCardId);
return Mono.just((MemberCard) cached); return Mono.just((MemberCard) cached);
@@ -76,7 +76,7 @@ public class MemberCardServiceImpl implements IMemberCardService {
return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId) return memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(memberCardId)
.doOnSuccess(card -> { .doOnSuccess(card -> {
if (card != null) { if (card != null) {
redisUtil.setWithExpire(cacheKey, card, CACHE_EXPIRE_SECONDS); cacheOperations.setWithExpire(cacheKey, card, CACHE_EXPIRE_SECONDS);
} }
}); });
} }
@@ -439,7 +439,7 @@ public class MemberCardServiceImpl implements IMemberCardService {
private void clearCardCache(Long memberCardId) { private void clearCardCache(Long memberCardId) {
String cacheKey = MEMBER_CARD_CACHE_PREFIX + memberCardId; String cacheKey = MEMBER_CARD_CACHE_PREFIX + memberCardId;
redisUtil.delete(cacheKey); cacheOperations.delete(cacheKey);
log.debug("清除会员卡缓存, memberCardId: {}", memberCardId); log.debug("清除会员卡缓存, memberCardId: {}", memberCardId);
} }
} }
@@ -18,7 +18,7 @@ import cn.novalon.gym.manage.member.util.AesUtil;
import cn.novalon.gym.manage.member.util.BeanConvertUtil; import cn.novalon.gym.manage.member.util.BeanConvertUtil;
import cn.novalon.gym.manage.member.util.EsSyncUtils; import cn.novalon.gym.manage.member.util.EsSyncUtils;
import cn.novalon.gym.manage.member.util.WechatPhoneUtil; import cn.novalon.gym.manage.member.util.WechatPhoneUtil;
import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.member.vo.MemberCardInfoVO; import cn.novalon.gym.manage.member.vo.MemberCardInfoVO;
import cn.novalon.gym.manage.member.vo.MemberDetailVO; import cn.novalon.gym.manage.member.vo.MemberDetailVO;
import cn.novalon.gym.manage.member.vo.MemberInfoVO; import cn.novalon.gym.manage.member.vo.MemberInfoVO;
@@ -50,7 +50,7 @@ public class MemberServiceImpl implements MemberService {
private final IMemberRepository memberRepository; private final IMemberRepository memberRepository;
private final MemberESRepository memberESRepository; private final MemberESRepository memberESRepository;
private final EsSyncUtils esSyncUtils; private final EsSyncUtils esSyncUtils;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer; private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
@@ -68,7 +68,7 @@ public class MemberServiceImpl implements MemberService {
String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId; String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
// 先查缓存 // 先查缓存
return redisUtil.get(cacheKey, MemberInfoVO.class) return cacheOperations.get(cacheKey, MemberInfoVO.class)
.flatMap(cached -> { .flatMap(cached -> {
if (cached != null) { if (cached != null) {
log.debug("从缓存获取会员信息, memberId: {}", memberId); log.debug("从缓存获取会员信息, memberId: {}", memberId);
@@ -91,7 +91,7 @@ public class MemberServiceImpl implements MemberService {
.map(this::buildMemberInfoResponse) .map(this::buildMemberInfoResponse)
.flatMap(vo -> { .flatMap(vo -> {
// 查询到数据后更新缓存 // 查询到数据后更新缓存
return redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS) return cacheOperations.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS)
.then(Mono.just(vo)); .then(Mono.just(vo));
}) })
.switchIfEmpty(Mono.error(() -> { .switchIfEmpty(Mono.error(() -> {
@@ -269,7 +269,7 @@ public class MemberServiceImpl implements MemberService {
String cacheKey = MEMBER_DETAIL_CACHE_PREFIX + memberId; String cacheKey = MEMBER_DETAIL_CACHE_PREFIX + memberId;
return redisUtil.get(cacheKey, MemberDetailVO.class) return cacheOperations.get(cacheKey, MemberDetailVO.class)
.filter(cached -> cached != null) .filter(cached -> cached != null)
.switchIfEmpty(Mono.defer(() -> .switchIfEmpty(Mono.defer(() ->
memberRepository.findById(memberId) memberRepository.findById(memberId)
@@ -312,7 +312,7 @@ public class MemberServiceImpl implements MemberService {
return memberDetailVO; return memberDetailVO;
} }
) )
.flatMap(vo -> redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS) .flatMap(vo -> cacheOperations.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS)
.then(Mono.just(vo))) .then(Mono.just(vo)))
)); ));
} }
@@ -383,8 +383,8 @@ public class MemberServiceImpl implements MemberService {
private Mono<Long> clearMemberCache(Long memberId) { private Mono<Long> clearMemberCache(Long memberId) {
String infoCacheKey = MEMBER_INFO_CACHE_PREFIX + memberId; String infoCacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
String detailCacheKey = MEMBER_DETAIL_CACHE_PREFIX + memberId; String detailCacheKey = MEMBER_DETAIL_CACHE_PREFIX + memberId;
return redisUtil.delete(infoCacheKey) return cacheOperations.delete(infoCacheKey)
.then(redisUtil.delete(detailCacheKey)) .then(cacheOperations.delete(detailCacheKey))
.doOnSuccess(result -> log.debug("清除会员缓存, memberId: {}", memberId)); .doOnSuccess(result -> log.debug("清除会员缓存, memberId: {}", memberId));
} }
} }
@@ -5,7 +5,7 @@ import cn.novalon.gym.manage.member.entity.RefundApplication;
import cn.novalon.gym.manage.member.enums.RefundStatus; import cn.novalon.gym.manage.member.enums.RefundStatus;
import cn.novalon.gym.manage.member.repository.RefundApplicationRepository; import cn.novalon.gym.manage.member.repository.RefundApplicationRepository;
import cn.novalon.gym.manage.member.service.IRefundApplicationService; import cn.novalon.gym.manage.member.service.IRefundApplicationService;
import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.common.cache.CacheOperations;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@@ -23,14 +23,14 @@ import java.time.LocalDateTime;
public class RefundApplicationServiceImpl implements IRefundApplicationService { public class RefundApplicationServiceImpl implements IRefundApplicationService {
private final RefundApplicationRepository refundApplicationRepository; private final RefundApplicationRepository refundApplicationRepository;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private static final String REFUND_APPLICATION_CACHE_PREFIX = "member:refund:"; private static final String REFUND_APPLICATION_CACHE_PREFIX = "member:refund:";
private static final long CACHE_EXPIRE_SECONDS = 300; private static final long CACHE_EXPIRE_SECONDS = 300;
public RefundApplicationServiceImpl(RefundApplicationRepository refundApplicationRepository, RedisUtil redisUtil) { public RefundApplicationServiceImpl(RefundApplicationRepository refundApplicationRepository, CacheOperations cacheOperations) {
this.refundApplicationRepository = refundApplicationRepository; this.refundApplicationRepository = refundApplicationRepository;
this.redisUtil = redisUtil; this.cacheOperations = cacheOperations;
} }
@Override @Override
@@ -103,7 +103,7 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService {
@Override @Override
public Mono<RefundApplication> findByRecordId(Long recordId) { public Mono<RefundApplication> findByRecordId(Long recordId) {
String cacheKey = REFUND_APPLICATION_CACHE_PREFIX + recordId; String cacheKey = REFUND_APPLICATION_CACHE_PREFIX + recordId;
Object cached = redisUtil.get(cacheKey); Object cached = cacheOperations.get(cacheKey);
if (cached != null && cached instanceof RefundApplication) { if (cached != null && cached instanceof RefundApplication) {
log.debug("从缓存获取退款申请, recordId: {}", recordId); log.debug("从缓存获取退款申请, recordId: {}", recordId);
return Mono.just((RefundApplication) cached); return Mono.just((RefundApplication) cached);
@@ -112,14 +112,14 @@ public class RefundApplicationServiceImpl implements IRefundApplicationService {
return refundApplicationRepository.findByRecordId(recordId) return refundApplicationRepository.findByRecordId(recordId)
.doOnSuccess(application -> { .doOnSuccess(application -> {
if (application != null) { if (application != null) {
redisUtil.setWithExpire(cacheKey, application, CACHE_EXPIRE_SECONDS); cacheOperations.setWithExpire(cacheKey, application, CACHE_EXPIRE_SECONDS);
} }
}); });
} }
private void clearRefundCache(Long recordId) { private void clearRefundCache(Long recordId) {
String cacheKey = REFUND_APPLICATION_CACHE_PREFIX + recordId; String cacheKey = REFUND_APPLICATION_CACHE_PREFIX + recordId;
redisUtil.delete(cacheKey); cacheOperations.delete(cacheKey);
log.debug("清除退款申请缓存, recordId: {}", recordId); log.debug("清除退款申请缓存, recordId: {}", recordId);
} }
} }
@@ -4,7 +4,7 @@ import cn.novalon.gym.manage.common.exception.ErrorCode;
import cn.novalon.gym.manage.common.exception.SystemException; import cn.novalon.gym.manage.common.exception.SystemException;
import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.config.WechatProperties;
import cn.novalon.gym.manage.member.service.WechatApiService; import cn.novalon.gym.manage.member.service.WechatApiService;
import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.common.cache.CacheOperations;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -31,7 +31,7 @@ import java.util.Map;
public class WechatApiServiceImpl implements WechatApiService { public class WechatApiServiceImpl implements WechatApiService {
private final WechatProperties wechatProperties; private final WechatProperties wechatProperties;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private static final String ACCESS_TOKEN_CACHE_PREFIX = "wechat:access_token:"; private static final String ACCESS_TOKEN_CACHE_PREFIX = "wechat:access_token:";
private static final long ACCESS_TOKEN_EXPIRE_SECONDS = 7000; // 比官方过期时间短100秒 private static final long ACCESS_TOKEN_EXPIRE_SECONDS = 7000; // 比官方过期时间短100秒
@@ -173,7 +173,7 @@ public class WechatApiServiceImpl implements WechatApiService {
String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + appType; String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + appType;
return redisUtil.get(cacheKey, String.class) return cacheOperations.get(cacheKey, String.class)
.flatMap(cachedToken -> { .flatMap(cachedToken -> {
if (cachedToken != null) { if (cachedToken != null) {
log.debug("从缓存获取access_token, appType: {}", appType); log.debug("从缓存获取access_token, appType: {}", appType);
@@ -203,7 +203,7 @@ public class WechatApiServiceImpl implements WechatApiService {
String accessToken = (String) response.get("access_token"); String accessToken = (String) response.get("access_token");
Integer expiresIn = (Integer) response.get("expires_in"); Integer expiresIn = (Integer) response.get("expires_in");
log.info("获取access_token成功, expires_in: {}s", expiresIn); log.info("获取access_token成功, expires_in: {}s", expiresIn);
return redisUtil.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS) return cacheOperations.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS)
.then(Mono.just(accessToken)); .then(Mono.just(accessToken));
} else { } else {
String errmsg = (String) response.get("errmsg"); String errmsg = (String) response.get("errmsg");
@@ -14,7 +14,7 @@ import cn.novalon.gym.manage.member.service.WechatAuthService;
import cn.novalon.gym.manage.member.util.AesUtil; import cn.novalon.gym.manage.member.util.AesUtil;
import cn.novalon.gym.manage.member.util.EsSyncUtils; import cn.novalon.gym.manage.member.util.EsSyncUtils;
import cn.novalon.gym.manage.member.util.MemberNoGenerator; import cn.novalon.gym.manage.member.util.MemberNoGenerator;
import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.member.util.WechatPhoneUtil; import cn.novalon.gym.manage.member.util.WechatPhoneUtil;
import cn.novalon.gym.manage.member.vo.WechatLoginVO; import cn.novalon.gym.manage.member.vo.WechatLoginVO;
import cn.novalon.gym.manage.sys.security.JwtTokenProvider; import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
@@ -46,7 +46,7 @@ public class WechatAuthServiceImpl implements WechatAuthService {
private final MemberESRepository memberESRepository; private final MemberESRepository memberESRepository;
private final EsSyncUtils esSyncUtils; private final EsSyncUtils esSyncUtils;
private final JwtTokenProvider jwtTokenProvider; private final JwtTokenProvider jwtTokenProvider;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer; private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
@@ -217,7 +217,7 @@ public class WechatAuthServiceImpl implements WechatAuthService {
private void clearMemberCache(Long memberId) { private void clearMemberCache(Long memberId) {
String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId; String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
redisUtil.delete(cacheKey); cacheOperations.delete(cacheKey);
log.debug("清除会员缓存, memberId: {}", memberId); log.debug("清除会员缓存, memberId: {}", memberId);
} }
@@ -8,7 +8,7 @@ import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
import cn.novalon.gym.manage.member.repository.IMemberRepository; import cn.novalon.gym.manage.member.repository.IMemberRepository;
import cn.novalon.gym.manage.member.service.WechatOfficialService; import cn.novalon.gym.manage.member.service.WechatOfficialService;
import cn.novalon.gym.manage.member.util.EsSyncUtils; import cn.novalon.gym.manage.member.util.EsSyncUtils;
import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.member.vo.WechatUserInfoVO; import cn.novalon.gym.manage.member.vo.WechatUserInfoVO;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -43,7 +43,7 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
private final ObjectMapper objectMapper = new ObjectMapper() private final ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private final EsSyncUtils esSyncUtils; private final EsSyncUtils esSyncUtils;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer; private EsSyncUtils.EntitySyncer<Member, MemberES, String> memberSyncer;
@@ -297,7 +297,7 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
private Mono<String> getAccessToken() { private Mono<String> getAccessToken() {
String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + "mp"; String cacheKey = ACCESS_TOKEN_CACHE_PREFIX + "mp";
return redisUtil.get(cacheKey, String.class) return cacheOperations.get(cacheKey, String.class)
.flatMap(cachedToken -> { .flatMap(cachedToken -> {
if (cachedToken != null) { if (cachedToken != null) {
log.debug("从缓存获取服务号access_token"); log.debug("从缓存获取服务号access_token");
@@ -322,7 +322,7 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg")); throw new RuntimeException("获取AccessToken失败: " + response.get("errmsg"));
} }
String accessToken = (String) response.get("access_token"); String accessToken = (String) response.get("access_token");
return redisUtil.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS) return cacheOperations.setWithExpire(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_SECONDS)
.then(Mono.just(accessToken)); .then(Mono.just(accessToken));
}); });
}); });
@@ -373,7 +373,7 @@ public class WechatOfficialServiceImpl implements WechatOfficialService {
private Mono<Long> clearMemberCache(Long memberId) { private Mono<Long> clearMemberCache(Long memberId) {
String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId; String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId;
return redisUtil.delete(cacheKey) return cacheOperations.delete(cacheKey)
.doOnSuccess(result -> log.debug("清除会员缓存, memberId: {}", memberId)); .doOnSuccess(result -> log.debug("清除会员缓存, memberId: {}", memberId));
} }
} }
@@ -1,6 +1,6 @@
package cn.novalon.gym.manage.member.service.impl; package cn.novalon.gym.manage.member.service.impl;
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.MemberCard;
import cn.novalon.gym.manage.member.entity.MemberCardRecord; import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.enums.MemberCardRecordStatus; import cn.novalon.gym.manage.member.enums.MemberCardRecordStatus;
@@ -37,7 +37,7 @@ class MemberCardRecordServiceImplTest {
private MemberCardRepository memberCardRepository; private MemberCardRepository memberCardRepository;
@Mock @Mock
private RedisUtil redisUtil; private CacheOperations cacheOperations;
@InjectMocks @InjectMocks
private MemberCardRecordServiceImpl memberCardRecordService; private MemberCardRecordServiceImpl memberCardRecordService;
@@ -82,7 +82,7 @@ class MemberCardRecordServiceImplTest {
@DisplayName("缓存命中时应从缓存返回") @DisplayName("缓存命中时应从缓存返回")
void shouldReturnFromCacheWhenHit() { void shouldReturnFromCacheWhenHit() {
MemberCardRecord record = createRecord(); MemberCardRecord record = createRecord();
when(redisUtil.get("member:card:record:" + RECORD_ID)).thenReturn(Mono.just(record)); when(cacheOperations.get("member:card:record:" + RECORD_ID)).thenReturn(Mono.just(record));
Mono<MemberCardRecord> result = memberCardRecordService.findById(RECORD_ID); Mono<MemberCardRecord> result = memberCardRecordService.findById(RECORD_ID);
@@ -97,7 +97,7 @@ class MemberCardRecordServiceImplTest {
@DisplayName("缓存未命中时应从数据库查询并更新缓存") @DisplayName("缓存未命中时应从数据库查询并更新缓存")
void shouldQueryDatabaseWhenCacheMiss() { void shouldQueryDatabaseWhenCacheMiss() {
MemberCardRecord record = createRecord(); MemberCardRecord record = createRecord();
when(redisUtil.get("member:card:record:" + RECORD_ID)).thenReturn(Mono.empty()); when(cacheOperations.get("member:card:record:" + RECORD_ID)).thenReturn(Mono.empty());
when(memberCardRecordRepository.findById(anyLong())).thenReturn(Mono.just(record)); when(memberCardRecordRepository.findById(anyLong())).thenReturn(Mono.just(record));
Mono<MemberCardRecord> result = memberCardRecordService.findById(RECORD_ID); Mono<MemberCardRecord> result = memberCardRecordService.findById(RECORD_ID);
@@ -112,7 +112,7 @@ class MemberCardRecordServiceImplTest {
@Test @Test
@DisplayName("记录不存在时应返回空") @DisplayName("记录不存在时应返回空")
void shouldReturnEmptyWhenNotFound() { void shouldReturnEmptyWhenNotFound() {
when(redisUtil.get("member:card:record:" + RECORD_ID)).thenReturn(Mono.empty()); when(cacheOperations.get("member:card:record:" + RECORD_ID)).thenReturn(Mono.empty());
when(memberCardRecordRepository.findById(anyLong())).thenReturn(Mono.empty()); when(memberCardRecordRepository.findById(anyLong())).thenReturn(Mono.empty());
Mono<MemberCardRecord> result = memberCardRecordService.findById(RECORD_ID); Mono<MemberCardRecord> result = memberCardRecordService.findById(RECORD_ID);
@@ -210,7 +210,7 @@ class MemberCardRecordServiceImplTest {
@DisplayName("扣减成功时应清除缓存") @DisplayName("扣减成功时应清除缓存")
void shouldClearCacheWhenDeducted() { void shouldClearCacheWhenDeducted() {
when(memberCardRecordRepository.deductUsage(RECORD_ID, 1, 0.0)).thenReturn(Mono.just(1)); when(memberCardRecordRepository.deductUsage(RECORD_ID, 1, 0.0)).thenReturn(Mono.just(1));
when(redisUtil.delete("member:card:record:" + RECORD_ID)).thenReturn(Mono.just(1L)); when(cacheOperations.delete("member:card:record:" + RECORD_ID)).thenReturn(Mono.just(1L));
Mono<Integer> result = memberCardRecordService.deductUsage(RECORD_ID, 1, 0.0); Mono<Integer> result = memberCardRecordService.deductUsage(RECORD_ID, 1, 0.0);
@@ -218,7 +218,7 @@ class MemberCardRecordServiceImplTest {
.assertNext(updated -> assertThat(updated).isEqualTo(1)) .assertNext(updated -> assertThat(updated).isEqualTo(1))
.verifyComplete(); .verifyComplete();
verify(redisUtil).delete("member:card:record:" + RECORD_ID); verify(cacheOperations).delete("member:card:record:" + RECORD_ID);
} }
@Test @Test
@@ -232,7 +232,7 @@ class MemberCardRecordServiceImplTest {
.assertNext(updated -> assertThat(updated).isZero()) .assertNext(updated -> assertThat(updated).isZero())
.verifyComplete(); .verifyComplete();
verify(redisUtil, never()).delete(anyString()); verify(cacheOperations, never()).delete(anyString());
} }
} }
@@ -247,7 +247,7 @@ class MemberCardRecordServiceImplTest {
void shouldClearCacheWhenRenewed() { void shouldClearCacheWhenRenewed() {
LocalDateTime newExpire = LocalDateTime.now().plusDays(30); LocalDateTime newExpire = LocalDateTime.now().plusDays(30);
when(memberCardRecordRepository.renewCard(RECORD_ID, 10, null, newExpire)).thenReturn(Mono.just(1)); when(memberCardRecordRepository.renewCard(RECORD_ID, 10, null, newExpire)).thenReturn(Mono.just(1));
when(redisUtil.delete("member:card:record:" + RECORD_ID)).thenReturn(Mono.just(1L)); when(cacheOperations.delete("member:card:record:" + RECORD_ID)).thenReturn(Mono.just(1L));
Mono<Integer> result = memberCardRecordService.renewCard(RECORD_ID, 10, null, newExpire); Mono<Integer> result = memberCardRecordService.renewCard(RECORD_ID, 10, null, newExpire);
@@ -267,7 +267,7 @@ class MemberCardRecordServiceImplTest {
@DisplayName("状态更新成功时应清除缓存") @DisplayName("状态更新成功时应清除缓存")
void shouldClearCacheWhenStatusUpdated() { void shouldClearCacheWhenStatusUpdated() {
when(memberCardRecordRepository.updateStatus(RECORD_ID, "USED_UP")).thenReturn(Mono.just(1)); when(memberCardRecordRepository.updateStatus(RECORD_ID, "USED_UP")).thenReturn(Mono.just(1));
when(redisUtil.delete("member:card:record:" + RECORD_ID)).thenReturn(Mono.just(1L)); when(cacheOperations.delete("member:card:record:" + RECORD_ID)).thenReturn(Mono.just(1L));
Mono<Integer> result = memberCardRecordService.updateStatus(RECORD_ID, "USED_UP"); Mono<Integer> result = memberCardRecordService.updateStatus(RECORD_ID, "USED_UP");
@@ -1,6 +1,6 @@
package cn.novalon.gym.manage.member.service.impl; package cn.novalon.gym.manage.member.service.impl;
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.MemberCard;
import cn.novalon.gym.manage.member.entity.MemberCardRecord; import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.enums.CardEvent; import cn.novalon.gym.manage.member.enums.CardEvent;
@@ -57,7 +57,7 @@ class MemberCardServiceImplTest {
private RefundSagaHandler refundSagaHandler; private RefundSagaHandler refundSagaHandler;
@Mock @Mock
private RedisUtil redisUtil; private CacheOperations cacheOperations;
@InjectMocks @InjectMocks
private MemberCardServiceImpl memberCardService; private MemberCardServiceImpl memberCardService;
@@ -155,7 +155,7 @@ class MemberCardServiceImplTest {
@DisplayName("缓存未命中时应从数据库查询并更新缓存") @DisplayName("缓存未命中时应从数据库查询并更新缓存")
void shouldQueryDatabaseWhenCacheMiss() { void shouldQueryDatabaseWhenCacheMiss() {
MemberCard card = createTimeCard(); MemberCard card = createTimeCard();
when(redisUtil.get("member:card:" + MEMBER_CARD_ID)).thenReturn(Mono.empty()); when(cacheOperations.get("member:card:" + MEMBER_CARD_ID)).thenReturn(Mono.empty());
when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID)) when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID))
.thenReturn(Mono.just(card)); .thenReturn(Mono.just(card));
@@ -171,7 +171,7 @@ class MemberCardServiceImplTest {
@Test @Test
@DisplayName("卡片不存在时应返回空") @DisplayName("卡片不存在时应返回空")
void shouldReturnEmptyWhenCardNotFound() { void shouldReturnEmptyWhenCardNotFound() {
when(redisUtil.get("member:card:" + MEMBER_CARD_ID)).thenReturn(Mono.empty()); when(cacheOperations.get("member:card:" + MEMBER_CARD_ID)).thenReturn(Mono.empty());
when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID)) when(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(MEMBER_CARD_ID))
.thenReturn(Mono.empty()); .thenReturn(Mono.empty());
@@ -214,7 +214,7 @@ class MemberCardServiceImplTest {
void shouldClearCacheWhenSaved() { void shouldClearCacheWhenSaved() {
MemberCard card = createTimeCard(); MemberCard card = createTimeCard();
when(memberCardRepository.save(card)).thenReturn(Mono.just(card)); when(memberCardRepository.save(card)).thenReturn(Mono.just(card));
when(redisUtil.delete("member:card:" + MEMBER_CARD_ID)).thenReturn(Mono.just(1L)); when(cacheOperations.delete("member:card:" + MEMBER_CARD_ID)).thenReturn(Mono.just(1L));
Mono<MemberCard> result = memberCardService.save(card); Mono<MemberCard> result = memberCardService.save(card);
@@ -222,7 +222,7 @@ class MemberCardServiceImplTest {
.assertNext(c -> assertThat(c.getMemberCardName()).isEqualTo("月卡")) .assertNext(c -> assertThat(c.getMemberCardName()).isEqualTo("月卡"))
.verifyComplete(); .verifyComplete();
verify(redisUtil).delete("member:card:" + MEMBER_CARD_ID); verify(cacheOperations).delete("member:card:" + MEMBER_CARD_ID);
} }
} }
@@ -274,7 +274,7 @@ class MemberCardServiceImplTest {
@DisplayName("购买时间卡应成功创建记录") @DisplayName("购买时间卡应成功创建记录")
void shouldPurchaseTimeCardSuccessfully() { void shouldPurchaseTimeCardSuccessfully() {
MemberCard timeCard = createTimeCard(); MemberCard timeCard = createTimeCard();
lenient().when(redisUtil.get("member:card:" + MEMBER_CARD_ID)).thenReturn(Mono.empty()); lenient().when(cacheOperations.get("member:card:" + MEMBER_CARD_ID)).thenReturn(Mono.empty());
when(memberCardRepository.findByIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(timeCard)); when(memberCardRepository.findByIdAndDeletedAtIsNull(MEMBER_CARD_ID)).thenReturn(Mono.just(timeCard));
when(recordRepository.insertActiveRecord(anyLong(), anyLong(), any(), anyInt(), anyDouble(), any())) when(recordRepository.insertActiveRecord(anyLong(), anyLong(), any(), anyInt(), anyDouble(), any()))
.thenReturn(Mono.just(createActiveRecord(timeCard, MemberCardRecordStatus.ACTIVE))); .thenReturn(Mono.just(createActiveRecord(timeCard, MemberCardRecordStatus.ACTIVE)));
@@ -4,7 +4,7 @@ import cn.novalon.gym.manage.common.exception.ConflictException;
import cn.novalon.gym.manage.common.exception.ErrorCode; import cn.novalon.gym.manage.common.exception.ErrorCode;
import cn.novalon.gym.manage.common.exception.NotFoundException; import cn.novalon.gym.manage.common.exception.NotFoundException;
import cn.novalon.gym.manage.common.util.HtmlEscapeUtil; import cn.novalon.gym.manage.common.util.HtmlEscapeUtil;
import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.member.dto.SearchMemberDto; import cn.novalon.gym.manage.member.dto.SearchMemberDto;
import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto;
import cn.novalon.gym.manage.member.entity.Member; import cn.novalon.gym.manage.member.entity.Member;
@@ -52,7 +52,7 @@ class MemberServiceImplTest {
private EsSyncUtils esSyncUtils; private EsSyncUtils esSyncUtils;
@Mock @Mock
private RedisUtil redisUtil; private CacheOperations cacheOperations;
@InjectMocks @InjectMocks
private MemberServiceImpl memberServiceImpl; private MemberServiceImpl memberServiceImpl;
@@ -140,7 +140,7 @@ class MemberServiceImplTest {
@DisplayName("缓存命中时应直接返回缓存数据") @DisplayName("缓存命中时应直接返回缓存数据")
void shouldReturnCachedDataWhenCacheHit() { void shouldReturnCachedDataWhenCacheHit() {
MemberInfoVO cachedVO = createTestMemberInfoVO(); MemberInfoVO cachedVO = createTestMemberInfoVO();
when(redisUtil.get(CACHE_INFO_KEY, MemberInfoVO.class)).thenReturn(Mono.just(cachedVO)); when(cacheOperations.get(CACHE_INFO_KEY, MemberInfoVO.class)).thenReturn(Mono.just(cachedVO));
Mono<MemberInfoVO> result = memberServiceImpl.getMemberInfo(MEMBER_ID); Mono<MemberInfoVO> result = memberServiceImpl.getMemberInfo(MEMBER_ID);
@@ -151,17 +151,17 @@ class MemberServiceImplTest {
}) })
.verifyComplete(); .verifyComplete();
verify(redisUtil).get(CACHE_INFO_KEY, MemberInfoVO.class); verify(cacheOperations).get(CACHE_INFO_KEY, MemberInfoVO.class);
verifyNoInteractions(memberRepository); verifyNoInteractions(memberRepository);
} }
@Test @Test
@DisplayName("缓存未命中时应从数据库查询并更新缓存") @DisplayName("缓存未命中时应从数据库查询并更新缓存")
void shouldQueryDatabaseAndCacheWhenCacheMiss() { void shouldQueryDatabaseAndCacheWhenCacheMiss() {
when(redisUtil.get(CACHE_INFO_KEY, MemberInfoVO.class)).thenReturn(Mono.empty()); when(cacheOperations.get(CACHE_INFO_KEY, MemberInfoVO.class)).thenReturn(Mono.empty());
Member member = createTestMember(); Member member = createTestMember();
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member)); when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member));
when(redisUtil.setWithExpire(eq(CACHE_INFO_KEY), any(MemberInfoVO.class), anyLong())) when(cacheOperations.setWithExpire(eq(CACHE_INFO_KEY), any(MemberInfoVO.class), anyLong()))
.thenReturn(Mono.just(true)); .thenReturn(Mono.just(true));
try (MockedStatic<AesUtil> aesUtilMock = mockStatic(AesUtil.class)) { try (MockedStatic<AesUtil> aesUtilMock = mockStatic(AesUtil.class)) {
@@ -179,14 +179,14 @@ class MemberServiceImplTest {
.verifyComplete(); .verifyComplete();
verify(memberRepository).findById(MEMBER_ID); verify(memberRepository).findById(MEMBER_ID);
verify(redisUtil).setWithExpire(eq(CACHE_INFO_KEY), any(MemberInfoVO.class), eq(300L)); verify(cacheOperations).setWithExpire(eq(CACHE_INFO_KEY), any(MemberInfoVO.class), eq(300L));
} }
} }
@Test @Test
@DisplayName("会员不存在时应抛出 NotFoundException") @DisplayName("会员不存在时应抛出 NotFoundException")
void shouldThrowNotFoundExceptionWhenMemberNotFound() { void shouldThrowNotFoundExceptionWhenMemberNotFound() {
when(redisUtil.get(CACHE_INFO_KEY, MemberInfoVO.class)).thenReturn(Mono.empty()); when(cacheOperations.get(CACHE_INFO_KEY, MemberInfoVO.class)).thenReturn(Mono.empty());
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.empty()); when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.empty());
Mono<MemberInfoVO> result = memberServiceImpl.getMemberInfo(MEMBER_ID); Mono<MemberInfoVO> result = memberServiceImpl.getMemberInfo(MEMBER_ID);
@@ -198,7 +198,7 @@ class MemberServiceImplTest {
&& throwable.getMessage().equals("会员不存在")) && throwable.getMessage().equals("会员不存在"))
.verify(); .verify();
verify(redisUtil).get(CACHE_INFO_KEY, MemberInfoVO.class); verify(cacheOperations).get(CACHE_INFO_KEY, MemberInfoVO.class);
verify(memberRepository).findById(MEMBER_ID); verify(memberRepository).findById(MEMBER_ID);
} }
} }
@@ -217,8 +217,8 @@ class MemberServiceImplTest {
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member)); when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member));
when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> Mono.just(invocation.getArgument(0))); when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> Mono.just(invocation.getArgument(0)));
when(redisUtil.delete("member:info:" + MEMBER_ID)).thenReturn(Mono.just(1L)); when(cacheOperations.delete("member:info:" + MEMBER_ID)).thenReturn(Mono.just(1L));
when(redisUtil.delete("member:detail:" + MEMBER_ID)).thenReturn(Mono.just(1L)); when(cacheOperations.delete("member:detail:" + MEMBER_ID)).thenReturn(Mono.just(1L));
try (MockedStatic<AesUtil> aesUtilMock = mockStatic(AesUtil.class)) { try (MockedStatic<AesUtil> aesUtilMock = mockStatic(AesUtil.class)) {
aesUtilMock.when(() -> AesUtil.decrypt(ENCRYPTED_PHONE)).thenReturn(DECRYPTED_PHONE); aesUtilMock.when(() -> AesUtil.decrypt(ENCRYPTED_PHONE)).thenReturn(DECRYPTED_PHONE);
@@ -236,8 +236,8 @@ class MemberServiceImplTest {
verify(memberRepository).findById(MEMBER_ID); verify(memberRepository).findById(MEMBER_ID);
verify(memberRepository).save(any(Member.class)); verify(memberRepository).save(any(Member.class));
verify(mockSyncer).sync(any(Member.class)); verify(mockSyncer).sync(any(Member.class));
verify(redisUtil).delete("member:info:" + MEMBER_ID); verify(cacheOperations).delete("member:info:" + MEMBER_ID);
verify(redisUtil).delete("member:detail:" + MEMBER_ID); verify(cacheOperations).delete("member:detail:" + MEMBER_ID);
} }
} }
@@ -274,8 +274,8 @@ class MemberServiceImplTest {
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member)); when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member));
when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> Mono.just(invocation.getArgument(0))); when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> Mono.just(invocation.getArgument(0)));
when(redisUtil.delete("member:info:" + MEMBER_ID)).thenReturn(Mono.just(1L)); when(cacheOperations.delete("member:info:" + MEMBER_ID)).thenReturn(Mono.just(1L));
when(redisUtil.delete("member:detail:" + MEMBER_ID)).thenReturn(Mono.just(1L)); when(cacheOperations.delete("member:detail:" + MEMBER_ID)).thenReturn(Mono.just(1L));
try (MockedStatic<AesUtil> aesUtilMock = mockStatic(AesUtil.class)) { try (MockedStatic<AesUtil> aesUtilMock = mockStatic(AesUtil.class)) {
aesUtilMock.when(() -> AesUtil.encrypt(TEST_PHONE)).thenReturn(ENCRYPTED_PHONE); aesUtilMock.when(() -> AesUtil.encrypt(TEST_PHONE)).thenReturn(ENCRYPTED_PHONE);
@@ -493,7 +493,7 @@ class MemberServiceImplTest {
.nickname("测试会员") .nickname("测试会员")
.build(); .build();
when(redisUtil.get(CACHE_DETAIL_KEY, MemberDetailVO.class)).thenReturn(Mono.just(cachedDetail)); when(cacheOperations.get(CACHE_DETAIL_KEY, MemberDetailVO.class)).thenReturn(Mono.just(cachedDetail));
Mono<MemberDetailVO> result = memberServiceImpl.getMemberDetail(MEMBER_ID); Mono<MemberDetailVO> result = memberServiceImpl.getMemberDetail(MEMBER_ID);
@@ -504,14 +504,14 @@ class MemberServiceImplTest {
}) })
.verifyComplete(); .verifyComplete();
verify(redisUtil).get(CACHE_DETAIL_KEY, MemberDetailVO.class); verify(cacheOperations).get(CACHE_DETAIL_KEY, MemberDetailVO.class);
verifyNoInteractions(memberRepository); verifyNoInteractions(memberRepository);
} }
@Test @Test
@DisplayName("缓存未命中时应从数据库查询并构建详情") @DisplayName("缓存未命中时应从数据库查询并构建详情")
void shouldQueryDatabaseAndBuildDetailWhenCacheMiss() { void shouldQueryDatabaseAndBuildDetailWhenCacheMiss() {
when(redisUtil.get(CACHE_DETAIL_KEY, MemberDetailVO.class)).thenReturn(Mono.empty()); when(cacheOperations.get(CACHE_DETAIL_KEY, MemberDetailVO.class)).thenReturn(Mono.empty());
Member member = createTestMember(); Member member = createTestMember();
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member)); when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member));
@@ -520,7 +520,7 @@ class MemberServiceImplTest {
when(memberRepository.findCardRecordsWithCardInfoByMemberId(MEMBER_ID)) when(memberRepository.findCardRecordsWithCardInfoByMemberId(MEMBER_ID))
.thenReturn(Flux.just(cardInfo)); .thenReturn(Flux.just(cardInfo));
when(redisUtil.setWithExpire(eq(CACHE_DETAIL_KEY), any(MemberDetailVO.class), anyLong())) when(cacheOperations.setWithExpire(eq(CACHE_DETAIL_KEY), any(MemberDetailVO.class), anyLong()))
.thenReturn(Mono.just(true)); .thenReturn(Mono.just(true));
Mono<MemberDetailVO> result = memberServiceImpl.getMemberDetail(MEMBER_ID); Mono<MemberDetailVO> result = memberServiceImpl.getMemberDetail(MEMBER_ID);
@@ -539,13 +539,13 @@ class MemberServiceImplTest {
verify(memberRepository).findById(MEMBER_ID); verify(memberRepository).findById(MEMBER_ID);
verify(memberRepository).findCardRecordsWithCardInfoByMemberId(MEMBER_ID); verify(memberRepository).findCardRecordsWithCardInfoByMemberId(MEMBER_ID);
verify(redisUtil).setWithExpire(eq(CACHE_DETAIL_KEY), any(MemberDetailVO.class), eq(300L)); verify(cacheOperations).setWithExpire(eq(CACHE_DETAIL_KEY), any(MemberDetailVO.class), eq(300L));
} }
@Test @Test
@DisplayName("会员不存在时应抛出 NotFoundException") @DisplayName("会员不存在时应抛出 NotFoundException")
void shouldThrowNotFoundExceptionWhenMemberNotFound() { void shouldThrowNotFoundExceptionWhenMemberNotFound() {
when(redisUtil.get(CACHE_DETAIL_KEY, MemberDetailVO.class)).thenReturn(Mono.empty()); when(cacheOperations.get(CACHE_DETAIL_KEY, MemberDetailVO.class)).thenReturn(Mono.empty());
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.empty()); when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.empty());
// zipWith 会订阅所有源,即使另一个是 error,也需要桩避免 NPE // zipWith 会订阅所有源,即使另一个是 error,也需要桩避免 NPE
lenient().when(memberRepository.findCardRecordsWithCardInfoByMemberId(MEMBER_ID)) lenient().when(memberRepository.findCardRecordsWithCardInfoByMemberId(MEMBER_ID))
@@ -577,8 +577,8 @@ class MemberServiceImplTest {
when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member)); when(memberRepository.findById(MEMBER_ID)).thenReturn(Mono.just(member));
when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> Mono.just(invocation.getArgument(0))); when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> Mono.just(invocation.getArgument(0)));
when(redisUtil.delete("member:info:" + MEMBER_ID)).thenReturn(Mono.just(1L)); when(cacheOperations.delete("member:info:" + MEMBER_ID)).thenReturn(Mono.just(1L));
when(redisUtil.delete("member:detail:" + MEMBER_ID)).thenReturn(Mono.just(1L)); when(cacheOperations.delete("member:detail:" + MEMBER_ID)).thenReturn(Mono.just(1L));
Mono<Boolean> result = memberServiceImpl.adminUpdateMemberInfo(MEMBER_ID, dto); Mono<Boolean> result = memberServiceImpl.adminUpdateMemberInfo(MEMBER_ID, dto);
@@ -589,8 +589,8 @@ class MemberServiceImplTest {
verify(memberRepository).findById(MEMBER_ID); verify(memberRepository).findById(MEMBER_ID);
verify(memberRepository).save(any(Member.class)); verify(memberRepository).save(any(Member.class));
verify(mockSyncer).sync(any(Member.class)); verify(mockSyncer).sync(any(Member.class));
verify(redisUtil).delete("member:info:" + MEMBER_ID); verify(cacheOperations).delete("member:info:" + MEMBER_ID);
verify(redisUtil).delete("member:detail:" + MEMBER_ID); verify(cacheOperations).delete("member:detail:" + MEMBER_ID);
} }
@Test @Test
@@ -1,6 +1,6 @@
package cn.novalon.gym.manage.payment.service.impl; package cn.novalon.gym.manage.payment.service.impl;
import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.payment.config.HuifuProperties; import cn.novalon.gym.manage.payment.config.HuifuProperties;
import cn.novalon.gym.manage.payment.dto.CreatePaymentRequest; import cn.novalon.gym.manage.payment.dto.CreatePaymentRequest;
import cn.novalon.gym.manage.payment.dto.PaymentResponse; import cn.novalon.gym.manage.payment.dto.PaymentResponse;
@@ -46,16 +46,16 @@ public class PaymentServiceImpl implements PaymentService {
private final HuifuProperties huifuProperties; private final HuifuProperties huifuProperties;
private final PaymentOrderRepository paymentOrderRepository; private final PaymentOrderRepository paymentOrderRepository;
private final RedisUtil redisUtil; private final CacheOperations cacheOperations;
private final PaymentNotifyService paymentNotifyService; private final PaymentNotifyService paymentNotifyService;
public PaymentServiceImpl(HuifuProperties huifuProperties, public PaymentServiceImpl(HuifuProperties huifuProperties,
PaymentOrderRepository paymentOrderRepository, PaymentOrderRepository paymentOrderRepository,
RedisUtil redisUtil, CacheOperations cacheOperations,
PaymentNotifyService paymentNotifyService) { PaymentNotifyService paymentNotifyService) {
this.huifuProperties = huifuProperties; this.huifuProperties = huifuProperties;
this.paymentOrderRepository = paymentOrderRepository; this.paymentOrderRepository = paymentOrderRepository;
this.redisUtil = redisUtil; this.cacheOperations = cacheOperations;
this.paymentNotifyService = paymentNotifyService; this.paymentNotifyService = paymentNotifyService;
} }
@@ -860,7 +860,7 @@ public class PaymentServiceImpl implements PaymentService {
} }
private Mono<Boolean> isTradeProcessed(String reqSeqId) { private Mono<Boolean> isTradeProcessed(String reqSeqId) {
return redisUtil.hasKey(PROCESSED_TRADE_PREFIX + reqSeqId) return cacheOperations.hasKey(PROCESSED_TRADE_PREFIX + reqSeqId)
.onErrorResume(e -> { .onErrorResume(e -> {
log.error("[Huifu] 检查交易处理状态异常: reqSeqId={}", reqSeqId, e); log.error("[Huifu] 检查交易处理状态异常: reqSeqId={}", reqSeqId, e);
return Mono.just(false); return Mono.just(false);
@@ -868,7 +868,7 @@ public class PaymentServiceImpl implements PaymentService {
} }
private Mono<Void> markTradeProcessed(String reqSeqId) { private Mono<Void> markTradeProcessed(String reqSeqId) {
return redisUtil.setWithExpire(PROCESSED_TRADE_PREFIX + reqSeqId, "1", IDEMPOTENT_EXPIRE_SECONDS) return cacheOperations.setWithExpire(PROCESSED_TRADE_PREFIX + reqSeqId, "1", IDEMPOTENT_EXPIRE_SECONDS)
.onErrorResume(e -> { .onErrorResume(e -> {
log.error("[Huifu] 标记交易处理状态异常: reqSeqId={}", reqSeqId, e); log.error("[Huifu] 标记交易处理状态异常: reqSeqId={}", reqSeqId, e);
return Mono.empty(); return Mono.empty();
@@ -1,6 +1,6 @@
package cn.novalon.gym.manage.payment.service.impl; package cn.novalon.gym.manage.payment.service.impl;
import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.payment.config.HuifuProperties; import cn.novalon.gym.manage.payment.config.HuifuProperties;
import cn.novalon.gym.manage.payment.dto.CreatePaymentRequest; import cn.novalon.gym.manage.payment.dto.CreatePaymentRequest;
import cn.novalon.gym.manage.payment.dto.PaymentResponse; import cn.novalon.gym.manage.payment.dto.PaymentResponse;
@@ -35,7 +35,7 @@ class PaymentServiceImplTest {
private PaymentOrderRepository paymentOrderRepository; private PaymentOrderRepository paymentOrderRepository;
@Mock @Mock
private RedisUtil redisUtil; private CacheOperations cacheOperations;
@Mock @Mock
private PaymentNotifyService paymentNotifyService; private PaymentNotifyService paymentNotifyService;
@@ -58,7 +58,7 @@ class PaymentServiceImplTest {
lenient().when(huifuProperties.isSdkInitialized()).thenReturn(false); lenient().when(huifuProperties.isSdkInitialized()).thenReturn(false);
paymentService = new PaymentServiceImpl(huifuProperties, paymentOrderRepository, redisUtil, paymentNotifyService); paymentService = new PaymentServiceImpl(huifuProperties, paymentOrderRepository, cacheOperations, paymentNotifyService);
} }
private PaymentOrder createTestOrder(String status) { private PaymentOrder createTestOrder(String status) {
@@ -369,7 +369,7 @@ class PaymentServiceImplTest {
params.put("resp_data", "{\"req_seq_id\":\"REQ123\",\"hf_seq_id\":\"HF123\",\"trans_stat\":\"S\"}"); params.put("resp_data", "{\"req_seq_id\":\"REQ123\",\"hf_seq_id\":\"HF123\",\"trans_stat\":\"S\"}");
params.put("sign", "testSign"); params.put("sign", "testSign");
when(redisUtil.hasKey("huifu:processed:trade:REQ123")).thenReturn(Mono.just(true)); when(cacheOperations.hasKey("huifu:processed:trade:REQ123")).thenReturn(Mono.just(true));
Mono<String> result = paymentService.handleAlipayNotify(params); Mono<String> result = paymentService.handleAlipayNotify(params);
@@ -388,10 +388,10 @@ class PaymentServiceImplTest {
PaymentOrder order = createTestOrder("PENDING"); PaymentOrder order = createTestOrder("PENDING");
when(redisUtil.hasKey("huifu:processed:trade:REQ123")).thenReturn(Mono.just(false)); when(cacheOperations.hasKey("huifu:processed:trade:REQ123")).thenReturn(Mono.just(false));
when(paymentOrderRepository.findByReqSeqId("REQ123")).thenReturn(Mono.just(order)); when(paymentOrderRepository.findByReqSeqId("REQ123")).thenReturn(Mono.just(order));
when(paymentOrderRepository.save(any(PaymentOrder.class))).thenReturn(Mono.just(order)); when(paymentOrderRepository.save(any(PaymentOrder.class))).thenReturn(Mono.just(order));
when(redisUtil.setWithExpire("huifu:processed:trade:REQ123", "1", 86400)).thenReturn(Mono.just(true)); when(cacheOperations.setWithExpire("huifu:processed:trade:REQ123", "1", 86400)).thenReturn(Mono.just(true));
when(paymentNotifyService.notifyPaymentStatus(order.getOrderNo(), "SUCCESS", MEMBER_ID)).thenReturn(Mono.empty()); when(paymentNotifyService.notifyPaymentStatus(order.getOrderNo(), "SUCCESS", MEMBER_ID)).thenReturn(Mono.empty());
Mono<String> result = paymentService.handleAlipayNotify(params); Mono<String> result = paymentService.handleAlipayNotify(params);
@@ -401,7 +401,7 @@ class PaymentServiceImplTest {
.verifyComplete(); .verifyComplete();
verify(paymentOrderRepository).save(any(PaymentOrder.class)); verify(paymentOrderRepository).save(any(PaymentOrder.class));
verify(redisUtil).setWithExpire("huifu:processed:trade:REQ123", "1", 86400); verify(cacheOperations).setWithExpire("huifu:processed:trade:REQ123", "1", 86400);
verify(paymentNotifyService).notifyPaymentStatus(order.getOrderNo(), "SUCCESS", MEMBER_ID); verify(paymentNotifyService).notifyPaymentStatus(order.getOrderNo(), "SUCCESS", MEMBER_ID);
} }
@@ -413,10 +413,10 @@ class PaymentServiceImplTest {
PaymentOrder order = createTestOrder("PENDING"); PaymentOrder order = createTestOrder("PENDING");
when(redisUtil.hasKey("huifu:processed:trade:REQ123")).thenReturn(Mono.just(false)); when(cacheOperations.hasKey("huifu:processed:trade:REQ123")).thenReturn(Mono.just(false));
when(paymentOrderRepository.findByReqSeqId("REQ123")).thenReturn(Mono.just(order)); when(paymentOrderRepository.findByReqSeqId("REQ123")).thenReturn(Mono.just(order));
when(paymentOrderRepository.save(any(PaymentOrder.class))).thenReturn(Mono.just(order)); when(paymentOrderRepository.save(any(PaymentOrder.class))).thenReturn(Mono.just(order));
when(redisUtil.setWithExpire("huifu:processed:trade:REQ123", "1", 86400)).thenReturn(Mono.just(true)); when(cacheOperations.setWithExpire("huifu:processed:trade:REQ123", "1", 86400)).thenReturn(Mono.just(true));
when(paymentNotifyService.notifyPaymentStatus(order.getOrderNo(), "FAIL", MEMBER_ID)).thenReturn(Mono.empty()); when(paymentNotifyService.notifyPaymentStatus(order.getOrderNo(), "FAIL", MEMBER_ID)).thenReturn(Mono.empty());
Mono<String> result = paymentService.handleAlipayNotify(params); Mono<String> result = paymentService.handleAlipayNotify(params);
@@ -426,7 +426,7 @@ class PaymentServiceImplTest {
.verifyComplete(); .verifyComplete();
verify(paymentOrderRepository).save(any(PaymentOrder.class)); verify(paymentOrderRepository).save(any(PaymentOrder.class));
verify(redisUtil).setWithExpire("huifu:processed:trade:REQ123", "1", 86400); verify(cacheOperations).setWithExpire("huifu:processed:trade:REQ123", "1", 86400);
verify(paymentNotifyService).notifyPaymentStatus(order.getOrderNo(), "FAIL", MEMBER_ID); verify(paymentNotifyService).notifyPaymentStatus(order.getOrderNo(), "FAIL", MEMBER_ID);
} }
@@ -438,7 +438,7 @@ class PaymentServiceImplTest {
PaymentOrder order = createTestOrder("PENDING"); PaymentOrder order = createTestOrder("PENDING");
when(redisUtil.hasKey("huifu:processed:trade:REQ123")).thenReturn(Mono.just(false)); when(cacheOperations.hasKey("huifu:processed:trade:REQ123")).thenReturn(Mono.just(false));
when(paymentOrderRepository.findByReqSeqId("REQ123")).thenReturn(Mono.just(order)); when(paymentOrderRepository.findByReqSeqId("REQ123")).thenReturn(Mono.just(order));
when(paymentNotifyService.notifyPaymentStatus(order.getOrderNo(), "PENDING", MEMBER_ID)).thenReturn(Mono.empty()); when(paymentNotifyService.notifyPaymentStatus(order.getOrderNo(), "PENDING", MEMBER_ID)).thenReturn(Mono.empty());
@@ -458,7 +458,7 @@ class PaymentServiceImplTest {
params.put("resp_data", "{\"req_seq_id\":\"REQ123\",\"hf_seq_id\":\"HF123\",\"trans_stat\":\"S\"}"); params.put("resp_data", "{\"req_seq_id\":\"REQ123\",\"hf_seq_id\":\"HF123\",\"trans_stat\":\"S\"}");
params.put("sign", "testSign"); params.put("sign", "testSign");
when(redisUtil.hasKey("huifu:processed:trade:REQ123")).thenReturn(Mono.just(false)); when(cacheOperations.hasKey("huifu:processed:trade:REQ123")).thenReturn(Mono.just(false));
when(paymentOrderRepository.findByReqSeqId("REQ123")).thenReturn(Mono.empty()); when(paymentOrderRepository.findByReqSeqId("REQ123")).thenReturn(Mono.empty());
Mono<String> result = paymentService.handleAlipayNotify(params); Mono<String> result = paymentService.handleAlipayNotify(params);
@@ -474,7 +474,7 @@ class PaymentServiceImplTest {
params.put("resp_data", "{\"req_seq_id\":\"REQ123\",\"hf_seq_id\":\"HF123\",\"trans_stat\":\"S\"}"); params.put("resp_data", "{\"req_seq_id\":\"REQ123\",\"hf_seq_id\":\"HF123\",\"trans_stat\":\"S\"}");
params.put("sign", "testSign"); params.put("sign", "testSign");
when(redisUtil.hasKey("huifu:processed:trade:REQ123")).thenReturn(Mono.just(false)); when(cacheOperations.hasKey("huifu:processed:trade:REQ123")).thenReturn(Mono.just(false));
when(paymentOrderRepository.findByReqSeqId("REQ123")).thenReturn(Mono.error(new RuntimeException("DB error"))); when(paymentOrderRepository.findByReqSeqId("REQ123")).thenReturn(Mono.error(new RuntimeException("DB error")));
Mono<String> result = paymentService.handleAlipayNotify(params); Mono<String> result = paymentService.handleAlipayNotify(params);
@@ -1,6 +1,10 @@
server: server:
port: 8084 port: 8084
gym:
cache:
type: redis
spring: spring:
aop: aop:
proxy-target-class: true 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.config.DCloudUniverifyConfig;
import cn.novalon.gym.manage.auth.service.PhoneAuthService; import cn.novalon.gym.manage.auth.service.PhoneAuthService;
import cn.novalon.gym.manage.auth.service.SmsService; 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.member.es.repository.MemberESRepository;
import cn.novalon.gym.manage.payment.config.HuifuProperties; import cn.novalon.gym.manage.payment.config.HuifuProperties;
import cn.novalon.gym.manage.payment.service.PaymentNotifyService; import cn.novalon.gym.manage.payment.service.PaymentNotifyService;
@@ -74,9 +74,9 @@ public abstract class BaseContractTest {
@MockBean @MockBean
protected ReactiveStringRedisTemplate reactiveStringRedisTemplate; protected ReactiveStringRedisTemplate reactiveStringRedisTemplate;
/** Mock RedisUtil — 阻止 Handler 中的 Redis 操作导致异常 */ /** Mock CacheOperations — 阻止 Handler 中的 Redis 操作导致异常 */
@MockBean @MockBean
protected RedisUtil redisUtil; protected CacheOperations cacheOperations;
/** Mock ES Repository — 防止连接 Elasticsearch */ /** Mock ES Repository — 防止连接 Elasticsearch */
@MockBean @MockBean
@@ -1,7 +1,7 @@
package cn.novalon.gym.manage.app.integration; package cn.novalon.gym.manage.app.integration;
import cn.novalon.gym.manage.app.contract.BaseContractTest; 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.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -26,13 +26,13 @@ import static org.mockito.Mockito.when;
class GroupCourseHandlerIntegrationTest extends BaseContractTest { class GroupCourseHandlerIntegrationTest extends BaseContractTest {
@Autowired @Autowired
private RedisUtil redisUtil; private CacheOperations cacheOperations;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
// Mock Redis 操作避免 GroupCourseService 中的 Redis 缓存逻辑导致 NPE // Mock Redis 操作避免 GroupCourseService 中的 Redis 缓存逻辑导致 NPE
when(redisUtil.get(anyString(), any())).thenReturn(Mono.empty()); when(cacheOperations.get(anyString(), any())).thenReturn(Mono.empty());
when(redisUtil.setWithExpire(anyString(), any(), anyLong())).thenReturn(Mono.just(true)); when(cacheOperations.setWithExpire(anyString(), any(), anyLong())).thenReturn(Mono.just(true));
} }
@Test @Test
@@ -1,7 +1,7 @@
package cn.novalon.gym.manage.app.integration; package cn.novalon.gym.manage.app.integration;
import cn.novalon.gym.manage.app.contract.BaseContractTest; 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.entity.Member;
import cn.novalon.gym.manage.member.es.entity.MemberES; import cn.novalon.gym.manage.member.es.entity.MemberES;
import cn.novalon.gym.manage.member.es.repository.MemberESRepository; import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
@@ -70,10 +70,10 @@ class MemberHandlerIntegrationTest extends BaseContractTest {
memberRepository.deleteAll().block(); memberRepository.deleteAll().block();
when(authUtil.getMemberIdOrThrow(any())).thenReturn(1L); when(authUtil.getMemberIdOrThrow(any())).thenReturn(1L);
// Mock Redis 缓存未命中确保每次查询都走数据库 // Mock Redis 缓存未命中确保每次查询都走数据库
when(redisUtil.get(anyString(), any())).thenReturn(Mono.empty()); when(cacheOperations.get(anyString(), any())).thenReturn(Mono.empty());
when(redisUtil.setWithExpire(anyString(), any(), anyLong())).thenReturn(Mono.just(true)); when(cacheOperations.setWithExpire(anyString(), any(), anyLong())).thenReturn(Mono.just(true));
// Mock Redis 删除操作clearMemberCache 调用防止 NPE 导致 adminUpdateMemberInfo 返回 false // 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 // Mock ES 搜索返回空结果防止因 ES 不可用导致 500
when(memberESRepository.findByMemberNoOrPhoneOrNicknameContaining(anyString(), anyString(), anyString(), any())) when(memberESRepository.findByMemberNoOrPhoneOrNicknameContaining(anyString(), anyString(), anyString(), any()))
.thenReturn(Flux.empty()); .thenReturn(Flux.empty());
+4
View File
@@ -60,6 +60,10 @@
<groupId>org.springframework.data</groupId> <groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId> <artifactId>spring-data-redis</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -0,0 +1,21 @@
package cn.novalon.gym.manage.common.cache;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* 缓存自动配置
* <p>
* 启用 {@link CacheProperties} 配置绑定并扫描 {@code cn.novalon.gym.manage.common.cache}
* 包下的缓存实现组件{@link RedisCacheOperations} / {@link CaffeineCacheOperations}
* </p>
*
* @author 张翔
* @date 2026-08-02
*/
@Configuration
@EnableConfigurationProperties(CacheProperties.class)
@ComponentScan(basePackageClasses = CacheAutoConfiguration.class)
public class CacheAutoConfiguration {
}
@@ -0,0 +1,88 @@
package cn.novalon.gym.manage.common.cache;
import reactor.core.publisher.Mono;
/**
* 缓存操作接口
* <p>
* 定义统一的缓存操作抽象支持通过配置切换不同的缓存实现Caffeine / Redis
* 所有业务模块应依赖此接口而非具体实现实现类由 {@link CacheAutoConfiguration} 按配置自动装配
* </p>
*
* @author 张翔
* @date 2026-08-02
* @see RedisCacheOperations
* @see CaffeineCacheOperations
*/
public interface CacheOperations {
/**
* 获取缓存值带类型转换
*
* @param key 缓存键
* @param clazz 目标类型
* @param <T> 返回值类型
* @return 缓存值不存在时返回 {@link Mono#empty()}
*/
<T> Mono<T> get(String key, Class<T> clazz);
/**
* 获取缓存值返回 Object
*
* @param key 缓存键
* @return 缓存值不存在时返回 {@link Mono#empty()}
*/
Mono<Object> get(String key);
/**
* 设置缓存值无过期时间
*
* @param key 缓存键
* @param value 缓存值
* @return true 设置成功
*/
Mono<Boolean> set(String key, Object value);
/**
* 设置缓存值并指定过期时间
*
* @param key 缓存键
* @param value 缓存值
* @param timeoutSeconds 过期时间
* @return true 设置成功
*/
Mono<Boolean> setWithExpire(String key, Object value, long timeoutSeconds);
/**
* 删除缓存
*
* @param key 缓存键
* @return 被删除的键数量
*/
Mono<Long> delete(String key);
/**
* 判断缓存键是否存在
*
* @param key 缓存键
* @return true 存在
*/
Mono<Boolean> hasKey(String key);
/**
* 设置过期时间
*
* @param key 缓存键
* @param timeoutSeconds 过期时间
* @return true 设置成功
*/
Mono<Boolean> expire(String key, long timeoutSeconds);
/**
* 根据通配符模式删除缓存键
*
* @param pattern 通配符模式 {@code member:info:*}
* @return 被删除的键数量
*/
Mono<Long> deleteByPattern(String pattern);
}
@@ -0,0 +1,88 @@
package cn.novalon.gym.manage.common.cache;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 缓存配置属性
* <p>
* 通过 {@code gym.cache.type} 配置选择缓存实现
* <ul>
* <li>{@code redis} 使用 Redis 实现默认</li>
* <li>{@code caffeine} 使用 Caffeine 本地缓存实现</li>
* </ul>
* </p>
*
* <pre>{@code
* gym:
* cache:
* type: redis # redis | caffeine
* caffeine:
* initial-capacity: 100
* max-size: 10000
* expire-after-write-seconds: 300
* }</pre>
*
* @author 张翔
* @date 2026-08-02
*/
@ConfigurationProperties(prefix = "gym.cache")
public class CacheProperties {
/** 缓存类型:redis / caffeine,默认 redis */
private String type = "redis";
/** Caffeine 本地缓存专属配置 */
private Caffeine caffeine = new Caffeine();
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Caffeine getCaffeine() {
return caffeine;
}
public void setCaffeine(Caffeine caffeine) {
this.caffeine = caffeine;
}
public static class Caffeine {
/** 初始容量 */
private int initialCapacity = 100;
/** 最大条数 */
private long maxSize = 10000;
/** 写入后过期时间(秒),默认 5 分钟 */
private long expireAfterWriteSeconds = 300;
public int getInitialCapacity() {
return initialCapacity;
}
public void setInitialCapacity(int initialCapacity) {
this.initialCapacity = initialCapacity;
}
public long getMaxSize() {
return maxSize;
}
public void setMaxSize(long maxSize) {
this.maxSize = maxSize;
}
public long getExpireAfterWriteSeconds() {
return expireAfterWriteSeconds;
}
public void setExpireAfterWriteSeconds(long expireAfterWriteSeconds) {
this.expireAfterWriteSeconds = expireAfterWriteSeconds;
}
}
}
@@ -0,0 +1,111 @@
package cn.novalon.gym.manage.common.cache;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
* Caffeine 本地缓存实现
* <p>
* 基于 Caffeine 的进程内缓存适合单机部署或开发测试环境
* {@code gym.cache.type=caffeine} 时生效
* </p>
*
* @author 张翔
* @date 2026-08-02
* @see CacheOperations
* @see RedisCacheOperations
*/
@Component
@ConditionalOnProperty(name = "gym.cache.type", havingValue = "caffeine")
public class CaffeineCacheOperations implements CacheOperations {
private static final Logger log = LoggerFactory.getLogger(CaffeineCacheOperations.class);
private final Cache<String, Object> cache;
public CaffeineCacheOperations(CacheProperties cacheProperties) {
CacheProperties.Caffeine config = cacheProperties.getCaffeine();
this.cache = Caffeine.newBuilder()
.initialCapacity(config.getInitialCapacity())
.maximumSize(config.getMaxSize())
.expireAfterWrite(config.getExpireAfterWriteSeconds(), TimeUnit.SECONDS)
.recordStats()
.build();
log.info("Caffeine 缓存已初始化: initialCapacity={}, maxSize={}, expireAfterWrite={}s",
config.getInitialCapacity(), config.getMaxSize(), config.getExpireAfterWriteSeconds());
}
@Override
@SuppressWarnings("unchecked")
public <T> Mono<T> get(String key, Class<T> clazz) {
Object value = cache.getIfPresent(key);
if (value == null) {
return Mono.empty();
}
if (clazz.isInstance(value)) {
return Mono.just((T) value);
}
log.warn("Caffeine 缓存类型不匹配: key={}, expected={}, actual={}",
key, clazz.getName(), value.getClass().getName());
return Mono.empty();
}
@Override
public Mono<Object> get(String key) {
Object value = cache.getIfPresent(key);
if (value == null) {
return Mono.empty();
}
return Mono.just(value);
}
@Override
public Mono<Boolean> set(String key, Object value) {
cache.put(key, value);
return Mono.just(true);
}
@Override
public Mono<Boolean> setWithExpire(String key, Object value, long timeoutSeconds) {
// Caffeine 在构建时已指定 expireAfterWrite此处仅做 put 操作
// 如需 per-entry TTL可考虑 Caffeine Expiry 接口但当前场景统一 TTL 已足够
cache.put(key, value);
return Mono.just(true);
}
@Override
public Mono<Long> delete(String key) {
cache.invalidate(key);
return Mono.just(1L);
}
@Override
public Mono<Boolean> hasKey(String key) {
return Mono.just(cache.getIfPresent(key) != null);
}
@Override
public Mono<Boolean> expire(String key, long timeoutSeconds) {
// Caffeine 不支持为单个 key 动态设置过期时间直接返回 true过期由构建时策略统一管理
log.debug("Caffeine 不支持动态设置过期时间,忽略: key={}, timeoutSeconds={}", key, timeoutSeconds);
return Mono.just(true);
}
@Override
public Mono<Long> deleteByPattern(String pattern) {
Pattern regex = Pattern.compile(pattern.replace("*", ".*"));
long count = cache.asMap().keySet().stream()
.filter(k -> regex.matcher(k).matches())
.peek(cache::invalidate)
.count();
return Mono.just(count);
}
}
@@ -0,0 +1,83 @@
package cn.novalon.gym.manage.common.cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.time.Duration;
/**
* Redis 缓存实现
* <p>
* 基于 {@link ReactiveRedisTemplate} 的响应式 Redis 缓存操作
* {@code gym.cache.type=redis} 时生效默认值
* </p>
*
* @author 张翔
* @date 2026-08-02
* @see CacheOperations
* @see CaffeineCacheOperations
*/
@Component
@ConditionalOnProperty(name = "gym.cache.type", havingValue = "redis", matchIfMissing = true)
public class RedisCacheOperations implements CacheOperations {
private static final Logger log = LoggerFactory.getLogger(RedisCacheOperations.class);
private final ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
public RedisCacheOperations(ReactiveRedisTemplate<String, Object> reactiveRedisTemplate) {
this.reactiveRedisTemplate = reactiveRedisTemplate;
}
@Override
public <T> Mono<T> get(String key, Class<T> clazz) {
return reactiveRedisTemplate.opsForValue().get(key)
.filter(clazz::isInstance)
.map(clazz::cast)
.onErrorResume(e -> {
log.warn("读取 Redis 缓存失败(可能为旧格式): key={}, error={}", key, e.getMessage());
return Mono.empty();
});
}
@Override
public Mono<Object> get(String key) {
return reactiveRedisTemplate.opsForValue().get(key);
}
@Override
public Mono<Boolean> set(String key, Object value) {
return reactiveRedisTemplate.opsForValue().set(key, value);
}
@Override
public Mono<Boolean> setWithExpire(String key, Object value, long timeoutSeconds) {
return reactiveRedisTemplate.opsForValue().set(key, value, Duration.ofSeconds(timeoutSeconds));
}
@Override
public Mono<Long> delete(String key) {
return reactiveRedisTemplate.delete(key);
}
@Override
public Mono<Boolean> hasKey(String key) {
return reactiveRedisTemplate.hasKey(key);
}
@Override
public Mono<Boolean> expire(String key, long timeoutSeconds) {
return reactiveRedisTemplate.expire(key, Duration.ofSeconds(timeoutSeconds));
}
@Override
public Mono<Long> deleteByPattern(String pattern) {
return reactiveRedisTemplate.keys(pattern)
.flatMap(keys -> reactiveRedisTemplate.delete(keys))
.count();
}
}
@@ -1 +1 @@
cn.novalon.gym.manage.common.config.JwtProperties cn.novalon.gym.manage.common.cache.CacheAutoConfiguration
@@ -1,5 +1,6 @@
package cn.novalon.gym.manage.gateway.cache; package cn.novalon.gym.manage.gateway.cache;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -12,15 +13,10 @@ import java.util.concurrent.ConcurrentHashMap;
/** /**
* 请求缓存服务 * 请求缓存服务
* * <p>
* 文件定义实现网关请求的缓存机制 * 底层缓存存储委托给 {@link CacheOperations} 接口支持通过配置切换 Redis / Caffeine 实现
* 涉及业务响应缓存缓存失效缓存统计 * 请求级别的缓存键生成命中统计等逻辑保留在此服务中
* * </p>
* 核心功能
* 1. 请求响应缓存
* 2. 缓存键生成
* 3. 缓存失效管理
* 4. 缓存统计
* *
* @author 张翔 * @author 张翔
* @date 2026-03-26 * @date 2026-03-26
@@ -30,35 +26,33 @@ public class RequestCacheService {
private static final Logger logger = LoggerFactory.getLogger(RequestCacheService.class); private static final Logger logger = LoggerFactory.getLogger(RequestCacheService.class);
private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>(); private final CacheOperations cacheOperations;
private final Map<String, CacheStats> stats = new ConcurrentHashMap<>(); private final Map<String, CacheStats> stats = new ConcurrentHashMap<>();
private boolean cacheEnabled = true; private boolean cacheEnabled = true;
private Duration defaultTtl = Duration.ofMinutes(5); private Duration defaultTtl = Duration.ofMinutes(5);
private int maxCacheSize = 10000; private int maxCacheSize = 10000;
public RequestCacheService(CacheOperations cacheOperations) {
this.cacheOperations = cacheOperations;
}
public Mono<String> get(ServerHttpRequest request) { public Mono<String> get(ServerHttpRequest request) {
if (!cacheEnabled) { if (!cacheEnabled) {
return Mono.empty(); return Mono.empty();
} }
String cacheKey = generateCacheKey(request); String cacheKey = generateCacheKey(request);
CacheEntry entry = cache.get(cacheKey);
if (entry == null) {
recordMiss(cacheKey);
return Mono.empty();
}
if (isExpired(entry)) {
cache.remove(cacheKey);
recordMiss(cacheKey);
return Mono.empty();
}
return cacheOperations.get(cacheKey, String.class)
.doOnNext(value -> {
recordHit(cacheKey); recordHit(cacheKey);
logger.debug("Cache hit for key: {}", cacheKey); logger.debug("Cache hit for key: {}", cacheKey);
return Mono.just(entry.getValue()); })
.switchIfEmpty(Mono.defer(() -> {
recordMiss(cacheKey);
return Mono.empty();
}));
} }
public void put(ServerHttpRequest request, String response) { public void put(ServerHttpRequest request, String response) {
@@ -67,37 +61,31 @@ public class RequestCacheService {
} }
String cacheKey = generateCacheKey(request); String cacheKey = generateCacheKey(request);
cacheOperations.setWithExpire(cacheKey, response, defaultTtl.toSeconds())
if (cache.size() >= maxCacheSize) { .doOnSuccess(result -> logger.debug("Cached response for key: {}", cacheKey))
evictOldestEntries(); .subscribe();
}
CacheEntry entry = new CacheEntry(
response,
System.currentTimeMillis(),
defaultTtl.toMillis()
);
cache.put(cacheKey, entry);
logger.debug("Cached response for key: {}", cacheKey);
} }
public void evict(ServerHttpRequest request) { public void evict(ServerHttpRequest request) {
String cacheKey = generateCacheKey(request); String cacheKey = generateCacheKey(request);
cache.remove(cacheKey); cacheOperations.delete(cacheKey)
logger.debug("Evicted cache for key: {}", cacheKey); .doOnSuccess(result -> logger.debug("Evicted cache for key: {}", cacheKey))
.subscribe();
} }
public void evictByPattern(String pattern) { public void evictByPattern(String pattern) {
cache.keySet().removeIf(key -> key.matches(pattern)); cacheOperations.deleteByPattern(pattern)
logger.info("Evicted cache entries matching pattern: {}", pattern); .doOnSuccess(count -> logger.info("Evicted {} cache entries matching pattern: {}", count, pattern))
.subscribe();
} }
public void clear() { public void clear() {
int size = cache.size(); cacheOperations.deleteByPattern(".*")
cache.clear(); .doOnSuccess(count -> {
stats.clear(); stats.clear();
logger.info("Cleared all cache entries. Removed {} entries", size); logger.info("Cleared all cache entries. Removed {} entries", count);
})
.subscribe();
} }
private String generateCacheKey(ServerHttpRequest request) { private String generateCacheKey(ServerHttpRequest request) {
@@ -106,7 +94,7 @@ public class RequestCacheService {
String query = request.getURI().getQuery(); String query = request.getURI().getQuery();
StringBuilder keyBuilder = new StringBuilder(); StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(method).append(":").append(path); keyBuilder.append("gateway:").append(method).append(":").append(path);
if (query != null && !query.isEmpty()) { if (query != null && !query.isEmpty()) {
keyBuilder.append("?").append(query); keyBuilder.append("?").append(query);
@@ -115,25 +103,6 @@ public class RequestCacheService {
return keyBuilder.toString(); return keyBuilder.toString();
} }
private boolean isExpired(CacheEntry entry) {
long currentTime = System.currentTimeMillis();
return (currentTime - entry.getCreatedAt()) > entry.getTtl();
}
private void evictOldestEntries() {
int entriesToRemove = maxCacheSize / 10;
cache.entrySet().stream()
.sorted((e1, e2) ->
Long.compare(e1.getValue().getCreatedAt(),
e2.getValue().getCreatedAt()))
.limit(entriesToRemove)
.map(Map.Entry::getKey)
.forEach(cache::remove);
logger.info("Evicted {} oldest cache entries", entriesToRemove);
}
private void recordHit(String cacheKey) { private void recordHit(String cacheKey) {
stats.compute(cacheKey, (key, stat) -> { stats.compute(cacheKey, (key, stat) -> {
if (stat == null) { if (stat == null) {
@@ -154,10 +123,6 @@ public class RequestCacheService {
}); });
} }
public int getCacheSize() {
return cache.size();
}
public long getHitCount() { public long getHitCount() {
return stats.values().stream() return stats.values().stream()
.mapToLong(CacheStats::getHits) .mapToLong(CacheStats::getHits)
@@ -197,30 +162,6 @@ public class RequestCacheService {
logger.info("Max cache size set to: {}", maxSize); logger.info("Max cache size set to: {}", maxSize);
} }
private static class CacheEntry {
private final String value;
private final long createdAt;
private final long ttl;
public CacheEntry(String value, long createdAt, long ttl) {
this.value = value;
this.createdAt = createdAt;
this.ttl = ttl;
}
public String getValue() {
return value;
}
public long getCreatedAt() {
return createdAt;
}
public long getTtl() {
return ttl;
}
}
private static class CacheStats { private static class CacheStats {
private long hits = 0; private long hits = 0;
private long misses = 0; private long misses = 0;
@@ -64,6 +64,11 @@ rate:
limit-refresh-period: ${RATE_LIMIT_USER_PERIOD:1s} limit-refresh-period: ${RATE_LIMIT_USER_PERIOD:1s}
timeout-duration: ${RATE_LIMIT_USER_TIMEOUT:0} timeout-duration: ${RATE_LIMIT_USER_TIMEOUT:0}
# 缓存配置(网关使用本地 Caffeine 缓存,无需 Redis
gym:
cache:
type: caffeine
signature: signature:
enabled: ${SIGNATURE_ENABLED:true} enabled: ${SIGNATURE_ENABLED:true}
secret: ${SIGNATURE_SECRET:NovalonManageSystemSecretKey2026} secret: ${SIGNATURE_SECRET:NovalonManageSystemSecretKey2026}
@@ -1,5 +1,8 @@
package cn.novalon.gym.manage.gateway.cache; package cn.novalon.gym.manage.gateway.cache;
import cn.novalon.gym.manage.common.cache.CacheOperations;
import cn.novalon.gym.manage.common.cache.CaffeineCacheOperations;
import cn.novalon.gym.manage.common.cache.CacheProperties;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -14,10 +17,14 @@ import static org.junit.jupiter.api.Assertions.*;
class RequestCacheServiceTest { class RequestCacheServiceTest {
private RequestCacheService cacheService; private RequestCacheService cacheService;
private CacheOperations cacheOperations;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
cacheService = new RequestCacheService(); CacheProperties cacheProperties = new CacheProperties();
cacheProperties.setType("caffeine");
cacheOperations = new CaffeineCacheOperations(cacheProperties);
cacheService = new RequestCacheService(cacheOperations);
} }
@Test @Test
@@ -52,7 +59,6 @@ class RequestCacheServiceTest {
String response = "{\"data\":\"test\"}"; String response = "{\"data\":\"test\"}";
cacheService.put(request, response); cacheService.put(request, response);
cacheService.evict(request); cacheService.evict(request);
StepVerifier.create(cacheService.get(request)) StepVerifier.create(cacheService.get(request))
@@ -71,9 +77,13 @@ class RequestCacheServiceTest {
cacheService.put(request1, "response1"); cacheService.put(request1, "response1");
cacheService.put(request2, "response2"); cacheService.put(request2, "response2");
cacheService.evictByPattern("GET:/api/test.*"); cacheService.evictByPattern(".*/api/test.*");
assertEquals(0, cacheService.getCacheSize()); // 验证缓存已清空
StepVerifier.create(cacheService.get(request1))
.verifyComplete();
StepVerifier.create(cacheService.get(request2))
.verifyComplete();
} }
@Test @Test
@@ -90,7 +100,11 @@ class RequestCacheServiceTest {
cacheService.clear(); cacheService.clear();
assertEquals(0, cacheService.getCacheSize()); // 验证缓存已清空
StepVerifier.create(cacheService.get(request1))
.verifyComplete();
StepVerifier.create(cacheService.get(request2))
.verifyComplete();
} }
@Test @Test
@@ -105,8 +119,6 @@ class RequestCacheServiceTest {
StepVerifier.create(cacheService.get(request)) StepVerifier.create(cacheService.get(request))
.verifyComplete(); .verifyComplete();
assertEquals(0, cacheService.getCacheSize());
} }
@Test @Test
@@ -140,20 +152,6 @@ class RequestCacheServiceTest {
assertEquals(0.0, cacheService.getHitRate()); assertEquals(0.0, cacheService.getHitRate());
} }
@Test
void testMaxCacheSize() {
cacheService.setMaxCacheSize(5);
for (int i = 0; i < 10; i++) {
ServerHttpRequest request = MockServerHttpRequest
.get("/api/test" + i)
.build();
cacheService.put(request, "response" + i);
}
assertTrue(cacheService.getCacheSize() <= 10);
}
@Test @Test
void testCacheWithQueryParams() { void testCacheWithQueryParams() {
ServerHttpRequest request = MockServerHttpRequest ServerHttpRequest request = MockServerHttpRequest