Эх сурвалжийг харах

feat: 新增standard_stock字段,同步层模版标准库存到设备库存表

- DeviceInventory新增standardStock字段 + SQL迁移
- DeviceInventoryService新增recalculateStandardStock,解析层模版JSON累加标准库存
- 层模版create/update/delete/sync后自动触发标准库存重算
- getSuggestions简化为直接读standardStock,删除模板解析累加逻辑
- 前端/设备列表标准库存改用真实standard_stock字段
- Mapper统计查询standard_stock别名改用真实字段

Co-Authored-By: Claude <noreply@anthropic.com>
skyline 12 цаг өмнө
parent
commit
c9d4a93325

+ 1 - 1
haha-admin-mp/src/pages/replenish/operation.vue

@@ -51,7 +51,7 @@
           <view class="item-stock">
             <view class="stock-col">
               <text class="stock-label">标准库存</text>
-              <text class="stock-num">{{ item.warning_threshold || item.warningThreshold || '-' }}</text>
+              <text class="stock-num">{{ item.standard_stock || item.standardStock || '-' }}</text>
             </view>
             <view class="stock-col">
               <text class="stock-label">剩余库存</text>

+ 2 - 1
haha-admin/src/main/java/com/haha/admin/controller/ReplenisherOperationController.java

@@ -171,8 +171,9 @@ public class ReplenisherOperationController {
             for (Map<String, Object> inv : inventoryList) {
                 Integer stock = inv.get("stock") != null ? ((Number) inv.get("stock")).intValue() : 0;
                 Integer warningThreshold = inv.get("warning_threshold") != null ? ((Number) inv.get("warning_threshold")).intValue() : 0;
+                Integer invStandardStock = inv.get("standard_stock") != null ? ((Number) inv.get("standard_stock")).intValue() : 0;
                 totalStock += stock;
-                standardStock += warningThreshold;
+                standardStock += invStandardStock;
                 if (stock == 0) {
                     zeroStockCount++;
                 } else if (stock <= warningThreshold) {

+ 6 - 0
haha-admin/src/main/resources/sql/inventory.sql

@@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS `t_device_inventory` (
     `shelf_num` INT DEFAULT NULL COMMENT '所在货架层号',
     `position` VARCHAR(20) DEFAULT NULL COMMENT '货道位置(left/right)',
     `warning_threshold` INT DEFAULT 5 COMMENT '库存预警阈值',
+    `standard_stock` INT DEFAULT 0 COMMENT '标准库存(来自层模版汇总)',
     `last_restock_time` DATETIME DEFAULT NULL COMMENT '最后补货时间',
     `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
@@ -136,3 +137,8 @@ CREATE TABLE IF NOT EXISTS `t_replenisher_device` (
     INDEX `idx_device_id` (`device_id`),
     INDEX `idx_replenisher_id` (`replenisher_id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='补货员-设备关联表';
+
+-- ==================== 迁移脚本(已存在的数据库执行) ====================
+
+-- 添加 standard_stock 字段(标准库存,来自层模版汇总)
+ALTER TABLE `t_device_inventory` ADD COLUMN IF NOT EXISTS `standard_stock` INT DEFAULT 0 COMMENT '标准库存(来自层模版汇总)' AFTER `warning_threshold`;

+ 5 - 0
haha-entity/src/main/java/com/haha/entity/DeviceInventory.java

@@ -65,6 +65,11 @@ public class DeviceInventory implements Serializable {
      */
     private Integer warningThreshold;
 
+    /**
+     * 标准库存(来自层模版汇总)
+     */
+    private Integer standardStock;
+
     /**
      * 最后补货时间
      */

+ 1 - 1
haha-mapper/src/main/java/com/haha/mapper/DeviceInventoryMapper.java

@@ -57,7 +57,7 @@ public interface DeviceInventoryMapper extends BaseMapper<DeviceInventory> {
             "  s.name as shop_name, " +
             "  s.address as shop_address, " +
             "  COALESCE(SUM(di.stock), 0) as remaining_stock, " +
-            "  COALESCE(SUM(di.warning_threshold), 0) as standard_stock, " +
+            "  COALESCE(SUM(di.standard_stock), 0) as standard_stock, " +
             "  COUNT(di.id) as product_count, " +
             "  MAX(di.last_restock_time) as last_restock_time, " +
             "  SUM(CASE WHEN di.stock = 0 THEN 1 ELSE 0 END) as zero_stock_count, " +

+ 9 - 0
haha-service/src/main/java/com/haha/service/DeviceInventoryService.java

@@ -101,6 +101,15 @@ public interface DeviceInventoryService extends IService<DeviceInventory> {
      */
     DeviceInventory getByDeviceAndProduct(String deviceId, Long productId);
 
+    /**
+     * 重新计算指定设备的标准库存(从层模版汇总)
+     * 解析层模版的 leftFloors/rightFloors,按 productCode 累加各商品的标准库存,
+     * 写入 DeviceInventory.standardStock
+     *
+     * @param deviceId 设备SN号
+     */
+    void recalculateStandardStock(String deviceId);
+
     /**
      * 分页查询设备库存统计列表
      *

+ 75 - 0
haha-service/src/main/java/com/haha/service/impl/DeviceInventoryServiceImpl.java

@@ -1,16 +1,23 @@
 package com.haha.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.haha.common.dto.FloorConfig;
+import com.haha.common.dto.GoodsItem;
 import com.haha.common.exception.BusinessException;
 import com.haha.entity.Device;
 import com.haha.entity.DeviceInventory;
 import com.haha.entity.InventoryLog;
+import com.haha.entity.LayerTemplate;
 import com.haha.entity.dto.InventoryQueryDTO;
 import com.haha.mapper.DeviceInventoryMapper;
 import com.haha.mapper.DeviceMapper;
+import com.haha.mapper.LayerTemplateMapper;
 import com.haha.service.DeviceInventoryService;
 import com.haha.service.InventoryLogService;
 import lombok.RequiredArgsConstructor;
@@ -39,6 +46,8 @@ public class DeviceInventoryServiceImpl extends ServiceImpl<DeviceInventoryMappe
 
     private final DeviceMapper deviceMapper;
 
+    private final LayerTemplateMapper layerTemplateMapper;
+
     @Override
     public IPage<DeviceInventory> getPage(int page, int pageSize, InventoryQueryDTO queryDTO) {
         LambdaQueryWrapper<DeviceInventory> wrapper = new LambdaQueryWrapper<>();
@@ -227,6 +236,72 @@ public class DeviceInventoryServiceImpl extends ServiceImpl<DeviceInventoryMappe
         return deviceInventoryMapper.selectByDeviceAndProduct(deviceId, productId);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void recalculateStandardStock(String deviceId) {
+        // 1. 查询该设备的所有层模版
+        List<LayerTemplate> templates = layerTemplateMapper.selectByDeviceId(deviceId);
+        if (templates == null || templates.isEmpty()) {
+            // 无层模版,清空所有标准库存
+            LambdaUpdateWrapper<DeviceInventory> clearWrapper = new LambdaUpdateWrapper<DeviceInventory>()
+                    .eq(DeviceInventory::getDeviceId, deviceId);
+            DeviceInventory clearRecord = new DeviceInventory();
+            clearRecord.setStandardStock(0);
+            update(clearRecord, clearWrapper);
+            log.info("标准库存重算完成(无层模版): deviceId={}, 标准库存全部置0", deviceId);
+            return;
+        }
+
+        // 2. 解析层模版,按 productCode 累加标准库存
+        Map<String, Integer> standardMap = new HashMap<>();
+        ObjectMapper mapper = new ObjectMapper();
+
+        for (LayerTemplate template : templates) {
+            accumulateFromFloors(mapper, template.getLeftFloors(), standardMap);
+            accumulateFromFloors(mapper, template.getRightFloors(), standardMap);
+        }
+
+        // 3. 查询设备所有库存记录并更新
+        List<DeviceInventory> inventoryList = list(
+                new LambdaQueryWrapper<DeviceInventory>()
+                        .eq(DeviceInventory::getDeviceId, deviceId));
+
+        if (inventoryList != null) {
+            for (DeviceInventory inv : inventoryList) {
+                String code = inv.getProductCode();
+                Integer std = standardMap.get(code);
+                inv.setStandardStock(std != null ? std : 0);
+                updateById(inv);
+            }
+        }
+
+        log.info("标准库存重算完成: deviceId={}, 商品数={}, 有层模版标准的={}",
+                deviceId, inventoryList != null ? inventoryList.size() : 0, standardMap.size());
+    }
+
+    /**
+     * 从层模版JSON中解析商品标准库存并累加到map
+     */
+    private void accumulateFromFloors(ObjectMapper mapper, String floorsJson, Map<String, Integer> standardMap) {
+        if (floorsJson == null || floorsJson.isEmpty()) return;
+        try {
+            List<FloorConfig> floors = mapper.readValue(floorsJson,
+                    new TypeReference<List<FloorConfig>>() {});
+            if (floors == null) return;
+            for (FloorConfig floor : floors) {
+                if (floor.getGoods() == null) continue;
+                for (GoodsItem goods : floor.getGoods()) {
+                    String code = goods.getKey();
+                    if (code == null || code.isEmpty()) continue;
+                    Integer templateStock = goods.getStock() != null ? goods.getStock() : 0;
+                    standardMap.merge(code, templateStock, Integer::sum);
+                }
+            }
+        } catch (Exception e) {
+            log.warn("解析层模版JSON失败: {}", e.getMessage());
+        }
+    }
+
     @Override
     public IPage<Map<String, Object>> getDeviceInventoryStats(int page, int pageSize, InventoryQueryDTO queryDTO) {
         Page<Map<String, Object>> pageParam = new Page<>(page, pageSize);

+ 29 - 0
haha-service/src/main/java/com/haha/service/impl/LayerTemplateServiceImpl.java

@@ -176,6 +176,13 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
 
         log.info("层模版创建成功: id={}, deviceId={}", layerTemplate.getId(), layerTemplate.getDeviceId());
 
+        // 重算标准库存
+        try {
+            deviceInventoryService.recalculateStandardStock(dto.getDeviceId());
+        } catch (Exception e) {
+            log.error("层模版创建后重算标准库存失败: deviceId={}, error={}", dto.getDeviceId(), e.getMessage());
+        }
+
         fillLabels(layerTemplate);
         return layerTemplate;
     }
@@ -226,6 +233,13 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
 
         log.info("层模版更新成功: id={}", id);
 
+        // 重算标准库存
+        try {
+            deviceInventoryService.recalculateStandardStock(existing.getDeviceId());
+        } catch (Exception e) {
+            log.error("层模版更新后重算标准库存失败: deviceId={}, error={}", existing.getDeviceId(), e.getMessage());
+        }
+
         fillLabels(existing);
         return existing;
     }
@@ -254,6 +268,14 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
         boolean result = this.update(updateWrapper);
 
         log.info("层模版软删除结果: id={}, result={}", id, result);
+
+        // 重算标准库存(删除后标准库存可能归零)
+        try {
+            deviceInventoryService.recalculateStandardStock(layerTemplate.getDeviceId());
+        } catch (Exception e) {
+            log.error("层模版删除后重算标准库存失败: deviceId={}, error={}", layerTemplate.getDeviceId(), e.getMessage());
+        }
+
         return result;
     }
 
@@ -339,6 +361,13 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
                 log.error("层模版同步后初始化库存失败: deviceId={}", deviceId, e);
             }
 
+            // 步骤6.5: 重算标准库存(同步后标准库存可能与层模版一致)
+            try {
+                deviceInventoryService.recalculateStandardStock(deviceId);
+            } catch (Exception e) {
+                log.error("层模版同步后重算标准库存失败: deviceId={}, error={}", deviceId, e.getMessage());
+            }
+
             // 步骤7: 更新同步状态为"已同步"并记录时间
             updateSyncStatus(deviceId, SyncConstants.STATUS_SYNCED, LocalDateTime.now());
 

+ 19 - 113
haha-service/src/main/java/com/haha/service/impl/ReplenishmentOrderServiceImpl.java

@@ -4,10 +4,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.haha.common.dto.FloorConfig;
-import com.haha.common.dto.GoodsItem;
 import com.haha.common.enums.ReplenishmentOrderStatusEnum;
 import com.haha.common.exception.BusinessException;
 import com.haha.entity.*;
@@ -49,13 +45,10 @@ public class ReplenishmentOrderServiceImpl extends ServiceImpl<ReplenishmentOrde
     private final ReplenishmentOrderItemMapper orderItemMapper;
     private final DeviceMapper deviceMapper;
     private final ShopMapper shopMapper;
-    private final LayerTemplateMapper layerTemplateMapper;
     private final DeviceInventoryMapper deviceInventoryMapper;
     private final ProductMapper productMapper;
     private final ErpGoodsMapper erpGoodsMapper;
 
-    private final ObjectMapper objectMapper = new ObjectMapper();
-
     @Autowired(required = false)
     private QdbClient qdbClient;
 
@@ -588,39 +581,14 @@ public class ReplenishmentOrderServiceImpl extends ServiceImpl<ReplenishmentOrde
                     .collect(Collectors.toMap(Shop::getId, Shop::getName, (a, b) -> a));
         }
 
-        // 批量查询层模版
-        List<LayerTemplate> allTemplates = new java.util.ArrayList<>();
-        for (String deviceId : deviceIds) {
-            allTemplates.addAll(layerTemplateMapper.selectByDeviceId(deviceId));
-        }
-        Map<String, List<LayerTemplate>> templateMap = allTemplates.stream()
-                .collect(Collectors.groupingBy(LayerTemplate::getDeviceId));
-
-        // 批量查询当前库存
+        // 批量查询库存(含 standardStock 字段)
         List<DeviceInventory> allInventories = deviceInventoryMapper.selectList(
                 new LambdaQueryWrapper<DeviceInventory>().in(DeviceInventory::getDeviceId, deviceIds));
-        Map<String, Map<String, DeviceInventory>> inventoryMap = new HashMap<>();
-        for (DeviceInventory inv : allInventories) {
-            inventoryMap.computeIfAbsent(inv.getDeviceId(), k -> new HashMap<>())
-                    .put(inv.getProductCode(), inv);
-        }
-
-        // 收集所有需要查询的 productCode
+        Map<String, List<DeviceInventory>> inventoryByDevice = new HashMap<>();
         Set<String> allCodes = new HashSet<>();
-        for (String deviceId : deviceIds) {
-            List<LayerTemplate> temps = templateMap.getOrDefault(deviceId, java.util.Collections.emptyList());
-            for (LayerTemplate temp : temps) {
-                for (FloorConfig floor : parseFloors(temp.getLeftFloors())) {
-                    for (GoodsItem goods : floor.getGoods()) {
-                        if (goods.getKey() != null) allCodes.add(goods.getKey());
-                    }
-                }
-                for (FloorConfig floor : parseFloors(temp.getRightFloors())) {
-                    for (GoodsItem goods : floor.getGoods()) {
-                        if (goods.getKey() != null) allCodes.add(goods.getKey());
-                    }
-                }
-            }
+        for (DeviceInventory inv : allInventories) {
+            inventoryByDevice.computeIfAbsent(inv.getDeviceId(), k -> new java.util.ArrayList<>()).add(inv);
+            if (inv.getProductCode() != null) allCodes.add(inv.getProductCode());
         }
 
         // 批量查询 Product(按 code)
@@ -631,58 +599,34 @@ public class ReplenishmentOrderServiceImpl extends ServiceImpl<ReplenishmentOrde
             productMap = products.stream().collect(Collectors.toMap(Product::getCode, p -> p, (a, b) -> a));
         }
 
-        // 生成补货建议
+        // 生成补货建议:直接使用 DeviceInventory.standardStock
         List<ReplenishmentSuggestionVO> result = new java.util.ArrayList<>();
 
         for (String deviceId : deviceIds) {
             Device device = deviceMap.get(deviceId);
             if (device == null) continue;
 
-            // 解析模版,按 productCode 累加标准库存
-            Map<String, Accumulator> accumulators = new LinkedHashMap<>();
-            List<LayerTemplate> temps = templateMap.getOrDefault(deviceId, java.util.Collections.emptyList());
-            for (LayerTemplate temp : temps) {
-                int floorNum = temp.getShelfNum() != null ? temp.getShelfNum() : 1;
-                for (FloorConfig floor : parseFloors(temp.getLeftFloors())) {
-                    for (GoodsItem goods : floor.getGoods()) {
-                        if (goods.getKey() != null && goods.getStock() != null && goods.getStock() > 0) {
-                            accumulators.computeIfAbsent(goods.getKey(), k -> new Accumulator())
-                                    .addTemplateStock(goods.getStock(), floorNum, "left", goods.getPic());
-                        }
-                    }
-                }
-                for (FloorConfig floor : parseFloors(temp.getRightFloors())) {
-                    for (GoodsItem goods : floor.getGoods()) {
-                        if (goods.getKey() != null && goods.getStock() != null && goods.getStock() > 0) {
-                            accumulators.computeIfAbsent(goods.getKey(), k -> new Accumulator())
-                                    .addTemplateStock(goods.getStock(), floorNum, "right", goods.getPic());
-                        }
-                    }
-                }
-            }
-
-            if (accumulators.isEmpty()) continue;
-
-            // 获取当前库存
-            Map<String, DeviceInventory> deviceInvs = inventoryMap.getOrDefault(deviceId, new HashMap<>());
+            List<DeviceInventory> deviceInvs = inventoryByDevice.getOrDefault(deviceId, new java.util.ArrayList<>());
+            if (deviceInvs.isEmpty()) continue;
 
             List<SuggestionItem> items = new java.util.ArrayList<>();
-            for (Map.Entry<String, Accumulator> entry : accumulators.entrySet()) {
-                String code = entry.getKey();
-                Accumulator acc = entry.getValue();
-                int currentStock = deviceInvs.containsKey(code) ? deviceInvs.get(code).getStock() : 0;
-                int suggestedQty = acc.templateStock - currentStock;
+            for (DeviceInventory inv : deviceInvs) {
+                int standardStock = inv.getStandardStock() != null ? inv.getStandardStock() : 0;
+                if (standardStock == 0) continue; // 无标准库存的跳过
+
+                int currentStock = inv.getStock() != null ? inv.getStock() : 0;
+                int suggestedQty = standardStock - currentStock;
                 if (suggestedQty <= 0) continue;
 
                 SuggestionItem item = new SuggestionItem();
-                item.setProductCode(code);
-                item.setTemplateStock(acc.templateStock);
+                item.setProductCode(inv.getProductCode());
+                item.setTemplateStock(standardStock);
                 item.setCurrentStock(currentStock);
                 item.setSuggestedQuantity(suggestedQty);
-                item.setShelfNum(acc.shelfNum);
-                item.setPosition(acc.position);
+                item.setShelfNum(inv.getShelfNum());
+                item.setPosition(inv.getPosition());
 
-                Product product = productMap.get(code);
+                Product product = productMap.get(inv.getProductCode());
                 if (product != null) {
                     item.setProductId(product.getId());
                     item.setProductName(product.getName());
@@ -693,9 +637,6 @@ public class ReplenishmentOrderServiceImpl extends ServiceImpl<ReplenishmentOrde
                         item.setProductImage(product.getPic());
                     }
                 }
-                if (item.getProductImage() == null && acc.productImage != null) {
-                    item.setProductImage(acc.productImage);
-                }
                 items.add(item);
             }
 
@@ -713,41 +654,6 @@ public class ReplenishmentOrderServiceImpl extends ServiceImpl<ReplenishmentOrde
         return result;
     }
 
-    /**
-     * 解析楼层 JSON 字符串
-     */
-    private List<FloorConfig> parseFloors(String floorsJson) {
-        if (!StringUtils.hasText(floorsJson)) {
-            return java.util.Collections.emptyList();
-        }
-        try {
-            return objectMapper.readValue(floorsJson, new TypeReference<List<FloorConfig>>() {});
-        } catch (Exception e) {
-            log.warn("解析楼层JSON失败: {}", e.getMessage());
-            return java.util.Collections.emptyList();
-        }
-    }
-
-    /**
-     * 累加器:合并同一商品在多层的标准库存
-     */
-    private static class Accumulator {
-        int templateStock = 0;
-        int shelfNum = 0;
-        String position = "";
-        String productImage;
-
-        void addTemplateStock(int stock, int floor, String pos, String image) {
-            this.templateStock += stock;
-            if (this.position.isEmpty()) {
-                this.shelfNum = floor;
-                this.position = pos;
-            }
-            if (this.productImage == null && image != null) {
-                this.productImage = image;
-            }
-        }
-    }
     /**
      * 生成补货单号:RO + yyyyMMdd + 4位序号
      */