|
|
@@ -0,0 +1,711 @@
|
|
|
+package com.fs.common.core.redis.service;
|
|
|
+
|
|
|
+import com.fs.common.core.redis.RedisCache;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
+import org.springframework.data.redis.core.script.DefaultRedisScript;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.CompletableFuture;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class ActivityStockService {
|
|
|
+
|
|
|
+ public final RedisTemplate<String, Object> redisTemplate;
|
|
|
+ public final RedisCache redisCache;
|
|
|
+
|
|
|
+ /** 统一活动信息key前缀 */
|
|
|
+ private static final String ACTIVITY_INFO_KEY = "activity:info:";
|
|
|
+ /** 原商品规格库存key前缀 */
|
|
|
+ private static final String PRODUCT_SPEC_STOCK_KEY = "product:spec:stock:";
|
|
|
+
|
|
|
+ /** 扣减规格库存 Lua脚本(活动不再有独立库存,直接扣规格库存) */
|
|
|
+ private static final DefaultRedisScript<Long> ACTIVITY_STOCK_DEDUCT_SCRIPT;
|
|
|
+
|
|
|
+ static {
|
|
|
+ ACTIVITY_STOCK_DEDUCT_SCRIPT = new DefaultRedisScript<>();
|
|
|
+ // KEYS[1] = 原商品规格库存key(product:spec:stock:{specId})
|
|
|
+ // ARGV[1] = 扣减数量
|
|
|
+ // 返回: >=0 成功(规格剩余库存), -1 库存不足, -2 key不存在, -3 值非数字, -4 扣减数量无效
|
|
|
+ ACTIVITY_STOCK_DEDUCT_SCRIPT.setScriptText(
|
|
|
+ "local deductNum = tonumber(ARGV[1]); " +
|
|
|
+ "if deductNum == nil or deductNum <= 0 then return -4; end " +
|
|
|
+ "if redis.call('exists', KEYS[1]) ~= 1 then return -2; end " +
|
|
|
+ "local stock = tonumber(redis.call('get', KEYS[1])); " +
|
|
|
+ "if stock == nil then return -3; end " +
|
|
|
+ "if stock < deductNum then return -1; end " +
|
|
|
+ "return redis.call('decrby', KEYS[1], deductNum);"
|
|
|
+ );
|
|
|
+ ACTIVITY_STOCK_DEDUCT_SCRIPT.setResultType(Long.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 回滚规格库存 Lua脚本 */
|
|
|
+ private static final DefaultRedisScript<Long> ACTIVITY_STOCK_ROLLBACK_SCRIPT;
|
|
|
+
|
|
|
+ static {
|
|
|
+ ACTIVITY_STOCK_ROLLBACK_SCRIPT = new DefaultRedisScript<>();
|
|
|
+ // KEYS[1] = 原商品规格库存key(product:spec:stock:{specId})
|
|
|
+ // ARGV[1] = 回滚数量
|
|
|
+ ACTIVITY_STOCK_ROLLBACK_SCRIPT.setScriptText(
|
|
|
+ "local rollbackNum = tonumber(ARGV[1]); " +
|
|
|
+ "if rollbackNum == nil or rollbackNum <= 0 then return -4; end " +
|
|
|
+ "if redis.call('exists', KEYS[1]) == 1 then " +
|
|
|
+ " return redis.call('incrby', KEYS[1], rollbackNum); " +
|
|
|
+ "end " +
|
|
|
+ "return 0;"
|
|
|
+ );
|
|
|
+ ACTIVITY_STOCK_ROLLBACK_SCRIPT.setResultType(Long.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 单独扣减活动库存(无specId时不扣商品库存) */
|
|
|
+ private static final DefaultRedisScript<Long> SINGLE_DEDUCT_SCRIPT;
|
|
|
+
|
|
|
+ static {
|
|
|
+ SINGLE_DEDUCT_SCRIPT = new DefaultRedisScript<>();
|
|
|
+ SINGLE_DEDUCT_SCRIPT.setScriptText(
|
|
|
+ "if redis.call('exists', KEYS[1]) ~= 1 then return -2; end " +
|
|
|
+ "local stock_str = redis.call('get', KEYS[1]); " +
|
|
|
+ "local stock = tonumber(stock_str); " +
|
|
|
+ "if stock == nil then return -3; end " +
|
|
|
+ "local deductNum_str = ARGV[1]; " +
|
|
|
+ "local deductNum = tonumber(deductNum_str); " +
|
|
|
+ "if deductNum == nil or deductNum <= 0 then return -4; end " +
|
|
|
+ "if stock >= deductNum then " +
|
|
|
+ " return redis.call('decrby', KEYS[1], deductNum); " +
|
|
|
+ "else " +
|
|
|
+ " return -1; " +
|
|
|
+ "end"
|
|
|
+ );
|
|
|
+ SINGLE_DEDUCT_SCRIPT.setResultType(Long.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ private ActivityStockDataProvider dataProvider;
|
|
|
+
|
|
|
+ public ActivityStockService(RedisTemplate<String, Object> redisTemplate, RedisCache redisCache) {
|
|
|
+ this.redisTemplate = redisTemplate;
|
|
|
+ this.redisCache = redisCache;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setDataProvider(ActivityStockDataProvider dataProvider) {
|
|
|
+ this.dataProvider = dataProvider;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 初始化方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化活动信息到Redis(活动不再有独立库存,只存活动信息用于校验)
|
|
|
+ */
|
|
|
+ public void initActivityInfo(Long activityId, Integer status, Long startTime, Long endTime, Long productId, Long specId, Long stock) {
|
|
|
+ String infoKey = ACTIVITY_INFO_KEY + activityId;
|
|
|
+ Map<String, Object> activityInfo = new HashMap<>();
|
|
|
+ activityInfo.put("status", status);
|
|
|
+ activityInfo.put("startTime", startTime);
|
|
|
+ activityInfo.put("endTime", endTime);
|
|
|
+ activityInfo.put("productId", productId);
|
|
|
+ activityInfo.put("specId", specId);
|
|
|
+ redisCache.setCacheMap(infoKey, activityInfo);
|
|
|
+ // 设置过期时间:活动结束后1小时自动清理缓存
|
|
|
+ long ttlMs = endTime - System.currentTimeMillis() + 3600000;
|
|
|
+ if (ttlMs > 0) {
|
|
|
+ redisTemplate.expire(infoKey, ttlMs, TimeUnit.MILLISECONDS);
|
|
|
+ }
|
|
|
+ log.info("活动商品{}活动信息初始化完成,specId={}", activityId, specId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化原商品规格库存到Redis
|
|
|
+ */
|
|
|
+ public void initProductSpecStock(Long specId, Integer stock) {
|
|
|
+ if (specId == null) return;
|
|
|
+ String stockKey = PRODUCT_SPEC_STOCK_KEY + specId;
|
|
|
+ if (!redisCache.hasKey(stockKey)) {
|
|
|
+ redisTemplate.opsForValue().set(stockKey, stock);
|
|
|
+ log.info("商品规格{}库存初始化到Redis完成,库存:{}", specId, stock);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 兼容旧接口(已废弃,活动无独立库存) ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @deprecated 活动无独立库存,规格库存用initProductSpecStock初始化
|
|
|
+ */
|
|
|
+ @Deprecated
|
|
|
+ public void initFlashSaleStock(Long flashSaleId, Long stock) {
|
|
|
+ // 不再初始化独立活动库存
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @deprecated 活动无独立库存,规格库存用initProductSpecStock初始化
|
|
|
+ */
|
|
|
+ @Deprecated
|
|
|
+ public void initDiscountStock(Long discountId, Long stock) {
|
|
|
+ // 不再初始化独立活动库存
|
|
|
+ }
|
|
|
+
|
|
|
+ public void initFlashSaleInfo(Long flashSaleId, Integer status, Long startTime, Long endTime, Long productId, Long stock) {
|
|
|
+ initActivityInfo(flashSaleId, status, startTime, endTime, productId, null, stock);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void initDiscountInfo(Long discountId, Integer status, Long startTime, Long endTime, Long productId, Long stock) {
|
|
|
+ initActivityInfo(discountId, status, startTime, endTime, productId, null, stock);
|
|
|
+ }
|
|
|
+
|
|
|
+ public Integer getFlashSaleStock(Long flashSaleId) {
|
|
|
+ return getStock(6, flashSaleId);
|
|
|
+ }
|
|
|
+
|
|
|
+ public Integer getDiscountStock(Long discountId) {
|
|
|
+ return getStock(7, discountId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 核心方法 ====================
|
|
|
+
|
|
|
+ public int getStock(Integer orderType, Long associatedId) {
|
|
|
+ // 活动无独立库存,从活动信息中获取specId查规格库存
|
|
|
+ String infoKey = ACTIVITY_INFO_KEY + associatedId;
|
|
|
+ Map<String, Object> activityInfo = redisCache.getCacheMap(infoKey);
|
|
|
+ Long specId = null;
|
|
|
+ if (activityInfo != null) {
|
|
|
+ specId = parseLong(activityInfo.get("specId"));
|
|
|
+ }
|
|
|
+ if (specId == null) {
|
|
|
+ loadActivityFromDb(orderType, associatedId);
|
|
|
+ activityInfo = redisCache.getCacheMap(infoKey);
|
|
|
+ if (activityInfo != null) {
|
|
|
+ specId = parseLong(activityInfo.get("specId"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (specId == null) return 0;
|
|
|
+ String specStockKey = PRODUCT_SPEC_STOCK_KEY + specId;
|
|
|
+ Object stockObj = redisTemplate.opsForValue().get(specStockKey);
|
|
|
+ return parseStockValue(stockObj);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Redis Pipeline 批量获取库存
|
|
|
+ * 先从 ACTIVITY_INFO_KEY 获取各活动的 specId,再用 PRODUCT_SPEC_STOCK_KEY Pipeline 批量查规格库存
|
|
|
+ */
|
|
|
+ public Map<Long, Integer> batchGetStock(Integer orderType, List<Long> associatedIds) {
|
|
|
+ if (associatedIds == null || associatedIds.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Map<Long, Integer> result = new LinkedHashMap<>();
|
|
|
+ try {
|
|
|
+ //Redis缓存读取每个活动的specId映射
|
|
|
+ Map<Long, Long> specIdMap = new LinkedHashMap<>();
|
|
|
+ for (Long id : associatedIds) {
|
|
|
+ String infoKey = ACTIVITY_INFO_KEY + id;
|
|
|
+ Map<String, Object> activityInfo = redisCache.getCacheMap(infoKey);
|
|
|
+ if (activityInfo != null) {
|
|
|
+ Long specId = parseLong(activityInfo.get("specId"));
|
|
|
+ if (specId != null) {
|
|
|
+ specIdMap.put(id, specId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 若specIdMap 为空(活动信息不在Redis),批量从DB加载后逐个getStock降级
|
|
|
+ if (specIdMap.isEmpty()) {
|
|
|
+ log.warn("batchGetStock: 活动信息全部不在Redis,降级逐个加载,orderType={}, size={}", orderType, associatedIds.size());
|
|
|
+ for (Long id : associatedIds) {
|
|
|
+ loadActivityFromDb(orderType, id);
|
|
|
+ result.put(id, getStock(orderType, id));
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ //associatedId 顺序构建 specKey 列表(未命中的记录为 null)
|
|
|
+ List<Long> pipelineIds = new ArrayList<>();
|
|
|
+ List<String> specKeys = new ArrayList<>();
|
|
|
+ for (Long id : associatedIds) {
|
|
|
+ Long specId = specIdMap.get(id);
|
|
|
+ if (specId != null) {
|
|
|
+ pipelineIds.add(id);
|
|
|
+ specKeys.add(PRODUCT_SPEC_STOCK_KEY + specId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ //规格库存
|
|
|
+ final List<String> finalSpecKeys = specKeys;
|
|
|
+ List<Object> stockValues = redisTemplate.executePipelined(
|
|
|
+ (org.springframework.data.redis.core.RedisCallback<Object>) connection -> {
|
|
|
+ for (String key : finalSpecKeys) {
|
|
|
+ connection.get(key.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ //结果映射回 associatedId
|
|
|
+ Map<Long, Integer> pipelineResult = new LinkedHashMap<>();
|
|
|
+ List<Long> needFallbackIds = new ArrayList<>(); // Pipeline结果为null需要降级的ID
|
|
|
+ for (int i = 0; i < pipelineIds.size(); i++) {
|
|
|
+ Long id = pipelineIds.get(i);
|
|
|
+ Object value = (i < stockValues.size()) ? stockValues.get(i) : null;
|
|
|
+ if (value == null) {
|
|
|
+ // product:spec:stock:{specId} key不存在,需要降级从DB加载规格库存
|
|
|
+ needFallbackIds.add(id);
|
|
|
+ pipelineResult.put(id, 0);
|
|
|
+ } else {
|
|
|
+ pipelineResult.put(id, parseStockValue(value));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ //合并结果;specIdMap 中没有的 id(缓存缺失)降级逐个查
|
|
|
+ for (Long id : associatedIds) {
|
|
|
+ if (pipelineResult.containsKey(id)) {
|
|
|
+ result.put(id, pipelineResult.get(id));
|
|
|
+ } else {
|
|
|
+ //specId,降级加载
|
|
|
+ loadActivityFromDb(orderType, id);
|
|
|
+ result.put(id, getStock(orderType, id));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对Pipeline返回null的记录,降级从DB加载规格库存并重新获取
|
|
|
+ if (!needFallbackIds.isEmpty()) {
|
|
|
+ log.info("batchGetStock: {}个活动规格库存key不存在,降级加载", needFallbackIds.size());
|
|
|
+ for (Long id : needFallbackIds) {
|
|
|
+ Long specId = specIdMap.get(id);
|
|
|
+ if (specId != null && dataProvider != null) {
|
|
|
+ dataProvider.loadProductSpecStock(specId);
|
|
|
+ }
|
|
|
+ // 重新读取规格库存
|
|
|
+ String specStockKey = PRODUCT_SPEC_STOCK_KEY + specIdMap.get(id);
|
|
|
+ Object stockObj = redisTemplate.opsForValue().get(specStockKey);
|
|
|
+ result.put(id, parseStockValue(stockObj));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("Pipeline批量获取库存异常,orderType={}", orderType, e);
|
|
|
+ for (Long id : associatedIds) {
|
|
|
+ result.put(id, getStock(orderType, id));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证活动是否有效(上架且在活动时间内)
|
|
|
+ */
|
|
|
+ public ActivityValidateResult validateActivityWithDetail(Integer orderType, Long associatedId) {
|
|
|
+ String infoKey = ACTIVITY_INFO_KEY + associatedId;
|
|
|
+ Map<String, Object> activityInfo = redisCache.getCacheMap(infoKey);
|
|
|
+ if (activityInfo == null || activityInfo.isEmpty()) {
|
|
|
+ log.info("活动信息不存在于Redis,尝试从数据库加载。orderType={}, associatedId={}", orderType, associatedId);
|
|
|
+ boolean loaded = loadActivityFromDb(orderType, associatedId);
|
|
|
+ if (!loaded) {
|
|
|
+ return ActivityValidateResult.fail("活动不存在");
|
|
|
+ }
|
|
|
+ activityInfo = redisCache.getCacheMap(infoKey);
|
|
|
+ if (activityInfo == null || activityInfo.isEmpty()) {
|
|
|
+ return ActivityValidateResult.fail("活动信息加载失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return doValidateActivity(activityInfo);
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean validateActivity(Integer orderType, Long associatedId) {
|
|
|
+ return validateActivityWithDetail(orderType, associatedId).isValid();
|
|
|
+ }
|
|
|
+
|
|
|
+ private ActivityValidateResult doValidateActivity(Map<String, Object> activityInfo) {
|
|
|
+ Integer status = parseInteger(activityInfo.get("status"));
|
|
|
+ if (status == null || status != 1) {
|
|
|
+ return ActivityValidateResult.fail("活动已下架");
|
|
|
+ }
|
|
|
+ Long startTime = parseLong(activityInfo.get("startTime"));
|
|
|
+ Long endTime = parseLong(activityInfo.get("endTime"));
|
|
|
+ if (startTime == null || endTime == null) {
|
|
|
+ return ActivityValidateResult.fail("活动时间信息不完整");
|
|
|
+ }
|
|
|
+ long now = System.currentTimeMillis();
|
|
|
+ if (now < startTime) {
|
|
|
+ return ActivityValidateResult.fail("活动尚未开始");
|
|
|
+ }
|
|
|
+ if (now > endTime) {
|
|
|
+ return ActivityValidateResult.fail("活动已结束");
|
|
|
+ }
|
|
|
+ return ActivityValidateResult.success();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 扣减活动商品库存(直接扣规格库存,活动无独立库存)
|
|
|
+ * @param orderType 活动类型 6=秒杀 7=折扣
|
|
|
+ * @param associatedId 中间表记录ID
|
|
|
+ * @param deductNum 扣减数量
|
|
|
+ * @return 是否成功
|
|
|
+ */
|
|
|
+ public boolean deductStock(Integer orderType, Long associatedId, Integer deductNum) {
|
|
|
+ Integer num = Optional.ofNullable(deductNum).orElse(1);
|
|
|
+
|
|
|
+ if (!validateActivity(orderType, associatedId)) {
|
|
|
+ log.warn("活动校验失败,可能已下架或不在活动时间内。orderType={}, associatedId={}", orderType, associatedId);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 从活动信息中获取specId
|
|
|
+ String infoKey = ACTIVITY_INFO_KEY + associatedId;
|
|
|
+ Map<String, Object> activityInfo = redisCache.getCacheMap(infoKey);
|
|
|
+ Long specId = null;
|
|
|
+ if (activityInfo != null) {
|
|
|
+ specId = parseLong(activityInfo.get("specId"));
|
|
|
+ }
|
|
|
+ if (specId == null || specId <= 0) {
|
|
|
+ log.error("活动规格ID不存在,无法扣减库存。associatedId={}", associatedId);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ String specStockKey = PRODUCT_SPEC_STOCK_KEY + specId;
|
|
|
+ if (!redisCache.hasKey(specStockKey)) {
|
|
|
+ if (dataProvider != null) {
|
|
|
+ dataProvider.loadProductSpecStock(specId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Long remainingStock = redisTemplate.execute(
|
|
|
+ ACTIVITY_STOCK_DEDUCT_SCRIPT,
|
|
|
+ Collections.singletonList(specStockKey),
|
|
|
+ num
|
|
|
+ );
|
|
|
+
|
|
|
+ log.info("Lua脚本返回值:{}", remainingStock);
|
|
|
+
|
|
|
+ if (remainingStock == null) {
|
|
|
+ log.error("Lua脚本返回null,库存扣减结果未知,orderType={},associatedId={}", orderType, associatedId);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (remainingStock >= 0) {
|
|
|
+ log.info("活动商品库存扣减成功,orderType={},associatedId={},规格剩余库存:{}", orderType, associatedId, remainingStock);
|
|
|
+ // 库存归零时异步标记,通知DB同步
|
|
|
+ if (remainingStock == 0) {
|
|
|
+ final Long finalAssociatedId = associatedId;
|
|
|
+ final Integer finalOrderType = orderType;
|
|
|
+ CompletableFuture.runAsync(() -> {
|
|
|
+ try {
|
|
|
+ if (finalOrderType == 6) {
|
|
|
+ syncFlashSaleStockToDb(finalAssociatedId, 0L);
|
|
|
+ } else if (finalOrderType == 7) {
|
|
|
+ syncDiscountStockToDb(finalAssociatedId, 0L);
|
|
|
+ }
|
|
|
+ } catch (Exception ex) {
|
|
|
+ log.error("库存归零异步同步DB异常,orderType={},associatedId={}", finalOrderType, finalAssociatedId, ex);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ } else {
|
|
|
+ String errorMsg = parseErrorCode(remainingStock);
|
|
|
+ log.warn("活动商品扣减失败:orderType={},associatedId={},原因:{}", orderType, associatedId, errorMsg);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("活动库存扣减异常,orderType={},associatedId={}", orderType, associatedId, e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public CompletableFuture<Boolean> deductStockAsync(Integer orderType, Long associatedId, Integer deductNum) {
|
|
|
+ return CompletableFuture.supplyAsync(() -> deductStock(orderType, associatedId, deductNum));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 回滚活动商品库存(直接回滚规格库存,活动无独立库存)
|
|
|
+ */
|
|
|
+ public boolean rollbackStock(Integer orderType, Long associatedId, Integer quantity) {
|
|
|
+ if (orderType == null || associatedId == null || quantity == null || quantity <= 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ // 从活动信息中获取specId
|
|
|
+ String infoKey = ACTIVITY_INFO_KEY + associatedId;
|
|
|
+ Map<String, Object> activityInfo = redisCache.getCacheMap(infoKey);
|
|
|
+ Long specId = null;
|
|
|
+ if (activityInfo != null) {
|
|
|
+ specId = parseLong(activityInfo.get("specId"));
|
|
|
+ }
|
|
|
+ // 活动信息key可能因TTL过期被清理,从DB降级加载
|
|
|
+ if (specId == null || specId <= 0) {
|
|
|
+ log.info("rollbackStock: 活动信息不在Redis,尝试从DB加载,orderType={}, associatedId={}", orderType, associatedId);
|
|
|
+ boolean loaded = loadActivityFromDb(orderType, associatedId);
|
|
|
+ if (loaded) {
|
|
|
+ activityInfo = redisCache.getCacheMap(infoKey);
|
|
|
+ if (activityInfo != null) {
|
|
|
+ specId = parseLong(activityInfo.get("specId"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (specId == null || specId <= 0) {
|
|
|
+ log.error("rollbackStock: 活动规格ID无法获取,Redis库存回滚失败!orderType={}, associatedId={}, quantity={},需人工处理",
|
|
|
+ orderType, associatedId, quantity);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果规格库存key不存在,先从DB加载初始化
|
|
|
+ String specStockKey = PRODUCT_SPEC_STOCK_KEY + specId;
|
|
|
+ if (!redisCache.hasKey(specStockKey)) {
|
|
|
+ log.info("rollbackStock: 规格库存key不存在,尝试从DB加载,specId={}", specId);
|
|
|
+ if (dataProvider != null) {
|
|
|
+ dataProvider.loadProductSpecStock(specId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Long result = redisTemplate.execute(
|
|
|
+ ACTIVITY_STOCK_ROLLBACK_SCRIPT,
|
|
|
+ Collections.singletonList(specStockKey),
|
|
|
+ quantity
|
|
|
+ );
|
|
|
+
|
|
|
+ if (result != null && result >= 0) {
|
|
|
+ log.info("活动规格库存回滚成功,orderType={},associatedId={},specId={},回滚数量={}", orderType, associatedId, specId, quantity);
|
|
|
+ return true;
|
|
|
+ } else {
|
|
|
+ log.warn("活动规格库存回滚Lua失败,降级为incrby,associatedId={},specId={}", associatedId, specId);
|
|
|
+ redisCache.incr(specStockKey, Long.valueOf(quantity));
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("活动库存回滚异常,orderType={}, associatedId={}, quantity={}", orderType, associatedId, quantity, e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 同步方法 ====================
|
|
|
+
|
|
|
+ public void syncFlashSaleStockToDb(Long flashSaleId, Long dbStock) {
|
|
|
+ syncActivityStockToDb(flashSaleId, dbStock);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void syncDiscountStockToDb(Long discountId, Long dbStock) {
|
|
|
+ syncActivityStockToDb(discountId, dbStock);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void syncActivityStockToDb(Long activityId, Long dbStock) {
|
|
|
+ try {
|
|
|
+ // 从活动info获取specId
|
|
|
+ String infoKey = ACTIVITY_INFO_KEY + activityId;
|
|
|
+ Map<String, Object> activityInfo = redisCache.getCacheMap(infoKey);
|
|
|
+ if (activityInfo == null || activityInfo.isEmpty()) {
|
|
|
+ log.warn("活动{} 信息不存在,无法同步规格库存到DB", activityId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Long specId = parseLong(activityInfo.get("specId"));
|
|
|
+ if (specId == null || specId <= 0) {
|
|
|
+ log.warn("活动{} 未配置规格ID,无法同步规格库存到DB", activityId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取真实的规格库存
|
|
|
+ String specStockKey = PRODUCT_SPEC_STOCK_KEY + specId;
|
|
|
+ Object stockObj = redisTemplate.opsForValue().get(specStockKey);
|
|
|
+ if (stockObj == null) {
|
|
|
+ log.warn("规格{} Redis库存不存在,跳过同步到DB", specId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ long redisStock = parseLongFromObject(stockObj);
|
|
|
+
|
|
|
+ // 比较并同步
|
|
|
+ if (dbStock == null || redisStock != dbStock.longValue()) {
|
|
|
+ if (dataProvider != null) {
|
|
|
+ dataProvider.updateProductSpecStockToDb(specId, redisStock);
|
|
|
+ log.info("活动{} 规格{} 库存同步到DB成功,redisStock={}", activityId, specId, redisStock);
|
|
|
+ } else {
|
|
|
+ log.warn("dataProvider未注入,无法同步活动{}规格{}库存到DB", activityId, specId);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ log.debug("活动{} 规格{} 库存一致(={}),无需同步", activityId, specId, redisStock);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("活动{} 同步规格库存到DB异常", activityId, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 同步指定规格的Redis库存到数据库(活动过期时调用)
|
|
|
+ */
|
|
|
+ public void syncProductSpecStockToDbBySpecId(Long specId) {
|
|
|
+ if (specId == null || dataProvider == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String specStockKey = PRODUCT_SPEC_STOCK_KEY + specId;
|
|
|
+ Object stockObj = redisTemplate.opsForValue().get(specStockKey);
|
|
|
+ if (stockObj != null) {
|
|
|
+ long redisStock = parseLongFromObject(stockObj);
|
|
|
+ dataProvider.updateProductSpecStockToDb(specId, redisStock);
|
|
|
+ log.info("活动过期同步规格库存到DB,specId={}, redisStock={}", specId, redisStock);
|
|
|
+ } else {
|
|
|
+ log.info("活动过期规格库存key不存在,跳过同步,specId={}", specId);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("同步规格库存到数据库异常,specId={}", specId, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 定时对账:将Redis规格库存同步到数据库(兤底,防止Redis回滚失败导致不一致)
|
|
|
+ * 活动中间表已无stock字段,只同步规格库存
|
|
|
+ */
|
|
|
+ public void syncAllStockToDb() {
|
|
|
+ if (dataProvider == null) {
|
|
|
+ log.warn("ActivityStockDataProvider 未设置,无法同步库存到数据库");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ // 同步商品规格库存到数据库(核心对账逻辑)
|
|
|
+ syncProductSpecStockToDb();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("全量库存同步到数据库异常", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 同步Redis中的商品规格库存到数据库
|
|
|
+ */
|
|
|
+ private void syncProductSpecStockToDb() {
|
|
|
+ try {
|
|
|
+ Set<String> specKeys = scanKeys(PRODUCT_SPEC_STOCK_KEY + "*");
|
|
|
+ if (specKeys == null || specKeys.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (String specKey : specKeys) {
|
|
|
+ try {
|
|
|
+ Long specId = Long.parseLong(specKey.substring(PRODUCT_SPEC_STOCK_KEY.length()));
|
|
|
+ Object stockObj = redisTemplate.opsForValue().get(specKey);
|
|
|
+ if (stockObj != null) {
|
|
|
+ long redisStock = parseLongFromObject(stockObj);
|
|
|
+ if (dataProvider != null) {
|
|
|
+ dataProvider.updateProductSpecStockToDb(specId, redisStock);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("商品规格库存同步异常,key={}", specKey, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("商品规格库存同步到数据库完成,key数={}", specKeys.size());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("商品规格库存同步到数据库异常", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 内部工具方法 ====================
|
|
|
+
|
|
|
+ private boolean loadActivityFromDb(Integer orderType, Long associatedId) {
|
|
|
+ if (dataProvider == null) return false;
|
|
|
+ try {
|
|
|
+ ActivityStockData data = dataProvider.loadActivityData(orderType, associatedId);
|
|
|
+ if (data == null) return false;
|
|
|
+ String infoKey = ACTIVITY_INFO_KEY + associatedId;
|
|
|
+ Map<String, Object> activityInfo = new HashMap<>();
|
|
|
+ activityInfo.put("status", data.getStatus());
|
|
|
+ activityInfo.put("startTime", data.getStartTime());
|
|
|
+ activityInfo.put("endTime", data.getEndTime());
|
|
|
+ activityInfo.put("productId", data.getProductId());
|
|
|
+ activityInfo.put("specId", data.getSpecId());
|
|
|
+ redisCache.setCacheMap(infoKey, activityInfo);
|
|
|
+ if (data.getEndTime() != null) {
|
|
|
+ long ttlMs = data.getEndTime() - System.currentTimeMillis() + 3600000;
|
|
|
+ if (ttlMs > 0) {
|
|
|
+ redisTemplate.expire(infoKey, ttlMs, TimeUnit.MILLISECONDS);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 初始化规格库存到Redis
|
|
|
+ if (data.getSpecId() != null) {
|
|
|
+ dataProvider.loadProductSpecStock(data.getSpecId());
|
|
|
+ }
|
|
|
+ log.info("从数据库加载活动信息到Redis成功,associatedId={}, specId={}", associatedId, data.getSpecId());
|
|
|
+ return true;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("从数据库加载活动信息异常,orderType={}, associatedId={}", orderType, associatedId, e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Set<String> scanKeys(String pattern) {
|
|
|
+ Set<String> keys = new HashSet<>();
|
|
|
+ try {
|
|
|
+ org.springframework.data.redis.core.ScanOptions options =
|
|
|
+ org.springframework.data.redis.core.ScanOptions.scanOptions()
|
|
|
+ .match(pattern)
|
|
|
+ .count(100)
|
|
|
+ .build();
|
|
|
+ redisTemplate.execute((org.springframework.data.redis.core.RedisCallback<Void>) connection -> {
|
|
|
+ org.springframework.data.redis.core.Cursor<byte[]> cursor = connection.scan(options);
|
|
|
+ try {
|
|
|
+ while (cursor.hasNext()) {
|
|
|
+ keys.add(new String(cursor.next(), java.nio.charset.StandardCharsets.UTF_8));
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ try { cursor.close(); } catch (java.io.IOException e) { log.warn("关闭Redis cursor失败", e); }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("SCAN扫描key异常,pattern={}", pattern, e);
|
|
|
+ }
|
|
|
+ return keys;
|
|
|
+ }
|
|
|
+
|
|
|
+ private int parseStockValue(Object stockObj) {
|
|
|
+ if (stockObj == null) return 0;
|
|
|
+ if (stockObj instanceof Integer) return (Integer) stockObj;
|
|
|
+ if (stockObj instanceof Long) return ((Long) stockObj).intValue();
|
|
|
+ if (stockObj instanceof String) {
|
|
|
+ try { return Integer.parseInt((String) stockObj); }
|
|
|
+ catch (NumberFormatException e) { return 0; }
|
|
|
+ }
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private long parseLongFromObject(Object obj) {
|
|
|
+ if (obj instanceof Integer) return ((Integer) obj).longValue();
|
|
|
+ if (obj instanceof Long) return (Long) obj;
|
|
|
+ if (obj instanceof String) return Long.parseLong((String) obj);
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Integer parseInteger(Object obj) {
|
|
|
+ if (obj instanceof Integer) return (Integer) obj;
|
|
|
+ if (obj instanceof String) {
|
|
|
+ try { return Integer.parseInt((String) obj); } catch (NumberFormatException e) { return null; }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Long parseLong(Object obj) {
|
|
|
+ if (obj instanceof Long) return (Long) obj;
|
|
|
+ if (obj instanceof Integer) return ((Integer) obj).longValue();
|
|
|
+ if (obj instanceof String) {
|
|
|
+ try { return Long.parseLong((String) obj); } catch (NumberFormatException e) { return null; }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String parseErrorCode(Long code) {
|
|
|
+ if (code == null) return "Lua脚本返回null";
|
|
|
+ switch (code.intValue()) {
|
|
|
+ case -1: return "活动库存不足";
|
|
|
+ case -2: return "活动库存Key不存在";
|
|
|
+ case -3: return "库存值非数字";
|
|
|
+ case -4: return "扣减数量无效";
|
|
|
+ case -5: return "商品规格库存Key不存在";
|
|
|
+ case -6: return "商品规格库存不足";
|
|
|
+ default: return "未知错误,错误码:" + code;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 兼容旧接口 ====================
|
|
|
+
|
|
|
+ public CompletableFuture<Integer> getFlashSaleStockAsync(Long flashSaleId) {
|
|
|
+ return CompletableFuture.supplyAsync(() -> getFlashSaleStock(flashSaleId));
|
|
|
+ }
|
|
|
+
|
|
|
+ public CompletableFuture<Integer> getDiscountStockAsync(Long discountId) {
|
|
|
+ return CompletableFuture.supplyAsync(() -> getDiscountStock(discountId));
|
|
|
+ }
|
|
|
+}
|