diff --git a/src/main/java/com/ycwl/basic/dto/MobileOrderRequest.java b/src/main/java/com/ycwl/basic/dto/MobileOrderRequest.java new file mode 100644 index 0000000..e16608f --- /dev/null +++ b/src/main/java/com/ycwl/basic/dto/MobileOrderRequest.java @@ -0,0 +1,54 @@ +package com.ycwl.basic.dto; + +import com.ycwl.basic.pricing.dto.ProductItem; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 移动端下单请求DTO + */ +@Data +public class MobileOrderRequest { + + /** + * 商品列表 + */ + private List products; + + /** + * 人脸ID(必填,用于权限验证) + */ + private Long faceId; + + /** + * 预期原价(用于价格验证) + */ + private BigDecimal expectedOriginalAmount; + + /** + * 预期最终价格(用于价格验证) + */ + private BigDecimal expectedFinalAmount; + + /** + * 是否自动使用优惠券 + */ + private Boolean autoUseCoupon = true; + + /** + * 用户输入的券码 + */ + private String voucherCode; + + /** + * 是否自动使用券码优惠 + */ + private Boolean autoUseVoucher = true; + + /** + * 订单备注 + */ + private String remarks; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/service/PriceCacheService.java b/src/main/java/com/ycwl/basic/service/PriceCacheService.java new file mode 100644 index 0000000..8743e20 --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/PriceCacheService.java @@ -0,0 +1,226 @@ +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(5); + + /** + * 生成缓存键 + * 格式: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、数量等信息生成唯一哈希 + * + * @param products 商品列表 + * @return 哈希值 + */ + private String calculateProductsHash(List products) { + try { + // 将商品列表转换为JSON字符串 + String productsJson = objectMapper.writeValueAsString(products); + + // 计算SHA-256哈希 + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(productsJson.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 (JsonProcessingException | NoSuchAlgorithmException e) { + log.error("计算商品列表哈希值失败", e); + // 降级策略:使用商品列表的hashCode + return String.valueOf(products.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; + } +} \ No newline at end of file