zss
2026-04-25 78652d3173ffbe79a6e9b88035e5481a65020f97
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -2,66 +2,38 @@
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.basic.mapper.StorageAttachmentMapper;
import com.ruoyi.basic.mapper.StorageBlobMapper;
import com.ruoyi.basic.pojo.StorageAttachment;
import com.ruoyi.basic.pojo.StorageBlob;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.constant.StorageAttachmentConstants;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.production.bean.dto.ProductionOrderDto;
import com.ruoyi.production.bean.vo.ProductionOrderVo;
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.ProductionOrderPickMapper;
import com.ruoyi.production.mapper.ProductionOrderPickRecordMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingOperationMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingOperationParamMapper;
import com.ruoyi.production.mapper.ProductionPlanMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.pojo.ProductionOrderPick;
import com.ruoyi.production.pojo.ProductionOrderPickRecord;
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.ProductionPlan;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.production.enums.ProductOrderStatusEnum;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.pojo.*;
import com.ruoyi.production.service.ProductionOrderService;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.production.service.ProductionOrderService;
import com.ruoyi.technology.mapper.TechnologyBomMapper;
import com.ruoyi.technology.mapper.TechnologyBomStructureMapper;
import com.ruoyi.technology.mapper.TechnologyRoutingMapper;
import com.ruoyi.technology.mapper.TechnologyRoutingOperationMapper;
import com.ruoyi.technology.mapper.TechnologyRoutingOperationParamMapper;
import com.ruoyi.technology.pojo.TechnologyBom;
import com.ruoyi.technology.pojo.TechnologyBomStructure;
import com.ruoyi.technology.pojo.TechnologyRouting;
import com.ruoyi.technology.pojo.TechnologyRoutingOperation;
import com.ruoyi.technology.pojo.TechnologyRoutingOperationParam;
import com.ruoyi.technology.mapper.*;
import com.ruoyi.technology.pojo.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
@Service
@@ -79,29 +51,40 @@
    private final ProductionOrderPickMapper productionOrderPickMapper;
    private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper;
    private final ProductionPlanMapper productionPlanMapper;
    private final StorageAttachmentMapper storageAttachmentMapper;
    private final StorageBlobMapper storageBlobMapper;
    private final SalesLedgerMapper salesLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final TechnologyRoutingMapper technologyRoutingMapper;
    private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
    private final TechnologyRoutingOperationParamMapper technologyRoutingOperationParamMapper;
    private final TechnologyOperationMapper technologyOperationMapper;
    private final TechnologyBomMapper technologyBomMapper;
    private final TechnologyBomStructureMapper technologyBomStructureMapper;
    private final FileUtil fileUtil;
    @Override
    public com.baomidou.mybatisplus.core.metadata.IPage<ProductionOrderVo> pageProductionOrder(Page<ProductionOrderDto> page, ProductionOrderDto dto) {
        Page<ProductionOrder> entityPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        return this.page(entityPage, buildQueryWrapper(dto)).convert(item -> BeanUtil.copyProperties(item, ProductionOrderVo.class));
    public IPage<ProductionOrderVo> pageProductionOrder(Page<ProductionOrderDto> page, ProductionOrderDto dto) {
        Page<ProductionOrderVo> result = (Page<ProductionOrderVo>) baseMapper.pageProductionOrder(page, dto);
        fillProductImages(result.getRecords());
        return result;
    }
    @Override
    public List<ProductionOrderVo> listProductionOrder(ProductionOrderDto dto) {
        return BeanUtil.copyToList(this.list(buildQueryWrapper(dto)), ProductionOrderVo.class);
        List<ProductionOrderVo> records = baseMapper.listProductionOrder(dto);
        fillProductImages(records);
        return records;
    }
    @Override
    public ProductionOrderVo getProductionOrderInfo(Long id) {
        ProductionOrder item = this.getById(id);
        return item == null ? null : BeanUtil.copyProperties(item, ProductionOrderVo.class);
        ProductionOrderVo item = baseMapper.getProductionOrderInfo(id);
        if (item == null) {
            return null;
        }
        fillProductImages(java.util.Collections.singletonList(item));
        return item;
    }
    @Override
@@ -154,20 +137,64 @@
    }
    @Override
    public Integer bindingRoute(ProductionOrderDto productionOrderDto) {
        if (productionOrderDto == null || productionOrderDto.getId() == null) {
            throw new ServiceException("生产订单ID不能为空");
        }
        ProductionOrder productionOrder = this.getById(productionOrderDto.getId());
        if (productionOrder == null) {
            throw new ServiceException("生产订单不存在");
        }
        Long targetRoutingId = productionOrderDto.getTechnologyRoutingId() == null
                ? productionOrder.getTechnologyRoutingId()
                : productionOrderDto.getTechnologyRoutingId();
        if (targetRoutingId == null) {
            throw new ServiceException("工艺路线ID不能为空");
        }
        TechnologyRouting targetRouting = technologyRoutingMapper.selectById(targetRoutingId);
        if (targetRouting == null) {
            throw new ServiceException("工艺路线不存在");
        }
        if (productionOrder.getProductModelId() != null
                && !Objects.equals(productionOrder.getProductModelId(), targetRouting.getProductModelId())) {
            throw new ServiceException("工艺路线与生产订单产品规格不匹配");
        }
        if (ProductOrderStatusEnum.isStarted(productionOrder.getStatus())
                && !Objects.equals(productionOrder.getTechnologyRoutingId(), targetRoutingId)) {
            throw new ServiceException("生产订单已开工,不能修改工艺路线");
        }
        if (!Objects.equals(productionOrder.getTechnologyRoutingId(), targetRoutingId)) {
            ProductionOrder update = new ProductionOrder();
            update.setId(productionOrder.getId());
            update.setTechnologyRoutingId(targetRoutingId);
            if (!this.updateById(update)) {
                throw new ServiceException("绑定工艺路线失败");
            }
        }
        // 绑定路线仅重建订单侧快照数据
        return syncProductionOrderSnapshot(productionOrder.getId());
    }
    @Override
    public int syncProductionOrderSnapshot(Long productionOrderId) {
        ProductionOrder productionOrder = this.getById(productionOrderId);
        if (productionOrder == null) {
            throw new ServiceException("Production order not found");
            throw new ServiceException("生产订单不存在");
        }
        if (productionOrder.getTechnologyRoutingId() == null) {
            throw new ServiceException("technologyRoutingId is required");
            throw new ServiceException("工艺路线ID不能为空");
        }
        TechnologyRouting technologyRouting = technologyRoutingMapper.selectById(productionOrder.getTechnologyRoutingId());
        if (technologyRouting == null) {
            throw new ServiceException("Technology routing not found");
            throw new ServiceException("工艺路线不存在");
        }
        // 订单快照按“先清后建”处理,保证工艺路线、工序、参数、BOM 全部来自同一版本。
        clearProductionSnapshot(productionOrderId);
        ProductionOrderBom orderBom = syncProductionOrderBomSnapshot(productionOrder, technologyRouting);
        ProductionOrderRouting orderRouting = new ProductionOrderRouting();
        orderRouting.setProductionOrderId(productionOrder.getId());
@@ -176,6 +203,7 @@
        orderRouting.setProcessRouteCode(technologyRouting.getProcessRouteCode());
        orderRouting.setDescription(technologyRouting.getDescription());
        orderRouting.setBomId(technologyRouting.getBomId());
        orderRouting.setOrderBomId(orderBom == null ? null : orderBom.getId());
        productionOrderRoutingMapper.insert(orderRouting);
        int syncedParamCount = 0;
@@ -184,6 +212,13 @@
                        .eq(TechnologyRoutingOperation::getTechnologyRoutingId, technologyRouting.getId())
                        .orderByAsc(TechnologyRoutingOperation::getDragSort)
                        .orderByAsc(TechnologyRoutingOperation::getId));
        Map<Long, String> operationNameMap = technologyOperationMapper.selectBatchIds(
                        routingOperations.stream()
                                .map(TechnologyRoutingOperation::getTechnologyOperationId)
                                .filter(Objects::nonNull)
                                .collect(Collectors.toSet()))
                .stream()
                .collect(Collectors.toMap(TechnologyOperation::getId, TechnologyOperation::getName, (a, b) -> a));
        for (TechnologyRoutingOperation sourceOperation : routingOperations) {
            // 订单工序保存的是工艺工序快照,后续报工只依赖快照,不再直接引用工艺主数据。
            ProductionOrderRoutingOperation targetOperation = new ProductionOrderRoutingOperation();
@@ -193,6 +228,7 @@
            targetOperation.setProductModelId(sourceOperation.getProductModelId());
            targetOperation.setDragSort(sourceOperation.getDragSort());
            targetOperation.setIsQuality(sourceOperation.getIsQuality());
            targetOperation.setOperationName(operationNameMap.get(sourceOperation.getTechnologyOperationId()));
            productionOrderRoutingOperationMapper.insert(targetOperation);
            ProductionOperationTask task = new ProductionOperationTask();
@@ -230,18 +266,17 @@
            }
        }
        syncProductionOrderBomSnapshot(productionOrder, technologyRouting);
        upsertOrderPick(productionOrder);
        return syncedParamCount;
    }
    private void syncProductionOrderBomSnapshot(ProductionOrder productionOrder, TechnologyRouting technologyRouting) {
    private ProductionOrderBom syncProductionOrderBomSnapshot(ProductionOrder productionOrder, TechnologyRouting technologyRouting) {
        if (technologyRouting.getBomId() == null) {
            return;
            return null;
        }
        TechnologyBom technologyBom = technologyBomMapper.selectById(technologyRouting.getBomId());
        if (technologyBom == null) {
            throw new ServiceException("Technology BOM not found");
            throw new ServiceException("工艺BOM不存在");
        }
        List<TechnologyBomStructure> structureList = technologyBomStructureMapper.selectList(
                Wrappers.<TechnologyBomStructure>lambdaQuery()
@@ -275,6 +310,7 @@
            productionBomStructureMapper.insert(target);
            idMap.put(source.getId(), target.getId());
        }
        return orderBom;
    }
    private void clearProductionSnapshot(Long productionOrderId) {
@@ -283,7 +319,7 @@
                Wrappers.<ProductionOrderPickRecord>lambdaQuery()
                        .eq(ProductionOrderPickRecord::getProductionOrderId, productionOrderId)) > 0;
        if (hasPickRecord) {
            throw new ServiceException("Production order pick records already exist, snapshot cannot be regenerated");
            throw new ServiceException("生产订单已存在领料记录,不能重新生成快照");
        }
        List<Long> taskIds = productionOperationTaskMapper.selectList(
                        Wrappers.<ProductionOperationTask>lambdaQuery()
@@ -295,7 +331,7 @@
                    Wrappers.<ProductionProductMain>lambdaQuery()
                            .in(ProductionProductMain::getProductionOperationTaskId, taskIds)) > 0;
            if (started) {
                throw new ServiceException("Production order already started, snapshot cannot be regenerated");
                throw new ServiceException("生产订单已开工,不能重新生成快照");
            }
            productionOperationTaskMapper.delete(Wrappers.<ProductionOperationTask>lambdaQuery()
                    .eq(ProductionOperationTask::getProductionOrderId, productionOrderId));
@@ -368,56 +404,56 @@
    private void validateAndFillOrder(ProductionOrder productionOrder, ProductionOrder oldOrder) {
        if (productionOrder == null) {
            throw new ServiceException("Production order is required");
            throw new ServiceException("生产订单不能为空");
        }
        fillFromSalesLedgerProduct(productionOrder);
        fillFromProductionPlans(productionOrder);
        if (productionOrder.getProductModelId() == null) {
            throw new ServiceException("productModelId is required");
            throw new ServiceException("产品规格ID不能为空");
        }
        if (defaultDecimal(productionOrder.getQuantity()).compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("quantity must be greater than 0");
            throw new ServiceException("下单数量必须大于0");
        }
        if (productionOrder.getTechnologyRoutingId() == null) {
            // 未显式指定工艺路线时,按产品规格选最新一条工艺作为默认路线。
            TechnologyRouting technologyRouting = technologyRoutingMapper.selectOne(
                    Wrappers.<TechnologyRouting>lambdaQuery()
                            .eq(TechnologyRouting::getProductModelId, productionOrder.getProductModelId())
                            .orderByDesc(TechnologyRouting::getId)
                            .last("limit 1"));
            if (technologyRouting == null) {
                throw new ServiceException("No technology routing found for the product model");
            }
            productionOrder.setTechnologyRoutingId(technologyRouting.getId());
        }
//        if (productionOrder.getTechnologyRoutingId() == null) {
//            // 未显式指定工艺路线时,按产品规格选最新一条工艺作为默认路线。
//            TechnologyRouting technologyRouting = technologyRoutingMapper.selectOne(
//                    Wrappers.<TechnologyRouting>lambdaQuery()
//                            .eq(TechnologyRouting::getProductModelId, productionOrder.getProductModelId())
//                            .orderByDesc(TechnologyRouting::getId)
//                            .last("limit 1"));
//            if (technologyRouting == null) {
//                throw new ServiceException("未找到该产品规格对应的工艺路线");
//            }
//            productionOrder.setTechnologyRoutingId(technologyRouting.getId());
//        }
        if (oldOrder != null && ProductOrderStatusEnum.isStarted(oldOrder.getStatus())) {
            // 开工后只允许修正非核心字段,核心生产依据锁定。
            if (!Objects.equals(oldOrder.getProductModelId(), productionOrder.getProductModelId())
                    || !Objects.equals(oldOrder.getTechnologyRoutingId(), productionOrder.getTechnologyRoutingId())
                    || compareDecimal(oldOrder.getQuantity(), productionOrder.getQuantity()) != 0) {
                throw new ServiceException("Started production orders cannot modify product, routing or quantity");
                throw new ServiceException("生产订单已开工,不能修改产品、工艺路线或数量");
            }
        }
    }
    private void fillFromSalesLedgerProduct(ProductionOrder productionOrder) {
        if (productionOrder.getSaleLedgerProductId() == null) {
        if (productionOrder.getSalesLedgerProductId() == null) {
            return;
        }
        // 销售明细是订单来源时,以销售明细为准回填销售台账、产品规格和默认数量。
        SalesLedgerProduct salesLedgerProduct = salesLedgerProductMapper.selectById(productionOrder.getSaleLedgerProductId().longValue());
        SalesLedgerProduct salesLedgerProduct = salesLedgerProductMapper.selectById(productionOrder.getSalesLedgerProductId().longValue());
        if (salesLedgerProduct == null) {
            throw new ServiceException("Sales ledger product not found");
            throw new ServiceException("销售台账产品不存在");
        }
        if (productionOrder.getSalesLedgerId() == null) {
            productionOrder.setSalesLedgerId(salesLedgerProduct.getSalesLedgerId());
        } else if (!Objects.equals(productionOrder.getSalesLedgerId(), salesLedgerProduct.getSalesLedgerId())) {
            throw new ServiceException("salesLedgerId does not match the sales ledger product");
            throw new ServiceException("销售台账ID与销售台账产品不一致");
        }
        if (productionOrder.getProductModelId() == null) {
            productionOrder.setProductModelId(salesLedgerProduct.getProductModelId());
        } else if (!Objects.equals(productionOrder.getProductModelId(), salesLedgerProduct.getProductModelId())) {
            throw new ServiceException("productModelId does not match the sales ledger product");
            throw new ServiceException("产品规格ID与销售台账产品不一致");
        }
        if (productionOrder.getQuantity() == null || productionOrder.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            productionOrder.setQuantity(salesLedgerProduct.getQuantity());
@@ -438,22 +474,22 @@
        // 多计划合并转单时,所有计划必须属于同一规格,且只能下发一次。
        List<ProductionPlan> productionPlans = productionPlanMapper.selectBatchIds(planIds);
        if (productionPlans.size() != planIds.size()) {
            throw new ServiceException("Some production plans do not exist");
            throw new ServiceException("部分生产计划不存在");
        }
        Set<Long> productModelIds = productionPlans.stream()
                .map(ProductionPlan::getProductModelId)
                .collect(Collectors.toSet());
        if (productModelIds.size() > 1) {
            throw new ServiceException("Selected production plans must belong to the same product model");
            throw new ServiceException("所选生产计划必须属于同一产品规格");
        }
        if (Boolean.TRUE.equals(productionPlans.stream().anyMatch(item -> Boolean.TRUE.equals(item.getIssued())))) {
            throw new ServiceException("Selected production plans already issued");
        if (productionPlans.stream().anyMatch(item -> item.getStatus() != null && item.getStatus() == 2)) {
            throw new ServiceException("所选生产计划已下发");
        }
        ProductionPlan firstPlan = productionPlans.get(0);
        if (productionOrder.getProductModelId() == null) {
            productionOrder.setProductModelId(firstPlan.getProductModelId());
        } else if (!Objects.equals(productionOrder.getProductModelId(), firstPlan.getProductModelId())) {
            throw new ServiceException("productModelId does not match the production plans");
            throw new ServiceException("产品规格ID与生产计划不一致");
        }
        if (productionOrder.getQuantity() == null || productionOrder.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            productionOrder.setQuantity(productionPlans.stream()
@@ -574,10 +610,10 @@
            return null;
        }
        if (productionPlan.getPromisedDeliveryDate() != null) {
            return productionPlan.getPromisedDeliveryDate().toLocalDate();
            return productionPlan.getPromisedDeliveryDate();
        }
        if (productionPlan.getRequiredDate() != null) {
            return productionPlan.getRequiredDate().toLocalDate();
            return productionPlan.getRequiredDate();
        }
        return null;
    }
@@ -585,4 +621,65 @@
    private int compareDecimal(BigDecimal left, BigDecimal right) {
        return defaultDecimal(left).compareTo(defaultDecimal(right));
    }
    private void fillProductImages(List<ProductionOrderVo> records) {
        if (records == null || records.isEmpty()) {
            return;
        }
        List<Long> productModelIds = records.stream()
                .map(ProductionOrderVo::getProductModelId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (productModelIds.isEmpty()) {
            return;
        }
        List<StorageAttachment> attachments = storageAttachmentMapper.selectList(
                Wrappers.<StorageAttachment>lambdaQuery()
                        .in(StorageAttachment::getRecordId, productModelIds)
                        .eq(StorageAttachment::getApplication, StorageAttachmentConstants.StorageAttachmentImage)
                        .eq(StorageAttachment::getDeleted, 0L)
                        .orderByAsc(StorageAttachment::getId));
        if (attachments == null || attachments.isEmpty()) {
            return;
        }
        Map<Long, List<StorageAttachment>> attachmentMap = attachments.stream()
                .collect(Collectors.groupingBy(StorageAttachment::getRecordId, java.util.LinkedHashMap::new, Collectors.toList()));
        List<Long> blobIds = attachments.stream()
                .map(StorageAttachment::getStorageBlobId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (blobIds.isEmpty()) {
            return;
        }
        Map<Long, StorageBlob> blobMap = storageBlobMapper.selectBatchIds(blobIds).stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toMap(StorageBlob::getId, item -> item));
        for (ProductionOrderVo record : records) {
            List<StorageAttachment> modelAttachments = attachmentMap.get(record.getProductModelId());
            if (modelAttachments == null || modelAttachments.isEmpty()) {
                continue;
            }
            List<StorageBlobVO> images = modelAttachments.stream()
                    .map(StorageAttachment::getStorageBlobId)
                    .map(blobMap::get)
                    .filter(Objects::nonNull)
                    .map(this::toStorageBlobVO)
                    .collect(Collectors.toList());
            if (!images.isEmpty()) {
                record.setProductImages(images);
            }
        }
    }
    private StorageBlobVO toStorageBlobVO(StorageBlob blob) {
        StorageBlobVO vo = BeanUtil.copyProperties(blob, StorageBlobVO.class);
        vo.setPreviewURL(fileUtil.buildSignedPreviewUrl(vo));
        vo.setDownloadURL(fileUtil.buildSignedDownloadUrl(vo));
        return vo;
    }
}