src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
@@ -1,89 +1,593 @@
package com.ruoyi.stock.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
import com.ruoyi.approve.vo.ApproveProcessVO;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
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;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.dto.StockOutRecordDto;
import com.ruoyi.stock.dto.StockUninventoryDto;
import com.ruoyi.stock.execl.StockInventoryExportData;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.service.StockInRecordService;
import com.ruoyi.stock.service.StockInventoryService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.stock.service.StockOutRecordService;
import lombok.AllArgsConstructor;
import com.ruoyi.stock.service.StockUninventoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
/**
 * <p>
 * 库存表 服务实现类
 * </p>
 *
 * @author 芯导软件(江苏)有限公司
 * @since 2026-01-21 04:16:36
 */
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
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;
@Service
@AllArgsConstructor
public class StockInventoryServiceImpl extends ServiceImpl<StockInventoryMapper, StockInventory> implements StockInventoryService {
    private  StockInventoryMapper stockInventoryMapper;
    @Autowired
    private StockInventoryMapper stockInventoryMapper;
    @Autowired
    private StockInRecordService stockInRecordService;
    @Autowired
    private StockOutRecordService stockOutRecordService;
    @Autowired
    private SalesLedgerProductMapper salesLedgerProductMapper;
    @Autowired
    private ApproveProcessServiceImpl approveProcessService;
    @Autowired
    private ProductModelMapper productModelMapper;
    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private StockUninventoryService stockUninventoryService;
    @Override
    public IPage<StockInventoryDto> pagestockInventory(Page page, StockInventoryDto stockInventoryDto) {
        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());
        stockInRecordDto.setStockInNum(stockInventoryDto.getQualitity());
        stockInRecordDto.setProductModelId(stockInventoryDto.getProductModelId());
        stockInRecordService.add(stockInRecordDto);
        //再进行新增库存数量库存
        //先查询库存表中的产品是否存在,不存在新增,存在更新
        StockInventory oldStockInventory = stockInventoryMapper.selectOne(new QueryWrapper<StockInventory>().lambda().eq(StockInventory::getProductModelId, stockInventoryDto.getProductModelId()));
        if (ObjectUtils.isEmpty(oldStockInventory)) {
            StockInventory newStockInventory = new StockInventory();
            newStockInventory.setProductModelId(stockInventoryDto.getProductModelId());
            newStockInventory.setQualitity(stockInventoryDto.getQualitity());
            newStockInventory.setVersion(1);
            stockInventoryMapper.insert(newStockInventory);
        }else {
             stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
        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 (StringUtils.isBlank(stockInventoryDto.getBatchNo())) {
            String batchNo;
            LocalDate now = LocalDate.now();
            String monthFlag = now.format(DateTimeFormatter.ofPattern("MM"));
            int maxSeq = getCurrentMonthMaxSeq(stockInventoryDto, monthFlag, stockInventoryList);
            int newSeq = maxSeq + 1;
            String seqStr = String.format("%03d", newSeq);
            ProductModel productModel = productModelMapper.selectById(stockInventoryDto.getProductModelId());
            batchNo = stockInventoryDto.getMaterialCode() + productModel.getModel() + "P" + monthFlag + seqStr;
            stockInRecordDto.setBatchNo(batchNo);
        } else {
            stockInRecordDto.setBatchNo(stockInventoryDto.getBatchNo());
        }
        Long id = stockInRecordService.add(stockInRecordDto);
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (id != null) {
            try {
                addApproveByPurchase(loginUser, stockInRecordDto, id);
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage());
            }
        }
        return true;
    }
    //出库调用
    private static int getCurrentMonthMaxSeq(StockInventoryDto dto, String monthFlag, List<StockInventory> existingList) {
        int maxSeq = 0;
        String prefix = dto.getMaterialCode() + dto.getProductModelName() + "P" + monthFlag;
        Pattern pattern = Pattern.compile(Pattern.quote(prefix) + "(\\d{3})");
        for (StockInventory item : existingList) {
            String batchNo = item.getBatchNo();
            if (batchNo == null) {
                continue;
            }
            Matcher matcher = pattern.matcher(batchNo);
            if (matcher.find()) {
                int seq = Integer.parseInt(matcher.group(1));
                if (seq > maxSeq) {
                    maxSeq = seq;
                }
            }
        }
        return maxSeq;
    }
    @Override
    public void addApproveByPurchase(LoginUser loginUser, StockInRecordDto stockInRecordDto, Long id) throws Exception {
        ApproveProcessVO approveProcessVO = new ApproveProcessVO();
        approveProcessVO.setApproveType(9);
        approveProcessVO.setApproveDeptId(loginUser.getCurrentDeptId());
        approveProcessVO.setApproveReason(stockInRecordDto.getInboundBatches());
        approveProcessVO.setApproveUserIds(String.valueOf(1));
        approveProcessVO.setApproveUser(loginUser.getUserId());
        approveProcessVO.setApproveTime(LocalDate.now().toString());
        approveProcessVO.setInventoryReview(false);
        approveProcessVO.setStorageType("合格入库");
        approveProcessVO.setRecordId(id);
        approveProcessService.addApprove(approveProcessVO);
    }
    /**
     * 按库存唯一键合并库存。
     * 成品使用 product_model_id + processCategory + voltage;其他入库只按 product_model_id。
     */
    @Override
    public void updateOrCreateStockInventory(StockInRecord stockInRecord) {
        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(defaultDecimal(stockInRecord.getStockInNum()));
            newStockInventory.setVersion(1);
            newStockInventory.setRemark(stockInRecord.getRemark());
            newStockInventory.setLockedQuantity(defaultDecimal(stockInRecord.getLockedQuantity()));
            newStockInventory.setWarnNum(stockInRecord.getWarnNum());
            newStockInventory.setProcessCategory(processCategory);
            newStockInventory.setVoltage(voltage);
            newStockInventory.setBatchNo(StringUtils.trimToEmpty(stockInRecord.getBatchNo()));
            stockInventoryMapper.insert(newStockInventory);
            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);
        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());
        stockOutRecordDto.setStockOutNum(stockInventoryDto.getQualitity());
        stockOutRecordDto.setProductModelId(stockInventoryDto.getProductModelId());
        stockOutRecordDto.setType("0");
        stockOutRecordService.add(stockOutRecordDto);
        StockInventory oldStockInventory = stockInventoryMapper.selectOne(new QueryWrapper<StockInventory>().lambda().eq(StockInventory::getProductModelId, stockInventoryDto.getProductModelId()));
        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));
            if (ObjectUtils.isEmpty(stockInventories)) {
                throw new RuntimeException("产品库存不存在");
            }
            BigDecimal remainingQty = stockInventoryDto.getQualitity();
            for (StockInventory stockInventory : stockInventories) {
                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(defaultDecimal(stockInventory.getQualitity()).subtract(deductQty));
                stockInventory.setVersion(stockInventory.getVersion() == null ? 1 : stockInventory.getVersion() + 1);
                stockInventory.setUpdateTime(LocalDateTime.now());
                stockInventoryMapper.updateById(stockInventory);
                remainingQty = remainingQty.subtract(deductQty);
                if (remainingQty.compareTo(BigDecimal.ZERO) <= 0) {
                    return true;
                }
            }
            ProductModel productModel = productModelMapper.selectById(stockInventoryDto.getProductModelId());
            Product product = productMapper.selectById(productModel.getProductId());
            throw new RuntimeException(product.getProductName() + "/" + productModel.getModel() + "库存不足,无法出库");
        }
        StockInventory oldStockInventory = findInventoryForMerge(
                stockInventoryDto.getProductModelId(),
                stockInventoryDto.getProcessCategory(),
                stockInventoryDto.getVoltage()
        );
        if (ObjectUtils.isEmpty(oldStockInventory)) {
            throw new RuntimeException("产品库存不存在");
        }else {
            stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
        }
        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() + "库存不足,无法出库");
        }
        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.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);
                        stockInventoryDto.setRecordType(StockInQualifiedRecordTypeEnum.CUSTOMIZATION_STOCK_IN.getCode());
                        stockInventoryDto.setQualitity(dto.getQualifiedQuantity());
                        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("合格冻结数量不能超过本次导入的合格库存数量");
                            }
                            stockInventoryDto.setLockedQuantity(dto.getQualifiedLockedQuantity());
                        } else {
                            stockInventoryDto.setLockedQuantity(BigDecimal.ZERO);
                        }
                        stockInventoryDto.setProductModelId(matchedProduct.getProductModelId());
                        this.addstockInventory(stockInventoryDto);
                        successCount++;
                    }
                    if (dto.getUnQualifiedQuantity() != null && dto.getUnQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0) {
                        StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
                        stockUninventoryDto.setRecordId(0L);
                        stockUninventoryDto.setRecordType(StockInUnQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_IN.getCode());
                        stockUninventoryDto.setQualitity(dto.getUnQualifiedQuantity());
                        stockUninventoryDto.setRemark(dto.getRemark());
                        stockUninventoryDto.setBatchNo(dto.getUnQualifiedBatchNo());
                        if (ObjectUtils.isNotEmpty(dto.getUnQualifiedLockedQuantity())) {
                            if (dto.getUnQualifiedLockedQuantity().compareTo(dto.getUnQualifiedQuantity()) > 0) {
                                throw new RuntimeException("不合格冻结数量不能超过本次导入的不合格库存数量");
                            }
                            stockUninventoryDto.setLockedQuantity(dto.getUnQualifiedLockedQuantity());
                        } else {
                            stockUninventoryDto.setLockedQuantity(BigDecimal.ZERO);
                        }
                        stockUninventoryDto.setProductModelId(matchedProduct.getProductModelId());
                        stockUninventoryService.addStockUninventory(stockUninventoryDto);
                        successCount++;
                    }
                } else {
                    String unmatchedRecord = "产品名称:" + dto.getProductName() + ",型号:" + dto.getModel() + " 未匹配到库存产品";
                    unmatchedRecords.add(unmatchedRecord);
                }
            }
            StringBuilder message = new StringBuilder();
            if (!unmatchedRecords.isEmpty()) {
                message.append("导入成功 ").append(successCount).append(" 条记录,以下产品未匹配:\n");
                for (String record : unmatchedRecords) {
                    message.append(record).append("\n");
                }
                return R.ok(message.toString());
            }
            return R.ok("导入成功,共处理 " + successCount + " 条记录");
        } catch (Exception e) {
            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, "库存信息");
    }
    @Override
    public IPage<StockInRecordDto> stockInventoryPage(StockInventoryDto stockInventoryDto, Page page) {
        return stockInventoryMapper.stockInventoryPage(stockInventoryDto, page);
    }
    @Override
    public IPage<StockInventoryDto> stockInAndOutRecord(StockInventoryDto stockInventoryDto, Page page) {
        return stockInventoryMapper.stockInAndOutRecord(stockInventoryDto, page);
    }
    @Override
    public Boolean frozenStock(StockInventoryDto stockInventoryDto) {
        StockInventory stockInventory = stockInventoryMapper.selectById(stockInventoryDto.getId());
        if (stockInventory.getQualitity().compareTo(stockInventoryDto.getLockedQuantity()) < 0) {
            throw new RuntimeException("冻结数量不能超过库存数量");
        }
        if (ObjectUtils.isEmpty(stockInventory.getLockedQuantity())) {
            stockInventory.setLockedQuantity(stockInventoryDto.getLockedQuantity());
        } else {
            stockInventory.setLockedQuantity(stockInventory.getLockedQuantity().add(stockInventoryDto.getLockedQuantity()));
        }
        return this.updateById(stockInventory);
    }
    @Override
    public Boolean thawStock(StockInventoryDto stockInventoryDto) {
        StockInventory stockInventory = stockInventoryMapper.selectById(stockInventoryDto.getId());
        if (stockInventory.getLockedQuantity().compareTo(stockInventoryDto.getLockedQuantity()) < 0) {
            throw new RuntimeException("解冻数量不能超过冻结数量");
        }
        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);
    }
}