You've already forked FrameTour-BE
- 将 PriceCacheService 类中的 CACHE_DURATION常量值从 5 分钟修改为 10 分钟 - 此修改旨在减少缓存刷新频率,提高系统性能
238 lines
8.9 KiB
Java
238 lines
8.9 KiB
Java
package com.ycwl.basic.service;
|
|
|
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.ycwl.basic.pricing.dto.PriceCalculationResult;
|
|
import com.ycwl.basic.pricing.dto.ProductItem;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
import java.security.MessageDigest;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.time.Duration;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* 价格缓存服务
|
|
* 使用 Redis 实现价格查询结果的缓存,缓存时间为5分钟
|
|
*/
|
|
@Slf4j
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
public class PriceCacheService {
|
|
|
|
private final StringRedisTemplate redisTemplate;
|
|
private final ObjectMapper objectMapper;
|
|
|
|
// 缓存Key前缀
|
|
private static final String CACHE_PREFIX = "pricing:cache:";
|
|
|
|
// 缓存过期时间:5分钟
|
|
private static final Duration CACHE_DURATION = Duration.ofMinutes(10);
|
|
|
|
/**
|
|
* 生成缓存键
|
|
* 格式:pricing:cache:{userId}:{scenicId}:{productsHash}
|
|
*
|
|
* @param userId 用户ID
|
|
* @param scenicId 景区ID
|
|
* @param products 商品列表
|
|
* @return 缓存键
|
|
*/
|
|
public String generateCacheKey(Long userId, Long scenicId, List<ProductItem> products) {
|
|
String productsHash = calculateProductsHash(products);
|
|
return CACHE_PREFIX + userId + ":" + scenicId + ":" + productsHash;
|
|
}
|
|
|
|
/**
|
|
* 计算商品列表的哈希值
|
|
* 基于商品类型、ID、购买数量等必传字段生成唯一哈希,忽略可选的quantity字段
|
|
*
|
|
* @param products 商品列表
|
|
* @return 哈希值
|
|
*/
|
|
private String calculateProductsHash(List<ProductItem> products) {
|
|
try {
|
|
// 构建缓存用的商品信息字符串,只包含必传字段
|
|
StringBuilder cacheInfo = new StringBuilder();
|
|
for (ProductItem product : products) {
|
|
cacheInfo.append(product.getProductType()).append(":")
|
|
.append(product.getProductId()).append(":")
|
|
.append(product.getPurchaseCount() != null ? product.getPurchaseCount() : 1).append(":")
|
|
.append(product.getScenicId()).append(";");
|
|
}
|
|
|
|
// 计算SHA-256哈希
|
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
byte[] hash = digest.digest(cacheInfo.toString().getBytes());
|
|
|
|
// 转换为16进制字符串
|
|
StringBuilder hexString = new StringBuilder();
|
|
for (byte b : hash) {
|
|
String hex = Integer.toHexString(0xff & b);
|
|
if (hex.length() == 1) {
|
|
hexString.append('0');
|
|
}
|
|
hexString.append(hex);
|
|
}
|
|
|
|
// 返回前16位作为哈希值(足够避免冲突)
|
|
return hexString.substring(0, 16);
|
|
|
|
} catch (NoSuchAlgorithmException e) {
|
|
log.error("计算商品列表哈希值失败", e);
|
|
// 降级策略:使用关键字段的hashCode
|
|
StringBuilder fallback = new StringBuilder();
|
|
for (ProductItem product : products) {
|
|
fallback.append(product.getProductType()).append(":")
|
|
.append(product.getProductId()).append(":")
|
|
.append(product.getPurchaseCount() != null ? product.getPurchaseCount() : 1).append(";");
|
|
}
|
|
return String.valueOf(fallback.toString().hashCode());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 缓存价格计算结果
|
|
*
|
|
* @param userId 用户ID
|
|
* @param scenicId 景区ID
|
|
* @param products 商品列表
|
|
* @param result 价格计算结果
|
|
* @return 缓存键
|
|
*/
|
|
public String cachePriceResult(Long userId, Long scenicId, List<ProductItem> products, PriceCalculationResult result) {
|
|
String cacheKey = generateCacheKey(userId, scenicId, products);
|
|
|
|
try {
|
|
String resultJson = objectMapper.writeValueAsString(result);
|
|
redisTemplate.opsForValue().set(cacheKey, resultJson, CACHE_DURATION);
|
|
|
|
log.info("价格计算结果已缓存: cacheKey={}, userId={}, scenicId={}",
|
|
cacheKey, userId, scenicId);
|
|
|
|
return cacheKey;
|
|
|
|
} catch (JsonProcessingException e) {
|
|
log.error("缓存价格计算结果失败: userId={}, scenicId={}", userId, scenicId, e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取缓存的价格计算结果
|
|
*
|
|
* @param userId 用户ID
|
|
* @param scenicId 景区ID
|
|
* @param products 商品列表
|
|
* @return 缓存的价格计算结果,如果不存在则返回null
|
|
*/
|
|
public PriceCalculationResult getCachedPriceResult(Long userId, Long scenicId, List<ProductItem> products) {
|
|
String cacheKey = generateCacheKey(userId, scenicId, products);
|
|
return getCachedPriceResult(cacheKey);
|
|
}
|
|
|
|
/**
|
|
* 根据缓存键获取价格计算结果
|
|
*
|
|
* @param cacheKey 缓存键
|
|
* @return 缓存的价格计算结果,如果不存在则返回null
|
|
*/
|
|
public PriceCalculationResult getCachedPriceResult(String cacheKey) {
|
|
try {
|
|
String resultJson = redisTemplate.opsForValue().get(cacheKey);
|
|
if (resultJson == null) {
|
|
log.debug("价格缓存不存在: cacheKey={}", cacheKey);
|
|
return null;
|
|
}
|
|
|
|
PriceCalculationResult result = objectMapper.readValue(resultJson, PriceCalculationResult.class);
|
|
log.info("获取到缓存的价格计算结果: cacheKey={}", cacheKey);
|
|
|
|
return result;
|
|
|
|
} catch (JsonProcessingException e) {
|
|
log.error("解析缓存的价格计算结果失败: cacheKey={}", cacheKey, e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 检查价格缓存是否存在
|
|
*
|
|
* @param userId 用户ID
|
|
* @param scenicId 景区ID
|
|
* @param products 商品列表
|
|
* @return true如果缓存存在且未过期
|
|
*/
|
|
public boolean isCacheExists(Long userId, Long scenicId, List<ProductItem> products) {
|
|
String cacheKey = generateCacheKey(userId, scenicId, products);
|
|
return Boolean.TRUE.equals(redisTemplate.hasKey(cacheKey));
|
|
}
|
|
|
|
/**
|
|
* 验证并消费价格缓存(一次性使用)
|
|
* 查询缓存结果后立即删除,确保缓存只能被使用一次
|
|
*
|
|
* @param userId 用户ID
|
|
* @param scenicId 景区ID
|
|
* @param products 商品列表
|
|
* @return 缓存的价格计算结果,如果不存在则返回null
|
|
*/
|
|
public PriceCalculationResult validateAndConsumePriceCache(Long userId, Long scenicId, List<ProductItem> products) {
|
|
String cacheKey = generateCacheKey(userId, scenicId, products);
|
|
|
|
try {
|
|
// 使用 Redis 的 GETDEL 命令原子性地获取并删除缓存
|
|
String resultJson = redisTemplate.opsForValue().getAndDelete(cacheKey);
|
|
|
|
if (resultJson == null) {
|
|
log.warn("价格缓存不存在或已过期: userId={}, scenicId={}, cacheKey={}",
|
|
userId, scenicId, cacheKey);
|
|
return null;
|
|
}
|
|
|
|
PriceCalculationResult result = objectMapper.readValue(resultJson, PriceCalculationResult.class);
|
|
log.info("价格缓存验证成功并已消费: userId={}, scenicId={}, cacheKey={}",
|
|
userId, scenicId, cacheKey);
|
|
|
|
return result;
|
|
|
|
} catch (JsonProcessingException e) {
|
|
log.error("解析价格缓存失败: userId={}, scenicId={}, cacheKey={}",
|
|
userId, scenicId, cacheKey, e);
|
|
// 缓存解析失败时也要删除无效缓存
|
|
redisTemplate.delete(cacheKey);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 删除价格缓存
|
|
*
|
|
* @param userId 用户ID
|
|
* @param scenicId 景区ID
|
|
* @param products 商品列表
|
|
* @return true如果删除成功
|
|
*/
|
|
public boolean deletePriceCache(Long userId, Long scenicId, List<ProductItem> products) {
|
|
String cacheKey = generateCacheKey(userId, scenicId, products);
|
|
return Boolean.TRUE.equals(redisTemplate.delete(cacheKey));
|
|
}
|
|
|
|
/**
|
|
* 获取缓存剩余过期时间
|
|
*
|
|
* @param userId 用户ID
|
|
* @param scenicId 景区ID
|
|
* @param products 商品列表
|
|
* @return 剩余时间(秒),-1表示永不过期,-2表示不存在
|
|
*/
|
|
public long getCacheExpireTime(Long userId, Long scenicId, List<ProductItem> products) {
|
|
String cacheKey = generateCacheKey(userId, scenicId, products);
|
|
Long expire = redisTemplate.getExpire(cacheKey);
|
|
return expire != null ? expire : -2;
|
|
}
|
|
} |