Prechádzať zdrojové kódy

调整商品库存、销量变更逻辑

codingliang 3 mesiacov pred
rodič
commit
9e184098ee

+ 7 - 0
db/update_260319.sql

@@ -0,0 +1,7 @@
+-- 给goods_shop_relevancy表添加版本号字段
+ALTER TABLE goods_shop_relevancy
+    ADD COLUMN version INT DEFAULT 0 COMMENT '乐观锁版本号';
+
+-- goods_shop_relevancy添加索引优化查询
+ALTER TABLE goods_shop_relevancy
+    ADD INDEX idx_shop_goods (shop_id, goods_id);

+ 19 - 0
src/main/java/com/sqx/modules/goods/bo/StockUpdateItemBO.java

@@ -0,0 +1,19 @@
+package com.sqx.modules.goods.bo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 商品库存更新bo
+ * @author codingliang
+ * @date 2026-03-19
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class StockUpdateItemBO {
+    private Long goodsId;
+    private Integer num;
+    private Integer version;
+}

+ 17 - 1
src/main/java/com/sqx/modules/goods/dao/GoodsShopRelevancyDao.java

@@ -1,15 +1,31 @@
 package com.sqx.modules.goods.dao;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.sqx.modules.goods.bo.StockUpdateItemBO;
 import com.sqx.modules.goods.entity.GoodsShopRelevancy;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 
+import java.util.List;
+
 @Mapper
 public interface GoodsShopRelevancyDao extends BaseMapper<GoodsShopRelevancy> {
 
     int selectGoodsCount(@Param("orderId") Long orderId1,@Param("goodsId") Long goodsId);
 
-    // GoodsShopRelevancy selectInventory(Long shopId, Long goodsId);
+    /**
+     * 批量乐观锁更新
+     * @param items 更新项列表
+     * @return 更新成功的记录数
+     */
+    int batchOptimisticUpdateStock(@Param("list") List<StockUpdateItemBO> items);
+
+    /**
+     * 查询商品带版本号
+     */
+    GoodsShopRelevancy selectWithVersion(@Param("goodsId") Long goodsId);
 
+    List<GoodsShopRelevancy> selectBatchWithVersion(List<Long> goodsIds);
+
+    // GoodsShopRelevancy selectInventory(Long shopId, Long goodsId);
 }

+ 3 - 0
src/main/java/com/sqx/modules/goods/entity/GoodsShopRelevancy.java

@@ -39,5 +39,8 @@ public class GoodsShopRelevancy implements Serializable {
     @ApiModelProperty("添加时间")
     private String createTime;
 
+    @ApiModelProperty("版本号")
+    private Integer version;
+
     public GoodsShopRelevancy() {}
 }

+ 86 - 42
src/main/java/com/sqx/modules/order/service/impl/AppAppOrderServiceImpl.java

@@ -55,6 +55,7 @@ import com.sqx.modules.errand.entity.TbIndentSmsSendLog;
 import com.sqx.modules.errand.service.TbIndentService;
 import com.sqx.modules.errand.service.TbIndentSmsSendLogService;
 import com.sqx.modules.errand.service.TbIndentSmsTemplateService;
+import com.sqx.modules.goods.bo.StockUpdateItemBO;
 import com.sqx.modules.goods.dao.GoodsDao;
 import com.sqx.modules.goods.dao.GoodsShopDao;
 import com.sqx.modules.goods.dao.GoodsShopRelevancyDao;
@@ -636,33 +637,75 @@ public class AppAppOrderServiceImpl extends ServiceImpl<AppOrderDao, TbOrder> im
         }
     }
 
+    /**
+     * 乐观锁扣减库存 - 独立事务
+     * 允许负数,不检查库存
+     */
     @Override
