|
@@ -40,6 +40,7 @@ import org.jeecg.modules.pay.config.RatiosUtil;
|
|
|
import org.jeecg.modules.pay.config.WechatConstants;
|
|
import org.jeecg.modules.pay.config.WechatConstants;
|
|
|
import org.jeecg.modules.pay.config.WechatUrlConstants;
|
|
import org.jeecg.modules.pay.config.WechatUrlConstants;
|
|
|
import org.jeecg.modules.rabbitmq.DelayedMessageService;
|
|
import org.jeecg.modules.rabbitmq.DelayedMessageService;
|
|
|
|
|
+import org.jeecg.modules.redission.RedisLockUtils;
|
|
|
import org.jeecg.modules.system.app.dto.ClassGroupingDTO;
|
|
import org.jeecg.modules.system.app.dto.ClassGroupingDTO;
|
|
|
import org.jeecg.modules.system.app.dto.receiptPaymentDetails.ReceiptPaymentDetailsInfoVo;
|
|
import org.jeecg.modules.system.app.dto.receiptPaymentDetails.ReceiptPaymentDetailsInfoVo;
|
|
|
import org.jeecg.modules.system.app.entity.*;
|
|
import org.jeecg.modules.system.app.entity.*;
|
|
@@ -594,37 +595,50 @@ public class OrderServiceImpl extends ServiceImpl<AppOrderMapper, AppOrder> impl
|
|
|
productKey = "ORDER_TYPE_1_PRODUCT_" + productId; // ORDER_TYPE_1_PRODUCT_N001
|
|
productKey = "ORDER_TYPE_1_PRODUCT_" + productId; // ORDER_TYPE_1_PRODUCT_N001
|
|
|
stockKey = "ORDER_TYPE_1_PRODUCT_STOCK_" + productId; // ORDER_TYPE_1_PRODUCT_STOCK_N001
|
|
stockKey = "ORDER_TYPE_1_PRODUCT_STOCK_" + productId; // ORDER_TYPE_1_PRODUCT_STOCK_N001
|
|
|
|
|
|
|
|
- // 查询库存
|
|
|
|
|
- Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
|
|
|
|
|
//使用人
|
|
//使用人
|
|
|
List<String> ids = Arrays.stream(createOrderForm.getFamilyIds().split(",")).collect(Collectors.toList());
|
|
List<String> ids = Arrays.stream(createOrderForm.getFamilyIds().split(",")).collect(Collectors.toList());
|
|
|
- log.info("学校场地预约商品库存数量:{}", stock);
|
|
|
|
|
- // 缓存没有商品库存,查询数据库
|
|
|
|
|
- if (stock == null || stock == 0 || stock < ids.size()) {
|
|
|
|
|
- AppSitePriceRules product = appSitePriceRulesMapper.selectById(productId);
|
|
|
|
|
- if (Objects.isNull(product)) {
|
|
|
|
|
- throw new JeecgBootException("订单提交失败,商品已下架");
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 加分布式锁,防止并发超卖
|
|
|
|
|
+ String stockLockKey = "LOCK_ORDER_STOCK_" + productId;
|
|
|
|
|
+ boolean locked = RedisLockUtils.tryLock(stockLockKey, TimeUnit.SECONDS, 3, 10);
|
|
|
|
|
+ if (!locked) {
|
|
|
|
|
+ throw new JeecgBootException("系统正忙,请稍后再试");
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 查询库存(优先从Redis读)
|
|
|
|
|
+ Object stockObj = redisTemplate.opsForValue().get(stockKey);
|
|
|
|
|
+ Integer stock = stockObj != null ? Integer.parseInt(stockObj.toString()) : null;
|
|
|
|
|
+ log.info("学校场地预约商品库存数量:{}", stock);
|
|
|
|
|
+
|
|
|
|
|
+ // 缓存没有商品库存,查询数据库
|
|
|
|
|
+ if (stock == null) {
|
|
|
|
|
+ AppSitePriceRules product = appSitePriceRulesMapper.selectById(productId);
|
|
|
|
|
+ if (Objects.isNull(product)) {
|
|
|
|
|
+ throw new JeecgBootException("订单提交失败,商品已下架");
|
|
|
|
|
+ }
|
|
|
|
|
+ redisTemplate.opsForValue().set(productKey, JSON.toJSONString(product), 60 * 60 * 24, TimeUnit.SECONDS);
|
|
|
|
|
+ if (product.getTicketNum() == null) {
|
|
|
|
|
+ throw new JeecgBootException("订单提交失败,当前商品库存为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ stock = product.getTicketNum();
|
|
|
|
|
+ redisTemplate.opsForValue().set(stockKey, stock, 60 * 60 * 24, TimeUnit.SECONDS);
|
|
|
}
|
|
}
|
|
|
- redisTemplate.opsForValue().set(productKey, JSON.toJSONString(product), 60 * 60 * 24, TimeUnit.SECONDS);
|
|
|
|
|
- // 数据库的库存信息要根据实际业务来获取,如果商品有规格信息,库存应该根据规格来获取
|
|
|
|
|
- if (product.getTicketNum() == null) {
|
|
|
|
|
- throw new JeecgBootException("订单提交失败,当前商品库存为空");
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 检查库存是否足够
|
|
|
|
|
+ if (stock < ids.size()) {
|
|
|
|
|
+ throw new JeecgBootException("订单提交失败,库存不足");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 原子扣减数据库库存(防并发覆盖写)
|
|
|
|
|
+ log.info("更新学校场地数据库中的库存数据,扣减数量:{}", ids.size());
|
|
|
|
|
+ int row = appSitePriceRulesMapper.deductStock(productId, ids.size());
|
|
|
|
|
+ if (row <= 0) {
|
|
|
|
|
+ throw new JeecgBootException("订单提交失败,库存不足");
|
|
|
}
|
|
}
|
|
|
- stock = product.getTicketNum();
|
|
|
|
|
- redisTemplate.opsForValue().set(stockKey, stock, 60 * 60 * 24, TimeUnit.SECONDS);
|
|
|
|
|
- }
|
|
|
|
|
- // 检查库存是否足够
|
|
|
|
|
- if (stock < ids.size()) {
|
|
|
|
|
- throw new JeecgBootException("订单提交失败,库存不足");
|
|
|
|
|
- }
|
|
|
|
|
- // 更新数据库中的库存数据
|
|
|
|
|
- log.info("更新学校场地数据库中的库存数据:{}", stock - ids.size());
|
|
|
|
|
- int row = appSitePriceRulesMapper.update(null, Wrappers.<AppSitePriceRules>lambdaUpdate()
|
|
|
|
|
- .eq(AppSitePriceRules::getId, productId)
|
|
|
|
|
- .set(AppSitePriceRules::getTicketNum, (stock - ids.size())));
|
|
|
|
|
- if (row > 0) {
|
|
|
|
|
// 更新Redis中缓存的商品库存数据
|
|
// 更新Redis中缓存的商品库存数据
|
|
|
redisTemplate.opsForValue().decrement(stockKey, ids.size());
|
|
redisTemplate.opsForValue().decrement(stockKey, ids.size());
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ RedisLockUtils.unlock(stockLockKey);
|
|
|
}
|
|
}
|
|
|
// 库存扣减完,创建订单
|
|
// 库存扣减完,创建订单
|
|
|
appOrder
|
|
appOrder
|
|
@@ -2568,6 +2582,9 @@ public class OrderServiceImpl extends ServiceImpl<AppOrderMapper, AppOrder> impl
|
|
|
if (!Objects.equals(appOrder.getOrderType(), CommonConstant.ORDER_PRO_INFO_TYPE_0)){
|
|
if (!Objects.equals(appOrder.getOrderType(), CommonConstant.ORDER_PRO_INFO_TYPE_0)){
|
|
|
throw new JeecgBootException("只能主动取消学校场地预约订单!");
|
|
throw new JeecgBootException("只能主动取消学校场地预约订单!");
|
|
|
}
|
|
}
|
|
|
|
|
+ if (Objects.equals(appOrder.getOrderStatus(), CommonConstant.ORDER_STATUS_4)) {
|
|
|
|
|
+ throw new JeecgBootException("当前订单已取消,请勿重复操作!");
|
|
|
|
|
+ }
|
|
|
List<AppOrderProInfo> orderProInfoList = appOrderProInfoMapper.selectList(Wrappers.<AppOrderProInfo>lambdaQuery().eq(AppOrderProInfo::getOrderId, orderId));
|
|
List<AppOrderProInfo> orderProInfoList = appOrderProInfoMapper.selectList(Wrappers.<AppOrderProInfo>lambdaQuery().eq(AppOrderProInfo::getOrderId, orderId));
|
|
|
if (CollUtil.isNotEmpty(orderProInfoList)) {
|
|
if (CollUtil.isNotEmpty(orderProInfoList)) {
|
|
|
// 按productId分组统计需要恢复的库存数量(排除保险类型的子订单)
|
|
// 按productId分组统计需要恢复的库存数量(排除保险类型的子订单)
|
|
@@ -2580,20 +2597,19 @@ public class OrderServiceImpl extends ServiceImpl<AppOrderMapper, AppOrder> impl
|
|
|
String productId = entry.getKey();
|
|
String productId = entry.getKey();
|
|
|
int restoreCount = entry.getValue().intValue();
|
|
int restoreCount = entry.getValue().intValue();
|
|
|
|
|
|
|
|
- // 恢复数据库库存
|
|
|
|
|
- AppSitePriceRules priceRules = appSitePriceRulesMapper.selectById(productId);
|
|
|
|
|
- if (ObjectUtil.isNotEmpty(priceRules)) {
|
|
|
|
|
- int newTicketNum = (priceRules.getTicketNum() == null ? 0 : priceRules.getTicketNum()) + restoreCount;
|
|
|
|
|
- appSitePriceRulesMapper.update(null, Wrappers.<AppSitePriceRules>lambdaUpdate()
|
|
|
|
|
- .eq(AppSitePriceRules::getId, productId)
|
|
|
|
|
- .set(AppSitePriceRules::getTicketNum, newTicketNum));
|
|
|
|
|
- log.info("取消学校订单恢复库存,productId={},恢复数量={},恢复后库存={}", productId, restoreCount, newTicketNum);
|
|
|
|
|
|
|
+ // 恢复数据库库存(原子操作,使用原生SQL不受@TableLogic影响)
|
|
|
|
|
+ int restored = appSitePriceRulesMapper.restoreStock(productId, restoreCount);
|
|
|
|
|
+ if (restored > 0) {
|
|
|
|
|
+ log.info("取消学校订单恢复库存,productId={},恢复数量={}", productId, restoreCount);
|
|
|
|
|
|
|
|
// 恢复Redis缓存库存
|
|
// 恢复Redis缓存库存
|
|
|
String stockKey = "ORDER_TYPE_1_PRODUCT_STOCK_" + productId;
|
|
String stockKey = "ORDER_TYPE_1_PRODUCT_STOCK_" + productId;
|
|
|
- Integer cachedStock = (Integer) redisTemplate.opsForValue().get(stockKey);
|
|
|
|
|
- if (cachedStock != null) {
|
|
|
|
|
|
|
+ Object stockObj = redisTemplate.opsForValue().get(stockKey);
|
|
|
|
|
+ if (stockObj != null) {
|
|
|
redisTemplate.opsForValue().increment(stockKey, restoreCount);
|
|
redisTemplate.opsForValue().increment(stockKey, restoreCount);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 缓存不存在时直接删除key,下次下单时会从已提交的DB重新加载
|
|
|
|
|
+ redisTemplate.delete(stockKey);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|