Kaynağa Gözat

perf: 库存初始化和补货完成批量优化,消除 N+1 查询

- initInventoryFromTemplate: 3 次批量操作替代 3N 次单条操作
  1. listByCodes() 批量查商品 (1 SELECT IN)
  2. lambdaQuery 批量查已有库存 (1 SELECT)
  3. saveBatch() 批量插入 (1 INSERT)
- completeOrder: 批量查库存+批量写入替代逐条操作
  1. 批量查已有库存 (1 SELECT IN)
  2. saveBatch/updateBatchById 批量写入

Co-Authored-By: Claude <noreply@anthropic.com>
skyline 3 gün önce
ebeveyn
işleme
af39b857e5

+ 6 - 1
haha-service/src/main/java/com/haha/service/ProductService.java

@@ -90,7 +90,12 @@ public interface ProductService extends IService<Product> {
      * 根据商品编码获取商品
      */
     Product getProductByCode(String code);
-    
+
+    /**
+     * 根据商品编码列表批量查询商品
+     */
+    List<Product> listByCodes(java.util.Collection<String> codes);
+
     /**
      * 更新同步状态
      */

+ 49 - 31
haha-service/src/main/java/com/haha/service/impl/LayerTemplateServiceImpl.java

@@ -1149,42 +1149,60 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
         }
 
         log.info("从设备模版提取到 {} 个商品编码,开始初始化库存: deviceId={}", productCodes.size(), deviceId);
-        int created = 0;
+
+        // 1. 批量查询本地商品(1 次 SELECT)
+        List<Product> localProducts = productService.listByCodes(productCodes);
+        Map<String, Product> codeToProduct = new HashMap<>();
+        for (Product p : localProducts) {
+            codeToProduct.put(p.getCode(), p);
+        }
+
+        // 2. 批量查询已有库存记录(1 次 SELECT)
+        List<DeviceInventory> existingInventories = deviceInventoryService.lambdaQuery()
+                .eq(DeviceInventory::getDeviceId, deviceId)
+                .list();
+        java.util.Set<Long> existingProductIds = new java.util.HashSet<>();
+        for (DeviceInventory inv : existingInventories) {
+            if (inv.getProductId() != null) {
+                existingProductIds.add(inv.getProductId());
+            }
+        }
+
+        // 3. 收集需要新建的库存记录
+        List<DeviceInventory> newInventories = new ArrayList<>();
         int skipped = 0;
 
         for (String code : productCodes) {
-            try {
-                Product product = productService.getProductByCode(code);
-                if (product == null) {
-                    log.debug("商品编码在本地库中不存在,跳过: code={}, deviceId={}", code, deviceId);
-                    skipped++;
-                    continue;
-                }
+            Product product = codeToProduct.get(code);
+            if (product == null) {
+                log.debug("商品编码在本地库中不存在,跳过: code={}, deviceId={}", code, deviceId);
+                skipped++;
+                continue;
+            }
+            if (existingProductIds.contains(product.getId())) {
+                log.debug("库存记录已存在,跳过: deviceId={}, productCode={}", deviceId, code);
+                skipped++;
+                continue;
+            }
 
-                DeviceInventory existing = deviceInventoryService.getByDeviceAndProduct(deviceId, product.getId());
-                if (existing != null) {
-                    log.debug("库存记录已存在,跳过: deviceId={}, productCode={}", deviceId, code);
-                    skipped++;
-                    continue;
-                }
+            DeviceInventory inventory = new DeviceInventory();
+            inventory.setDeviceId(deviceId);
+            inventory.setProductId(product.getId());
+            inventory.setProductCode(code);
+            inventory.setProductName(product.getName());
+            inventory.setStock(0);
+            inventory.setWarningThreshold(5);
+            inventory.setCreateTime(LocalDateTime.now());
+            inventory.setUpdateTime(LocalDateTime.now());
+            newInventories.add(inventory);
+        }
 
-                DeviceInventory inventory = new DeviceInventory();
-                inventory.setDeviceId(deviceId);
-                inventory.setProductId(product.getId());
-                inventory.setProductCode(code);
-                inventory.setProductName(product.getName());
-                inventory.setStock(0);
-                inventory.setWarningThreshold(5);
-                inventory.setCreateTime(LocalDateTime.now());
-                inventory.setUpdateTime(LocalDateTime.now());
-                deviceInventoryService.save(inventory);
-                created++;
-
-                log.info("初始化库存记录: deviceId={}, productCode={}, productName={}, stock=0",
-                        deviceId, code, product.getName());
-            } catch (Exception e) {
-                log.error("初始化单条库存记录失败: deviceId={}, productCode={}", deviceId, code, e);
-            }
+        // 4. 批量插入(1 次批量 INSERT)
+        int created = 0;
+        if (!newInventories.isEmpty()) {
+            deviceInventoryService.saveBatch(newInventories);
+            created = newInventories.size();
+            log.info("批量初始化库存记录: deviceId={}, 创建={}", deviceId, created);
         }
 
         log.info("库存初始化完成: deviceId={}, 创建={}, 跳过={}", deviceId, created, skipped);

