修复因网络延迟问题存在的重复提交问题,增加售货机订单分布式锁
This commit is contained in:
1 parent
67d3247d80
commit
4be1427f7c
8 files changed
+235
-183
No files matched your search
+8
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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)) {
|
||||
|
||||
+7
@@ -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:";
|
||||
|
||||
}
|
||||
+5
@@ -23,6 +23,11 @@ public interface TradeOrderUpdateService {
|
||||
|
||||
void isSkuPurchaseLimit(Long payUserId, List<Long> skuIdList);
|
||||
|
||||
/**
|
||||
* 校验限购(含 SKU 购买数量)
|
||||
*/
|
||||
void validatePurchaseLimit(Long userId, List<AppTradeOrderSettlementReqVO.Item> items);
|
||||
|
||||
/**
|
||||
* 获得订单结算信息
|
||||
*
|
||||
|
||||
+34
-53
@@ -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);
|
||||
|
||||
+163
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+12
-126
@@ -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();
|
||||
}
|
||||
}
|
||||
+4
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user