feat(basic): 新增移动端下单请求DTO和价格缓存服务- 创建 MobileOrderRequest 类用于移动端下单请求

- 实现 PriceCacheService 类提供价格缓存相关功能
- 使用 Redis 缓存价格计算结果,提高查询效率
This commit is contained in:
2025-08-28 18:14:34 +08:00
parent 5c2629237e
commit af79a5ffa6
2 changed files with 280 additions and 0 deletions

View File

@@ -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<ProductItem> 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;
}

View File

@@ -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<ProductItem> products) {
String productsHash = calculateProductsHash(products);
return CACHE_PREFIX + userId + ":" + scenicId + ":" + productsHash;
}
/**
* 计算商品列表的哈希值
* 基于商品类型、ID、数量等信息生成唯一哈希
*
* @param products 商品列表
* @return 哈希值
*/
private String calculateProductsHash(List<ProductItem> 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<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;
}
}