+ 8 - 0
haha-service/src/main/java/com/haha/service/impl/ProductServiceImpl.java

@@ -184,6 +184,14 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
         return lambdaQuery().eq(Product::getCode, code).one();
     }
 
+    @Override
+    public List<Product> listByCodes(java.util.Collection<String> codes) {
+        if (codes == null || codes.isEmpty()) {
+            return List.of();
+        }
+        return lambdaQuery().in(Product::getCode, codes).list();
+    }
+
     @Override
     public boolean updateSyncStatus(Long productId, Integer syncStatus) {
         return lambdaUpdate()

+ 61 - 35
haha-service/src/main/java/com/haha/service/impl/ReplenishmentOrderServiceImpl.java

@@ -48,6 +48,7 @@ public class ReplenishmentOrderServiceImpl extends ServiceImpl<ReplenishmentOrde
     private final DeviceInventoryMapper deviceInventoryMapper;
     private final ProductMapper productMapper;
     private final ErpGoodsMapper erpGoodsMapper;
+    private final com.haha.service.DeviceInventoryService deviceInventoryService;
 
     @Autowired(required = false)
     private QdbClient qdbClient;
@@ -454,48 +455,73 @@ public class ReplenishmentOrderServiceImpl extends ServiceImpl<ReplenishmentOrde
             throw new BusinessException(400, "仅已同步ERP的补货单可完成");
         }
 
-        // 同步更新设备库存
+        // 同步更新设备库存(批量优化)
         List<ReplenishmentOrderItem> items = orderItemMapper.selectByOrderId(id);
