修复因网络延迟问题存在的重复提交问题,增加售货机订单分布式锁

This commit is contained in:
zjf committed 2026-06-09 22:50:21 +08:00
1 parent 67d3247d80
commit 4be1427f7c
8 files changed
+235 -183

No files matched your search

@@ -67,8 +67,14 @@ public class AppTradeOrderController {
@PostMapping("/judgePurchase")
@Operation(summary = "判断是否为限购商品")
public CommonResult<Boolean> judgePurchase(@RequestBody @Valid AppTradeOrderJudgePurchaseRespVo settlementReqVO) {
List<Long> skuList = settlementReqVO.getItems().stream().map(AppTradeOrderJudgePurchaseRespVo.Item::getSkuId).collect(Collectors.toList());
tradeOrderUpdateService.isSkuPurchaseLimit(getLoginUserId(), skuList);
List<AppTradeOrderSettlementReqVO.Item> items = settlementReqVO.getItems().stream().map(item -> {
AppTradeOrderSettlementReqVO.Item settlementItem = new AppTradeOrderSettlementReqVO.Item();
settlementItem.setSkuId(item.getSkuId());
settlementItem.setCount(item.getCount());
settlementItem.setCartId(item.getCartId());
return settlementItem;
}).collect(Collectors.toList());
tradeOrderUpdateService.validatePurchaseLimit(getLoginUserId(), items);
return success(true);
}
@@ -95,7 +95,7 @@ public interface TradeOrderItemMapper extends BaseMapperX<TradeOrderItemDO> {
}
/**
* 查询用户在指定时间范围内购买某个 SPU 的总数量(仅统计已支付订单)
* 查询用户在指定时间范围内购买某个 SPU 的总数量(统计未取消订单,含待支付)
*
* @param userId 用户 ID
* @param spuId 商品 SPU ID
@@ -109,7 +109,7 @@ public interface TradeOrderItemMapper extends BaseMapperX<TradeOrderItemDO> {
.eq(TradeOrderItemDO::getSpuId, spuId)
.ge(TradeOrderItemDO::getCreateTime, startTime)
.leftJoin(TradeOrderDO.class, TradeOrderDO::getId, TradeOrderItemDO::getOrderId)
.eq(TradeOrderDO::getPayStatus, true);
.ne(TradeOrderDO::getStatus, TradeOrderStatusEnum.CANCELED.getStatus());
List<Map<String, Object>> result = selectMaps(query);
if (CollUtil.isEmpty(result)) {
@@ -23,4 +23,11 @@ public interface RedisKeyConstants {
*/
String EXPRESS_TRACK = "express_track";
/**
* 售货机限购校验分布式锁
*
* KEY 格式:trade_purchase_limit_lock:{userId}:{spuId}
*/
String TRADE_PURCHASE_LIMIT_LOCK = "trade_purchase_limit_lock:";
}
@@ -23,6 +23,11 @@ public interface TradeOrderUpdateService {
void isSkuPurchaseLimit(Long payUserId, List<Long> skuIdList);
/**
* 校验限购(含 SKU 购买数量)
*/
void validatePurchaseLimit(Long userId, List<AppTradeOrderSettlementReqVO.Item> items);
/**
* 获得订单结算信息
*
@@ -30,7 +30,6 @@ import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.product.enums.spu.PurchaseLimitTypeEnum;
import cn.iocoder.yudao.module.promotion.api.giftvoucher.GiftVoucherApi;
import cn.iocoder.yudao.module.promotion.api.giftvoucher.dto.GiftVoucherRespDTO;
import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
@@ -79,7 +78,6 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
import java.util.stream.Collectors;
@@ -147,6 +145,9 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
@Resource
private AdminUserApi adminUserApi;
@Resource
private TradePurchaseLimitService tradePurchaseLimitService;
// =================== Order ===================
/**
@@ -154,12 +155,13 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
*/
@Override
public void isSkuPurchaseLimit(Long payUserId, List<Long> skuIdList) {
if (CollUtil.isEmpty(skuIdList)) {
return;
}
List<ProductSkuRespDTO> skuList = productSkuApi.getSkuList(skuIdList);
if (CollUtil.isEmpty(skuList)) {
return;
}
// 本次请求每个 SPU 的购买数量(按 skuList 聚合;结算页通常每条 sku 一项)
Map<Long, Integer> currentPurchaseBySpu = new HashMap<>();
for (ProductSkuRespDTO sku : skuList) {
if (sku == null || sku.getSpuId() == null) {
@@ -167,56 +169,36 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
}
currentPurchaseBySpu.merge(sku.getSpuId(), 1, Integer::sum);
}
if (currentPurchaseBySpu.isEmpty()) {
return;
}
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(new ArrayList<>(currentPurchaseBySpu.keySet()));
if (CollUtil.isEmpty(spuList)) {
return;
}
for (ProductSpuRespDTO spu : spuList) {
if (spu == null || spu.getId() == null) {
continue;
}
Integer limitType = spu.getPurchaseLimitType();
Integer limitCount = spu.getPurchaseLimitCount();
if (limitType == null || Objects.equals(limitType, PurchaseLimitTypeEnum.NONE.getType())
|| limitCount == null || limitCount <= 0) {
continue; // 不限购
}
PurchaseLimitTypeEnum limitTypeEnum = PurchaseLimitTypeEnum.valueOf(limitType);
if (limitTypeEnum == null) {
continue;
}
LocalDateTime startTime = getStartTimeByLimitType(limitTypeEnum);
if (startTime == null) {
startTime = LocalDateTime.of(2000, 1, 1, 0, 0);
}
int historyPurchaseCount = tradeOrderItemMapper.selectPurchaseCountByUserAndSpu(payUserId, spu.getId(), startTime);
int currentPurchaseCount = currentPurchaseBySpu.getOrDefault(spu.getId(), 0);
if (historyPurchaseCount + currentPurchaseCount > limitCount) {
throw exception(ORDER_CREATE_PURCHASE_LIMIT_EXCEED, spu.getName(), limitTypeEnum.getName(), limitCount);
}
}
tradePurchaseLimitService.validate(payUserId, currentPurchaseBySpu);
}
private LocalDateTime getStartTimeByLimitType(PurchaseLimitTypeEnum limitType) {
LocalDateTime now = LocalDateTime.now();
switch (limitType) {
case DAILY:
return now.toLocalDate().atStartOfDay();
case WEEKLY:
return now.with(TemporalAdjusters.previousOrSame(java.time.DayOfWeek.MONDAY))
.toLocalDate().atStartOfDay();
case LIFETIME:
return null;
default:
return null;
/**
* 结算阶段限购预校验(含购买数量)
*/
@Override
public void validatePurchaseLimit(Long userId, List<AppTradeOrderSettlementReqVO.Item> items) {
validatePurchaseLimitBySettlementItems(userId, items);
}
private void validatePurchaseLimitBySettlementItems(Long userId, List<AppTradeOrderSettlementReqVO.Item> items) {
if (CollUtil.isEmpty(items)) {
return;
}
List<Long> skuIds = convertList(items, AppTradeOrderSettlementReqVO.Item::getSkuId);
List<ProductSkuRespDTO> skuList = productSkuApi.getSkuList(skuIds);
Map<Long, ProductSkuRespDTO> skuMap = convertMap(skuList, ProductSkuRespDTO::getId);
Map<Long, Integer> currentPurchaseBySpu = new HashMap<>();
for (AppTradeOrderSettlementReqVO.Item item : items) {
if (item == null || item.getSkuId() == null || item.getCount() == null) {
continue;
}
ProductSkuRespDTO sku = skuMap.get(item.getSkuId());
if (sku == null || sku.getSpuId() == null) {
continue;
}
currentPurchaseBySpu.merge(sku.getSpuId(), item.getCount(), Integer::sum);
}
tradePurchaseLimitService.validate(userId, currentPurchaseBySpu);
}
@Override
@@ -228,8 +210,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
}
// TODO 新需求 查看商品有没有限购
List<Long> skuList = settlementReqVO.getItems().stream().map(AppTradeOrderSettlementReqVO.Item::getSkuId).collect(Collectors.toList());
isSkuPurchaseLimit(userId, skuList);
validatePurchaseLimitBySettlementItems(userId, settlementReqVO.getItems());
// 2. 计算价格
TradePriceCalculateRespBO calculateRespBO = calculatePrice(userId, settlementReqVO);
@@ -0,0 +1,163 @@
package cn.iocoder.yudao.module.trade.service.order;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.product.enums.spu.PurchaseLimitTypeEnum;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderItemMapper;
import cn.iocoder.yudao.module.trade.dal.redis.RedisKeyConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_CREATE_PURCHASE_LIMIT_EXCEED;
/**
* 售货机商品限购校验(含并发保护)
*/
@Service
@Slf4j
public class TradePurchaseLimitService {
private static final Duration LOCK_TIMEOUT = Duration.ofSeconds(10);
@Resource
private ProductSpuApi productSpuApi;
@Resource
private TradeOrderItemMapper tradeOrderItemMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 按订单项校验限购(创建订单时使用)
*/
public void validateByOrderItems(Long userId, List<TradeOrderItemDO> orderItems) {
if (userId == null || CollUtil.isEmpty(orderItems)) {
return;
}
Map<Long, Integer> currentPurchaseBySpu = new HashMap<>();
for (TradeOrderItemDO item : orderItems) {
if (item == null || item.getSpuId() == null || item.getCount() == null) {
continue;
}
currentPurchaseBySpu.merge(item.getSpuId(), item.getCount(), Integer::sum);
}
validate(userId, currentPurchaseBySpu);
}
/**
* 按 SPU 购买数量校验限购
*/
public void validate(Long userId, Map<Long, Integer> currentPurchaseBySpu) {
if (userId == null || CollUtil.isEmpty(currentPurchaseBySpu)) {
return;
}
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(new ArrayList<>(currentPurchaseBySpu.keySet()));
if (CollUtil.isEmpty(spuList)) {
return;
}
List<ProductSpuRespDTO> limitedSpus = new ArrayList<>();
for (ProductSpuRespDTO spu : spuList) {
if (spu == null || spu.getId() == null) {
continue;
}
if (spu.getPurchaseLimitType() != null
&& !Objects.equals(spu.getPurchaseLimitType(), PurchaseLimitTypeEnum.NONE.getType())
&& spu.getPurchaseLimitCount() != null
&& spu.getPurchaseLimitCount() > 0) {
limitedSpus.add(spu);
}
}
if (CollUtil.isEmpty(limitedSpus)) {
return;
}
// 按 SPU ID 排序后加锁,避免并发下单穿透限购
List<Long> lockSpuIds = new ArrayList<>();
for (ProductSpuRespDTO spu : limitedSpus) {
lockSpuIds.add(spu.getId());
}
Collections.sort(lockSpuIds);
List<String> acquiredLockKeys = new ArrayList<>(lockSpuIds.size());
try {
for (Long spuId : lockSpuIds) {
String lockKey = RedisKeyConstants.TRADE_PURCHASE_LIMIT_LOCK + userId + ":" + spuId;
Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_TIMEOUT);
if (!Boolean.TRUE.equals(locked)) {
ProductSpuRespDTO lockedSpu = limitedSpus.stream()
.filter(spu -> Objects.equals(spu.getId(), spuId))
.findFirst().orElse(null);
String spuName = lockedSpu != null ? lockedSpu.getName() : "商品";
Integer limitCount = lockedSpu != null ? lockedSpu.getPurchaseLimitCount() : 1;
PurchaseLimitTypeEnum limitTypeEnum = lockedSpu != null
? PurchaseLimitTypeEnum.valueOf(lockedSpu.getPurchaseLimitType()) : null;
String limitDesc = limitTypeEnum != null ? limitTypeEnum.getName() : "限购";
throw exception(ORDER_CREATE_PURCHASE_LIMIT_EXCEED, spuName, limitDesc, limitCount);
}
acquiredLockKeys.add(lockKey);
}
doValidate(userId, limitedSpus, currentPurchaseBySpu);
} finally {
for (String lockKey : acquiredLockKeys) {
stringRedisTemplate.delete(lockKey);
}
}
}
private void doValidate(Long userId, List<ProductSpuRespDTO> limitedSpus,
Map<Long, Integer> currentPurchaseBySpu) {
for (ProductSpuRespDTO spu : limitedSpus) {
int currentPurchaseCount = currentPurchaseBySpu.getOrDefault(spu.getId(), 0);
int historyPurchaseCount = getHistoryPurchaseCount(userId, spu.getId(), spu.getPurchaseLimitType());
if (historyPurchaseCount + currentPurchaseCount > spu.getPurchaseLimitCount()) {
PurchaseLimitTypeEnum limitTypeEnum = PurchaseLimitTypeEnum.valueOf(spu.getPurchaseLimitType());
String limitDesc = limitTypeEnum != null ? limitTypeEnum.getName() : "限购";
throw exception(ORDER_CREATE_PURCHASE_LIMIT_EXCEED, spu.getName(), limitDesc, spu.getPurchaseLimitCount());
}
}
}
private int getHistoryPurchaseCount(Long userId, Long spuId, Integer limitType) {
PurchaseLimitTypeEnum limitTypeEnum = PurchaseLimitTypeEnum.valueOf(limitType);
if (limitTypeEnum == null || limitTypeEnum == PurchaseLimitTypeEnum.NONE) {
return 0;
}
LocalDateTime startTime = getStartTimeByLimitType(limitTypeEnum);
if (startTime == null) {
startTime = LocalDateTime.of(2000, 1, 1, 0, 0);
}
return tradeOrderItemMapper.selectPurchaseCountByUserAndSpu(userId, spuId, startTime);
}
private LocalDateTime getStartTimeByLimitType(PurchaseLimitTypeEnum limitType) {
LocalDateTime now = LocalDateTime.now();
switch (limitType) {
case DAILY:
return now.toLocalDate().atStartOfDay();
case WEEKLY:
return now.with(TemporalAdjusters.previousOrSame(java.time.DayOfWeek.MONDAY))
.toLocalDate().atStartOfDay();
case LIFETIME:
return null;
default:
return null;
}
}
}
@@ -1,156 +1,42 @@
package cn.iocoder.yudao.module.trade.service.order.handler;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.product.enums.spu.PurchaseLimitTypeEnum;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderItemMapper;
import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.order.TradePurchaseLimitService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*;
import java.util.List;
import java.util.Objects;
/**
* 限购校验订单处理器
* <p>
* 在订单创建前(beforeOrderCreate),校验商品是否设置了限购规则,如果设置了则检查用户历史购买记录是否超限
* 在订单创建前(beforeOrderCreate),校验商品是否设置了限购规则
* <p>
* 限购规则仅对售货机订单(type = MACHINE)生效
*
* @author 芋道源码
* 限购规则对售货机订单生效(type = MACHINE 或 deliveryType = 自提柜)
*/
@Component
@Slf4j
public class TradePurchaseLimitOrderHandler implements TradeOrderHandler {
@Resource
private ProductSpuApi productSpuApi;
@Resource
private TradeOrderItemMapper tradeOrderItemMapper;
private TradePurchaseLimitService tradePurchaseLimitService;
@Override
public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
// 限购校验仅对售货机订单生效
if (!Objects.equals(order.getType(), TradeOrderTypeEnum.MACHINE.getType())) {
if (!isMachineOrder(order)) {
return;
}
// 获取订单中所有的 SPU ID
Set<Long> spuIds = orderItems.stream()
.map(TradeOrderItemDO::getSpuId)
.collect(Collectors.toSet());
if (CollUtil.isEmpty(spuIds)) {
return;
}
// 批量查询 SPU 信息
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(new ArrayList<>(spuIds));
if (CollUtil.isEmpty(spuList)) {
return;
}
// 过滤出有限购规则的商品
List<ProductSpuRespDTO> limitedSpus = spuList.stream()
.filter(spu -> spu.getPurchaseLimitType() != null
&& !Objects.equals(spu.getPurchaseLimitType(), PurchaseLimitTypeEnum.NONE.getType())
&& spu.getPurchaseLimitCount() != null
&& spu.getPurchaseLimitCount() > 0)
.collect(Collectors.toList());
if (CollUtil.isEmpty(limitedSpus)) {
return;
}
// 逐个校验限购规则
for (ProductSpuRespDTO spu : limitedSpus) {
// 查找本次订单中该 SPU 的购买数量
int currentPurchaseCount = orderItems.stream()
.filter(item -> Objects.equals(item.getSpuId(), spu.getId()))
.mapToInt(TradeOrderItemDO::getCount)
.sum();
// 查询用户历史购买记录(根据限购类型决定查询时间范围)
int historyPurchaseCount = getHistoryPurchaseCount(order.getUserId(), spu.getId(), spu.getPurchaseLimitType());
// 判断是否超限
int totalCount = currentPurchaseCount + historyPurchaseCount;
if (totalCount > spu.getPurchaseLimitCount()) {
String limitDesc = getLimitDescription(spu.getPurchaseLimitType());
throw exception(ORDER_CREATE_PURCHASE_LIMIT_EXCEED, spu.getName(), limitDesc, spu.getPurchaseLimitCount());
}
}
tradePurchaseLimitService.validateByOrderItems(order.getUserId(), orderItems);
}
/**
* 查询用户历史购买数量(只统计已支付的订单)
*
* @param userId 用户 ID
* @param spuId 商品 SPU ID
* @param limitType 限购类型
* @return 历史购买数量
*/
private int getHistoryPurchaseCount(Long userId, Long spuId, Integer limitType) {
PurchaseLimitTypeEnum limitTypeEnum = PurchaseLimitTypeEnum.valueOf(limitType);
if (limitTypeEnum == null || limitTypeEnum == PurchaseLimitTypeEnum.NONE) {
return 0;
}
LocalDateTime startTime = getStartTimeByLimitType(limitTypeEnum);
if (startTime == null) {
// 终身限购:不限制开始时间
startTime = LocalDateTime.of(2000, 1, 1, 0, 0);
}
// 查询该时间范围内,用户已支付的订单项(订单项表 trade_order_item)
// 注意:需要关联订单表(trade_order)判断订单是否已支付(pay_status = true)
return tradeOrderItemMapper.selectPurchaseCountByUserAndSpu(userId, spuId, startTime);
private boolean isMachineOrder(TradeOrderDO order) {
return Objects.equals(order.getType(), TradeOrderTypeEnum.MACHINE.getType())
|| Objects.equals(order.getDeliveryType(), DeliveryTypeEnum.DOOR.getType());
}
/**
* 根据限购类型获取查询起始时间
*
* @param limitType 限购类型
* @return 起始时间
*/
private LocalDateTime getStartTimeByLimitType(PurchaseLimitTypeEnum limitType) {
LocalDateTime now = LocalDateTime.now();
switch (limitType) {
case DAILY:
// 今天 00:00:00
return now.toLocalDate().atStartOfDay();
case WEEKLY:
// 本周一 00:00:00
return now.with(TemporalAdjusters.previousOrSame(java.time.DayOfWeek.MONDAY))
.toLocalDate().atStartOfDay();
case LIFETIME:
// 终身限购:不限制开始时间(从很早的时间开始统计)
return null;
default:
return null;
}
}
/**
* 获取限购类型的可读描述
*/
private String getLimitDescription(Integer limitType) {
PurchaseLimitTypeEnum limitTypeEnum = PurchaseLimitTypeEnum.valueOf(limitType);
if (limitTypeEnum == null) {
return "限购";
}
return limitTypeEnum.getName();
}
}
@@ -519,6 +519,10 @@ public class TradePriceCalculatorHelper {
result.setPrice(new TradePriceCalculateSummaryRespBO.Price());
recountSummaryAllPrice(result);
recountSummaryAllGivePoint(result);
// 售货机汇总单识别为 MACHINE 类型,触发限购等售货机专属逻辑
if (spuList.stream().anyMatch(spu -> ProductSpuTypesEnum.ENABLE.getTypes().equals(spu.getTypes()))) {
result.setType(TradeOrderTypeEnum.MACHINE.getType());
}
return result;
}