|
@@ -86,6 +86,8 @@ import java.time.format.DateTimeFormatter;
|
|
|
import java.util.*;
|
|
import java.util.*;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Collectors;
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 积分商品订单Service业务层处理
|
|
* 积分商品订单Service业务层处理
|
|
@@ -98,14 +100,13 @@ import java.util.stream.Collectors;
|
|
|
public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
{
|
|
{
|
|
|
/**
|
|
/**
|
|
|
- * 限购缓存 Redis Key 前缀
|
|
|
|
|
- * <p>完整格式:integral:purchase:count:{userId}:{goodsId},缓存用户已购某商品的有效订单数</p>
|
|
|
|
|
|
|
+ * 限购计数器 Redis Hash Key 前缀
|
|
|
|
|
+ * <p>完整格式:integral:purchase:count:{userId}(Hash 结构)
|
|
|
|
|
+ * HashKey 为 goodsId,Value 为已购有效订单数(仅排除 status=5已取消、-2已退款)。
|
|
|
|
|
+ * 通过 HINCRBY 原子增减保证一致性:下单成功 +N,取消/退款 -N。</p>
|
|
|
*/
|
|
*/
|
|
|
private static final String REDIS_KEY_INTEGRAL_PURCHASE_COUNT = "integral:purchase:count:";
|
|
private static final String REDIS_KEY_INTEGRAL_PURCHASE_COUNT = "integral:purchase:count:";
|
|
|
|
|
|
|
|
- /** 限购缓存过期时间(1小时),过期后回源数据库重建 */
|
|
|
|
|
- private static final Integer PURCHASE_COUNT_CACHE_TTL = 1;
|
|
|
|
|
-
|
|
|
|
|
@Autowired
|
|
@Autowired
|
|
|
private FsCouponMapper fsCouponMapper;
|
|
private FsCouponMapper fsCouponMapper;
|
|
|
|
|
|
|
@@ -150,6 +151,13 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
private IFsUserIntegralLogsService userIntegralLogsService;
|
|
private IFsUserIntegralLogsService userIntegralLogsService;
|
|
|
@Autowired
|
|
@Autowired
|
|
|
private RedisCache redisCache;
|
|
private RedisCache redisCache;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 限购计数器专用 RedisTemplate(Hash 结构)
|
|
|
|
|
+ * <p>Key: integral:purchase:count:{userId},HashKey: {goodsId},Value: 已购有效订单数
|
|
|
|
|
+ * 使用 GenericToStringSerializer 序列化 Integer,支持 HINCRBY 原子增减操作</p>
|
|
|
|
|
+ */
|
|
|
|
|
+ @Resource(name = "redisTemplateForInteger")
|
|
|
|
|
+ private RedisTemplate<String, Integer> redisTemplateForInteger;
|
|
|
@Autowired
|
|
@Autowired
|
|
|
private IPayService ybPayService;
|
|
private IPayService ybPayService;
|
|
|
/**
|
|
/**
|
|
@@ -328,6 +336,14 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
if (Objects.isNull(integralGoods)) {
|
|
if (Objects.isNull(integralGoods)) {
|
|
|
return R.error("商品不存在");
|
|
return R.error("商品不存在");
|
|
|
}
|
|
}
|
|
|
|
|
+ // 有效期校验:isLongTerm=1 非长期有效时,校验当前时间是否在有效期区间内
|
|
|
|
|
+ if (integralGoods.getIsLongTerm() != null && integralGoods.getIsLongTerm() == 1) {
|
|
|
|
|
+ Date now = new Date();
|
|
|
|
|
+ if (integralGoods.getValidStartTime() == null || integralGoods.getValidEndTime() == null
|
|
|
|
|
+ || now.before(integralGoods.getValidStartTime()) || now.after(integralGoods.getValidEndTime())) {
|
|
|
|
|
+ return R.error("积分商品已过兑换有效期限");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
if(integralGoods.getStock()<=0L){
|
|
if(integralGoods.getStock()<=0L){
|
|
|
return R.error("库存不足");
|
|
return R.error("库存不足");
|
|
|
}
|
|
}
|
|
@@ -335,9 +351,9 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
return R.error("积分不足");
|
|
return R.error("积分不足");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 限购校验:limitNum > 0 表示开启限购,校验用户已购数量是否已达上限
|
|
|
|
|
|
|
+ // 限购校验:limitNum > 0 表示开启限购,读 Redis 计数器(懒加载+降级查库)
|
|
|
if (integralGoods.getLimitNum() != null && integralGoods.getLimitNum() > 0) {
|
|
if (integralGoods.getLimitNum() != null && integralGoods.getLimitNum() > 0) {
|
|
|
- int purchasedCount = getUserPurchasedCount(param.getUserId(), param.getGoodsId());
|
|
|
|
|
|
|
+ int purchasedCount = getPurchasedCount(param.getUserId(), param.getGoodsId());
|
|
|
if (purchasedCount + 1 > integralGoods.getLimitNum()) {
|
|
if (purchasedCount + 1 > integralGoods.getLimitNum()) {
|
|
|
return R.error("超过限购数量,每人限购" + integralGoods.getLimitNum() + "件");
|
|
return R.error("超过限购数量,每人限购" + integralGoods.getLimitNum() + "件");
|
|
|
}
|
|
}
|
|
@@ -438,8 +454,10 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
userIntegralLogsService.insertLog(logs);
|
|
userIntegralLogsService.insertLog(logs);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 失效限购缓存,下次查询时从数据库重建(保证数据一致性)
|
|
|
|
|
- invalidatePurchaseCountCache(order.getUserId(), goodsItem.get(0).getGoodsId());
|
|
|
|
|
|
|
+ // 限购计数器 +N(遍历所有商品,购物车下单时 goodsItem 可能为多个)
|
|
|
|
|
+ for (FsIntegralGoods g : goodsItem) {
|
|
|
|
|
+ incrPurchasedCount(order.getUserId(), g.getGoodsId(), g.getNum() == null ? 1 : g.getNum());
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 积分支付
|
|
// 积分支付
|
|
|
if (order.getPayType() == 1) {
|
|
if (order.getPayType() == 1) {
|
|
@@ -495,44 +513,113 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 获取用户已购买指定积分商品的有效订单数(限购校验用)
|
|
|
|
|
- * <p>缓存策略:优先查 Redis,未命中则回源数据库并回填缓存(1小时过期)。
|
|
|
|
|
- * 缓存过期后会重新从数据库查询,保证用户取消订单后限购数量能及时更新。</p>
|
|
|
|
|
|
|
+ * 获取用户已购买指定积分商品的有效订单数(限购校验用,Redis 计数器方案)
|
|
|
|
|
+ * <p>读取 Redis Hash(key=integral:purchase:count:{userId}, hashKey={goodsId}),
|
|
|
|
|
+ * 未命中时从数据库懒加载并回填,Redis 不可用时降级查库。</p>
|
|
|
*
|
|
*
|
|
|
* @param userId 用户ID
|
|
* @param userId 用户ID
|
|
|
* @param goodsId 积分商品ID
|
|
* @param goodsId 积分商品ID
|
|
|
* @return 已购买有效订单数(非负)
|
|
* @return 已购买有效订单数(非负)
|
|
|
*/
|
|
*/
|
|
|
- private int getUserPurchasedCount(Long userId, Long goodsId) {
|
|
|
|
|
- String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId + ":" + goodsId;
|
|
|
|
|
- Integer count = redisCache.getCacheObject(redisKey);
|
|
|
|
|
- if (count != null) {
|
|
|
|
|
- return count;
|
|
|
|
|
- }
|
|
|
|
|
- // 回源数据库查询有效订单数(排除已取消和已删除)
|
|
|
|
|
- count = fsIntegralOrderMapper.selectUserPurchasedCount(userId, goodsId);
|
|
|
|
|
- if (count < 0) {
|
|
|
|
|
- count = 0;
|
|
|
|
|
- }
|
|
|
|
|
- // 回填 Redis 缓存,1小时过期
|
|
|
|
|
- redisCache.setCacheObject(redisKey, count, PURCHASE_COUNT_CACHE_TTL, TimeUnit.HOURS);
|
|
|
|
|
- return count;
|
|
|
|
|
|
|
+ private int getPurchasedCount(Long userId, Long goodsId) {
|
|
|
|
|
+ if (userId == null || goodsId == null) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId;
|
|
|
|
|
+ String hashKey = goodsId.toString();
|
|
|
|
|
+ try {
|
|
|
|
|
+ Integer count = (Integer) redisTemplateForInteger.opsForHash().get(redisKey, hashKey);
|
|
|
|
|
+ if (count != null) {
|
|
|
|
|
+ return count;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 懒加载:Redis 无记录时从 DB 初始化(并发下多次 put 只是覆盖为相同的 DB 值,结果一致)
|
|
|
|
|
+ int dbCount = fsIntegralOrderMapper.selectUserPurchasedCount(userId, goodsId);
|
|
|
|
|
+ redisTemplateForInteger.opsForHash().put(redisKey, hashKey, dbCount);
|
|
|
|
|
+ return dbCount;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ // Redis 异常降级查库,保证限购功能可用
|
|
|
|
|
+ log.warn("限购计数器读取Redis失败,降级查DB userId={}, goodsId={}", userId, goodsId, e);
|
|
|
|
|
+ return fsIntegralOrderMapper.selectUserPurchasedCount(userId, goodsId);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 增加用户已购商品计数(订单创建成功后调用)
|
|
|
|
|
+ * <p>通过 HINCRBY 原子增加,保证高并发下计数准确。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param userId 用户ID
|
|
|
|
|
+ * @param goodsId 积分商品ID
|
|
|
|
|
+ * @param num 本次购买数量
|
|
|
|
|
+ */
|
|
|
|
|
+ private void incrPurchasedCount(Long userId, Long goodsId, int num) {
|
|
|
|
|
+ if (userId == null || goodsId == null || num <= 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId;
|
|
|
|
|
+ redisTemplateForInteger.opsForHash().increment(redisKey, goodsId.toString(), num);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ // Redis 异常不影响主流程,下次限购校验时懒加载会从 DB 重建最新值
|
|
|
|
|
+ log.warn("限购计数器incr失败 userId={}, goodsId={}, num={}", userId, goodsId, num, e);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 失效用户购买指定商品的限购缓存
|
|
|
|
|
- * <p>订单创建成功后调用,确保下次限购校验时从数据库重新加载最新数据。</p>
|
|
|
|
|
|
|
+ * 减少用户已购商品计数(取消订单/退款成功后调用)
|
|
|
|
|
+ * <p>通过 HINCRBY 原子减少,保证计数与订单终态一致。
|
|
|
|
|
+ * 仅在订单变为终态无效(5已取消、-2已退款)时调用。</p>
|
|
|
*
|
|
*
|
|
|
* @param userId 用户ID
|
|
* @param userId 用户ID
|
|
|
* @param goodsId 积分商品ID
|
|
* @param goodsId 积分商品ID
|
|
|
|
|
+ * @param num 本次退还数量
|
|
|
*/
|
|
*/
|
|
|
- private void invalidatePurchaseCountCache(Long userId, Long goodsId) {
|
|
|
|
|
|
|
+ private void decrPurchasedCount(Long userId, Long goodsId, int num) {
|
|
|
|
|
+ if (userId == null || goodsId == null || num <= 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
try {
|
|
try {
|
|
|
- String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId + ":" + goodsId;
|
|
|
|
|
- redisCache.deleteObject(redisKey);
|
|
|
|
|
|
|
+ String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId;
|
|
|
|
|
+ // 使用 increment 传负数实现递减;递减后若计数为负,说明 Redis 与 DB 不一致(如懒加载前旧数据),重置为 0
|
|
|
|
|
+ Long newVal = redisTemplateForInteger.opsForHash().increment(redisKey, goodsId.toString(), -num);
|
|
|
|
|
+ if (newVal != null && newVal < 0) {
|
|
|
|
|
+ redisTemplateForInteger.opsForHash().put(redisKey, goodsId.toString(), 0);
|
|
|
|
|
+ }
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- // 缓存失效失败不影响主流程,下次限购校验时缓存过期后会自动重建
|
|
|
|
|
- log.warn("失效限购缓存失败, userId={}, goodsId={}", userId, goodsId, e);
|
|
|
|
|
|
|
+ // Redis 异常不影响主流程,下次限购校验时懒加载会从 DB 重建最新值
|
|
|
|
|
+ log.warn("限购计数器decr失败 userId={}, goodsId={}, num={}", userId, goodsId, num, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 减少订单中所有商品对应的限购计数(订单变为终态无效时调用)
|
|
|
|
|
+ * <p>解析 item_json 获取 goodsId 和购买数量,逐一调用 decr。
|
|
|
|
|
+ * 兼容 item_json 单对象(旧数据)和数组(新数据)两种格式。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param order 积分订单
|
|
|
|
|
+ */
|
|
|
|
|
+ private void decrPurchasedCountByOrder(FsIntegralOrder order) {
|
|
|
|
|
+ if (order == null || order.getUserId() == null || StringUtils.isEmpty(order.getItemJson())) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ String itemJson = order.getItemJson();
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (itemJson.startsWith("[") && itemJson.endsWith("]")) {
|
|
|
|
|
+ List<FsIntegralGoods> goodsItem = JSONUtil.toBean(itemJson, new TypeReference<List<FsIntegralGoods>>(){}, true);
|
|
|
|
|
+ for (FsIntegralGoods goods : goodsItem) {
|
|
|
|
|
+ if (goods != null && goods.getGoodsId() != null) {
|
|
|
|
|
+ decrPurchasedCount(order.getUserId(), goods.getGoodsId(),
|
|
|
|
|
+ goods.getNum() == null ? 1 : goods.getNum());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ FsIntegralGoods goods = JSONUtil.toBean(itemJson, FsIntegralGoods.class);
|
|
|
|
|
+ if (goods != null && goods.getGoodsId() != null) {
|
|
|
|
|
+ decrPurchasedCount(order.getUserId(), goods.getGoodsId(),
|
|
|
|
|
+ goods.getNum() == null ? 1 : goods.getNum());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("订单限购计数decr失败, orderId={}, itemJson={}", order.getOrderId(), itemJson, e);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -570,10 +657,27 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
if (Objects.isNull(integralGoods)) {
|
|
if (Objects.isNull(integralGoods)) {
|
|
|
throw new CustomException("商品不存在");
|
|
throw new CustomException("商品不存在");
|
|
|
}
|
|
}
|
|
|
|
|
+ // 有效期校验:非长期有效时,校验当前时间是否在有效期区间内
|
|
|
|
|
+ if (integralGoods.getIsLongTerm() != null && integralGoods.getIsLongTerm() == 1) {
|
|
|
|
|
+ Date now = new Date();
|
|
|
|
|
+ if (integralGoods.getValidStartTime() == null || integralGoods.getValidEndTime() == null
|
|
|
|
|
+ || now.before(integralGoods.getValidStartTime()) || now.after(integralGoods.getValidEndTime())) {
|
|
|
|
|
+ throw new CustomException("积分商品「" + integralGoods.getGoodsName() + "」已过兑换有效期限");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
if(integralGoods.getStock() < cart.getCartNum()){
|
|
if(integralGoods.getStock() < cart.getCartNum()){
|
|
|
throw new CustomException("库存不足");
|
|
throw new CustomException("库存不足");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 限购校验:limitNum > 0 表示开启限购,读 Redis 计数器(懒加载+降级查库)
|
|
|
|
|
+ if (integralGoods.getLimitNum() != null && integralGoods.getLimitNum() > 0) {
|
|
|
|
|
+ int purchasedCount = getPurchasedCount(param.getUserId(), cart.getGoodsId());
|
|
|
|
|
+ if (purchasedCount + cart.getCartNum() > integralGoods.getLimitNum()) {
|
|
|
|
|
+ throw new CustomException("商品「" + integralGoods.getGoodsName()
|
|
|
|
|
+ + "」超过限购数量,每人限购" + integralGoods.getLimitNum() + "件");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 减库存
|
|
// 减库存
|
|
|
if (fsIntegralGoodsMapper.subStock(integralGoods.getGoodsId(), cart.getCartNum()) <= 0) {
|
|
if (fsIntegralGoodsMapper.subStock(integralGoods.getGoodsId(), cart.getCartNum()) <= 0) {
|
|
|
throw new CustomException("库存不足");
|
|
throw new CustomException("库存不足");
|
|
@@ -586,7 +690,15 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
goodsItem.add(integralGoods);
|
|
goodsItem.add(integralGoods);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return createOrder(user, address, totalIntegral, totalCash, goodsItem, null);
|
|
|
|
|
|
|
+ R result = createOrder(user, address, totalIntegral, totalCash, goodsItem, null);
|
|
|
|
|
+ // 订单创建成功后,清除购物车中已下单的商品(限购计数器 +N 已在 createOrder 内部完成)
|
|
|
|
|
+ Object codeObj = result == null ? null : result.get("code");
|
|
|
|
|
+ if (codeObj != null && Integer.valueOf(200).equals(codeObj)) {
|
|
|
|
|
+ // 清除购物车中已下单的商品(按购物车ID删除,避免误删同商品的其他购物车记录)
|
|
|
|
|
+ List<Long> cartIds = carts.stream().map(FsIntegralCart::getId).collect(Collectors.toList());
|
|
|
|
|
+ cartService.delCartByIds(user.getUserId(), cartIds);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
@@ -706,6 +818,9 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
logs.setCreateTime(new Date());
|
|
logs.setCreateTime(new Date());
|
|
|
userIntegralLogsService.insertLog(logs);
|
|
userIntegralLogsService.insertLog(logs);
|
|
|
|
|
|
|
|
|
|
+ // 订单取消后,限购计数器 -N(status=5不计入限购),保证计数与订单终态一致
|
|
|
|
|
+ decrPurchasedCountByOrder(order);
|
|
|
|
|
+
|
|
|
return R.ok();
|
|
return R.ok();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -996,6 +1111,8 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
// 5. 无需等待回调的场景(纯积分订单无支付记录、或易宝退款立即成功),直接标记为已退款(-2)
|
|
// 5. 无需等待回调的场景(纯积分订单无支付记录、或易宝退款立即成功),直接标记为已退款(-2)
|
|
|
if (!needCallback) {
|
|
if (!needCallback) {
|
|
|
fsIntegralOrderMapper.refundOrder(order.getOrderId());
|
|
fsIntegralOrderMapper.refundOrder(order.getOrderId());
|
|
|
|
|
+ // 订单已变为已退款(-2)终态,限购计数器 -N(status=-2不计入限购),保证计数与订单终态一致
|
|
|
|
|
+ decrPurchasedCountByOrder(order);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return 1;
|
|
return 1;
|
|
@@ -1258,6 +1375,8 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
int rows = fsIntegralOrderMapper.refundOrder(orderId);
|
|
int rows = fsIntegralOrderMapper.refundOrder(orderId);
|
|
|
if (rows > 0) {
|
|
if (rows > 0) {
|
|
|
log.info("积分订单退款回调(Yop) 更新订单状态完成 orderId: {}, status: -1->-2", orderId);
|
|
log.info("积分订单退款回调(Yop) 更新订单状态完成 orderId: {}, status: -1->-2", orderId);
|
|
|
|
|
+ // 订单已变为已退款(-2)终态,限购计数器 -N(status=-2不计入限购),保证计数与订单终态一致
|
|
|
|
|
+ decrPurchasedCountByOrder(order);
|
|
|
} else {
|
|
} else {
|
|
|
log.warn("积分订单退款回调(Yop) 更新订单状态失败(可能已被其他流程处理) orderId: {}", orderId);
|
|
log.warn("积分订单退款回调(Yop) 更新订单状态失败(可能已被其他流程处理) orderId: {}", orderId);
|
|
|
}
|
|
}
|