liding
2026-04-24 270c132a66a26b29a540cf696e9078015fb58de4
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -2,39 +2,30 @@
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.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.enums.ProductOrderStatusEnum;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.pojo.*;
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.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.technology.mapper.*;
import com.ruoyi.technology.pojo.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -42,10 +33,7 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
@Service
@@ -60,49 +48,76 @@
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionBomStructureMapper productionBomStructureMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    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 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
    public boolean saveProductionOrder(ProductionOrder productionOrder) {
        ProductionOrder oldOrder = productionOrder.getId() == null ? null : this.getById(productionOrder.getId());
        // 下单入口统一补齐来源单据、计划和工艺信息,避免前端分别传多套字段。
        validateAndFillOrder(productionOrder, oldOrder);
        if (productionOrder.getNpsNo() == null || productionOrder.getNpsNo().trim().isEmpty()) {
            productionOrder.setNpsNo(generateNextOrderNo());
        }
        if (productionOrder.getCompleteQuantity() == null) {
            productionOrder.setCompleteQuantity(BigDecimal.ZERO);
        }
        if (productionOrder.getStatus() == null) {
            productionOrder.setStatus(ProductOrderStatusEnum.WAIT.getCode());
        }
        boolean saved = this.saveOrUpdate(productionOrder);
        if (!saved) {
            return false;
        }
        syncProductionPlanIssueStatus(oldOrder, productionOrder);
        boolean needSync = productionOrder.getTechnologyRoutingId() != null
                && (oldOrder == null
                || !Objects.equals(oldOrder.getTechnologyRoutingId(), productionOrder.getTechnologyRoutingId())
                || !Objects.equals(oldOrder.getProductModelId(), productionOrder.getProductModelId())
                || compareDecimal(oldOrder.getQuantity(), productionOrder.getQuantity()) != 0
                || productionOrderRoutingMapper.selectCount(Wrappers.<ProductionOrderRouting>lambdaQuery()
                        .eq(ProductionOrderRouting::getProductionOrderId, productionOrder.getId())) == 0);
        if (needSync) {
            // 工艺、产品或数量变化后,订单快照必须和当前下单数据重新对齐。
            syncProductionOrderSnapshot(productionOrder.getId());
        } else {
            // 未重建快照时,也要确保备料主单和订单数量保持同步。
            upsertOrderPick(productionOrder);
        }
        return true;
    }
@@ -113,7 +128,9 @@
            return false;
        }
        for (Long id : ids) {
            ProductionOrder productionOrder = this.getById(id);
            clearProductionSnapshot(id);
            releaseProductionPlanIssueStatus(productionOrder);
        }
        return this.removeByIds(ids);
    }
@@ -131,6 +148,7 @@
        if (technologyRouting == null) {
            throw new ServiceException("Technology routing not found");
        }
        // 订单快照按“先清后建”处理,保证工艺路线、工序、参数、BOM 全部来自同一版本。
        clearProductionSnapshot(productionOrderId);
        ProductionOrderRouting orderRouting = new ProductionOrderRouting();
@@ -149,6 +167,7 @@
                        .orderByAsc(TechnologyRoutingOperation::getDragSort)
                        .orderByAsc(TechnologyRoutingOperation::getId));
        for (TechnologyRoutingOperation sourceOperation : routingOperations) {
            // 订单工序保存的是工艺工序快照,后续报工只依赖快照,不再直接引用工艺主数据。
            ProductionOrderRoutingOperation targetOperation = new ProductionOrderRoutingOperation();
            targetOperation.setProductionOrderId(productionOrder.getId());
            targetOperation.setTechnologyRoutingOperationId(sourceOperation.getId());
@@ -164,7 +183,7 @@
            task.setPlanQuantity(defaultDecimal(productionOrder.getQuantity()));
            task.setCompleteQuantity(BigDecimal.ZERO);
            task.setWorkOrderNo(generateNextTaskNo());
            task.setStatus(1);
            task.setStatus(2);
            productionOperationTaskMapper.insert(task);
            List<TechnologyRoutingOperationParam> sourceParams = technologyRoutingOperationParamMapper.selectList(
@@ -172,6 +191,7 @@
                            .eq(TechnologyRoutingOperationParam::getTechnologyRoutingOperationId, sourceOperation.getId())
                            .orderByAsc(TechnologyRoutingOperationParam::getId));
            for (TechnologyRoutingOperationParam sourceParam : sourceParams) {
                // 工序执行参数同样做快照,避免工艺参数调整影响已下达订单。
                ProductionOrderRoutingOperationParam targetParam = new ProductionOrderRoutingOperationParam();
                targetParam.setProductionOrderId(productionOrder.getId());
                targetParam.setTechnologyRoutingOperationId(targetOperation.getId());
@@ -193,6 +213,7 @@
        }
        syncProductionOrderBomSnapshot(productionOrder, technologyRouting);
        upsertOrderPick(productionOrder);
        return syncedParamCount;
    }
