src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
@@ -2,7 +2,6 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -16,9 +15,12 @@
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockInUnQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.stock.dto.FinishedProductTreeDto;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.stock.dto.StockInRecordDto;
@@ -43,18 +45,15 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
 * <p>
 * 库存表 服务实现类
 * </p>
 *
 * @author 芯导软件(江苏)有限公司
 * @since 2026-01-21 04:16:36
 */
@Service
public class StockInventoryServiceImpl extends ServiceImpl<StockInventoryMapper, StockInventory> implements StockInventoryService {
@@ -80,17 +79,54 @@
        return stockInventoryMapper.pagestockInventory(page, stockInventoryDto);
    }
    /**
     * 查询成品库存树。
     * 返回结构沿用基础资料产品树,叶子节点补充成品库存维度:型号、工序分类、电压。
     */
    @Override
    public List<FinishedProductTreeDto> finishedProductList(StockInventoryDto stockInventoryDto) {
        List<StockInventoryDto> inventoryList = stockInventoryMapper.selectFinishedProductInventoryList(stockInventoryDto);
        if (inventoryList.isEmpty()) {
            return new ArrayList<>();
        }
        List<Product> allProducts = productMapper.selectList(null);
        Map<Long, Product> productMap = new HashMap<>();
        Map<Long, List<Product>> childrenMap = new HashMap<>();
        for (Product product : allProducts) {
            productMap.put(product.getId(), product);
            childrenMap.computeIfAbsent(product.getParentId(), key -> new ArrayList<>()).add(product);
        }
        Map<Long, List<FinishedProductTreeDto>> leafMap = buildFinishedProductLeafMap(inventoryList);
        Set<Long> visibleProductIds = collectVisibleProductIds(leafMap.keySet(), productMap);
        List<FinishedProductTreeDto> result = new ArrayList<>();
        for (Product rootProduct : allProducts) {
            if (!isFinishedRoot(rootProduct)) {
                continue;
            }
            FinishedProductTreeDto rootNode = buildProductNode(rootProduct);
            rootNode.setChildren(buildFinishedChildren(rootProduct.getId(), childrenMap, leafMap, visibleProductIds));
            if (!rootNode.getChildren().isEmpty()) {
                result.add(rootNode);
            }
        }
        return result;
    }
    @Override
    public IPage<StockInventoryDto> pageListCombinedStockInventory(Page page, StockInventoryDto stockInventoryDto) {
        return stockInventoryMapper.pageListCombinedStockInventory(page, stockInventoryDto);
    }
    //入库调用-添加入库记录
    /**
     * 合格入库:先生成入库记录,再走审批流。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean addstockInventory(StockInventoryDto stockInventoryDto) {
        List<StockInventory> stockInventoryList = stockInventoryMapper.selectList(null);
        //新增入库记录再添加库存
        StockInRecordDto stockInRecordDto = new StockInRecordDto();
        stockInRecordDto.setRecordId(stockInventoryDto.getRecordId());
        stockInRecordDto.setRecordType(stockInventoryDto.getRecordType());
@@ -99,22 +135,17 @@
        stockInRecordDto.setRemark(stockInventoryDto.getRemark());
        stockInRecordDto.setWarnNum(stockInventoryDto.getWarnNum());
        stockInRecordDto.setLockedQuantity(stockInventoryDto.getLockedQuantity());
        stockInRecordDto.setProcessCategory(normalizeDimension(stockInventoryDto.getProcessCategory()));
        stockInRecordDto.setVoltage(normalizeDimension(stockInventoryDto.getVoltage()));
        stockInRecordDto.setApproveStatus(0);
        stockInRecordDto.setType("0");
        if (stockInventoryDto.getBatchNo() == null || stockInventoryDto.getBatchNo().isEmpty()) {
        if (StringUtils.isBlank(stockInventoryDto.getBatchNo())) {
            String batchNo;
            // 获取当前月份(两位)
            LocalDate now = LocalDate.now();
            String monthFlag = now.format(DateTimeFormatter.ofPattern("MM"));
            // 获取当前月份的最大流水号
            int maxSeq = getCurrentMonthMaxSeq(stockInventoryDto, monthFlag, stockInventoryList);
            // 新流水号 = 最大流水号 + 1
            int newSeq = maxSeq + 1;
            String seqStr = String.format("%03d", newSeq);
            // 组装batchNo
            ProductModel productModel = productModelMapper.selectById(stockInventoryDto.getProductModelId());
            batchNo = stockInventoryDto.getMaterialCode() + productModel.getModel() + "P" + monthFlag + seqStr;
            stockInRecordDto.setBatchNo(batchNo);
@@ -133,21 +164,15 @@
        return true;
    }
    /**
     * 查询当前月份已存在的最大流水号
     */
    private static int getCurrentMonthMaxSeq(StockInventoryDto dto, String monthFlag, List<StockInventory> existingList) {
        int maxSeq = 0;
        String prefix = dto.getMaterialCode() + dto.getProductModelName() + "P" + monthFlag;
        // 正则匹配:前缀 + 3位数字
        Pattern pattern = Pattern.compile(Pattern.quote(prefix) + "(\\d{3})");
        for (StockInventory item : existingList) {
            String batchNo = item.getBatchNo();
            if (batchNo == null) continue;
            if (batchNo == null) {
                continue;
            }
            Matcher matcher = pattern.matcher(batchNo);
            if (matcher.find()) {
                int seq = Integer.parseInt(matcher.group(1));
@@ -156,10 +181,10 @@
                }
            }
        }
        return maxSeq;
    }
    @Override
    public void addApproveByPurchase(LoginUser loginUser, StockInRecordDto stockInRecordDto, Long id) throws Exception {
        ApproveProcessVO approveProcessVO = new ApproveProcessVO();
        approveProcessVO.setApproveType(9);
@@ -175,100 +200,77 @@
    }
    /**
     * 实际入库
     *
     * @param stockInRecord
     * 按库存唯一键合并库存。
     * 成品使用 product_model_id + processCategory + voltage;其他入库只按 product_model_id。
     */
    @Override
    public void updateOrCreateStockInventory(StockInRecord stockInRecord) {
        // 先查询库存表中的产品是否存在
        LambdaQueryWrapper<StockInventory> queryWrapper = new QueryWrapper<StockInventory>().lambda()
                .eq(StockInventory::getProductModelId, stockInRecord.getProductModelId());
        // 根据 batchNo 是否为空构建不同的查询条件
        if (stockInRecord.getBatchNo() != null && !stockInRecord.getBatchNo().isEmpty()) {
            queryWrapper.eq(StockInventory::getBatchNo, stockInRecord.getBatchNo());
        } else {
            queryWrapper.isNull(StockInventory::getBatchNo);
        }
        StockInventory oldStockInventory = stockInventoryMapper.selectOne(queryWrapper);
        String processCategory = normalizeDimension(stockInRecord.getProcessCategory());
        String voltage = normalizeDimension(stockInRecord.getVoltage());
        StockInventory oldStockInventory = findInventoryForMerge(
                stockInRecord.getProductModelId(),
                processCategory,
                voltage
        );
        if (ObjectUtils.isEmpty(oldStockInventory)) {
            // 不存在则新增
            StockInventory newStockInventory = new StockInventory();
            newStockInventory.setProductModelId(stockInRecord.getProductModelId());
            newStockInventory.setQualitity(stockInRecord.getStockInNum());
            newStockInventory.setQualitity(defaultDecimal(stockInRecord.getStockInNum()));
            newStockInventory.setVersion(1);
            newStockInventory.setRemark(stockInRecord.getRemark());
            newStockInventory.setLockedQuantity(stockInRecord.getLockedQuantity());
            newStockInventory.setLockedQuantity(defaultDecimal(stockInRecord.getLockedQuantity()));
            newStockInventory.setWarnNum(stockInRecord.getWarnNum());
            newStockInventory.setBatchNo(stockInRecord.getBatchNo());
            newStockInventory.setProcessCategory(processCategory);
            newStockInventory.setVoltage(voltage);
            newStockInventory.setBatchNo(StringUtils.trimToEmpty(stockInRecord.getBatchNo()));
            stockInventoryMapper.insert(newStockInventory);
        } else {
            // 存在则更新
            LambdaUpdateWrapper<StockInventory> updateWrapper = new LambdaUpdateWrapper<>();
            updateWrapper
                    .eq(StockInventory::getProductModelId, stockInRecord.getProductModelId());
            // 根据 batchNo 是否为空构建不同的更新条件
            if (stockInRecord.getBatchNo() != null && !stockInRecord.getBatchNo().isEmpty()) {
                updateWrapper.eq(StockInventory::getBatchNo, stockInRecord.getBatchNo());
            } else {
                updateWrapper.isNull(StockInventory::getBatchNo);
            }
            updateWrapper
                    .setSql(stockInRecord.getStockInNum() != null,
                            "qualitity = qualitity + " + stockInRecord.getStockInNum())
                    .setSql(true, "version = version + 1")
                    .set(stockInRecord.getRemark() != null && !stockInRecord.getRemark().isEmpty(),
                            StockInventory::getRemark, stockInRecord.getRemark())
                    .set(stockInRecord.getWarnNum() != null,
                            StockInventory::getWarnNum, stockInRecord.getWarnNum())
                    .setSql(stockInRecord.getLockedQuantity() != null,
                            "locked_quantity = locked_quantity + " + stockInRecord.getLockedQuantity())
                    .set(StockInventory::getUpdateTime, new Date());
            stockInventoryMapper.update(null, updateWrapper);
            return;
        }
        oldStockInventory.setQualitity(defaultDecimal(oldStockInventory.getQualitity()).add(defaultDecimal(stockInRecord.getStockInNum())));
        oldStockInventory.setVersion(oldStockInventory.getVersion() == null ? 1 : oldStockInventory.getVersion() + 1);
        if (StringUtils.isNotBlank(stockInRecord.getRemark())) {
            oldStockInventory.setRemark(stockInRecord.getRemark());
        }
        if (stockInRecord.getWarnNum() != null) {
            oldStockInventory.setWarnNum(stockInRecord.getWarnNum());
        }
        if (stockInRecord.getLockedQuantity() != null) {
            oldStockInventory.setLockedQuantity(defaultDecimal(oldStockInventory.getLockedQuantity()).add(stockInRecord.getLockedQuantity()));
        }
        oldStockInventory.setProcessCategory(processCategory);
        oldStockInventory.setVoltage(voltage);
        oldStockInventory.setUpdateTime(LocalDateTime.now());
        stockInventoryMapper.updateById(oldStockInventory);
    }
    //半成品直接入库
    /**
     * 不审核入库,直接写入入库记录和库存。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean addstockInventoryNoReview(StockInventoryDto stockInventoryDto) {
        //新增入库记录再添加库存
        StockInRecordDto stockInRecordDto = new StockInRecordDto();
        stockInRecordDto.setRecordId(stockInventoryDto.getRecordId());
        stockInRecordDto.setRecordType(stockInventoryDto.getRecordType());
        stockInRecordDto.setStockInNum(stockInventoryDto.getQualitity());
        stockInRecordDto.setProductModelId(stockInventoryDto.getProductModelId());
        stockInRecordDto.setRemark(stockInventoryDto.getRemark());
        stockInRecordDto.setWarnNum(stockInventoryDto.getWarnNum());
        stockInRecordDto.setLockedQuantity(stockInventoryDto.getLockedQuantity());
        stockInRecordDto.setProcessCategory(normalizeDimension(stockInventoryDto.getProcessCategory()));
        stockInRecordDto.setVoltage(normalizeDimension(stockInventoryDto.getVoltage()));
        stockInRecordDto.setBatchNo(stockInventoryDto.getBatchNo());
        stockInRecordDto.setType("0");
        stockInRecordService.add(stockInRecordDto);
        //再进行新增库存数量库存
        //先查询库存表中的产品是否存在,不存在新增,存在更新
        StockInventory oldStockInventory = stockInventoryMapper.selectOne(new QueryWrapper<StockInventory>().lambda().eq(StockInventory::getProductModelId, stockInventoryDto.getProductModelId())
                .eq(StockInventory::getBatchNo, stockInventoryDto.getBatchNo()));
        if (ObjectUtils.isEmpty(oldStockInventory)) {
            StockInventory newStockInventory = new StockInventory();
            newStockInventory.setProductModelId(stockInventoryDto.getProductModelId());
            newStockInventory.setQualitity(stockInventoryDto.getQualitity());
            newStockInventory.setVersion(1);
            newStockInventory.setRemark(stockInventoryDto.getRemark());
            newStockInventory.setLockedQuantity(stockInventoryDto.getLockedQuantity());
            newStockInventory.setWarnNum(stockInventoryDto.getWarnNum());
            stockInventoryMapper.insert(newStockInventory);
        } else {
            stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
        }
        updateOrCreateStockInventory(toStockInRecord(stockInRecordDto));
        return true;
    }
    //出库调用
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean subtractStockInventory(StockInventoryDto stockInventoryDto) {
        //  新增出库记录
        StockOutRecordDto stockOutRecordDto = new StockOutRecordDto();
        stockOutRecordDto.setRecordId(stockInventoryDto.getRecordId());
        stockOutRecordDto.setRecordType(stockInventoryDto.getRecordType());
@@ -276,7 +278,8 @@
        stockOutRecordDto.setProductModelId(stockInventoryDto.getProductModelId());
        stockOutRecordDto.setType("0");
        stockOutRecordService.add(stockOutRecordDto);
        if (ObjectUtils.isEmpty(stockInventoryDto.getBatchNo())) {
        if (StringUtils.isBlank(stockInventoryDto.getBatchNo()) && !usesDimensionIdentity(normalizeDimension(stockInventoryDto.getProcessCategory()), normalizeDimension(stockInventoryDto.getVoltage()))) {
            List<StockInventory> stockInventories = stockInventoryMapper.selectList(new QueryWrapper<StockInventory>().lambda()
                    .eq(StockInventory::getProductModelId, stockInventoryDto.getProductModelId())
                    .orderByAsc(StockInventory::getId));
@@ -286,14 +289,14 @@
            BigDecimal remainingQty = stockInventoryDto.getQualitity();
            for (StockInventory stockInventory : stockInventories) {
                BigDecimal lockedQty = stockInventory.getLockedQuantity() == null ? BigDecimal.ZERO : stockInventory.getLockedQuantity();
                BigDecimal availableQty = stockInventory.getQualitity().subtract(lockedQty);
                BigDecimal lockedQty = defaultDecimal(stockInventory.getLockedQuantity());
                BigDecimal availableQty = defaultDecimal(stockInventory.getQualitity()).subtract(lockedQty);
                if (availableQty.compareTo(BigDecimal.ZERO) <= 0) {
                    continue;
                }
                BigDecimal deductQty = remainingQty.min(availableQty);
                stockInventory.setQualitity(stockInventory.getQualitity().subtract(deductQty));
                stockInventory.setQualitity(defaultDecimal(stockInventory.getQualitity()).subtract(deductQty));
                stockInventory.setVersion(stockInventory.getVersion() == null ? 1 : stockInventory.getVersion() + 1);
                stockInventory.setUpdateTime(LocalDateTime.now());
                stockInventoryMapper.updateById(stockInventory);
@@ -306,55 +309,51 @@
            ProductModel productModel = productModelMapper.selectById(stockInventoryDto.getProductModelId());
            Product product = productMapper.selectById(productModel.getProductId());
            throw new RuntimeException(product.getProductName() + "/" + productModel.getModel() + "库存不足无法出库");
            throw new RuntimeException(product.getProductName() + "/" + productModel.getModel() + "库存不足,无法出库");
        }
        StockInventory oldStockInventory = stockInventoryMapper.selectOne(new QueryWrapper<StockInventory>().lambda().eq(StockInventory::getProductModelId, stockInventoryDto.getProductModelId())
                .eq(StockInventory::getBatchNo, stockInventoryDto.getBatchNo()));
        StockInventory oldStockInventory = findInventoryForMerge(
                stockInventoryDto.getProductModelId(),
                stockInventoryDto.getProcessCategory(),
                stockInventoryDto.getVoltage()
        );
        if (ObjectUtils.isEmpty(oldStockInventory)) {
            throw new RuntimeException("产品库存不存在");
        }
        BigDecimal lockedQty = oldStockInventory.getLockedQuantity();
        if (lockedQty == null) {
            lockedQty = BigDecimal.ZERO;
        }
        if (stockInventoryDto.getQualitity().compareTo(oldStockInventory.getQualitity().subtract(lockedQty)) > 0) {
            // 查询产品规格名
        BigDecimal lockedQty = defaultDecimal(oldStockInventory.getLockedQuantity());
        if (stockInventoryDto.getQualitity().compareTo(defaultDecimal(oldStockInventory.getQualitity()).subtract(lockedQty)) > 0) {
            ProductModel productModel = productModelMapper.selectById(stockInventoryDto.getProductModelId());
            Product product = productMapper.selectById(productModel.getProductId());
            throw new RuntimeException(product.getProductName() + "/" + productModel.getModel() + "库存不足无法出库");
            throw new RuntimeException(product.getProductName() + "/" + productModel.getModel() + "库存不足,无法出库");
        }
        stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
        oldStockInventory.setQualitity(defaultDecimal(oldStockInventory.getQualitity()).subtract(stockInventoryDto.getQualitity()));
        oldStockInventory.setVersion(oldStockInventory.getVersion() == null ? 1 : oldStockInventory.getVersion() + 1);
        oldStockInventory.setUpdateTime(LocalDateTime.now());
        stockInventoryMapper.updateById(oldStockInventory);
        return true;
    }
    @Override
    public R importStockInventory(MultipartFile file) {
        try {
            // 查询所有的产品并构建映射,提高查找效率
            List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectProduct();
            Map<String, SalesLedgerProduct> productMap = new HashMap<>();
            for (SalesLedgerProduct product : salesLedgerProducts) {
                // 使用产品类别和规格型号作为键
                String key = product.getProductCategory() + "|" + product.getSpecificationModel();
                productMap.put(key, product);
            }
            ExcelUtil<StockInventoryExportData> util = new ExcelUtil<StockInventoryExportData>(StockInventoryExportData.class);
            ExcelUtil<StockInventoryExportData> util = new ExcelUtil<>(StockInventoryExportData.class);
            List<StockInventoryExportData> list = util.importExcel(file.getInputStream());
            // 记录未找到匹配项的数据
            List<String> unmatchedRecords = new ArrayList<>();
            // 记录处理结果
            int successCount = 0;
            for (StockInventoryExportData dto : list) {
                // 构建查找键
                String key = dto.getProductName() + "|" + dto.getModel();
                SalesLedgerProduct matchedProduct = productMap.get(key);
                if (matchedProduct != null) {
                    // 处理合格库存
                    if (dto.getQualifiedQuantity() != null && dto.getQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0) {
                        StockInventoryDto stockInventoryDto = new StockInventoryDto();
                        stockInventoryDto.setRecordId(0L);
@@ -363,8 +362,9 @@
                        stockInventoryDto.setRemark(dto.getRemark());
                        stockInventoryDto.setWarnNum(dto.getWarnNum());
                        stockInventoryDto.setBatchNo(dto.getQualifiedBatchNo());
                        stockInventoryDto.setProcessCategory(dto.getProcessCategory());
                        stockInventoryDto.setVoltage(dto.getVoltage());
                        // 验证合格冻结数量
                        if (ObjectUtils.isNotEmpty(dto.getQualifiedLockedQuantity())) {
                            if (dto.getQualifiedLockedQuantity().compareTo(dto.getQualifiedQuantity()) > 0) {
                                throw new RuntimeException("合格冻结数量不能超过本次导入的合格库存数量");
@@ -379,7 +379,6 @@
                        successCount++;
                    }
                    // 处理不合格库存
                    if (dto.getUnQualifiedQuantity() != null && dto.getUnQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0) {
                        StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
                        stockUninventoryDto.setRecordId(0L);
@@ -388,7 +387,6 @@
                        stockUninventoryDto.setRemark(dto.getRemark());
                        stockUninventoryDto.setBatchNo(dto.getUnQualifiedBatchNo());
                        // 验证不合格冻结数量
                        if (ObjectUtils.isNotEmpty(dto.getUnQualifiedLockedQuantity())) {
                            if (dto.getUnQualifiedLockedQuantity().compareTo(dto.getUnQualifiedQuantity()) > 0) {
                                throw new RuntimeException("不合格冻结数量不能超过本次导入的不合格库存数量");
@@ -403,16 +401,14 @@
                        successCount++;
                    }
                } else {
                    // 记录未匹配的产品
                    String unmatchedRecord = "产品名称:" + dto.getProductName() + ",型号:" + dto.getModel();
                    String unmatchedRecord = "产品名称:" + dto.getProductName() + ",型号:" + dto.getModel() + " 未匹配到库存产品";
                    unmatchedRecords.add(unmatchedRecord);
                }
            }
            // 构建返回信息
            StringBuilder message = new StringBuilder();
            if (!unmatchedRecords.isEmpty()) {
                message.append("导入成功 " + successCount + " 条记录,以下产品未找到匹配项:\n");
                message.append("导入成功 ").append(successCount).append(" 条记录,以下产品未匹配:\n");
                for (String record : unmatchedRecords) {
                    message.append(record).append("\n");
                }
@@ -421,14 +417,13 @@
            return R.ok("导入成功,共处理 " + successCount + " 条记录");
        } catch (Exception e) {
            log.error("导入库存失败", e);
            return R.fail("导入失败:" + e.getMessage());
            log.error("库存导入失败", e);
            return R.fail("库存导入失败:" + e.getMessage());
        }
    }
    @Override
    public void exportStockInventory(HttpServletResponse response, StockInventoryDto stockInventoryDto) {
        List<StockInventoryExportData> list = stockInventoryMapper.listStockInventoryExportData(stockInventoryDto);
        ExcelUtil<StockInventoryExportData> util = new ExcelUtil<>(StockInventoryExportData.class);
        util.exportExcel(response, list, "库存信息");
@@ -467,4 +462,132 @@
        stockInventory.setLockedQuantity(stockInventory.getLockedQuantity().subtract(stockInventoryDto.getLockedQuantity()));
        return this.updateById(stockInventory);
    }
}
    // 入库合并唯一键:成品按类别和电压,其余只按产品规格。
    private StockInventory findInventoryForMerge(Long productModelId, String processCategory, String voltage) {
        LambdaQueryWrapper<StockInventory> queryWrapper = new LambdaQueryWrapper<StockInventory>()
                .eq(StockInventory::getProductModelId, productModelId)
                .orderByAsc(StockInventory::getId);
        if (usesDimensionIdentity(processCategory, voltage)) {
            queryWrapper.eq(StockInventory::getProcessCategory, normalizeDimension(processCategory));
            queryWrapper.eq(StockInventory::getVoltage, normalizeDimension(voltage));
        }
        List<StockInventory> inventories = stockInventoryMapper.selectList(queryWrapper);
        return inventories.isEmpty() ? null : inventories.get(0);
    }
    // 将入库记录 DTO 复制为持久化实体。
    private StockInRecord toStockInRecord(StockInRecordDto stockInRecordDto) {
        StockInRecord stockInRecord = new StockInRecord();
        stockInRecord.setRecordId(stockInRecordDto.getRecordId());
        stockInRecord.setRecordType(stockInRecordDto.getRecordType());
        stockInRecord.setStockInNum(stockInRecordDto.getStockInNum());
        stockInRecord.setProductModelId(stockInRecordDto.getProductModelId());
        stockInRecord.setRemark(stockInRecordDto.getRemark());
        stockInRecord.setType(stockInRecordDto.getType());
        stockInRecord.setLockedQuantity(stockInRecordDto.getLockedQuantity());
        stockInRecord.setWarnNum(stockInRecordDto.getWarnNum());
        stockInRecord.setApproveStatus(stockInRecordDto.getApproveStatus());
        stockInRecord.setBatchNo(stockInRecordDto.getBatchNo());
        stockInRecord.setProcessCategory(stockInRecordDto.getProcessCategory());
        stockInRecord.setVoltage(stockInRecordDto.getVoltage());
        return stockInRecord;
    }
    // 只要带有成品维度,就按维度作为唯一键。
    private boolean usesDimensionIdentity(String processCategory, String voltage) {
        return StringUtils.isNotBlank(processCategory) || StringUtils.isNotBlank(voltage);
    }
    private String normalizeDimension(String value) {
        return StringUtils.trimToEmpty(value);
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private Map<Long, List<FinishedProductTreeDto>> buildFinishedProductLeafMap(List<StockInventoryDto> inventoryList) {
        Map<Long, List<FinishedProductTreeDto>> leafMap = new HashMap<>();
        for (StockInventoryDto inventory : inventoryList) {
            FinishedProductTreeDto leafNode = new FinishedProductTreeDto();
            leafNode.setId(inventory.getId() == null ? null : -inventory.getId());
            leafNode.setParentId(inventory.getProductId());
            leafNode.setProductId(inventory.getProductId());
            leafNode.setProductModelId(inventory.getProductModelId());
            leafNode.setProductName(inventory.getProductName());
            leafNode.setLabel(buildLeafLabel(inventory));
            leafNode.setModel(inventory.getModel());
            leafNode.setMaterialCode(inventory.getMaterialCode());
            leafNode.setProcessCategory(inventory.getProcessCategory());
            leafNode.setVoltage(inventory.getVoltage());
            leafNode.setChildren(new ArrayList<>());
            leafMap.computeIfAbsent(inventory.getProductId(), key -> new ArrayList<>()).add(leafNode);
        }
        return leafMap;
    }
    private Set<Long> collectVisibleProductIds(Set<Long> leafProductIds, Map<Long, Product> productMap) {
        Set<Long> visibleIds = new HashSet<>();
        for (Long productId : leafProductIds) {
            Long currentId = productId;
            while (currentId != null) {
                Product current = productMap.get(currentId);
                if (current == null) {
                    break;
                }
                visibleIds.add(currentId);
                currentId = current.getParentId();
            }
        }
        return visibleIds;
    }
    private List<FinishedProductTreeDto> buildFinishedChildren(Long parentId,
                                                               Map<Long, List<Product>> childrenMap,
                                                               Map<Long, List<FinishedProductTreeDto>> leafMap,
                                                               Set<Long> visibleProductIds) {
        List<FinishedProductTreeDto> children = new ArrayList<>();
        List<Product> childProducts = childrenMap.getOrDefault(parentId, new ArrayList<>());
        for (Product childProduct : childProducts) {
            if (!visibleProductIds.contains(childProduct.getId())) {
                continue;
            }
            FinishedProductTreeDto childNode = buildProductNode(childProduct);
            List<FinishedProductTreeDto> subChildren = buildFinishedChildren(childProduct.getId(), childrenMap, leafMap, visibleProductIds);
            subChildren.addAll(leafMap.getOrDefault(childProduct.getId(), new ArrayList<>()));
            childNode.setChildren(subChildren);
            children.add(childNode);
        }
        return children;
    }
    private FinishedProductTreeDto buildProductNode(Product product) {
        FinishedProductTreeDto node = new FinishedProductTreeDto();
        BeanUtils.copyProperties(product, node);
        node.setLabel(product.getProductName());
        node.setChildren(new ArrayList<>());
        return node;
    }
    private boolean isFinishedRoot(Product product) {
        return product.getParentId() == null && "成品".equals(StringUtils.trimToEmpty(product.getProductName()));
    }
    private String buildLeafLabel(StockInventoryDto inventory) {
        List<String> parts = new ArrayList<>();
        if (StringUtils.isNotBlank(inventory.getProductName())) {
            parts.add(StringUtils.trim(inventory.getProductName()));
        }
        if (StringUtils.isNotBlank(inventory.getModel())) {
            parts.add(StringUtils.trim(inventory.getModel()));
        }
        if (StringUtils.isNotBlank(inventory.getProcessCategory())) {
            parts.add(StringUtils.trim(inventory.getProcessCategory()));
        }
        if (StringUtils.isNotBlank(inventory.getVoltage())) {
            parts.add(StringUtils.trim(inventory.getVoltage()));
        }
        return String.join(" / ", parts);
    }
}