-    public void subStock(TbOrder tbOrder) {
-        Long shopId1 = tbOrder.getShopId();
-        Long orderId = tbOrder.getOrderId();
-        List<OrderGoods> orderGoodsList = orderGoodsDao.selectList(
-                new QueryWrapper<OrderGoods>()
-                        .eq("order_id", orderId));
-        for (int a = 0; a < orderGoodsList.size(); a++) {
-            Integer goodsNum = orderGoodsList.get(a).getGoodsNum();
-            Long goodsId = orderGoodsList.get(a).getGoodsId();
-            GoodsShopRelevancy goodsShopRelevancy = goodsShopRelevancyDao.selectOne(
-                    new QueryWrapper<GoodsShopRelevancy>()
-                            .eq("shop_id", shopId1)
-                            .eq("goods_id", goodsId));
-            GoodsShopRelevancy goodsShopRelevancy1 = new GoodsShopRelevancy();
-            goodsShopRelevancy1.setId(goodsShopRelevancy.getId());
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public void subStock(TbOrder order) {
+        Long orderId = order.getOrderId();
 
-            //加销量
-            goodsShopRelevancy1.setSales(goodsShopRelevancy.getSales() + goodsNum);
+        log.info("订单{}开始乐观锁扣减库存", orderId);
 
-            //减库存
-            goodsShopRelevancy1.setInventory(goodsShopRelevancy.getInventory() - goodsNum);
-            goodsShopRelevancyDao.updateById(goodsShopRelevancy1);
-            // goodsShopDao.updateShopSales(1, goodsNum, goodsShopRelevancy.getShopId());
-        }
+        try {
+            // 1. 获取订单商品并聚合
+            List<OrderGoods> orderGoodsList = orderGoodsDao.selectList(
+                    new QueryWrapper<OrderGoods>().eq("order_id", orderId));
 
-//        goodsShopDao.updateShopSales(1, 1, tbOrder.getShopId());
+            if (orderGoodsList.isEmpty()) {
+                log.warn("订单{}没有商品", orderId);
+                return;
+            }
+
+            // 2. 按商品ID聚合数量
+            Map<Long, Integer> goodsNumMap = orderGoodsList.stream()
+                    .collect(Collectors.groupingBy(
+                            OrderGoods::getGoodsId,
+                            Collectors.summingInt(OrderGoods::getGoodsNum)
+                    ));
+
+            // 3. 批量查询当前版本号
+            List<Long> goodsIds = new ArrayList<>(goodsNumMap.keySet());
+            List<GoodsShopRelevancy> currentStocks = goodsShopRelevancyDao.selectBatchWithVersion(goodsIds);
+
+            // 4. 构建版本号Map
+            Map<Long, Integer> versionMap = currentStocks.stream()
+                    .collect(Collectors.toMap(
+                            GoodsShopRelevancy::getId,
+                            GoodsShopRelevancy::getVersion
+                    ));
+
+            // 5. 构建更新项
+            List<StockUpdateItemBO> updateItems = new ArrayList<>();
+            for (Map.Entry<Long, Integer> entry : goodsNumMap.entrySet()) {
+                Long goodsId = entry.getKey();
+                Integer num = entry.getValue();
+                Integer version = versionMap.get(goodsId);
+
+                if (version == null) {
+                    log.warn("商品{}不存在", goodsId);
+                    continue;
+                }
+
+                updateItems.add(new StockUpdateItemBO(goodsId, num, version));
+            }
+
+            // 6. 执行批量乐观锁更新
+            int updated = goodsShopRelevancyDao.batchOptimisticUpdateStock(updateItems);
+
+            boolean success = updated == updateItems.size();
+
+            if (success) {
+                log.info("订单{}库存扣减成功,更新{}个商品", orderId, updated);
+            } else {
+                // 允许更新失败,继续处理其他商品
+                log.warn("订单{}部分商品更新失败,预期{},实际{}", orderId, updateItems.size(), updated);
+            }
+        } catch (Exception e) {
+            log.error("订单{}库存扣减异常", orderId, e);
+            throw new SqxException("库存扣减失败", e);
+        }
     }
 
     @Override
@@ -675,12 +718,6 @@ public class AppAppOrderServiceImpl extends ServiceImpl<AppOrderDao, TbOrder> im
         // 店铺信息
         GoodsShop goodsShop = shopMessageService.getShopInfoById(order.getShopId());
 
-        // 小程序推送设置
-        CommonInfo mpPushConfig = commonInfoService.findOne(269);
-
-        // 用户信息
-        UserEntity userEntity = userDao.selectById(order.getUserId());
-
         // 是否自动接单
         boolean autoAccept = goodsShop.getAutoAcceptOrder() != null && goodsShop.getAutoAcceptOrder() == 0;
         // 是否预约订单
@@ -706,9 +743,6 @@ public class AppAppOrderServiceImpl extends ServiceImpl<AppOrderDao, TbOrder> im
             // 更新订单状态及订单序号
             updateOrderStatusAndSequence(order);
 
-            // 扣减库存
-            this.subStock(order);
-
             // 优惠券变成已使用状态
             updateCouponState(order);
 
@@ -719,15 +753,6 @@ public class AppAppOrderServiceImpl extends ServiceImpl<AppOrderDao, TbOrder> im
             if (order.getStatus() == 6) {
                 // 如果是外卖订单,则生成一个跑腿的订单
                 tbIndentService.insertIndent(order);
-
-                // 发送商家接单通知
-                MyGlobalThreadPool.execute(() -> {
-                    try {
-                        sendOrderAcceptMessage(order, goodsShop, mpPushConfig, userEntity);
-                    } catch (Exception e) {
-                        log.error("订单:{},商家接单通知发送失败,失败原因:{}", order.getOrderId(), e);
-                    }
-                });
             }
 
             // 在锁中提交事务
@@ -742,8 +767,27 @@ public class AppAppOrderServiceImpl extends ServiceImpl<AppOrderDao, TbOrder> im
             lock.unlock();
         }
 
