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 products) { String productsHash = calculateProductsHash(products); return CACHE_PREFIX + userId + ":" + scenicId + ":" + productsHash; } /** * 计算商品列表的哈希值 * 基于商品类型、ID、购买数量等必传字段生成唯一哈希,忽略可选的quantity字段 * * @param products 商品列表 * @return 哈希值 */ private String calculateProductsHash(List 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 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 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 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 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 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 products) { String cacheKey = generateCacheKey(userId, scenicId, products); Long expire = redisTemplate.getExpire(cacheKey); return expire != null ? expire : -2; } }