@@ -209,6 +230,7 @@
                        .eq(TechnologyBomStructure::getBomId, technologyBom.getId())
                        .orderByAsc(TechnologyBomStructure::getId));
        TechnologyBomStructure root = structureList.stream().filter(item -> item.getParentId() == null).findFirst().orElse(null);
        BigDecimal orderQuantity = defaultDecimal(productionOrder.getQuantity());
        ProductionOrderBom orderBom = new ProductionOrderBom();
        orderBom.setProductionOrderId(productionOrder.getId());
@@ -216,12 +238,13 @@
        orderBom.setProductModelId(root != null ? root.getProductModelId() : productionOrder.getProductModelId());
        orderBom.setTechnologyOperationId(root == null ? null : root.getOperationId());
        orderBom.setUnitQuantity(root != null && root.getUnitQuantity() != null ? root.getUnitQuantity() : BigDecimal.ONE);
        orderBom.setDemandedQuantity(root != null && root.getDemandedQuantity() != null ? root.getDemandedQuantity() : defaultDecimal(productionOrder.getQuantity()));
        orderBom.setDemandedQuantity(orderQuantity);
        orderBom.setUnit(root == null ? null : root.getUnit());
        productionOrderBomMapper.insert(orderBom);
        Map<Long, Long> idMap = new HashMap<>();
        for (TechnologyBomStructure source : structureList) {
            // 子节点 parentId 需要映射成新快照节点 id,才能保留原始 BOM 层级。
            ProductionBomStructure target = new ProductionBomStructure();
            target.setProductionOrderId(productionOrder.getId());
            target.setProductionOrderBomId(orderBom.getId());
@@ -229,7 +252,7 @@
            target.setProductModelId(source.getProductModelId());
            target.setTechnologyOperationId(source.getOperationId());
            target.setUnitQuantity(source.getUnitQuantity());
            target.setDemandedQuantity(source.getDemandedQuantity());
            target.setDemandedQuantity(resolveBomDemandQuantity(source, orderQuantity));
            target.setUnit(source.getUnit());
            productionBomStructureMapper.insert(target);
            idMap.put(source.getId(), target.getId());
@@ -237,11 +260,19 @@
    }
    private void clearProductionSnapshot(Long productionOrderId) {
        // 已产生领料记录后禁止重建,避免备料/投料依据与订单快照脱节。
        boolean hasPickRecord = productionOrderPickRecordMapper.selectCount(
                Wrappers.<ProductionOrderPickRecord>lambdaQuery()
                        .eq(ProductionOrderPickRecord::getProductionOrderId, productionOrderId)) > 0;
        if (hasPickRecord) {
            throw new ServiceException("Production order pick records already exist, snapshot cannot be regenerated");
        }
        List<Long> taskIds = productionOperationTaskMapper.selectList(
                        Wrappers.<ProductionOperationTask>lambdaQuery()
                                .eq(ProductionOperationTask::getProductionOrderId, productionOrderId))
                .stream().map(ProductionOperationTask::getId).collect(Collectors.toList());
        if (!taskIds.isEmpty()) {
            // 已有报工记录说明订单已开工,此时不允许再重建快照。
            boolean started = productionProductMainMapper.selectCount(
                    Wrappers.<ProductionProductMain>lambdaQuery()
                            .in(ProductionProductMain::getProductionOperationTaskId, taskIds)) > 0;
@@ -261,6 +292,8 @@
                .eq(ProductionBomStructure::getProductionOrderId, productionOrderId));
        productionOrderBomMapper.delete(Wrappers.<ProductionOrderBom>lambdaQuery()
                .eq(ProductionOrderBom::getProductionOrderId, productionOrderId));
        productionOrderPickMapper.delete(Wrappers.<ProductionOrderPick>lambdaQuery()
                .eq(ProductionOrderPick::getProductionOrderId, productionOrderId));
    }
    private LambdaQueryWrapper<ProductionOrder> buildQueryWrapper(ProductionOrderDto dto) {
@@ -314,4 +347,285 @@
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private void validateAndFillOrder(ProductionOrder productionOrder, ProductionOrder oldOrder) {
        if (productionOrder == null) {
            throw new ServiceException("Production order is required");
        }
        fillFromSalesLedgerProduct(productionOrder);
        fillFromProductionPlans(productionOrder);
        if (productionOrder.getProductModelId() == null) {
            throw new ServiceException("productModelId is required");
        }
        if (defaultDecimal(productionOrder.getQuantity()).compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("quantity must be greater than 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 (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");
            }
        }
    }
    private void fillFromSalesLedgerProduct(ProductionOrder productionOrder) {
        if (productionOrder.getSaleLedgerProductId() == null) {
            return;
        }
        // 销售明细是订单来源时,以销售明细为准回填销售台账、产品规格和默认数量。
        SalesLedgerProduct salesLedgerProduct = salesLedgerProductMapper.selectById(productionOrder.getSaleLedgerProductId().longValue());
        if (salesLedgerProduct == null) {
            throw new ServiceException("Sales ledger product not found");
        }
        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");
        }
        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");
        }
        if (productionOrder.getQuantity() == null || productionOrder.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            productionOrder.setQuantity(salesLedgerProduct.getQuantity());
        }
        if (productionOrder.getPlanCompleteTime() == null && productionOrder.getSalesLedgerId() != null) {
            SalesLedger salesLedger = salesLedgerMapper.selectById(productionOrder.getSalesLedgerId());
            if (salesLedger != null && salesLedger.getDeliveryDate() != null) {
                productionOrder.setPlanCompleteTime(salesLedger.getDeliveryDate());
            }
        }
    }
    private void fillFromProductionPlans(ProductionOrder productionOrder) {
        List<Long> planIds = parsePlanIds(productionOrder.getProductionPlanIds());
        if (planIds.isEmpty()) {
            return;
        }
        // 多计划合并转单时,所有计划必须属于同一规格,且只能下发一次。
        List<ProductionPlan> productionPlans = productionPlanMapper.selectBatchIds(planIds);
        if (productionPlans.size() != planIds.size()) {
            throw new ServiceException("Some production plans do not exist");
        }
        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");
        }
        if (Boolean.TRUE.equals(productionPlans.stream().anyMatch(item -> Boolean.TRUE.equals(item.getIssued())))) {
            throw new ServiceException("Selected production plans already issued");
        }
        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");
        }
        if (productionOrder.getQuantity() == null || productionOrder.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            productionOrder.setQuantity(productionPlans.stream()
                    .map(ProductionPlan::getQtyRequired)
                    .reduce(BigDecimal.ZERO, BigDecimal::add));
        }
        if (productionOrder.getPlanCompleteTime() == null) {
            LocalDate planCompleteTime = productionPlans.stream()
                    .map(this::resolvePlanCompleteDate)
                    .filter(Objects::nonNull)
                    .min(Comparator.naturalOrder())
                    .orElse(null);
            productionOrder.setPlanCompleteTime(planCompleteTime);
        }
        productionOrder.setProductionPlanIds(formatPlanIds(planIds));
    }
    private void syncProductionPlanIssueStatus(ProductionOrder oldOrder, ProductionOrder newOrder) {
        // 只处理本次增量变化,避免无关计划被重复写状态。
        Set<Long> oldIds = new LinkedHashSet<>(parsePlanIds(oldOrder == null ? null : oldOrder.getProductionPlanIds()));
        Set<Long> newIds = new LinkedHashSet<>(parsePlanIds(newOrder == null ? null : newOrder.getProductionPlanIds()));
        Set<Long> toRelease = new LinkedHashSet<>(oldIds);
        toRelease.removeAll(newIds);
        Set<Long> toIssue = new LinkedHashSet<>(newIds);
        toIssue.removeAll(oldIds);
        if (!toRelease.isEmpty()) {
            updatePlanIssuedFlag(new ArrayList<>(toRelease), false);
        }
        if (!toIssue.isEmpty()) {
            updatePlanIssuedFlag(new ArrayList<>(toIssue), true);
        }
    }
    private void releaseProductionPlanIssueStatus(ProductionOrder productionOrder) {
        if (productionOrder == null) {
            return;
        }
        List<Long> planIds = parsePlanIds(productionOrder.getProductionPlanIds());
        if (!planIds.isEmpty()) {
            updatePlanIssuedFlag(planIds, false);
        }
    }
    private void updatePlanIssuedFlag(List<Long> planIds, boolean issued) {
        if (planIds == null || planIds.isEmpty()) {
            return;
        }
        List<ProductionPlan> plans = productionPlanMapper.selectBatchIds(planIds);
        for (ProductionPlan plan : plans) {
            ProductionPlan update = new ProductionPlan();
            update.setId(plan.getId());
            update.setIssued(issued);
            productionPlanMapper.updateById(update);
        }
    }
    private void upsertOrderPick(ProductionOrder productionOrder) {
        if (productionOrder == null || productionOrder.getId() == null) {
            return;
        }
        // 订单下达后自动生成一张备料主单,后续领料记录都挂在这张单上。
        ProductionOrderPick orderPick = productionOrderPickMapper.selectOne(
                Wrappers.<ProductionOrderPick>lambdaQuery()
                        .eq(ProductionOrderPick::getProductionOrderId, productionOrder.getId())
                        .last("limit 1"));
        if (orderPick == null) {
            orderPick = new ProductionOrderPick();
            orderPick.setProductionOrderId(productionOrder.getId());
        }
        orderPick.setProductModelId(productionOrder.getProductModelId() == null ? null : Math.toIntExact(productionOrder.getProductModelId()));
        orderPick.setQuantity(defaultDecimal(productionOrder.getQuantity()));
        orderPick.setRemark("下单自动生成");
        if (orderPick.getId() == null) {
            productionOrderPickMapper.insert(orderPick);
        } else {
            productionOrderPickMapper.updateById(orderPick);
        }
    }
    private BigDecimal resolveBomDemandQuantity(TechnologyBomStructure source, BigDecimal orderQuantity) {
        // 工艺 BOM 中的需求量按“单件需求 * 订单数量”展开成订单级需求。
        BigDecimal baseQuantity = source.getDemandedQuantity() != null ? source.getDemandedQuantity() : source.getUnitQuantity();
        baseQuantity = baseQuantity == null ? BigDecimal.ZERO : baseQuantity;
        if (baseQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        return baseQuantity.multiply(orderQuantity);
    }
    private List<Long> parsePlanIds(String productionPlanIds) {
        if (productionPlanIds == null || productionPlanIds.trim().isEmpty()) {
            return new ArrayList<>();
        }
        String normalized = productionPlanIds.replace("[", "").replace("]", "").trim();
        if (normalized.isEmpty()) {
            return new ArrayList<>();
        }
        return java.util.Arrays.stream(normalized.split(","))
                .map(String::trim)
                .filter(item -> !item.isEmpty())
                .map(Long::valueOf)
                .distinct()
                .collect(Collectors.toList());
    }
    private String formatPlanIds(List<Long> planIds) {
        if (planIds == null || planIds.isEmpty()) {
            return null;
        }
        return planIds.stream()
                .distinct()
                .map(String::valueOf)
                .collect(Collectors.joining(",", "[", "]"));
    }
    private LocalDate resolvePlanCompleteDate(ProductionPlan productionPlan) {
        if (productionPlan == null) {
            return null;
        }
        if (productionPlan.getPromisedDeliveryDate() != null) {
            return productionPlan.getPromisedDeliveryDate();
        }
        if (productionPlan.getRequiredDate() != null) {
            return productionPlan.getRequiredDate();
        }
        return null;
    }
    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;
    }
}