-        for (ReplenishmentOrderItem item : items) {
-            if (item.getPlannedQuantity() == null || item.getPlannedQuantity() <= 0) continue;
-
-            // 查找或创建库存记录(用 productId 匹配,与其他路径保持一致)
-            DeviceInventory inv = deviceInventoryMapper.selectByDeviceAndProduct(
-                    order.getDeviceId(), item.getProductId());
-            if (inv == null) {
-                inv = new DeviceInventory();
-                inv.setDeviceId(order.getDeviceId());
-                inv.setProductId(item.getProductId());
-                inv.setProductCode(item.getProductCode());
-                inv.setProductName(item.getProductName());
-                inv.setStock(0);
-                inv.setShelfNum(item.getShelfNum());
-                inv.setPosition(item.getPosition());
-                inv.setWarningThreshold(5);
-                inv.setCreateTime(LocalDateTime.now());
-                inv.setUpdateTime(LocalDateTime.now());
+        List<ReplenishmentOrderItem> validItems = items.stream()
+                .filter(it -> it.getPlannedQuantity() != null && it.getPlannedQuantity() > 0)
+                .toList();
+
+        if (!validItems.isEmpty()) {
+            // 1. 批量查询已有库存(1 次 SELECT IN)
+            List<Long> productIds = validItems.stream()
+                    .map(ReplenishmentOrderItem::getProductId).toList();
+            List<DeviceInventory> existingInventories = deviceInventoryMapper.selectList(
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DeviceInventory>()
+                            .eq(DeviceInventory::getDeviceId, order.getDeviceId())
+                            .in(DeviceInventory::getProductId, productIds));
+            Map<Long, DeviceInventory> inventoryByProductId = new HashMap<>();
+            for (DeviceInventory inv : existingInventories) {
+                inventoryByProductId.put(inv.getProductId(), inv);
             }
 
-            int beforeStock = inv.getStock() != null ? inv.getStock() : 0;
-            int afterStock = beforeStock + item.getPlannedQuantity();
-            inv.setStock(afterStock);
-            inv.setLastRestockTime(LocalDateTime.now());
-            inv.setUpdateTime(LocalDateTime.now());
-            if (inv.getId() == null) {
-                deviceInventoryMapper.insert(inv);
-            } else {
-                deviceInventoryMapper.updateById(inv);
+            // 2. 区分新建和更新
+            List<DeviceInventory> toInsert = new ArrayList<>();
+            List<DeviceInventory> toUpdate = new ArrayList<>();
+
+            for (ReplenishmentOrderItem item : validItems) {
+                DeviceInventory inv = inventoryByProductId.get(item.getProductId());
+                if (inv == null) {
+                    inv = new DeviceInventory();
+                    inv.setDeviceId(order.getDeviceId());
+                    inv.setProductId(item.getProductId());
+                    inv.setProductCode(item.getProductCode());
+                    inv.setProductName(item.getProductName());
+                    inv.setStock(0);
+                    inv.setShelfNum(item.getShelfNum());
+                    inv.setPosition(item.getPosition());
+                    inv.setWarningThreshold(5);
+                    inv.setCreateTime(LocalDateTime.now());
+                }
+
+                int beforeStock = inv.getStock() != null ? inv.getStock() : 0;
+                int afterStock = beforeStock + item.getPlannedQuantity();
+                inv.setStock(afterStock);
+                inv.setLastRestockTime(LocalDateTime.now());
+                inv.setUpdateTime(LocalDateTime.now());
+                (inv.getId() == null ? toInsert : toUpdate).add(inv);
+
+                item.setBeforeStock(beforeStock);
+                item.setAfterStock(afterStock);
+                if (item.getActualQuantity() == null) {
+                    item.setActualQuantity(item.getPlannedQuantity());
+                }
+
+                log.info("更新设备库存: deviceId={}, productCode={}, {} -> {}",
+                        order.getDeviceId(), item.getProductCode(), beforeStock, afterStock);
             }
 
-            item.setBeforeStock(beforeStock);
-            item.setAfterStock(afterStock);
-            if (item.getActualQuantity() == null) {
-                item.setActualQuantity(item.getPlannedQuantity());
+            // 3. 批量写入(1 次批量 INSERT + 1 次批量 UPDATE)
+            if (!toInsert.isEmpty()) {
+                deviceInventoryService.saveBatch(toInsert);
+            }
+            if (!toUpdate.isEmpty()) {
+                deviceInventoryService.updateBatchById(toUpdate);
             }
-            orderItemMapper.updateById(item);
 
-            log.info("更新设备库存: deviceId={}, productCode={}, {} -> {}",
-                    order.getDeviceId(), item.getProductCode(), beforeStock, afterStock);
+            // 4. 更新补货单明细的 beforeStock/afterStock(已从内存设置,逐条 updateById)
+            for (ReplenishmentOrderItem item : validItems) {
+                orderItemMapper.updateById(item);
+            }
         }
 
         order.setStatus(ReplenishmentOrderStatusEnum.COMPLETED.getCode());