-        // 添加消息记录并且进行推送
         MyGlobalThreadPool.execute(() -> {
+            // 扣减库存,允许部分超卖,后续再优化超卖问题
+            try {
+                this.subStock(order);
+            } catch (Exception e) {
+                log.error("订单:{},扣减库存失败,失败原因:{}", order.getOrderId(), e);
+            }
+
+            // 小程序推送设置
+            CommonInfo mpPushConfig = commonInfoService.findOne(269);
+            // 用户信息
+            UserEntity userEntity = userDao.selectById(order.getUserId());
+
+            // 发送商家接单通知
+            try {
+                sendOrderAcceptMessage(order, goodsShop, mpPushConfig, userEntity);
+            } catch (Exception e) {
+                log.error("订单:{},商家接单通知发送失败,失败原因:{}", order.getOrderId(), e);
+            }
+
+            // 添加消息记录并且进行推送
             try {
                 addOrderMessageAndPush(order, goodsShop, mpPushConfig, userEntity);
             } catch (Exception e) {

+ 29 - 0
src/main/resources/mapper/goods/GoodsShopRelevancyMapper.xml

@@ -2,6 +2,35 @@
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.sqx.modules.goods.dao.GoodsShopRelevancyDao">
 
+    <update id="batchOptimisticUpdateStock">
+        <foreach collection="list" item="item" separator=";">
+            UPDATE goods_shop_relevancy
+            SET
+            inventory = inventory - #{item.num},
+            sales = sales + #{item.num},
+            version = version + 1
+            WHERE id = #{item.goodsId}
+            AND version = #{item.version}
+        </foreach>
+    </update>
+
+    <!-- 查询并获取版本号 -->
+    <select id="selectWithVersion" resultType="com.sqx.modules.goods.entity.GoodsShopRelevancy">
+        SELECT id, shop_id, goods_id, inventory, sales, version
+        FROM goods_shop_relevancy
+        WHERE id = #{goodsId}
+    </select>
+
+    <!-- 批量查询版本号 -->
+    <select id="selectBatchWithVersion" resultType="com.sqx.modules.goods.entity.GoodsShopRelevancy">
+        SELECT id, shop_id, goods_id, inventory, sales, version
+        FROM goods_shop_relevancy
+        WHERE id IN
+        <foreach collection="list" item="goodsId" open="(" separator="," close=")">
+            #{goodsId}
+        </foreach>
+    </select>
+
     <select id="selectGoodsCount" resultType="int">
         select ifnull(sum(goods_num), 0) from order_goods where order_id = #{orderId} and goods_id = #{goodsId}
     </select>