5 天以前 0d7d874912d0147376826b55667a1deb6547ed91
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java
@@ -3,25 +3,41 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.production.bean.dto.ProductionBomStructureDto;
import com.ruoyi.production.bean.vo.ProductionBomStructureVo;
import com.ruoyi.production.mapper.ProductionBomStructureMapper;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.mapper.ProductionOrderBomMapper;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingOperationMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingOperationParamMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.pojo.ProductionBomStructure;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionOrderBom;
import com.ruoyi.production.pojo.ProductionOrderRouting;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperation;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperationParam;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.production.service.ProductionBomStructureService;
import com.ruoyi.production.util.TaskPlanQuantityUtil;
import com.ruoyi.technology.mapper.TechnologyOperationMapper;
import com.ruoyi.technology.mapper.TechnologyOperationParamMapper;
import com.ruoyi.technology.mapper.TechnologyParamMapper;
import com.ruoyi.technology.pojo.TechnologyOperation;
import com.ruoyi.technology.pojo.TechnologyOperationParam;
import com.ruoyi.technology.pojo.TechnologyParam;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@@ -40,8 +56,14 @@
    private final ProductionBomStructureMapper productionBomStructureMapper;
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOrderRoutingMapper productionOrderRoutingMapper;
    private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper;
    private final ProductionOrderRoutingOperationParamMapper productionOrderRoutingOperationParamMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final TechnologyOperationMapper technologyOperationMapper;
    private final TechnologyOperationParamMapper technologyOperationParamMapper;
    private final TechnologyParamMapper technologyParamMapper;
    /**
     * 根据BOM查询并组装结构树。
@@ -177,12 +199,17 @@
                Wrappers.<ProductionBomStructure>lambdaQuery()
                        .eq(ProductionBomStructure::getProductionOrderBomId, orderBomId)
                        .orderByAsc(ProductionBomStructure::getId));
        //同步需求数量
        syncStructureDemandedQuantity(structureList, orderQuantity);
        Long rootProductModelId = orderBom.getProductModelId() != null ? orderBom.getProductModelId() : productionOrder.getProductModelId();
        //同步生产工艺路线
        syncRoutingOperationsByBom(currentProductionOrderId, productionOrder, orderBom, structureList, rootProductModelId);
        //同步工单
        syncTaskPlanQuantity(
                currentProductionOrderId,
                structureList,
                orderQuantity,
                orderBom.getProductModelId() != null ? orderBom.getProductModelId() : productionOrder.getProductModelId());
                rootProductModelId);
    }
    private void syncStructureDemandedQuantity(List<ProductionBomStructure> structureList, BigDecimal orderQuantity) {
@@ -190,19 +217,22 @@
            return;
        }
        List<ProductionBomStructure> updateList = new ArrayList<>();
        BigDecimal lastProcessDemandedQuantity = orderQuantity;
        for (ProductionBomStructure structure : structureList) {
            if (structure == null || structure.getId() == null) {
                continue;
            }
            BigDecimal demandedQuantity = defaultDecimal(structure.getUnitQuantity()).multiply(orderQuantity);
            if (compareDecimal(structure.getDemandedQuantity(), demandedQuantity) == 0) {
                continue;
            }
            BigDecimal demandedQuantity = lastProcessDemandedQuantity.multiply(defaultDecimal(structure.getUnitQuantity()));
//            if (compareDecimal(structure.getDemandedQuantity(), demandedQuantity) == 0) {
//                continue;
//            }
            ProductionBomStructure update = new ProductionBomStructure();
            update.setId(structure.getId());
            update.setDemandedQuantity(demandedQuantity);
            updateList.add(update);
            structure.setDemandedQuantity(demandedQuantity);
            lastProcessDemandedQuantity = demandedQuantity;
        }
        if (!updateList.isEmpty()) {
            this.updateBatchById(updateList);
@@ -233,13 +263,13 @@
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionOrderRoutingOperation::getId, item -> item, (left, right) -> left));
        // Keep task plan quantities aligned with the same order BOM snapshot demand used during snapshot creation.
        Map<String, BigDecimal> demandedQuantityMap = buildOperationDemandedQuantityMap(structureList, rootProductModelId);
        Map<String, BigDecimal> demandedQuantityMap = TaskPlanQuantityUtil.buildOperationDemandedQuantityMap(structureList, rootProductModelId);
        for (ProductionOperationTask task : taskList) {
            if (task == null || task.getId() == null || task.getProductionOrderRoutingOperationId() == null) {
                continue;
            }
            ProductionOrderRoutingOperation routingOperation = routingOperationMap.get(task.getProductionOrderRoutingOperationId());
            if (routingOperation == null || routingOperation.getTechnologyRoutingOperationId() == null) {
            if (routingOperation == null) {
                continue;
            }
            BigDecimal planQuantity = resolveTaskPlanQuantity(
@@ -256,6 +286,392 @@
            productionOperationTaskMapper.updateById(update);
        }
    }
    private void syncRoutingOperationsByBom(Long productionOrderId,
                                            ProductionOrder productionOrder,
                                            ProductionOrderBom orderBom,
                                            List<ProductionBomStructure> structureList,
                                            Long rootProductModelId) {
        // 重新查询BOM结构,按子节点优先排序
        List<ProductionBomStructure> routingStructureList = this.list(
                Wrappers.<ProductionBomStructure>lambdaQuery()
                        .eq(ProductionBomStructure::getProductionOrderBomId, orderBom.getId())
                        .orderByDesc(ProductionBomStructure::getParentId)
                        .orderByAsc(ProductionBomStructure::getId));
        ProductionOrderRouting orderRouting = getOrCreateOrderRoutingSnapshot(productionOrderId, productionOrder, orderBom, rootProductModelId);
        List<ProductionOrderRoutingOperation> desiredOperationList = buildDesiredRoutingOperationList(routingStructureList, rootProductModelId);
        List<ProductionOrderRoutingOperation> existingOperationList = productionOrderRoutingOperationMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperation>lambdaQuery()
                        .eq(ProductionOrderRoutingOperation::getOrderRoutingId, orderRouting.getId())
                        .eq(ProductionOrderRoutingOperation::getProductionOrderId, productionOrderId)
                        .orderByAsc(ProductionOrderRoutingOperation::getDragSort)
                        .orderByAsc(ProductionOrderRoutingOperation::getId));
        Map<String, Deque<ProductionOrderRoutingOperation>> existingBucketMap = buildExistingRoutingOperationBucketMap(existingOperationList);
        List<ProductionOrderRoutingOperation> finalOperationList = new ArrayList<>();
        for (ProductionOrderRoutingOperation desiredOperation : desiredOperationList) {
            String bucketKey = buildRoutingOperationBucketKey(
                    desiredOperation.getTechnologyOperationId(),
                    desiredOperation.getProductModelId());
            Deque<ProductionOrderRoutingOperation> matchedQueue = existingBucketMap.get(bucketKey);
            ProductionOrderRoutingOperation matchedOperation = matchedQueue == null ? null : matchedQueue.pollFirst();
            if (matchedOperation == null) {
                matchedOperation = insertRoutingOperationSnapshot(orderRouting.getId(), productionOrderId, desiredOperation);
            } else {
                updateRoutingOperationSnapshotIfNecessary(matchedOperation, orderRouting.getId(), productionOrderId, desiredOperation);
            }
            finalOperationList.add(matchedOperation);
        }
        for (Deque<ProductionOrderRoutingOperation> queue : existingBucketMap.values()) {
            while (queue != null && !queue.isEmpty()) {
                removeRoutingOperationSnapshot(queue.pollFirst());
            }
        }
        syncRoutingOperationTasks(productionOrderId, finalOperationList);
    }
    private ProductionOrderRouting getOrCreateOrderRoutingSnapshot(Long productionOrderId,
                                                                   ProductionOrder productionOrder,
                                                                   ProductionOrderBom orderBom,
                                                                   Long rootProductModelId) {
        ProductionOrderRouting orderRouting = productionOrderRoutingMapper.selectOne(
                Wrappers.<ProductionOrderRouting>lambdaQuery()
                        .eq(ProductionOrderRouting::getProductionOrderId, productionOrderId)
                        .orderByDesc(ProductionOrderRouting::getId)
                        .last("limit 1"));
        if (orderRouting == null) {
            orderRouting = new ProductionOrderRouting();
            orderRouting.setProductionOrderId(productionOrderId);
            orderRouting.setProductModelId(rootProductModelId);
            orderRouting.setTechnologyRoutingId(productionOrder == null ? null : productionOrder.getTechnologyRoutingId());
            orderRouting.setBomId(orderBom == null ? null : orderBom.getBomId());
            orderRouting.setOrderBomId(orderBom == null ? null : orderBom.getId());
            productionOrderRoutingMapper.insert(orderRouting);
            return orderRouting;
        }
        ProductionOrderRouting update = new ProductionOrderRouting();
        update.setId(orderRouting.getId());
        boolean changed = false;
        if (!Objects.equals(orderRouting.getProductModelId(), rootProductModelId)) {
            update.setProductModelId(rootProductModelId);
            orderRouting.setProductModelId(rootProductModelId);
            changed = true;
        }
        Long technologyRoutingId = productionOrder == null ? null : productionOrder.getTechnologyRoutingId();
        if (!Objects.equals(orderRouting.getTechnologyRoutingId(), technologyRoutingId)) {
            update.setTechnologyRoutingId(technologyRoutingId);
            orderRouting.setTechnologyRoutingId(technologyRoutingId);
            changed = true;
        }
        Long bomId = orderBom == null ? null : orderBom.getBomId();
        if (!Objects.equals(orderRouting.getBomId(), bomId)) {
            update.setBomId(bomId);
            orderRouting.setBomId(bomId);
            changed = true;
        }
        Long orderBomId = orderBom == null ? null : orderBom.getId();
        if (!Objects.equals(orderRouting.getOrderBomId(), orderBomId)) {
            update.setOrderBomId(orderBomId);
            orderRouting.setOrderBomId(orderBomId);
            changed = true;
        }
        if (changed) {
            productionOrderRoutingMapper.updateById(update);
        }
        return orderRouting;
    }
    private List<ProductionOrderRoutingOperation> buildDesiredRoutingOperationList(List<ProductionBomStructure> structureList,
                                                                                   Long rootProductModelId) {
        if (structureList == null || structureList.isEmpty()) {
            return Collections.emptyList();
        }
        Map<Long, ProductionBomStructure> structureById = structureList.stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionBomStructure::getId, item -> item, (left, right) -> left));
        // 构建父-子映射关系
        Map<Long, List<ProductionBomStructure>> treeMap = buildParentChildMap(structureList);
        // 使用后序遍历构建操作列表(先子后父,确保工艺路线顺序正确)
        Map<String, ProductionBomStructure> uniqueOperationMap = new LinkedHashMap<>();
        buildOperationListPostOrder(null, treeMap, uniqueOperationMap, structureById, rootProductModelId);
        List<ProductionOrderRoutingOperation> desiredOperationList = new ArrayList<>();
        int dragSort = 1;
        for (ProductionBomStructure bomStructure : uniqueOperationMap.values()) {
            Long outputProductModelId = resolveOutputProductModelId(resolveOperationOutputNode(bomStructure, structureById), rootProductModelId);
            TechnologyOperation technologyOperation = getTechnologyOperation(bomStructure.getTechnologyOperationId());
            ProductionOrderRoutingOperation routingOperation = new ProductionOrderRoutingOperation();
            routingOperation.setProductModelId(outputProductModelId);
            routingOperation.setTechnologyOperationId(bomStructure.getTechnologyOperationId());
            routingOperation.setOperationName(technologyOperation == null ? null : technologyOperation.getName());
            routingOperation.setIsQuality(technologyOperation == null ? null : technologyOperation.getIsQuality());
            routingOperation.setIsProduction(technologyOperation == null ? null : technologyOperation.getIsProduction());
            routingOperation.setType(technologyOperation == null ? null : technologyOperation.getType());
            routingOperation.setDragSort(dragSort++);
            desiredOperationList.add(routingOperation);
        }
        return desiredOperationList;
    }
    private Map<Long, List<ProductionBomStructure>> buildParentChildMap(List<ProductionBomStructure> structureList) {
        Map<Long, List<ProductionBomStructure>> treeMap = new LinkedHashMap<>();
        Map<Long, Integer> childCountMap = new HashMap<>();
        // 第一遍:统计每个节点的子节点数量,同时构建初始映射
        for (ProductionBomStructure structure : structureList) {
            if (structure == null) continue;
            Long parentId = structure.getParentId();
            childCountMap.merge(parentId, 1, Integer::sum);  // 统计每个父节点有多少个子节点
            treeMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(structure);
        }
        // 第二遍:对每个父节点下的子节点按子节点数量倒序排序(有子节点的优先)
        for (Map.Entry<Long, List<ProductionBomStructure>> entry : treeMap.entrySet()) {
            List<ProductionBomStructure> children = entry.getValue();
            children.sort((a, b) -> {
                int countA = childCountMap.getOrDefault(a.getId(), 0);
                int countB = childCountMap.getOrDefault(b.getId(), 0);
                return Integer.compare(countB, countA);  // 子节点多的排前面
            });
        }
        return treeMap;
    }
    private void buildOperationListPostOrder(Long parentId,
                                             Map<Long, List<ProductionBomStructure>> treeMap,
                                             Map<String, ProductionBomStructure> uniqueOperationMap,
                                             Map<Long, ProductionBomStructure> structureById,
                                             Long rootProductModelId) {
        List<ProductionBomStructure> children = treeMap.get(parentId);
        if (children == null || children.isEmpty()) {
            return;
        }
        for (ProductionBomStructure child : children) {
            // 先递归处理子节点
            buildOperationListPostOrder(child.getId(), treeMap, uniqueOperationMap, structureById, rootProductModelId);
            // 再处理当前节点
            if (child.getTechnologyOperationId() != null) {
                Long outputProductModelId = resolveOutputProductModelId(resolveOperationOutputNode(child, structureById), rootProductModelId);
                uniqueOperationMap.putIfAbsent(buildBomOperationDedupKey(child, outputProductModelId), child);
            }
        }
    }
    private Map<String, Deque<ProductionOrderRoutingOperation>> buildExistingRoutingOperationBucketMap(List<ProductionOrderRoutingOperation> existingOperationList) {
        Map<String, Deque<ProductionOrderRoutingOperation>> existingBucketMap = new LinkedHashMap<>();
        if (existingOperationList == null || existingOperationList.isEmpty()) {
            return existingBucketMap;
        }
        for (ProductionOrderRoutingOperation routingOperation : existingOperationList) {
            String bucketKey = buildRoutingOperationBucketKey(
                    routingOperation.getTechnologyOperationId(),
                    routingOperation.getProductModelId());
            existingBucketMap.computeIfAbsent(bucketKey, key -> new ArrayDeque<>()).addLast(routingOperation);
        }
        return existingBucketMap;
    }
    private ProductionOrderRoutingOperation insertRoutingOperationSnapshot(Long orderRoutingId,
                                                                           Long productionOrderId,
                                                                           ProductionOrderRoutingOperation desiredOperation) {
        ProductionOrderRoutingOperation insert = new ProductionOrderRoutingOperation();
        insert.setOrderRoutingId(orderRoutingId);
        insert.setProductionOrderId(productionOrderId);
        insert.setProductModelId(desiredOperation.getProductModelId());
        insert.setTechnologyOperationId(desiredOperation.getTechnologyOperationId());
        insert.setOperationName(desiredOperation.getOperationName());
        insert.setIsQuality(desiredOperation.getIsQuality());
        insert.setIsProduction(desiredOperation.getIsProduction());
        insert.setType(desiredOperation.getType());
        insert.setDragSort(desiredOperation.getDragSort());
        productionOrderRoutingOperationMapper.insert(insert);
        syncRoutingOperationParams(insert.getId(), productionOrderId, insert.getTechnologyOperationId());
        return insert;
    }
    private void updateRoutingOperationSnapshotIfNecessary(ProductionOrderRoutingOperation currentOperation,
                                                           Long orderRoutingId,
                                                           Long productionOrderId,
                                                           ProductionOrderRoutingOperation desiredOperation) {
        if (currentOperation == null || currentOperation.getId() == null) {
            return;
        }
        ProductionOrderRoutingOperation update = new ProductionOrderRoutingOperation();
        update.setId(currentOperation.getId());
        boolean changed = false;
        if (!Objects.equals(currentOperation.getOrderRoutingId(), orderRoutingId)) {
            update.setOrderRoutingId(orderRoutingId);
            currentOperation.setOrderRoutingId(orderRoutingId);
            changed = true;
        }
        if (!Objects.equals(currentOperation.getProductionOrderId(), productionOrderId)) {
            update.setProductionOrderId(productionOrderId);
            currentOperation.setProductionOrderId(productionOrderId);
            changed = true;
        }
        if (!Objects.equals(currentOperation.getProductModelId(), desiredOperation.getProductModelId())) {
            update.setProductModelId(desiredOperation.getProductModelId());
            currentOperation.setProductModelId(desiredOperation.getProductModelId());
            changed = true;
        }
        if (!Objects.equals(currentOperation.getTechnologyOperationId(), desiredOperation.getTechnologyOperationId())) {
            update.setTechnologyOperationId(desiredOperation.getTechnologyOperationId());
            currentOperation.setTechnologyOperationId(desiredOperation.getTechnologyOperationId());
            changed = true;
        }
        if (!Objects.equals(currentOperation.getOperationName(), desiredOperation.getOperationName())) {
            update.setOperationName(desiredOperation.getOperationName());
            currentOperation.setOperationName(desiredOperation.getOperationName());
            changed = true;
        }
        if (!Objects.equals(currentOperation.getIsQuality(), desiredOperation.getIsQuality())) {
            update.setIsQuality(desiredOperation.getIsQuality());
            currentOperation.setIsQuality(desiredOperation.getIsQuality());
            changed = true;
        }
        if (!Objects.equals(currentOperation.getIsProduction(), desiredOperation.getIsProduction())) {
            update.setIsProduction(desiredOperation.getIsProduction());
            currentOperation.setIsProduction(desiredOperation.getIsProduction());
            changed = true;
        }
        // 更新 dragSort 字段,确保工艺路线顺序正确
        if (!Objects.equals(currentOperation.getDragSort(), desiredOperation.getDragSort())) {
            update.setDragSort(desiredOperation.getDragSort());
            currentOperation.setDragSort(desiredOperation.getDragSort());
            changed = true;
        }
        if (!Objects.equals(currentOperation.getType(), desiredOperation.getType())) {
            update.setType(desiredOperation.getType());
            currentOperation.setType(desiredOperation.getType());
            changed = true;
        }
        if (!Objects.equals(currentOperation.getDragSort(), desiredOperation.getDragSort())) {
            update.setDragSort(desiredOperation.getDragSort());
            currentOperation.setDragSort(desiredOperation.getDragSort());
            changed = true;
        }
        if (changed) {
            productionOrderRoutingOperationMapper.updateById(update);
        }
    }
    private void removeRoutingOperationSnapshot(ProductionOrderRoutingOperation routingOperation) {
        if (routingOperation == null || routingOperation.getId() == null) {
            return;
        }
        ProductionOperationTask task = productionOperationTaskMapper.selectOne(
                Wrappers.<ProductionOperationTask>lambdaQuery()
                        .eq(ProductionOperationTask::getProductionOrderRoutingOperationId, routingOperation.getId())
                        .last("limit 1"));
        if (task != null) {
            validateTaskCanRemove(task);
            productionOperationTaskMapper.deleteById(task.getId());
        }
        productionOrderRoutingOperationParamMapper.delete(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .eq(ProductionOrderRoutingOperationParam::getProductionOrderRoutingOperationId, routingOperation.getId()));
        productionOrderRoutingOperationMapper.deleteById(routingOperation.getId());
    }
    private void syncRoutingOperationTasks(Long productionOrderId, List<ProductionOrderRoutingOperation> routingOperationList) {
        if (routingOperationList == null || routingOperationList.isEmpty()) {
            return;
        }
        List<Long> routingOperationIdList = routingOperationList.stream()
                .map(ProductionOrderRoutingOperation::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (routingOperationIdList.isEmpty()) {
            return;
        }
        Map<Long, ProductionOperationTask> taskByRoutingOperationId = productionOperationTaskMapper.selectList(
                        Wrappers.<ProductionOperationTask>lambdaQuery()
                                .in(ProductionOperationTask::getProductionOrderRoutingOperationId, routingOperationIdList)
                                .orderByAsc(ProductionOperationTask::getId))
                .stream()
                .filter(item -> item != null && item.getProductionOrderRoutingOperationId() != null)
                .collect(Collectors.toMap(
                        ProductionOperationTask::getProductionOrderRoutingOperationId,
                        item -> item,
                        (left, right) -> left,
                        LinkedHashMap::new));
        for (int i = 0; i < routingOperationList.size(); i++) {
            ProductionOrderRoutingOperation routingOperation = routingOperationList.get(i);
            if (routingOperation == null || routingOperation.getId() == null) {
                continue;
            }
            boolean shouldHaveTask = i == routingOperationList.size() - 1 || Boolean.TRUE.equals(routingOperation.getIsProduction());
            ProductionOperationTask existingTask = taskByRoutingOperationId.get(routingOperation.getId());
            if (shouldHaveTask) {
                if (existingTask == null) {
                    ProductionOperationTask task = new ProductionOperationTask();
                    task.setProductionOrderId(productionOrderId);
                    task.setProductionOrderRoutingOperationId(routingOperation.getId());
                    task.setPlanQuantity(BigDecimal.ZERO);
                    task.setCompleteQuantity(BigDecimal.ZERO);
                    task.setWorkOrderNo(generateNextTaskNo());
                    task.setStatus(2);
                    productionOperationTaskMapper.insert(task);
                }
                continue;
            }
            if (existingTask != null) {
                validateTaskCanRemove(existingTask);
                productionOperationTaskMapper.deleteById(existingTask.getId());
            }
        }
    }
    private void validateTaskCanRemove(ProductionOperationTask task) {
        if (task == null || task.getId() == null) {
            return;
        }
        if (defaultDecimal(task.getCompleteQuantity()).compareTo(BigDecimal.ZERO) > 0) {
            throw new ServiceException("工序已产生报工记录,无法根据 BOM 变更删除对应工序快照");
        }
        long reportCount = productionProductMainMapper.selectCount(
                Wrappers.<ProductionProductMain>lambdaQuery()
                        .eq(ProductionProductMain::getProductionOperationTaskId, task.getId()));
        if (reportCount > 0) {
            throw new ServiceException("工序已产生报工记录,无法根据 BOM 变更删除对应工单");
        }
    }
    private void syncRoutingOperationParams(Long routingOperationId, Long productionOrderId, Long technologyOperationId) {
        if (routingOperationId == null || technologyOperationId == null) {
            return;
        }
        List<TechnologyOperationParam> operationParamList = technologyOperationParamMapper.selectList(
                Wrappers.<TechnologyOperationParam>lambdaQuery()
                        .eq(TechnologyOperationParam::getTechnologyOperationId, technologyOperationId)
                        .orderByAsc(TechnologyOperationParam::getId));
        for (TechnologyOperationParam operationParam : operationParamList) {
            TechnologyParam technologyParam = technologyParamMapper.selectById(operationParam.getTechnologyParamId());
            if (technologyParam == null) {
                continue;
            }
            ProductionOrderRoutingOperationParam snapshot = new ProductionOrderRoutingOperationParam();
            snapshot.setProductionOrderId(productionOrderId);
            snapshot.setProductionOrderRoutingOperationId(routingOperationId);
            snapshot.setTechnologyOperationId(operationParam.getTechnologyOperationId());
            snapshot.setTechnologyOperationParamId(operationParam.getId());
            snapshot.setParamId(technologyParam.getId());
            snapshot.setParamCode(technologyParam.getParamCode());
            snapshot.setParamName(technologyParam.getParamName());
            snapshot.setParamType(technologyParam.getParamType());
            snapshot.setParamFormat(technologyParam.getParamFormat());
            snapshot.setUnit(technologyParam.getUnit());
            snapshot.setIsRequired(technologyParam.getIsRequired());
            snapshot.setRemark(technologyParam.getRemark());
            snapshot.setStandardValue(operationParam.getStandardValue());
            productionOrderRoutingOperationParamMapper.insert(snapshot);
        }
    }
    private Map<String, BigDecimal> buildOperationDemandedQuantityMap(List<ProductionBomStructure> structureList,
                                                                      Long rootProductModelId) {
        if (structureList == null || structureList.isEmpty()) {
@@ -289,6 +705,7 @@
        }
        return demandedQuantityMap;
    }
    private BigDecimal resolveTaskPlanQuantity(ProductionOrderRoutingOperation routingOperation,
                                               Map<String, BigDecimal> demandedQuantityMap,
                                               BigDecimal orderQuantity,
@@ -310,6 +727,16 @@
        return String.valueOf(operationId) + "#" + String.valueOf(outputProductModelId);
    }
    private String buildRoutingOperationBucketKey(Long operationId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputProductModelId);
    }
    private String buildBomOperationDedupKey(ProductionBomStructure bomStructure, Long outputProductModelId) {
        Long operationId = bomStructure == null ? null : bomStructure.getTechnologyOperationId();
        Long parentId = bomStructure == null ? null : bomStructure.getParentId();
        return operationId + "#" + outputProductModelId + "#" + parentId;
    }
    private String buildOperationOutputNodeKey(Long operationId, Long outputNodeId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputNodeId) + "#" + String.valueOf(outputProductModelId);
    }
@@ -326,12 +753,38 @@
        ProductionBomStructure parent = structureById.get(bomStructure.getParentId());
        return parent != null ? parent : bomStructure;
    }
    private Long resolveOutputProductModelId(ProductionBomStructure outputNode,
                                             Long rootProductModelId) {
        if (outputNode == null) {
            return rootProductModelId;
        }
        return outputNode.getProductModelId() != null ? outputNode.getProductModelId() : rootProductModelId;
    }
    private TechnologyOperation getTechnologyOperation(Long technologyOperationId) {
        if (technologyOperationId == null) {
            return null;
        }
        return technologyOperationMapper.selectById(technologyOperationId);
    }
    private String generateNextTaskNo() {
        String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        ProductionOperationTask latestTask = productionOperationTaskMapper.selectOne(
                Wrappers.<ProductionOperationTask>lambdaQuery()
                        .likeRight(ProductionOperationTask::getWorkOrderNo, "GD" + datePrefix)
                        .orderByDesc(ProductionOperationTask::getWorkOrderNo)
                        .last("limit 1"));
        int sequenceNumber = 1;
        if (latestTask != null && latestTask.getWorkOrderNo() != null && latestTask.getWorkOrderNo().startsWith("GD" + datePrefix)) {
            try {
                sequenceNumber = Integer.parseInt(latestTask.getWorkOrderNo().substring(("GD" + datePrefix).length())) + 1;
            } catch (NumberFormatException ignored) {
                sequenceNumber = 1;
            }
        }
        return "GD" + String.format("%s%03d", datePrefix, sequenceNumber);
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
@@ -351,8 +804,8 @@
            return;
        }
        for (ProductionBomStructureDto node : source) {
            flattenTree(node.getChildren(), result);  // 先递归添加子节点
            result.add(node);
            flattenTree(node.getChildren(), result);
        }
    }