zss
6 天以前 5ad54547dc97b76dc3d689b9499dec7364968235
Merge remote-tracking branch 'origin/dev_New_pro' into dev_New_pro
已修改20个文件
754 ■■■■ 文件已修改
src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionPlanController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionPlanService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionAccountServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickRecordServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java 182 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationParamServiceImpl.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java 314 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductInputServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductOutputServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/SalesLedgerProductionAccountingServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyBomServiceImpl.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyOperationParamServiceImpl.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyParamServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
@@ -465,21 +465,37 @@
                    {
                        val = Convert.toBigDecimal(val);
                    }
                    else if (Date.class == fieldType)
                    {
                        if (val instanceof String)
                        {
                            val = DateUtils.parseDate(val);
                    else if (Date.class == fieldType)
                    {
                        if (val instanceof String)
                        {
                            val = DateUtils.parseDate(val);
                        }
                        else if (val instanceof Double)
                        {
                            val = DateUtil.getJavaDate((Double) val);
                        }
                    }
                    else if (Boolean.TYPE == fieldType || Boolean.class == fieldType)
                    {
                        val = Convert.toBool(val, false);
                    }
                        {
                            val = DateUtil.getJavaDate((Double) val);
                        }
                    }
                    else if (LocalDate.class == fieldType)
                    {
                        if (val instanceof String)
                        {
                            Date date = DateUtils.parseDate(val);
                            val = StringUtils.isNull(date) ? null : DateUtils.toLocalDate(date);
                        }
                        else if (val instanceof Date)
                        {
                            val = DateUtils.toLocalDate((Date) val);
                        }
                        else if (val instanceof Double)
                        {
                            val = DateUtils.toLocalDate(DateUtil.getJavaDate((Double) val));
                        }
                    }
                    else if (Boolean.TYPE == fieldType || Boolean.class == fieldType)
                    {
                        val = Convert.toBool(val, false);
                    }
                    if (StringUtils.isNotNull(fieldType))
                    {
                        String propertyName = field.getName();
@@ -651,15 +667,24 @@
                        val = Convert.toFloat(val);
                    } else if (BigDecimal.class == fieldType) {
                        val = Convert.toBigDecimal(val);
                    } else if (Date.class == fieldType) {
                        if (val instanceof String) {
                            val = DateUtils.parseDate(val);
                        } else if (val instanceof Double) {
                            val = DateUtil.getJavaDate((Double) val);
                        }
                    } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) {
                        val = Convert.toBool(val, false);
                    }
                    } else if (Date.class == fieldType) {
                        if (val instanceof String) {
                            val = DateUtils.parseDate(val);
                        } else if (val instanceof Double) {
                            val = DateUtil.getJavaDate((Double) val);
                        }
                    } else if (LocalDate.class == fieldType) {
                        if (val instanceof String) {
                            Date date = DateUtils.parseDate(val);
                            val = StringUtils.isNull(date) ? null : DateUtils.toLocalDate(date);
                        } else if (val instanceof Date) {
                            val = DateUtils.toLocalDate((Date) val);
                        } else if (val instanceof Double) {
                            val = DateUtils.toLocalDate(DateUtil.getJavaDate((Double) val));
                        }
                    } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) {
                        val = Convert.toBool(val, false);
                    }
                    if (StringUtils.isNotNull(fieldType)) {
                        String propertyName = field.getName();
src/main/java/com/ruoyi/production/controller/ProductionPlanController.java
@@ -14,6 +14,7 @@
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@@ -85,10 +86,10 @@
        excelUtil.importTemplateExcel(response, "主生产计划导入模板");
    }
    @PostMapping("/import")
    @PostMapping(value = "/import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @Operation(summary = "主生产计划数据导入")
    @Log(title = "主生产计划数据导入", businessType = BusinessType.IMPORT)
    public R importProdData(@RequestParam("file") MultipartFile file) {
    public R importProdData(@RequestPart("file") MultipartFile file) {
        productionPlanService.importProdData(file);
        return R.ok("导入成功");
    }
@@ -96,8 +97,8 @@
    @PostMapping("/export")
    @Operation(summary = "主生产计划数据导出")
    @Log(title = "主生产计划数据导出", businessType = BusinessType.EXPORT)
    public void exportProdData(HttpServletResponse response, @RequestBody(required = false) List<Long> ids) {
        productionPlanService.exportProdData(response, ids);
    public void exportProdData(HttpServletResponse response, @RequestBody(required = false) ProductionPlanDto requestDto) {
        productionPlanService.exportProdData(response, requestDto);
    }
}
src/main/java/com/ruoyi/production/service/ProductionPlanService.java
@@ -51,6 +51,6 @@
    /**
     * 导出数据
     */
    void exportProdData(HttpServletResponse response, List<Long> ids);
    void exportProdData(HttpServletResponse response, ProductionPlanDto requestDto);
}
src/main/java/com/ruoyi/production/service/impl/ProductionAccountServiceImpl.java
@@ -24,16 +24,19 @@
    @Override
    public IPage<ProductionAccountVo> listPage(Page<ProductionAccountDto> page, ProductionAccountDto dto) {
        // 分页查询生产核算数据
        ProductionAccountDto queryDto = normalizeDateQuery(dto);
        return baseMapper.listPage(page, queryDto);
    }
    @Override
    public IPage<ProductionProductMainDto> listProductionDetails(ProductionAccountDto dto, Page page) {
        // 查询生产核算明细
        return productionProductMainMapper.listProductionDetails(normalizeDateQuery(dto), page);
    }
    private ProductionAccountDto normalizeDateQuery(ProductionAccountDto dto) {
        // 规范日期查询范围,补齐缺失的开始或结束时间
        if (dto == null) {
            return new ProductionAccountDto();
        }
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java
@@ -33,6 +33,7 @@
     */
    @Override
    public List<ProductionBomStructureVo> listByBomId(Long bomId) {
        // 按BOMID查询生产结构数据
        List<ProductionBomStructureVo> list = productionBomStructureMapper.listByBomId(bomId);
        Map<Long, ProductionBomStructureVo> map = new HashMap<>();
        for (ProductionBomStructureVo node : list) {
@@ -58,13 +59,17 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean addProductionBomStructure(ProductionBomStructureDto dto) {
        // 新增生产BOM结构
        // 读取当前订单BOM主键,并把前端树结构拍平成列表
        Long orderBomId = dto.getProductionOrderBomId();
        List<ProductionBomStructureDto> flatDtoList = new ArrayList<>();
        flattenTree(dto.getChildren(), flatDtoList);
        // 查询数据库已有结构,用于后续做增删改对比
        List<ProductionBomStructure> dbList = this.list(new LambdaQueryWrapper<ProductionBomStructure>()
                .eq(ProductionBomStructure::getProductionOrderBomId, orderBomId));
        // 收集前端仍然存在的节点ID
        Set<Long> frontendIds = new HashSet<>();
        for (ProductionBomStructureDto item : flatDtoList) {
            if (item.getId() != null) {
@@ -72,16 +77,19 @@
            }
        }
        // 计算需要删除的节点(数据库有、前端已删除)
        Set<Long> deleteIds = new HashSet<>();
        for (ProductionBomStructure dbItem : dbList) {
            if (!frontendIds.contains(dbItem.getId())) {
                deleteIds.add(dbItem.getId());
            }
        }
        // 先删掉前端已经移除的节点
        if (!deleteIds.isEmpty()) {
            this.removeByIds(deleteIds);
        }
        // 按是否有ID拆分为新增和更新,同时缓存新增节点的临时ID映射
        List<ProductionBomStructure> insertList = new ArrayList<>();
        List<ProductionBomStructure> updateList = new ArrayList<>();
        Map<String, ProductionBomStructure> tempEntityMap = new HashMap<>();
@@ -99,10 +107,12 @@
            }
        }
        // 批量新增,拿到数据库生成的真实ID
        if (!insertList.isEmpty()) {
            this.saveBatch(insertList);
        }
        // 新增节点二次回写父ID(前端传的是临时父ID)
        List<ProductionBomStructure> parentFixList = new ArrayList<>();
        for (ProductionBomStructureDto item : flatDtoList) {
            if (item.getId() == null && item.getParentTempId() != null) {
@@ -111,15 +121,18 @@
                    continue;
                }
                ProductionBomStructure parent = tempEntityMap.get(item.getParentTempId());
                // 父节点是本次新增时,直接用新增后的真实ID;否则回退为前端传入父ID
                Long realParentId = parent != null ? parent.getId() : Long.valueOf(item.getParentTempId());
                child.setParentId(realParentId);
                parentFixList.add(child);
            }
        }
        // 回写新增节点的父子关系
        if (!parentFixList.isEmpty()) {
            this.updateBatchById(parentFixList);
        }
        // 批量更新已有节点
        if (!updateList.isEmpty()) {
            this.updateBatchById(updateList);
        }
@@ -130,6 +143,7 @@
     * 将树形结构拍平成列表,便于统一保存。
     */
    private void flattenTree(List<ProductionBomStructureDto> source, List<ProductionBomStructureDto> result) {
        // 扁平化处理树
        if (source == null) {
            return;
        }
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
@@ -58,6 +58,7 @@
    @Override
    public IPage<ProductionOperationTaskVo> pageProductionOperationTask(Page<ProductionOperationTaskDto> page, ProductionOperationTaskDto dto) {
        // 分页查询生产工序任务
        Page<ProductionOperationTaskVo> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        IPage<ProductionOperationTaskVo> result = baseMapper.pageProductionOperationTask(voPage, dto);
        fillUserNames(result.getRecords());
@@ -66,6 +67,7 @@
    @Override
    public List<ProductionOperationTaskVo> listProductionOperationTask(ProductionOperationTaskDto dto) {
        // 查询工序任务列表
        List<ProductionOperationTaskVo> result = BeanUtil.copyToList(this.list(buildQueryWrapper(dto)), ProductionOperationTaskVo.class);
        fillUserNames(result);
        return result;
@@ -73,6 +75,7 @@
    @Override
    public ProductionOperationTaskVo getProductionOperationTaskInfo(Long id) {
        // 获取生产工序任务详情
        ProductionOperationTask item = this.getById(id);
        if (item == null) {
            return null;
@@ -90,21 +93,25 @@
    @Override
    public boolean saveProductionOperationTask(ProductionOperationTask productionOperationTask) {
        // 保存生产工序任务
        return this.saveOrUpdate(productionOperationTask);
    }
    @Override
    public boolean removeProductionOperationTask(List<Long> ids) {
        // 删除生产工序任务
        return ids != null && !ids.isEmpty() && this.removeByIds(ids);
    }
    @Override
    public int updateProductWorkOrder(ProductionOperationTaskDto dto) {
        // 更新工序任务对应的工单信息
        return baseMapper.updateById(dto);
    }
    @Override
    public boolean assign(ProductionOperationTaskDto dto) {
        // 分配工序任务执行人
        if (dto == null || dto.getId() == null) {
            throw new ServiceException("工单ID不能为空");
        }
@@ -120,6 +127,7 @@
    }
    private LambdaQueryWrapper<ProductionOperationTask> buildQueryWrapper(ProductionOperationTaskDto dto) {
        // 按条件动态构建数据库查询条件
        ProductionOperationTask query = dto == null ? new ProductionOperationTask() : dto;
        return Wrappers.<ProductionOperationTask>lambdaQuery()
                .eq(query.getId() != null, ProductionOperationTask::getId, query.getId())
@@ -133,10 +141,12 @@
    }
    private void fillUserNames(List<ProductionOperationTaskVo> voList) {
        // 填充用户名称
        if (voList == null || voList.isEmpty()) {
            return;
        }
        Set<Long> userIdSet = new LinkedHashSet<>();
        // 遍历处理数据并组装结果
        for (ProductionOperationTaskVo vo : voList) {
            if (vo == null) {
                continue;
@@ -172,6 +182,7 @@
    }
    private List<Long> parseUserIdList(String userIds, boolean strict) {
        // 解析并校验用户ID数组字符串
        if (StringUtils.isBlank(userIds)) {
            if (strict) {
                throw new ServiceException("userIds格式不正确,必须为JSON数字数组");
@@ -199,6 +210,7 @@
    @Override
    public void down(HttpServletResponse response, ProductionOperationTaskDto dto) {
        // 导出工序任务数据
        if (dto == null || dto.getId() == null) {
            throw new ServiceException("工单ID不能为空");
        }
@@ -250,15 +262,18 @@
    }
    private List<Map<String, Object>> buildTaskAttachmentImages(Long taskId) {
        // 组装任务附件图片数据用于导出
        List<Map<String, Object>> images = new ArrayList<>();
        StorageAttachmentDTO storageAttachmentDTO = new StorageAttachmentDTO();
        storageAttachmentDTO.setRecordType(RecordTypeEnum.PRODUCTION_OPERATION_TASK.getType());
        storageAttachmentDTO.setRecordId(taskId);
        List<StorageBlobVO> taskWorkOrderFiles =
                fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(storageAttachmentDTO);
        // 参数与前置条件校验
        if (CollectionUtils.isEmpty(taskWorkOrderFiles)) {
            return images;
        }
        // 遍历处理数据并组装结果
        for (StorageBlobVO blobVO : taskWorkOrderFiles) {
            if (blobVO == null) {
                continue;
@@ -286,6 +301,7 @@
    }
    private File resolveImageFile(StorageBlobVO blobVO) {
        // 将附件信息解析为本地图片文件对象
        if (blobVO == null || StringUtils.isBlank(blobVO.getUidFilename())) {
            return null;
        }
@@ -296,6 +312,7 @@
    }
    private PictureType resolvePictureType(StorageBlobVO blobVO) {
        // 按文件名或内容类型识别图片格式
        if (blobVO == null) {
            return null;
        }
@@ -311,6 +328,7 @@
    }
    private PictureType parsePictureTypeByFileName(String fileName) {
        // 根据文件后缀解析图片格式
        if (StringUtils.isBlank(fileName) || !fileName.contains(".")) {
            return null;
        }
@@ -322,6 +340,7 @@
    }
    private PictureType parsePictureTypeByContentType(String contentType) {
        // 根据Content-Type解析图片格式
        if (StringUtils.isBlank(contentType)) {
            return null;
        }
@@ -350,6 +369,7 @@
    @Override
    public List<ProductionOperationTaskVo> getOperation(ProductionOperationTaskDto dto) {
        // 查询工序任务列表
        return baseMapper.getOperation(dto);
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickRecordServiceImpl.java
@@ -24,6 +24,7 @@
    @Override
    public List<ProductionOrderPickRecordVo> listFeedingRecord(ProductionOrderPickRecordDto dto) {
        // 查询投料记录
        if (dto == null || dto.getProductionOrderId() == null || dto.getPickId() == null) {
            return Collections.emptyList();
        }
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
@@ -30,12 +30,8 @@
import java.util.stream.Collectors;
/**
 * <p>
 * 璁㈠崟棰嗘枡绾胯竟浠?鏈嶅姟瀹炵幇绫?
 * </p>
 *
 * @author 鑺杞欢锛堟睙鑻忥級鏈夐檺鍏徃
 * @since 2026-04-21 03:55:52
 * 生产订单领料服务实现。
 * 负责领料新增、更新、补料、退料及库存联动。
 */
@Service
@RequiredArgsConstructor
@@ -53,17 +49,26 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean savePick(ProductionOrderPickDto dto) {
        // 领料新增总流程:
        // 1) 解析前端行数据并逐行合并参数;
        // 2) 校验参数与批次;
        // 3) 先扣减库存,再落库领料主记录;
        // 4) 写入领料流水,记录数量变化轨迹。
        List<ProductionOrderPickDto> pickItems = resolvePickItems(dto);
        // 逐行处理领料数据,行号用于拼装精确的报错信息。
        for (int i = 0; i < pickItems.size(); i++) {
            int rowNo = i + 1;
            ProductionOrderPickDto resolvedDto = mergeDto(dto, pickItems.get(i));
            // 每行都做完整校验,异常信息带行号。
            validatePickParam(resolvedDto, rowNo);
            // 统一处理批次(支持单批次/多批次)。
            List<String> batchNoList = resolveBatchNoList(resolvedDto);
            String inventoryBatchNo = pickInventoryBatchNo(batchNoList);
            String storedBatchNo = formatBatchNoStorage(batchNoList);
            subtractInventory(resolvedDto.getProductModelId(), storedBatchNo, resolvedDto.getPickQuantity(), rowNo);
            // 保存领料主记录快照。
            ProductionOrderPick orderPick = new ProductionOrderPick();
            orderPick.setProductionOrderId(resolvedDto.getProductionOrderId());
            orderPick.setProductModelId(resolvedDto.getProductModelId());
@@ -75,8 +80,10 @@
            orderPick.setDemandedQuantity(resolvedDto.getDemandedQuantity());
            orderPick.setBom(resolvedDto.getBom());
            orderPick.setReturned(false);
            // 新增主记录。
            baseMapper.insert(orderPick);
            // 记录本次领料流水(before=0,after=本次领料量)。
            insertPickRecord(orderPick.getId(),
                    resolvedDto.getProductionOrderId(),
                    resolvedDto.getProductionOperationTaskId(),
@@ -95,8 +102,12 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean updatePick(ProductionOrderPickDto dto) {
        // 领料更新入口(同接口兼容三类业务):
        // 1) 普通领料改量/增删;
        // 2) 补料(pickType=2);
        // 3) 退料(returned=true)。
        if (dto == null) {
            throw new ServiceException("变更参数不能为空");
            throw new ServiceException("参数不能为空");
        }
        Long productionOrderId = resolveProductionOrderId(dto);
        if (productionOrderId == null) {
@@ -107,26 +118,32 @@
            throw new ServiceException("生产订单不存在");
        }
        // 查询订单下现有领料记录并构建ID索引。
        List<ProductionOrderPick> existingPickList = baseMapper.selectList(
                Wrappers.<ProductionOrderPick>lambdaQuery()
                        .eq(ProductionOrderPick::getProductionOrderId, productionOrderId));
        // 转成Map便于后续按ID快速校验与更新。
        Map<Long, ProductionOrderPick> existingPickMap = existingPickList.stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(ProductionOrderPick::getId, Function.identity(), (a, b) -> a));
        // 补料请求单独走补料分支。
        if (isFeedingRequest(dto)) {
            processFeedingPickItems(dto, existingPickMap, productionOrderId);
            return true;
        }
        // 退料请求单独走退料分支。
        if (isReturnRequest(dto)) {
            processReturnPickItems(dto, existingPickMap, productionOrderId);
            return true;
        }
        // 普通更新场景先处理显式删除。
        processDeletePickIds(dto, existingPickMap, productionOrderId);
        List<ProductionOrderPickDto> pickItems = resolveUpdateItems(dto);
        Set<Long> keepPickIdSet = new HashSet<>();
        // keepPickIdSet 用于标记本次前端仍然保留的旧记录,后续用于识别“未回传即删除”的行。
        for (int i = 0; i < pickItems.size(); i++) {
            int rowNo = i + 1;
            ProductionOrderPickDto resolvedDto = mergeDto(dto, pickItems.get(i));
@@ -145,12 +162,14 @@
            keepPickIdSet.add(resolvedDto.getId());
            updateExistingPick(resolvedDto, rowNo, existingPickMap);
        }
        // 清理前端未回传旧行并回补库存。
        processMissingPickItems(dto, existingPickMap, productionOrderId, keepPickIdSet);
        return true;
    }
    @Override
    public List<ProductionOrderPickVo> listPickedDetail(Long productionOrderId) {
        // 查询订单领料明细,并补齐批次展示字段。
        if (productionOrderId == null) {
            return Collections.emptyList();
        }
@@ -163,6 +182,11 @@
    private void processDeletePickIds(ProductionOrderPickDto rootDto,
                                      Map<Long, ProductionOrderPick> existingPickMap,
                                      Long productionOrderId) {
        // 处理前端显式删除ID:
        // 1) 校验删除目标是否属于当前订单;
        // 2) 回补库存;
        // 3) 删除主记录;
        // 4) 记录删除流水。
        if (rootDto.getDeletePickIds() == null || rootDto.getDeletePickIds().isEmpty()) {
            return;
        }
@@ -173,14 +197,14 @@
            }
            ProductionOrderPick existingPick = existingPickMap.get(deleteId);
            if (existingPick == null || !Objects.equals(existingPick.getProductionOrderId(), productionOrderId)) {
                throw new ServiceException("要删除的领料记录不存在或不属于当前订单,ID=" + deleteId);
                throw new ServiceException("删除失败:领料记录不存在或不属于当前订单,ID=" + deleteId);
            }
            String oldBatchNo = resolveInventoryBatchNoFromStored(existingPick.getBatchNo());
            BigDecimal oldQuantity = defaultDecimal(existingPick.getQuantity());
            addInventory(existingPick.getProductModelId(), oldBatchNo, oldQuantity);
            int affected = baseMapper.deleteById(deleteId);
            if (affected <= 0) {
                throw new ServiceException("删除领料失败,ID=" + deleteId);
                throw new ServiceException("删除领料记录失败,ID=" + deleteId);
            }
            insertPickRecord(existingPick.getId(),
                    existingPick.getProductionOrderId(),
@@ -201,6 +225,9 @@
                                         Map<Long, ProductionOrderPick> existingPickMap,
                                         Long productionOrderId,
                                         Set<Long> keepPickIdSet) {
        // 处理“前端未回传”的旧行:
        // 对应场景是用户在前端删除行但未放入 deletePickIds。
        // 这里兜底识别并执行回补库存 + 删除主记录 + 写流水。
        if (rootDto.getPickList() == null) {
            return;
        }
@@ -216,7 +243,7 @@
            addInventory(missingPick.getProductModelId(), oldBatchNo, oldQuantity);
            int affected = baseMapper.deleteById(missingPick.getId());
            if (affected <= 0) {
                throw new ServiceException("删除领料失败,ID=" + missingPick.getId());
                throw new ServiceException("删除未回传领料记录失败,ID=" + missingPick.getId());
            }
            insertPickRecord(missingPick.getId(),
                    missingPick.getProductionOrderId(),
@@ -234,6 +261,7 @@
    }
    private void addNewPickInUpdate(ProductionOrderPickDto dto, int rowNo) {
        // 更新场景下新增一条领料:扣库存 -> 新增主记录 -> 写流水。
        List<String> batchNoList = resolveBatchNoList(dto);
        String inventoryBatchNo = pickInventoryBatchNo(batchNoList);
        String storedBatchNo = formatBatchNoStorage(batchNoList);
@@ -268,6 +296,8 @@
    private void processFeedingPickItems(ProductionOrderPickDto rootDto,
                                         Map<Long, ProductionOrderPick> existingPickMap,
                                         Long productionOrderId) {
        // 补料流程入口:
        // 逐行校验补料参数,校验原领料归属,再执行补料库存扣减和主记录回写。
        List<ProductionOrderPickDto> pickItems = resolveUpdateItems(rootDto);
        for (int i = 0; i < pickItems.size(); i++) {
            int rowNo = i + 1;
@@ -276,7 +306,7 @@
                continue;
            }
            if (!isFeedingPick(resolvedDto)) {
                throw new ServiceException("补料请求中的领料类型必须全部为2");
                throw new ServiceException("补料请求中存在非补料类型数据");
            }
            if (resolvedDto.getProductionOrderId() == null) {
                resolvedDto.setProductionOrderId(productionOrderId);
@@ -285,15 +315,20 @@
            ProductionOrderPick oldPick = existingPickMap.get(resolvedDto.getId());
            if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), productionOrderId)) {
                throw new ServiceException("第" + rowNo + "条领料记录不存在或不属于当前订单");
                throw new ServiceException("第" + rowNo + "行补料失败:未找到对应的领料记录");
            }
            addFeedingPick(resolvedDto, oldPick, rowNo);
        }
    }
    private void addFeedingPick(ProductionOrderPickDto dto, ProductionOrderPick oldPick, int rowNo) {
        // 补料核心:
        // 1) 校验规格一致;
        // 2) 扣减补料库存;
        // 3) 写补料流水;
        // 4) 回写主单累计补料量和实际量。
        if (dto.getProductModelId() != null && !Objects.equals(dto.getProductModelId(), oldPick.getProductModelId())) {
            throw new ServiceException("第" + rowNo + "条补料产品规格与领料记录不一致");
            throw new ServiceException("第" + rowNo + "行补料失败:产品规格与原领料记录不一致");
        }
        Long productModelId = oldPick.getProductModelId();
        List<String> batchNoList = resolveBatchNoList(dto);
@@ -304,6 +339,7 @@
        subtractInventory(productModelId, inventoryBatchNo, feedingQuantity, rowNo);
        // 计算补料前后数量并写补料流水。
        BigDecimal beforeFeedingQty = sumFeedingQuantity(dto.getProductionOrderId(), oldPick.getId());
        BigDecimal afterFeedingQty = beforeFeedingQty.add(feedingQuantity);
        insertPickRecord(oldPick.getId(),
@@ -322,9 +358,10 @@
        updatePick.setId(oldPick.getId());
        updatePick.setFeedingQty(afterFeedingQty);
        updatePick.setActualQty(calculateActualQty(oldPick, afterFeedingQty));
        // 回写主记录的补料累计值与实际用量。
        int affected = baseMapper.updateById(updatePick);
        if (affected <= 0) {
            throw new ServiceException("第" + rowNo + "条补料总量更新失败");
            throw new ServiceException("第" + rowNo + "行补料失败:更新领料主记录失败");
        }
        oldPick.setFeedingQty(afterFeedingQty);
        oldPick.setActualQty(updatePick.getActualQty());
@@ -333,6 +370,8 @@
    private void processReturnPickItems(ProductionOrderPickDto rootDto,
                                        Map<Long, ProductionOrderPick> existingPickMap,
                                        Long productionOrderId) {
        // 退料流程入口:
        // 逐行校验退料参数与领料归属,再更新退料量与实际量字段。
        List<ProductionOrderPickDto> pickItems = resolveUpdateItems(rootDto);
        for (int i = 0; i < pickItems.size(); i++) {
            int rowNo = i + 1;
@@ -347,13 +386,14 @@
            ProductionOrderPick oldPick = existingPickMap.get(resolvedDto.getId());
            if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), productionOrderId)) {
                throw new ServiceException("第" + rowNo + "条领料记录不存在或不属于当前订单");
                throw new ServiceException("第" + rowNo + "行退料失败:未找到对应的领料记录");
            }
            updateReturnPick(resolvedDto, oldPick, rowNo);
        }
    }
    private void updateReturnPick(ProductionOrderPickDto dto, ProductionOrderPick oldPick, int rowNo) {
        // 退料更新只改主领料记录中的退料字段与实际量。
        ProductionOrderPick updatePick = new ProductionOrderPick();
        updatePick.setId(oldPick.getId());
        updatePick.setReturnQty(dto.getReturnQty());
@@ -361,7 +401,7 @@
        updatePick.setReturned(true);
        int affected = baseMapper.updateById(updatePick);
        if (affected <= 0) {
            throw new ServiceException("第" + rowNo + "条退料信息更新失败");
            throw new ServiceException("第" + rowNo + "行退料失败:更新领料主记录失败");
        }
        oldPick.setReturnQty(updatePick.getReturnQty());
        oldPick.setActualQty(updatePick.getActualQty());
@@ -371,9 +411,14 @@
    private void updateExistingPick(ProductionOrderPickDto dto,
                                    int rowNo,
                                    Map<Long, ProductionOrderPick> existingPickMap) {
        // 普通更新单行核心流程:
        // 1) 校验旧记录存在且属于当前订单;
        // 2) 比较新旧“规格+批次”,决定库存处理策略;
        // 3) 更新主记录;
        // 4) 写变更流水(记录前后数量变化)。
        ProductionOrderPick oldPick = existingPickMap.get(dto.getId());
        if (oldPick == null || !Objects.equals(oldPick.getProductionOrderId(), dto.getProductionOrderId())) {
            throw new ServiceException("第" + rowNo + "条领料记录不存在或不属于当前订单");
            throw new ServiceException("第" + rowNo + "行更新失败:未找到对应的领料记录");
        }
        Long oldProductModelId = oldPick.getProductModelId();
@@ -386,9 +431,11 @@
        String newStoredBatchNo = formatBatchNoStorage(newBatchNoList);
        BigDecimal newQuantity = dto.getPickQuantity();
        // 判断规格+批次是否变化,决定库存处理策略。
        boolean sameStockKey = Objects.equals(oldProductModelId, newProductModelId)
                && Objects.equals(oldBatchNo, newBatchNo);
        if (sameStockKey) {
            // 规格与批次不变:只按差值增减库存。
            BigDecimal delta = newQuantity.subtract(oldQuantity);
            if (delta.compareTo(BigDecimal.ZERO) > 0) {
                subtractInventory(newProductModelId, newStoredBatchNo, delta, rowNo);
@@ -396,6 +443,7 @@
                addInventory(oldProductModelId, oldBatchNo, delta.abs());
            }
        } else {
            // 规格或批次变化:先回补旧库存,再扣减新库存。
            addInventory(oldProductModelId, oldBatchNo, oldQuantity);
            subtractInventory(newProductModelId, newStoredBatchNo, newQuantity, rowNo);
        }
@@ -414,9 +462,10 @@
        }
        int affected = baseMapper.updateById(oldPick);
        if (affected <= 0) {
            throw new ServiceException("第" + rowNo + "条领料更新失败");
            throw new ServiceException("第" + rowNo + "行更新失败:更新领料记录失败");
        }
        // 写入更新流水,保留本次数量变化轨迹。
        BigDecimal recordQuantity = sameStockKey ? oldQuantity.subtract(newQuantity).abs() : newQuantity;
        if (recordQuantity.compareTo(BigDecimal.ZERO) > 0 || oldQuantity.compareTo(newQuantity) != 0 || !sameStockKey) {
            insertPickRecord(oldPick.getId(),
@@ -444,6 +493,7 @@
                                  Byte pickType,
                                  String remark,
                                  String feedingReason) {
        // 写领料流水记录:统一记录领料/补料/退料数量变化轨迹。
        ProductionOrderPickRecord pickRecord = new ProductionOrderPickRecord();
        pickRecord.setPickId(pickId);
        pickRecord.setProductionOrderId(productionOrderId);
@@ -460,7 +510,13 @@
    }
    private void subtractInventory(Long productModelId, String batchNo, BigDecimal quantity, int rowNo) {
        // 扣减库存总流程:
        // 1) 解析批次列表;
        // 2) 计算每个批次可用量与总可用量;
        // 3) 按批次顺序逐笔扣减,直到扣完目标数量;
        // 4) 任一步失败即抛错并回滚事务。
        BigDecimal deductQuantity = defaultDecimal(quantity);
        // 领料数量小于等于0时,不需要执行库存扣减。
        if (deductQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
@@ -470,9 +526,12 @@
            batchNoList = Collections.singletonList(null);
        }
        // 先计算各批次可用量,避免边扣边算导致判断不一致。
        Map<String, BigDecimal> availableQuantityMap = new LinkedHashMap<>();
        BigDecimal totalAvailableQuantity = BigDecimal.ZERO;
        // 遍历批次,计算每个批次可用库存。
        for (String currentBatchNo : batchNoList) {
            // 查询当前规格+批次的库存记录。
            StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, currentBatchNo));
            BigDecimal availableQuantity = BigDecimal.ZERO;
            if (stockInventory != null) {
@@ -488,10 +547,11 @@
        if (deductQuantity.compareTo(totalAvailableQuantity) > 0) {
            BigDecimal shortQuantity = deductQuantity.subtract(totalAvailableQuantity);
            throw new ServiceException("领料可用库存不足,可用库存为" + formatQuantity(totalAvailableQuantity)
                    + ",还差" + formatQuantity(shortQuantity));
            throw new ServiceException("第" + rowNo + "行扣减库存失败:可用库存不足,当前可用"
                    + formatQuantity(totalAvailableQuantity) + ",仍缺少" + formatQuantity(shortQuantity));
        }
        // 按批次顺序逐笔扣减库存。
        BigDecimal remainingQuantity = deductQuantity;
        for (Map.Entry<String, BigDecimal> entry : availableQuantityMap.entrySet()) {
            if (remainingQuantity.compareTo(BigDecimal.ZERO) <= 0) {
@@ -508,17 +568,18 @@
            stockInventoryDto.setQualitity(currentDeductQuantity);
            int affected = stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
            if (affected <= 0) {
                throw new ServiceException("第" + rowNo + "条领料扣减库存失败");
                throw new ServiceException("第" + rowNo + "行扣减库存失败:库存更新失败");
            }
            remainingQuantity = remainingQuantity.subtract(currentDeductQuantity);
        }
        if (remainingQuantity.compareTo(BigDecimal.ZERO) > 0) {
            throw new ServiceException("第" + rowNo + "条领料扣减库存失败,剩余待扣减数量为" + formatQuantity(remainingQuantity));
            throw new ServiceException("第" + rowNo + "行扣减库存失败:仍有未扣减数量" + formatQuantity(remainingQuantity));
        }
    }
    private void addInventory(Long productModelId, String batchNo, BigDecimal quantity) {
        // 回补库存(用于删除领料、改小领料、切换批次等场景)。
        BigDecimal addQuantity = defaultDecimal(quantity);
        if (addQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return;
@@ -533,8 +594,9 @@
    }
    private List<ProductionOrderPickDto> resolvePickItems(ProductionOrderPickDto dto) {
        // 解析新增场景的领料明细集合。
        if (dto == null) {
            throw new ServiceException("领料参数不能为空");
            throw new ServiceException("参数不能为空");
        }
        if (dto.getPickList() != null && !dto.getPickList().isEmpty()) {
            return dto.getPickList();
@@ -543,6 +605,7 @@
    }
    private List<ProductionOrderPickDto> resolveUpdateItems(ProductionOrderPickDto dto) {
        // 解析更新场景的领料明细集合。
        if (dto.getPickList() != null) {
            return dto.getPickList();
        }
@@ -553,6 +616,7 @@
    }
    private boolean isEmptyUpdateItem(ProductionOrderPickDto dto) {
        // 判断更新行是否为空白占位行。
        return dto.getId() == null
                && dto.getProductModelId() == null
                && dto.getPickQuantity() == null
@@ -573,6 +637,7 @@
    }
    private Long resolveProductionOrderId(ProductionOrderPickDto dto) {
        // 优先从主DTO解析订单ID,不存在时再从子项中回退查找。
        if (dto.getProductionOrderId() != null) {
            return dto.getProductionOrderId();
        }
@@ -588,7 +653,12 @@
    }
    private ProductionOrderPickDto mergeDto(ProductionOrderPickDto rootDto, ProductionOrderPickDto itemDto) {
        // 合并规则:
        // - itemDto 优先承载行级输入;
        // - itemDto 缺失字段从 rootDto 兜底继承;
        // - 输出 merged 作为统一业务入参。
        ProductionOrderPickDto merged = new ProductionOrderPickDto();
        // 先拷贝行级字段。
        if (itemDto != null) {
            merged.setId(itemDto.getId());
            merged.setProductionOrderId(itemDto.getProductionOrderId());
@@ -667,51 +737,55 @@
    }
    private void validatePickParam(ProductionOrderPickDto dto, int rowNo) {
        // 校验普通领料参数(订单、规格、数量、类型)。
        if (dto.getProductionOrderId() == null) {
            throw new ServiceException("第" + rowNo + "条生产订单ID不能为空");
            throw new ServiceException("第" + rowNo + "行参数错误:生产订单ID不能为空");
        }
        if (dto.getProductModelId() == null) {
            throw new ServiceException("第" + rowNo + "条产品规格ID不能为空");
            throw new ServiceException("第" + rowNo + "行参数错误:产品规格不能为空");
        }
        if (dto.getPickQuantity() == null || dto.getPickQuantity().compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("第" + rowNo + "条领料数量不能小于0");
            throw new ServiceException("第" + rowNo + "行参数错误:领料数量不能小于0");
        }
        if (dto.getPickType() != null && dto.getPickType() != PICK_TYPE_NORMAL && dto.getPickType() != PICK_TYPE_FEEDING) {
            throw new ServiceException("第" + rowNo + "条领料类型只能是1或2");
            throw new ServiceException("第" + rowNo + "行参数错误:领料类型仅支持1(领料)或2(补料)");
        }
    }
    private void validateFeedingParam(ProductionOrderPickDto dto, int rowNo) {
        // 校验补料参数(订单、领料ID、补料数量、类型)。
        if (dto.getProductionOrderId() == null) {
            throw new ServiceException("第" + rowNo + "条生产订单ID不能为空");
            throw new ServiceException("第" + rowNo + "行参数错误:生产订单ID不能为空");
        }
        if (dto.getId() == null) {
            throw new ServiceException("第" + rowNo + "条领料ID不能为空");
            throw new ServiceException("第" + rowNo + "行参数错误:领料记录ID不能为空");
        }
        if (dto.getFeedingQuantity() == null || dto.getFeedingQuantity().compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("第" + rowNo + "条本次补料数量不能小于0");
            throw new ServiceException("第" + rowNo + "行参数错误:补料数量不能小于0");
        }
        if (!isFeedingPick(dto)) {
            throw new ServiceException("第" + rowNo + "条补料类型必须为2");
            throw new ServiceException("第" + rowNo + "行参数错误:补料场景下领料类型必须为2");
        }
    }
    private void validateReturnParam(ProductionOrderPickDto dto, int rowNo) {
        // 校验退料参数(订单、领料ID、退料量、实际量)。
        if (dto.getProductionOrderId() == null) {
            throw new ServiceException("第" + rowNo + "条生产订单ID不能为空");
            throw new ServiceException("第" + rowNo + "行参数错误:生产订单ID不能为空");
        }
        if (dto.getId() == null) {
            throw new ServiceException("第" + rowNo + "条领料ID不能为空");
            throw new ServiceException("第" + rowNo + "行参数错误:领料记录ID不能为空");
        }
        if (dto.getReturnQty() == null || dto.getReturnQty().compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("第" + rowNo + "条退料数量不能为空且不能小于0");
            throw new ServiceException("第" + rowNo + "行参数错误:退料数量不能小于0");
        }
        if (dto.getActualQty() == null || dto.getActualQty().compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("第" + rowNo + "条实际数量不能为空且不能小于0");
            throw new ServiceException("第" + rowNo + "行参数错误:实际数量不能小于0");
        }
    }
    private boolean isFeedingRequest(ProductionOrderPickDto dto) {
        // 判断当前请求是否属于补料流程。
        if (isFeedingPick(dto)) {
            return true;
        }
@@ -724,10 +798,12 @@
    }
    private boolean isFeedingPick(ProductionOrderPickDto dto) {
        // 判断当前行是否为补料类型。
        return dto != null && Objects.equals(dto.getPickType(), PICK_TYPE_FEEDING);
    }
    private boolean isReturnRequest(ProductionOrderPickDto dto) {
        // 判断当前请求是否属于退料流程。
        if (isReturnPick(dto)) {
            return true;
        }
@@ -740,10 +816,12 @@
    }
    private boolean isReturnPick(ProductionOrderPickDto dto) {
        // 判断当前行是否为退料类型。
        return dto != null && Boolean.TRUE.equals(dto.getReturned());
    }
    private BigDecimal sumFeedingQuantity(Long productionOrderId, Long pickId) {
        // 汇总指定领料单的历史补料总量。
        List<ProductionOrderPickRecord> feedingRecords = productionOrderPickRecordMapper.selectList(
                Wrappers.<ProductionOrderPickRecord>lambdaQuery()
                        .eq(ProductionOrderPickRecord::getProductionOrderId, productionOrderId)
@@ -756,12 +834,14 @@
    }
    private BigDecimal calculateActualQty(ProductionOrderPick pick, BigDecimal feedingQty) {
        // 按“领料+补料-退料”计算实际用量。
        return defaultDecimal(pick.getQuantity())
                .add(defaultDecimal(feedingQty))
                .subtract(defaultDecimal(pick.getReturnQty()));
    }
    private String normalizeBatchNo(String batchNo) {
        // 标准化批次号(去空白、空串转null)。
        if (StringUtils.isEmpty(batchNo)) {
            return null;
        }
@@ -769,6 +849,7 @@
        return trimBatchNo.isEmpty() ? null : trimBatchNo;
    }
    private List<String> resolveBatchNoList(ProductionOrderPickDto dto) {
        // 优先解析 batchNoList,空则回退解析 batchNo 字符串。
        List<String> normalizedBatchNoList = normalizeBatchNoList(dto.getBatchNoList());
        if (!normalizedBatchNoList.isEmpty()) {
            return normalizedBatchNoList;
@@ -777,6 +858,7 @@
    }
    private String pickInventoryBatchNo(List<String> batchNoList) {
        // 从批次集合中取库存扣减使用的批次。
        if (batchNoList == null || batchNoList.isEmpty()) {
            return null;
        }
@@ -784,10 +866,12 @@
    }
    private String resolveInventoryBatchNoFromStored(String storedBatchNo) {
        // 从数据库存储批次字段中反解可用批次。
        return pickInventoryBatchNo(parseBatchNoValue(storedBatchNo));
    }
    private String formatBatchNoStorage(List<String> batchNoList) {
        // 将批次集合格式化为数据库存储值。
        if (batchNoList == null || batchNoList.isEmpty()) {
            return null;
        }
@@ -798,6 +882,7 @@
    }
    private List<String> normalizeBatchNoList(List<String> batchNoList) {
        // 批量标准化批次号并去重。
        if (batchNoList == null || batchNoList.isEmpty()) {
            return Collections.emptyList();
        }
@@ -812,6 +897,7 @@
    }
    private void fillBatchNoList(List<ProductionOrderPickVo> detailList) {
        // 将同订单+同规格+同工序的数据按组聚合批次,便于前端统一展示。
        if (detailList == null || detailList.isEmpty()) {
            return;
        }
@@ -832,9 +918,11 @@
    }
    private void fillSelectableBatchNoList(List<ProductionOrderPickVo> detailList) {
        // 合并“已选批次”和“库存可选批次”,用于前端下拉。
        if (detailList == null || detailList.isEmpty()) {
            return;
        }
        // 先收集明细中涉及的规格ID,批量查询库存批次。
        Set<Long> productModelIdSet = detailList.stream()
                .map(ProductionOrderPickVo::getProductModelId)
                .filter(Objects::nonNull)
@@ -868,30 +956,35 @@
    }
    private String buildBatchNoGroupKey(ProductionOrderPickVo detail) {
        return String.valueOf(detail.getProductionOrderId()) + "|"
                + String.valueOf(detail.getProductModelId()) + "|"
                + String.valueOf(detail.getTechnologyOperationId()) + "|"
                + String.valueOf(detail.getOperationName());
        // 构建批次聚合分组键。
        return detail.getProductionOrderId() + "|"
                + detail.getProductModelId() + "|"
                + detail.getTechnologyOperationId() + "|"
                + detail.getOperationName();
    }
    private List<String> parseBatchNoValue(String rawBatchNoValue) {
        // 批次解析兼容三种格式:
        // 1) 单值:A001
        // 2) 逗号分隔:A001,A002
        // 3) 类JSON数组字符串:["A001","A002"]
        String normalizedValue = normalizeBatchNo(rawBatchNoValue);
        if (StringUtils.isEmpty(normalizedValue)) {
            return Collections.emptyList();
        }
        if (normalizedValue.startsWith("[") && normalizedValue.endsWith("]")) {
        if (normalizedValue != null && normalizedValue.startsWith("[") && normalizedValue.endsWith("]")) {
            String value = normalizedValue.substring(1, normalizedValue.length() - 1);
            if (StringUtils.isEmpty(value)) {
                return Collections.emptyList();
            }
            List<String> parsed = Arrays.stream(value.split(","))
                    .map(item -> item == null ? null : item.trim().replace("\"", "").replace("'", ""))
                    .map(item -> item.trim().replace("\"", "").replace("'", ""))
                    .collect(Collectors.toList());
            return normalizeBatchNoList(parsed);
        }
        if (normalizedValue.contains(",")) {
        if (normalizedValue != null && normalizedValue.contains(",")) {
            List<String> parsed = Arrays.stream(normalizedValue.split(","))
                    .map(item -> item == null ? null : item.trim())
                    .map(item -> item.trim())
                    .collect(Collectors.toList());
            return normalizeBatchNoList(parsed);
        }
@@ -899,6 +992,7 @@
    }
    private LambdaQueryWrapper<StockInventory> buildStockWrapper(Long productModelId, String batchNo) {
        // 构建库存查询条件(规格 + 批次)。
        LambdaQueryWrapper<StockInventory> wrapper = Wrappers.<StockInventory>lambdaQuery()
                .eq(StockInventory::getProductModelId, productModelId);
        if (StringUtils.isEmpty(batchNo)) {
@@ -910,10 +1004,12 @@
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        // BigDecimal 空值兜底,统一按0处理。
        return value == null ? BigDecimal.ZERO : value;
    }
    private String formatQuantity(BigDecimal value) {
        // 数量格式化输出(去除末尾无效0)。
        return defaultDecimal(value).stripTrailingZeros().toPlainString();
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationParamServiceImpl.java
@@ -48,17 +48,20 @@
    @Override
    public List<ProductionOrderRoutingOperationParamVo> listProductionOrderRoutingOperationParam(ProductionOrderRoutingOperationParamDto dto) {
        // 查询生产订单工艺路线工序参数
        return BeanUtil.copyToList(this.list(buildQueryWrapper(dto)), ProductionOrderRoutingOperationParamVo.class);
    }
    @Override
    public ProductionOrderRoutingOperationParamVo getProductionOrderRoutingOperationParamInfo(Long id) {
        // 获取生产订单工艺路线工序参数详情
        ProductionOrderRoutingOperationParam item = this.getById(id);
        return item == null ? null : BeanUtil.copyProperties(item, ProductionOrderRoutingOperationParamVo.class);
    }
    @Override
    public boolean saveProductionOrderRoutingOperationParam(ProductionOrderRoutingOperationParam item) {
        // 保存生产订单工艺路线工序参数
        ProductionOrderRoutingOperation routingOperation = getRoutingOperation(item.getProductionOrderRoutingOperationId());
        fillFromSourceParam(item, routingOperation);
        validateManualFields(item);
@@ -68,10 +71,12 @@
    @Override
    public boolean removeProductionOrderRoutingOperationParam(Long id) {
        // 删除生产订单工艺路线工序参数
        return this.removeById(id);
    }
    private LambdaQueryWrapper<ProductionOrderRoutingOperationParam> buildQueryWrapper(ProductionOrderRoutingOperationParamDto dto) {
        // 按条件动态构建数据库查询条件
        ProductionOrderRoutingOperationParam query = dto == null ? new ProductionOrderRoutingOperationParam() : dto;
        return Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                .eq(query.getId() != null, ProductionOrderRoutingOperationParam::getId, query.getId())
@@ -96,29 +101,31 @@
    }
    private ProductionOrderRoutingOperation getRoutingOperation(Long productionOrderRoutingOperationId) {
        // 获取工艺路线工序
        if (productionOrderRoutingOperationId == null) {
            throw new ServiceException("productionOrderRoutingOperationId is required");
            throw new ServiceException("生产订单工艺路线工序ID不能为空");
        }
        ProductionOrderRoutingOperation routingOperation = productionOrderRoutingOperationMapper.selectById(productionOrderRoutingOperationId);
        if (routingOperation == null) {
            throw new ServiceException("Production order routing operation not found");
            throw new ServiceException("生产订单工艺路线工序不存在");
        }
        return routingOperation;
    }
    private void fillFromSourceParam(ProductionOrderRoutingOperationParam item, ProductionOrderRoutingOperation routingOperation) {
        // 从来源参数回填当前参数默认值
        item.setProductionOrderId(routingOperation.getProductionOrderId());
        item.setProductionOrderRoutingOperationId(routingOperation.getId());
        ProductionOrder productionOrder = productionOrderMapper.selectById(routingOperation.getProductionOrderId());
        if (productionOrder == null) {
            throw new ServiceException("Production order not found");
            throw new ServiceException("生产订单不存在");
        }
        if (item.getParamId() == null) {
            return;
        }
        TechnologyParam sourceParam = technologyParamMapper.selectById(item.getParamId());
        if (sourceParam == null) {
            throw new ServiceException("Technology  param not found");
            throw new ServiceException("工艺参数不存在");
        }
        if (item.getTechnologyOperationParamId() != null) {
            TechnologyRoutingOperationParam sourceRoutingOperationParam = technologyRoutingOperationParamMapper.selectById(item.getTechnologyOperationParamId());
@@ -141,15 +148,17 @@
    }
    private void validateManualFields(ProductionOrderRoutingOperationParam item) {
        // 校验手工录入字段的必填与格式
        if (item.getParamCode() == null || item.getParamCode().trim().isEmpty()) {
            throw new ServiceException("paramCode is required");
            throw new ServiceException("参数编码不能为空");
        }
        if (item.getParamName() == null || item.getParamName().trim().isEmpty()) {
            throw new ServiceException("paramName is required");
            throw new ServiceException("参数名称不能为空");
        }
    }
    private void checkDuplicate(ProductionOrderRoutingOperationParam item) {
        // 检查数据是否重复,避免重复保存
        boolean duplicate = productionOrderRoutingOperationParamMapper.selectCount(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .isNull(ProductionOrderRoutingOperationParam::getProductionProductMainId)
@@ -161,7 +170,7 @@
                        .ne(item.getId() != null, ProductionOrderRoutingOperationParam::getId, item.getId())
        ) > 0;
        if (duplicate) {
            throw new ServiceException("Duplicate production order routing operation param");
            throw new ServiceException("生产订单工艺路线工序参数重复");
        }
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java
@@ -45,12 +45,15 @@
    @Override
    public R addRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation) {
        // 新增工艺路线
        int insert = productionOrderRoutingOperationMapper.insert(productionOrderRoutingOperation);
        //工序关联的参数需要同步新增
        List<TechnologyOperationParam> technologyOperationParams = technologyOperationParamMapper.selectList(Wrappers.<TechnologyOperationParam>lambdaQuery()
                .eq(TechnologyOperationParam::getTechnologyOperationId, productionOrderRoutingOperation.getTechnologyOperationId()));
        // 参数与前置条件校验
        if (CollectionUtils.isNotEmpty(technologyOperationParams)){
            ArrayList<ProductionOrderRoutingOperationParam> productionOrderRoutingOperationParams = new ArrayList<>();
        // 遍历处理数据并组装结果
            for (TechnologyOperationParam technologyOperationParam : technologyOperationParams) {
                TechnologyParam technologyParam = technologyParamMapper.selectById(technologyOperationParam.getTechnologyParamId());
                ProductionOrderRoutingOperationParam productionOrderRoutingOperationParam = new ProductionOrderRoutingOperationParam();
@@ -103,11 +106,14 @@
    @Override
    public R deleteRouteItem(Long id) {
        // 删除工艺路线
        try {
        // 查询并准备业务数据
            ProductionOperationTask productionOperationTask = productionOperationTaskMapper.selectOne(
                    new LambdaQueryWrapper<ProductionOperationTask>()
                            .eq(ProductionOperationTask::getProductionOrderRoutingOperationId, id)
                            .last("limit 1"));
        // 参数与前置条件校验
            if (productionOperationTask == null) {
                throw new RuntimeException("删除失败:未找到关联的生产工单");
            }
@@ -118,6 +124,7 @@
            List<ProductionProductMain> productionProductMains = productionProductMainMapper.selectList(
                    new LambdaQueryWrapper<ProductionProductMain>()
                            .eq(ProductionProductMain::getProductionOperationTaskId, productionOperationTask.getId()));
        // 遍历处理数据并组装结果
            for (ProductionProductMain main : productionProductMains) {
                productionProductMainService.removeProductMain(main.getId());
            }
@@ -140,6 +147,7 @@
                    ProductionOrderRoutingOperation item = operationList.get(i);
                    if (!Integer.valueOf(i + 1).equals(item.getDragSort())) {
                        item.setDragSort(i + 1);
        // 持久化或输出处理结果
                        productionOrderRoutingOperationMapper.updateById(item);
                    }
                }
@@ -152,6 +160,7 @@
    @Override
    public int sortRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation) {
        // 排序工艺路线
        ProductionOrderRoutingOperation oldItem = productionOrderRoutingOperationMapper.selectById(productionOrderRoutingOperation.getId());
        List<ProductionOrderRoutingOperation> operationList = productionOrderRoutingOperationMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperation>lambdaQuery()
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingServiceImpl.java
@@ -21,6 +21,7 @@
    @Override
    public ProductionOrderRouting listMain(Long orderId) {
        // 查询主表ID集合
        return productionOrderRoutingMapper.selectOne(
                Wrappers.<ProductionOrderRouting>lambdaQuery()
                        .eq(ProductionOrderRouting::getProductionOrderId, orderId)
@@ -30,6 +31,7 @@
    @Override
    public List<ProductionOrderRoutingOperationVo> listItem(Long orderId) {
        // 查询工艺路线工序明细
        return productionOrderRoutingOperationMapper.selectVoListByOrderId(orderId);
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -82,6 +82,7 @@
    @Override
    public IPage<ProductionOrderVo> pageProductionOrder(Page<ProductionOrderDto> page, ProductionOrderDto dto) {
        // 分页查询生产订单
        Page<ProductionOrderVo> result = (Page<ProductionOrderVo>) baseMapper.pageProductionOrder(page, dto);
        fillProductImages(result.getRecords());
        return result;
@@ -89,6 +90,7 @@
    @Override
    public List<ProductionOrderVo> listProductionOrder(ProductionOrderDto dto) {
        // 查询生产订单列表
        List<ProductionOrderVo> records = baseMapper.listProductionOrder(dto);
        fillProductImages(records);
        return records;
@@ -96,6 +98,7 @@
    @Override
    public ProductionOrderVo getProductionOrderInfo(Long id) {
        // 获取生产订单详情
        ProductionOrderVo item = baseMapper.getProductionOrderInfo(id);
        if (item == null) {
            return null;
@@ -106,6 +109,7 @@
    @Override
    public boolean saveProductionOrder(ProductionOrder productionOrder) {
        // 保存生产订单
        ProductionOrder oldOrder = productionOrder.getId() == null ? null : this.getById(productionOrder.getId());
        // 下单入口统一补齐来源单据、计划和工艺信息,避免前端分别传多套字段。
        validateAndFillOrder(productionOrder, oldOrder);
@@ -137,6 +141,7 @@
    @Override
    public boolean removeProductionOrder(List<Long> ids) {
        // 删除生产订单
        if (ids == null || ids.isEmpty()) {
            return false;
        }
@@ -150,6 +155,7 @@
    @Override
    public Integer bindingRoute(ProductionOrderDto productionOrderDto) {
        // 为订单绑定工艺路线
        if (productionOrderDto == null || productionOrderDto.getId() == null) {
            throw new ServiceException("生产订单ID不能为空");
        }
@@ -182,6 +188,7 @@
            ProductionOrder update = new ProductionOrder();
            update.setId(productionOrder.getId());
            update.setTechnologyRoutingId(targetRoutingId);
        // 持久化或输出处理结果
            if (!this.updateById(update)) {
                throw new ServiceException("绑定工艺路线失败");
            }
@@ -193,6 +200,7 @@
    @Override
    public List<ProductionPlanVo> getSource(Long id) {
        // 查询订单关联来源计划
        ProductionOrder productionOrder = baseMapper.selectById(id);
        if (productionOrder != null && productionOrder.getProductionPlanIds() != null) {
            List<Long> planIds = parsePlanIds(productionOrder.getProductionPlanIds());
@@ -203,7 +211,9 @@
    @Override
    public int syncProductionOrderSnapshot(Long productionOrderId) {
        // 同步订单工艺、工序、参数和BOM快照
        ProductionOrder productionOrder = this.getById(productionOrderId);
        // 参数与前置条件校验
        if (productionOrder == null) {
            throw new ServiceException("生产订单不存在");
        }
@@ -226,15 +236,18 @@
        orderRouting.setDescription(technologyRouting.getDescription());
        orderRouting.setBomId(technologyRouting.getBomId());
        orderRouting.setOrderBomId(orderBom == null ? null : orderBom.getId());
        // 持久化或输出处理结果
        productionOrderRoutingMapper.insert(orderRouting);
        int syncedParamCount = 0;
        // 查询并准备业务数据
        List<TechnologyRoutingOperation> routingOperations = technologyRoutingOperationMapper.selectList(
                Wrappers.<TechnologyRoutingOperation>lambdaQuery()
                        .eq(TechnologyRoutingOperation::getTechnologyRoutingId, technologyRouting.getId())
                        .orderByDesc(TechnologyRoutingOperation::getDragSort)
                        .orderByDesc(TechnologyRoutingOperation::getId));
        Map<Long, String> operationNameMap = technologyOperationMapper.selectBatchIds(
        // 遍历处理数据并组装结果
                        routingOperations.stream()
                                .map(TechnologyRoutingOperation::getTechnologyOperationId)
                                .filter(Objects::nonNull)
@@ -301,6 +314,7 @@
    }
    private ProductionOrderBom syncProductionOrderBomSnapshot(ProductionOrder productionOrder, TechnologyRouting technologyRouting) {
        // 同步订单BOM快照结构
        if (technologyRouting.getBomId() == null) {
            return null;
        }
@@ -308,10 +322,12 @@
        if (technologyBom == null) {
            throw new ServiceException("工艺BOM不存在");
        }
        // 查询并准备业务数据
        List<TechnologyBomStructure> structureList = technologyBomStructureMapper.selectList(
                Wrappers.<TechnologyBomStructure>lambdaQuery()
                        .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());
@@ -322,6 +338,7 @@
        orderBom.setRemark(technologyBom.getRemark());
        orderBom.setBomNo(technologyBom.getBomNo());
        orderBom.setVersion(technologyBom.getVersion());
        // 持久化或输出处理结果
        productionOrderBomMapper.insert(orderBom);
        Map<Long, Long> idMap = new HashMap<>();
@@ -343,16 +360,19 @@
    }
    private void clearProductionSnapshot(Long productionOrderId) {
        // 已产生领料记录后禁止重建,避免备料/投料依据与订单快照脱节。
        // 清理订单已生成的工艺与BOM快照数据
        boolean hasPickRecord = productionOrderPickRecordMapper.selectCount(
        // 查询并准备业务数据
                Wrappers.<ProductionOrderPickRecord>lambdaQuery()
                        .eq(ProductionOrderPickRecord::getProductionOrderId, productionOrderId)) > 0;
        // 参数与前置条件校验
        if (hasPickRecord) {
            throw new ServiceException("生产订单已存在领料记录,不能重新生成快照");
        }
        List<Long> taskIds = productionOperationTaskMapper.selectList(
                        Wrappers.<ProductionOperationTask>lambdaQuery()
                                .eq(ProductionOperationTask::getProductionOrderId, productionOrderId))
        // 遍历处理数据并组装结果
                .stream().map(ProductionOperationTask::getId).collect(Collectors.toList());
        if (!taskIds.isEmpty()) {
            // 已有报工记录说明订单已开工,此时不允许再重建快照。
@@ -380,6 +400,7 @@
    }
    private LambdaQueryWrapper<ProductionOrder> buildQueryWrapper(ProductionOrderDto dto) {
        // 按条件动态构建数据库查询条件
        ProductionOrder query = dto == null ? new ProductionOrder() : dto;
        return Wrappers.<ProductionOrder>lambdaQuery()
                .eq(query.getId() != null, ProductionOrder::getId, query.getId())
@@ -390,6 +411,7 @@
    }
    private String generateNextOrderNo() {
        // 生成下一个生产订单号
        String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String prefix = "SC" + datePrefix;
        ProductionOrder latestOrder = this.getOne(Wrappers.<ProductionOrder>lambdaQuery()
@@ -408,6 +430,7 @@
    }
    private String generateNextTaskNo() {
        // 生成下一个生产工单号
        String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String prefix = "GD" + datePrefix;
        ProductionOperationTask lastTask = productionOperationTaskMapper.selectOne(
@@ -427,10 +450,12 @@
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        // 将空数量兜底为0,避免空指针异常
        return value == null ? BigDecimal.ZERO : value;
    }
    private void validateAndFillOrder(ProductionOrder productionOrder, ProductionOrder oldOrder) {
        // 校验订单参数并补齐默认值
        if (productionOrder == null) {
            throw new ServiceException("生产订单不能为空");
        }
@@ -463,7 +488,9 @@
    }
    private void fillFromProductionPlans(ProductionOrder productionOrder) {
        // 从关联生产计划回填订单关键字段
        List<Long> planIds = parsePlanIds(productionOrder.getProductionPlanIds());
        // 参数与前置条件校验
        if (planIds.isEmpty()) {
            return;
        }
@@ -472,6 +499,7 @@
        if (productionPlans.size() != planIds.size()) {
            throw new ServiceException("部分生产计划不存在");
        }
        // 遍历处理数据并组装结果
        Map<Long, ProductionPlan> planMap = productionPlans.stream()
                .collect(Collectors.toMap(ProductionPlan::getId, item -> item, (left, right) -> left));
        ProductionPlan mainPlan = planMap.get(planIds.get(0));
@@ -510,6 +538,7 @@
    }
    private void releaseProductionPlanIssueStatus(ProductionOrder productionOrder) {
        // 回退生产计划下发状态
        if (productionOrder == null) {
            return;
        }
@@ -522,6 +551,7 @@
    //生产订单删除,生产计划的已下发数量对应变更
    private void updatePlanIssuedFlag(List<Long> planIds, BigDecimal remainingAssignedQuantity) {
        // 更新计划下发标记和下发数量
        if (planIds == null || planIds.isEmpty()) {
            return;
        }
@@ -551,6 +581,7 @@
    }
    private BigDecimal resolveRemainingQuantity(ProductionPlan plan) {
        // 计算当前计划或记录的剩余数量
        if (plan == null) {
            return BigDecimal.ZERO;
        }
@@ -569,6 +600,7 @@
    }
    private int resolvePlanStatus(BigDecimal requiredQuantity, BigDecimal issuedQuantity) {
        // 根据需求量和下发量推导计划状态
        if (requiredQuantity == null || requiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return 0;
        }
@@ -579,6 +611,7 @@
    }
    private List<Long> parsePlanIds(String productionPlanIds) {
        // 将计划ID字符串解析为Long列表
        if (productionPlanIds == null || productionPlanIds.trim().isEmpty()) {
            return new ArrayList<>();
        }
@@ -595,6 +628,7 @@
    }
    private String formatPlanIds(List<Long> planIds) {
        // 将计划ID集合格式化为[1,2,3]字符串
        if (planIds == null || planIds.isEmpty()) {
            return null;
        }
@@ -605,6 +639,7 @@
    }
    private LocalDate resolvePlanCompleteDate(ProductionPlan productionPlan) {
        // 解析计划完成日期
        if (productionPlan == null) {
            return null;
        }
@@ -618,13 +653,16 @@
    }
    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)
@@ -634,6 +672,7 @@
            return;
        }
        // 查询并准备业务数据
        List<StorageAttachment> attachments = storageAttachmentMapper.selectList(
                Wrappers.<StorageAttachment>lambdaQuery()
                        .in(StorageAttachment::getRecordId, productModelIds)
@@ -676,6 +715,7 @@
    }
    private StorageBlobVO toStorageBlobVO(StorageBlob blob) {
        // 将存储文件对象转换为VO
        StorageBlobVO vo = BeanUtil.copyProperties(blob, StorageBlobVO.class);
        vo.setPreviewURL(fileUtil.buildSignedPreviewUrl(vo));
        vo.setDownloadURL(fileUtil.buildSignedDownloadUrl(vo));
@@ -684,8 +724,10 @@
    @Override
    public ProductionOrderWorkOrderDetailVo getWorkOrderReportInspectDetail(ProductionOrderDto dto) {
        // 获取工单订单报工质检明细
        Long productionOrderId = resolveProductionOrderId(dto);
        ProductionOrderVo orderInfo = getProductionOrderInfo(productionOrderId);
        // 参数与前置条件校验
        if (orderInfo == null) {
            throw new ServiceException("生产订单不存在");
        }
@@ -699,6 +741,7 @@
                new Page<ProductionOperationTaskVo>(1, -1), taskQuery);
        List<ProductionOperationTaskVo> workOrderList = workOrderPage == null || workOrderPage.getRecords() == null
                ? Collections.emptyList()
        // 遍历处理数据并组装结果
                : workOrderPage.getRecords().stream()
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(ProductionOperationTaskVo::getId, Comparator.nullsLast(Comparator.naturalOrder())))
@@ -714,6 +757,7 @@
                .collect(Collectors.toList());
        List<ProductionProductMain> reportMainList = workOrderIdList.isEmpty()
                ? Collections.emptyList()
        // 查询并准备业务数据
                : productionProductMainMapper.selectList(
                Wrappers.<ProductionProductMain>lambdaQuery()
                        .in(ProductionProductMain::getProductionOperationTaskId, workOrderIdList)
@@ -851,6 +895,7 @@
    }
    private Long resolveProductionOrderId(ProductionOrderDto dto) {
        // 从入参中解析生产订单ID并校验
        if (dto == null) {
            throw new ServiceException("请传入生产订单ID或生产订单号");
        }
@@ -872,10 +917,12 @@
    @Override
    public List<ProductionOrderPickVo> pick(Long productionOrderId) {
        // 查询订单领料、投料与退料明细
        if (productionOrderId == null) {
            return Collections.emptyList();
        }
        // 查询并准备业务数据
        ProductionOrderBom orderBom = productionOrderBomMapper.selectOne(
                Wrappers.<ProductionOrderBom>lambdaQuery()
                        .eq(ProductionOrderBom::getProductionOrderId, productionOrderId)
@@ -890,6 +937,7 @@
            return Collections.emptyList();
        }
        // 遍历处理数据并组装结果
        List<Long> productModelIds = bomStructureList.stream()
                .map(ProductionBomStructureVo::getProductModelId)
                .filter(Objects::nonNull)
@@ -946,6 +994,7 @@
    @Override
    public int updateOrder(ProductionOrderDto productionOrderDto) {
        // 更新生产订单主数据
        productionOrderDto.setStatus(5);
        return baseMapper.updateById(productionOrderDto);
    }
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java
@@ -6,6 +6,10 @@
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.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.StringUtils;
@@ -44,27 +48,32 @@
    private final ProductionPlanMapper productionPlanMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOrderService productionOrderService;
    private final ProductModelMapper productModelMapper;
    private final ProductMapper productMapper;
    @Override
    public IPage<ProductionPlanVo> listPage(Page<ProductionPlanDto> page, ProductionPlanDto productionPlanDto) {
        // 分页查询主生产计划列表
        return productionPlanMapper.listPage(page, productionPlanDto);
    }
    /**
     * 合并生产计划并下发生产订单。
     * 业务约束:
     * 约束:
     * 1. 仅允许同一产品型号的计划合并;
     * 2. 已下发或部分下发的计划禁止再次合并;
     * 3. 下发数量不能大于所选计划需求总量;
     * 4. 订单创建统一复用 ProductionOrderService.saveProductionOrder,确保工艺/BOM/领料主单等后续逻辑一致。
     * 2. 已下发或部分下发的计划不允许再次合并;
     * 3. 下发数量不能大于所选计划剩余需求总量;
     * 4. 下发时统一调用 ProductionOrderService.saveProductionOrder,确保后续工艺/BOM/领料逻辑一致。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean combine(ProductionPlanDto productionPlanDto) {
        // 基础入参校验:没有可下发计划则直接返回 false
        if (productionPlanDto == null || productionPlanDto.getIds() == null || productionPlanDto.getIds().isEmpty()) {
            return false;
        }
        // 去空、去重,得到本次参与合并的计划 ID
        List<Long> planIds = productionPlanDto.getIds().stream()
                .filter(Objects::nonNull)
                .distinct()
@@ -73,43 +82,51 @@
            throw new ServiceException("下发失败,未选择生产计划");
        }
        // 查询并校验计划是否都存在
        List<ProductionPlanDto> planLists = productionPlanMapper.selectWithMaterialByIds(planIds);
        if (planLists == null || planLists.isEmpty() || planLists.size() != planIds.size()) {
            throw new ServiceException("下发失败,生产计划不存在或已被删除");
        }
        // 以第一条计划作为型号基准
        ProductionPlanDto firstPlan = planLists.getFirst();
        if (firstPlan.getProductModelId() == null) {
            throw new ServiceException("下发失败,生产计划缺少产品型号");
        }
        // 仅允许同型号计划合并下发
        boolean hasDifferentModel = planLists.stream()
                .anyMatch(item -> !Objects.equals(item.getProductModelId(), firstPlan.getProductModelId()));
        if (hasDifferentModel) {
            throw new BaseException("合并失败,所选生产计划的产品型号不一致");
        }
        // 已下发或部分下发的计划不允许再次合并
        boolean hasIssuedPlan = planLists.stream()
                .anyMatch(item -> item.getStatus() != null && item.getStatus() == PLAN_STATUS_ISSUED);
                .anyMatch(item -> item.getStatus() != null
                        && (item.getStatus() == PLAN_STATUS_PARTIAL || item.getStatus() == PLAN_STATUS_ISSUED));
        if (hasIssuedPlan) {
            throw new BaseException("合并失败,所选生产计划存在已下发或部分下发数据");
            throw new BaseException("合并失败,所选生产计划存在已下发或部分下发的数据");
        }
        // 计算本次可下发的剩余需求总量
        BigDecimal totalRequiredQuantity = planLists.stream()
                .map(this::resolveRemainingQuantity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        if (totalRequiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("下发失败,所选生产计划需求总量必须大于0");
            throw new ServiceException("下发失败,所选生产计划剩余需求总量必须大于0");
        }
        // 校验下发数量
        BigDecimal assignedQuantity = productionPlanDto.getTotalAssignedQuantity();
        if (assignedQuantity == null || assignedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("下发失败,下发数量必须大于0");
        }
        if (assignedQuantity.compareTo(totalRequiredQuantity) > 0) {
            throw new ServiceException("下发失败,下发数量不能大于计划需求总量");
            throw new ServiceException("下发失败,下发数量不能大于计划剩余需求总量");
        }
        // 按计划顺序分摊下发数量,收集实际参与下发的计划 ID
        BigDecimal remainingForOrderBind = assignedQuantity;
        List<Long> issuedPlanIds = new ArrayList<>();
        for (ProductionPlanDto plan : planLists) {
@@ -124,21 +141,20 @@
            }
        }
        if (issuedPlanIds.isEmpty()) {
            throw new ServiceException("Issue failed, no quantity available for dispatch");
            throw new ServiceException("下发失败,无可下发数量");
        }
        // 生成生产订单主单,并绑定本次下发关联的计划
        ProductionOrder productionOrder = new ProductionOrder();
        productionOrder.setProductionPlanIds(formatPlanIds(issuedPlanIds));
        productionOrder.setProductModelId(firstPlan.getProductModelId());
        productionOrder.setQuantity(assignedQuantity);
        productionOrder.setPlanCompleteTime(productionPlanDto.getPlanCompleteTime());
        boolean saved = productionOrderService.saveProductionOrder(productionOrder);
        if (!saved) {
        if (!productionOrderService.saveProductionOrder(productionOrder)) {
            throw new ServiceException("下发失败,生产订单保存失败");
        }
        //已下发数量
        // 回写每条计划的累计下发量和状态
        BigDecimal remainingAssignedQuantity = assignedQuantity;
        List<ProductionPlan> updates = new ArrayList<>();
        for (ProductionPlanDto plan : planLists) {
@@ -163,6 +179,7 @@
            updates.add(update);
        }
        if (!updates.isEmpty()) {
            // 批量更新计划状态与数量
            this.updateBatchById(updates);
        }
        return true;
@@ -171,17 +188,24 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean add(ProductionPlanDto dto) {
        // 新增主生产计划
        if (StringUtils.isBlank(dto.getMpsNo())) {
            dto.setMpsNo(generateNextPlanNo(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))));
        }else checkMpsNoUnique(dto.getMpsNo(), null);
            String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
            dto.setMpsNo(buildPlanNo(datePrefix, resolveNextPlanSequence(datePrefix)));
        } else {
            checkMpsNoUnique(dto.getMpsNo(), null);
        }
        dto.setStatus(PLAN_STATUS_WAIT);
        dto.setSource("内部");
        if (StringUtils.isBlank(dto.getSource())) {
            dto.setSource("内部");
        }
        return productionPlanMapper.insert(dto) > 0;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean update(ProductionPlanDto dto) {
        // 更新主生产计划
        if (dto == null || dto.getId() == null) {
            throw new ServiceException("编辑失败,数据不能为空");
        }
@@ -202,20 +226,10 @@
        return productionPlanMapper.updateById(dto) > 0;
    }
    private void checkMpsNoUnique(String mpsNo, Long excludeId) {
        LambdaQueryWrapper<ProductionPlan> wrapper = Wrappers.lambdaQuery();
        wrapper.eq(ProductionPlan::getMpsNo, mpsNo);
        if (excludeId != null) {
            wrapper.ne(ProductionPlan::getId, excludeId);
        }
        if (productionPlanMapper.selectCount(wrapper) > 0) {
            throw new ServiceException("生产计划号 " + mpsNo + " 已存在");
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean delete(List<Long> ids) {
        // 删除主生产计划
        if (productionPlanMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery().in(ProductionPlan::getId, ids))
                .stream()
                .anyMatch(p -> p.getStatus() == PLAN_STATUS_PARTIAL || p.getStatus() == PLAN_STATUS_ISSUED)) {
@@ -234,6 +248,7 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void importProdData(MultipartFile file) {
        // 参数与前置条件校验
        if (file == null || file.isEmpty()) {
            throw new ServiceException("导入数据不能为空");
        }
@@ -247,104 +262,251 @@
        if (list == null || list.isEmpty()) {
            throw new ServiceException("Excel没有数据");
        }
        String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        int nextSequence = resolveNextPlanSequence(datePrefix);
        Set<String> mpsNos = new HashSet<>();
        // 遍历处理数据并组装结果
        for (int i = 0; i < list.size(); i++) {
            ProductionPlanImportDto dto = list.get(i);
            String mpsNo = dto.getMpsNo();
            String mpsNo = StringUtils.trim(dto.getMpsNo());
            if (StringUtils.isEmpty(mpsNo)) {
                generateNextPlanNo(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")));
                mpsNo = buildPlanNo(datePrefix, nextSequence++);
            }
            dto.setMpsNo(mpsNo);
            if (!mpsNos.add(mpsNo)) {
                throw new ServiceException("导入失败:Excel 中存在重复的申请单编号 " + mpsNo);
                throw new ServiceException("导入失败,Excel中存在重复的主生产计划号 " + mpsNo);
            }
            if (dto.getQtyRequired() == null || dto.getQtyRequired().compareTo(BigDecimal.ZERO) <= 0) {
                throw new ServiceException("导入失败:第" + (i + 2) + "行需求数量必须大于0");
            }
        }
        Long existApplyNoCount = baseMapper.selectCount(Wrappers.<ProductionPlan>lambdaQuery()
                .in(ProductionPlan::getMpsNo, mpsNos));
        // 查询并准备业务数据
        Long existApplyNoCount = baseMapper.selectCount(Wrappers.<ProductionPlan>lambdaQuery().in(ProductionPlan::getMpsNo, mpsNos));
        if (existApplyNoCount > 0) {
            List<String> existMpsNos = baseMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery()
                            .in(ProductionPlan::getMpsNo, mpsNos))
            List<String> existMpsNos = baseMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery().in(ProductionPlan::getMpsNo, mpsNos))
                    .stream()
                    .map(ProductionPlan::getMpsNo)
                    .collect(Collectors.toList());
            throw new ServiceException("导入失败,生产计划号已存在: " + String.join(", ", existMpsNos));
            throw new ServiceException("导入失败,主生产计划号已存在: " + String.join(", ", existMpsNos));
        }
        List<ProductModel> allModels = productModelMapper.selectList(Wrappers.<ProductModel>lambdaQuery());
        Set<Long> productIds = allModels.stream()
                .map(ProductModel::getProductId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, String> productNameById = productIds.isEmpty()
                ? Collections.emptyMap()
                : productMapper.selectBatchIds(productIds).stream()
                .collect(Collectors.toMap(Product::getId, Product::getProductName, (a, b) -> a));
        LocalDateTime now = LocalDateTime.now();
        List<ProductionPlan> entityList = list.stream().map(dto -> {
        List<ProductionPlan> entityList = new ArrayList<>();
        for (int i = 0; i < list.size(); i++) {
            ProductionPlanImportDto dto = list.get(i);
            ProductionPlan entity = new ProductionPlan();
            BeanUtils.copyProperties(dto, entity);
            entity.setProductModelId(resolveProductModelId(dto, i + 2, allModels, productNameById));
            entity.setStatus(PLAN_STATUS_WAIT);
            entity.setSource("内部");
            entity.setSource(StringUtils.isNotEmpty(dto.getSource()) ? StringUtils.trim(dto.getSource()) : "内部");
            entity.setQuantityIssued(BigDecimal.ZERO);
            entity.setCreateTime(now);
            entity.setUpdateTime(now);
            return entity;
        }).collect(Collectors.toList());
        this.saveBatch(entityList);
            entityList.add(entity);
        }
        // 持久化或输出处理结果
        if (!this.saveBatch(entityList)) {
            throw new ServiceException("导入失败,保存生产计划数据失败");
        }
    }
    @Override
    public void exportProdData(HttpServletResponse response, List<Long> ids) {
        List<ProductionPlanDto> list = productionPlanMapper.selectWithMaterialByIds(ids);
    public void exportProdData(HttpServletResponse response, ProductionPlanDto requestDto) {
        // 导出主生产计划数据
        List<Long> ids = requestDto == null || requestDto.getIds() == null
                ? Collections.emptyList()
                : requestDto.getIds().stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
        List<ProductionPlanImportDto> exportList = new ArrayList<>();
        for (ProductionPlanDto entity : list) {
            ProductionPlanImportDto dto = new ProductionPlanImportDto();
            BeanUtils.copyProperties(entity, dto);
            exportList.add(dto);
        if (!ids.isEmpty()) {
            List<ProductionPlanDto> list = productionPlanMapper.selectWithMaterialByIds(ids);
            for (ProductionPlanDto item : list) {
                ProductionPlanImportDto dto = new ProductionPlanImportDto();
                BeanUtils.copyProperties(item, dto);
                dto.setAssignedQuantity(item.getQuantityIssued());
                exportList.add(dto);
            }
        } else {
            ProductionPlanDto query = new ProductionPlanDto();
            if (requestDto != null) {
                BeanUtils.copyProperties(requestDto, query);
            }
            IPage<ProductionPlanVo> page = productionPlanMapper.listPage(new Page<>(1, -1), query);
            if (page != null && page.getRecords() != null) {
                for (ProductionPlanVo item : page.getRecords()) {
                    ProductionPlanImportDto dto = new ProductionPlanImportDto();
                    BeanUtils.copyProperties(item, dto);
                    dto.setAssignedQuantity(item.getQuantityIssued());
                    exportList.add(dto);
                }
            }
        }
        ExcelUtil<ProductionPlanImportDto> util = new ExcelUtil<>(ProductionPlanImportDto.class);
        util.exportExcel(response, exportList, "主生产计划");
    }
    private BigDecimal resolveRemainingQuantity(ProductionPlan plan) {
        if (plan == null) {
            return BigDecimal.ZERO;
    /**
     * 校验主生产计划号唯一性,可通过 excludeId 排除当前记录。
     */
    private void checkMpsNoUnique(String mpsNo, Long excludeId) {
        // 按主生产计划号查询重复记录
        LambdaQueryWrapper<ProductionPlan> wrapper = Wrappers.lambdaQuery();
        wrapper.eq(ProductionPlan::getMpsNo, mpsNo);
        if (excludeId != null) {
            // 更新时排除当前记录本身
            wrapper.ne(ProductionPlan::getId, excludeId);
        }
        BigDecimal requiredQuantity = Optional.ofNullable(plan.getQtyRequired()).orElse(BigDecimal.ZERO);
        if (requiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        if (productionPlanMapper.selectCount(wrapper) > 0) {
            // 存在重复计划号,直接拦截
            throw new ServiceException("生产计划号 " + mpsNo + " 已存在");
        }
        BigDecimal issuedQuantity = Optional.ofNullable(plan.getQuantityIssued()).orElse(BigDecimal.ZERO);
        if (issuedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return requiredQuantity;
        }
        if (issuedQuantity.compareTo(requiredQuantity) >= 0) {
            return BigDecimal.ZERO;
        }
        return requiredQuantity.subtract(issuedQuantity);
    }
    private int resolvePlanStatus(BigDecimal requiredQuantity, BigDecimal issuedQuantity) {
        if (requiredQuantity == null || requiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return PLAN_STATUS_WAIT;
    /**
     * 根据导入行的型号、产品名称、单位定位唯一的产品型号 ID。
     */
    private Long resolveProductModelId(ProductionPlanImportDto dto, int rowNo, List<ProductModel> allModels,
                                       Map<Long, String> productNameById) {
        // 先按规格型号做第一轮过滤
        String model = StringUtils.trim(dto.getModel());
        if (StringUtils.isEmpty(model)) {
            throw new ServiceException("导入失败:第" + rowNo + "行规格型号不能为空");
        }
        if (issuedQuantity == null || issuedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return PLAN_STATUS_WAIT;
        List<ProductModel> candidates = allModels.stream()
                .filter(item -> model.equals(StringUtils.trim(item.getModel())))
                .collect(Collectors.toList());
        if (candidates.isEmpty()) {
            throw new ServiceException("导入失败:第" + rowNo + "行规格型号不存在,型号:" + model);
        }
        return issuedQuantity.compareTo(requiredQuantity) < 0 ? PLAN_STATUS_PARTIAL : PLAN_STATUS_ISSUED;
        // 若传了产品名称,再做第二轮过滤
        String productName = StringUtils.trim(dto.getProductName());
        if (StringUtils.isNotEmpty(productName)) {
            candidates = candidates.stream()
                    .filter(item -> productName.equals(StringUtils.trim(productNameById.get(item.getProductId()))))
                    .collect(Collectors.toList());
            if (candidates.isEmpty()) {
                throw new ServiceException("导入失败:第" + rowNo + "行产品名称与规格型号不匹配");
            }
        }
        // 若传了单位,再做第三轮过滤
        String unit = StringUtils.trim(dto.getUnit());
        if (StringUtils.isNotEmpty(unit)) {
            candidates = candidates.stream()
                    .filter(item -> unit.equals(StringUtils.trim(item.getUnit())))
                    .collect(Collectors.toList());
            if (candidates.isEmpty()) {
                throw new ServiceException("导入失败:第" + rowNo + "行单位与规格型号不匹配");
            }
        }
        // 仍然多条说明信息不足以唯一定位
        if (candidates.size() > 1) {
            throw new ServiceException("导入失败:第" + rowNo + "行规格型号匹配到多个产品,请补充产品名称或单位");
        }
        return candidates.get(0).getId();
    }
    private String formatPlanIds(List<Long> planIds) {
        return planIds.stream()
                .filter(Objects::nonNull)
                .distinct()
                .map(String::valueOf)
                .collect(Collectors.joining(",", "[", "]"));
    /**
     * 生成主生产计划号,格式:JH + yyyyMMdd + 4位流水号。
     */
    private String buildPlanNo(String datePrefix, int sequence) {
        // 统一计划号格式:JH + 日期 + 4位流水号
        return "JH" + datePrefix + String.format("%04d", sequence);
    }
    private String generateNextPlanNo(String datePrefix) {
    /**
     * 查询当日已存在的最大流水号,并返回下一个可用流水号。
     */
    private int resolveNextPlanSequence(String datePrefix) {
        // 查询当日最新一条计划号
        QueryWrapper<ProductionPlan> queryWrapper = new QueryWrapper<>();
        queryWrapper.likeRight("mps_no", "JH" + datePrefix);
        queryWrapper.orderByDesc("mps_no");
        queryWrapper.last("LIMIT 1");
        ProductionPlan latestPlan = productionPlanMapper.selectOne(queryWrapper);
        // 默认从 0001 开始
        int sequence = 1;
        if (latestPlan != null && latestPlan.getMpsNo() != null && !latestPlan.getMpsNo().isEmpty()) {
        if (latestPlan != null && StringUtils.isNotEmpty(latestPlan.getMpsNo())) {
            // 截取末尾流水号并递增
            String sequenceStr = latestPlan.getMpsNo().substring(("JH" + datePrefix).length());
            try {
                sequence = Integer.parseInt(sequenceStr) + 1;
            } catch (NumberFormatException e) {
            } catch (NumberFormatException ignored) {
                // 历史数据格式异常时回退到 0001
                sequence = 1;
            }
        }
        return "JH" + datePrefix + String.format("%04d", sequence);
        return sequence;
    }
    /**
     * 计算生产计划的剩余未下发数量(需求量 - 已下发量,最小为 0)。
     */
    private BigDecimal resolveRemainingQuantity(ProductionPlan plan) {
        // 空对象按 0 处理
        if (plan == null) {
            return BigDecimal.ZERO;
        }
        // 需求量为空或小于等于 0,视为无剩余
        BigDecimal requiredQuantity = Optional.ofNullable(plan.getQtyRequired()).orElse(BigDecimal.ZERO);
        if (requiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        // 已下发量为空或小于等于 0,剩余即需求量
        BigDecimal issuedQuantity = Optional.ofNullable(plan.getQuantityIssued()).orElse(BigDecimal.ZERO);
        if (issuedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return requiredQuantity;
        }
        // 已下发量大于等于需求量,剩余归零
        if (issuedQuantity.compareTo(requiredQuantity) >= 0) {
            return BigDecimal.ZERO;
        }
        // 正常场景返回差值
        return requiredQuantity.subtract(issuedQuantity);
    }
    /**
     * 按需求量与累计下发量推导计划状态。
     */
    private int resolvePlanStatus(BigDecimal requiredQuantity, BigDecimal issuedQuantity) {
        // 无有效需求量时,状态保持待下发
        if (requiredQuantity == null || requiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return PLAN_STATUS_WAIT;
        }
        // 有需求但未下发,状态仍为待下发
        if (issuedQuantity == null || issuedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return PLAN_STATUS_WAIT;
        }
        // 已下发量小于需求量为部分下发,否则为已下发
        return issuedQuantity.compareTo(requiredQuantity) < 0 ? PLAN_STATUS_PARTIAL : PLAN_STATUS_ISSUED;
    }
    /**
     * 将计划 ID 集合转成 [1,2,3] 形式,写入生产订单关联字段。
     */
    private String formatPlanIds(List<Long> planIds) {
        // 去重并拼接为 [1,2,3] 形式的字符串
        return planIds.stream()
                .filter(Objects::nonNull)
                .distinct()
                .map(String::valueOf)
                .collect(Collectors.joining(",", "[", "]"));
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionProductInputServiceImpl.java
@@ -17,6 +17,7 @@
    @Override
    public IPage<ProductionProductInputDto> listPageProductionProductInputDto(Page page, ProductionProductInputDto productionProductInputDto) {
        // 分页查询生产产品入库
        return productionProductInputMapper.listPageProductionProductInputDto(page, productionProductInputDto);
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -82,6 +82,7 @@
    @Override
    public IPage<ProductionProductMainDto> listPageProductionProductMainDto(Page page, ProductionProductMainDto productionProductMainDto) {
        // 分页查询生产报工主表
        IPage<ProductionProductMainDto> result = productionProductMainMapper.listPageProductionProductMainDto(page, productionProductMainDto);
        fillOperationParamList(result.getRecords());
        return result;
@@ -89,20 +90,24 @@
    @Override
    public IPage<ProductionProductMainDto> pageProductionProductMain(Page page, ProductionProductMainDto productionProductMainDto) {
        // 分页查询生产报工主表
        return listPageProductionProductMainDto(page, productionProductMainDto);
    }
    @Override
    public ProductionProductMainDto getProductionProductMainInfo(Long id) {
        // 获取生产产品主表详情
        return listPageProductionProductMainDto(new Page<>(1, 1), new ProductionProductMainDto() {{
            setId(id);
        }}).getRecords().stream().findFirst().orElse(null);
    }
    private void fillOperationParamList(List<ProductionProductMainDto> recordList) {
        // 填充工序参数列表
        if (recordList == null || recordList.isEmpty()) {
            return;
        }
        // 遍历处理数据并组装结果
        Set<Long> mainIdSet = recordList.stream()
                .map(ProductionProductMainDto::getId)
                .filter(Objects::nonNull)
@@ -112,6 +117,7 @@
            return;
        }
        // 查询并准备业务数据
        List<ProductionOrderRoutingOperationParam> paramList = productionOrderRoutingOperationParamMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .in(ProductionOrderRoutingOperationParam::getProductionProductMainId, mainIdSet)
@@ -211,6 +217,7 @@
    @Override
    public Boolean addProductMain(ProductionProductMainDto dto) {
        // 新增生产报工主记录
        Long taskId = resolveTaskId(dto);
        if (taskId == null) {
            throw new ServiceException("请传入生产工单ID");
@@ -220,11 +227,13 @@
    @Override
    public Boolean saveProductionProductMain(ProductionProductMainDto productionProductMainDto) {
        // 保存生产报工主记录
        return addProductMain(productionProductMainDto);
    }
    @Override
    public Boolean removeProductMain(Long id) {
        // 删除生产报工主记录
        ProductionProductMain currentMain = productionProductMainMapper.selectById(id);
        if (currentMain == null) {
            return true;
@@ -233,10 +242,10 @@
    }
    private Boolean addProductMainByProductionTask(ProductionProductMainDto dto) {
        // 报工以订单工序快照为准,避免工艺主数据变更后影响历史工单执行。
        // 按生产任务新增报工主记录
        Long taskId = resolveTaskId(dto);
        if (taskId == null) {
            throw new ServiceException("productionOperationTaskId can not be null");
            throw new ServiceException("生产工单ID不能为空");
        }
        SysUser user = userMapper.selectUserById(dto.getUserId());
        ProductionOperationTask productionOperationTask = productionOperationTaskMapper.selectById(taskId);
@@ -522,12 +531,14 @@
    }
    private Boolean removeProductMainByProductionTask(ProductionProductMain productionProductMain) {
        // 删除报工需要同步回滚质检、库存、工时核算和订单/工单进度。
        // 按生产任务回滚并删除报工主记录
        List<QualityInspect> qualityInspects = qualityInspectMapper.selectList(
                Wrappers.<QualityInspect>lambdaQuery().eq(QualityInspect::getProductMainId, productionProductMain.getId()));
        // 参数与前置条件校验
        if (qualityInspects.size() > 0) {
            List<QualityUnqualified> qualityUnqualifieds = qualityUnqualifiedMapper.selectList(
                    Wrappers.<QualityUnqualified>lambdaQuery()
        // 遍历处理数据并组装结果
                            .in(QualityUnqualified::getInspectId, qualityInspects.stream().map(QualityInspect::getId).collect(Collectors.toList())));
            if (qualityUnqualifieds.size() > 0 && qualityUnqualifieds.get(0).getInspectState() == 1) {
                throw new ServiceException("该条报工已经不合格处理了,不允许删除");
@@ -552,6 +563,7 @@
            } else {
                productionOperationTask.setStatus(3);
            }
        // 持久化或输出处理结果
            productionOperationTaskMapper.updateById(productionOperationTask);
            ProductionOrder productionOrder = productionOrderMapper.selectById(productionOperationTask.getProductionOrderId());
@@ -600,6 +612,7 @@
    }
    private String generateProductNo() {
        // 生成下一个生产产品编号
        String datePrefix = "BG" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd"));
        QueryWrapper<ProductionProductMain> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("MAX(product_no) as maxNo").likeRight("product_no", datePrefix);
@@ -622,10 +635,12 @@
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        // 将空数量兜底为0,避免空指针异常
        return value == null ? BigDecimal.ZERO : value;
    }
    private Long resolveTaskId(ProductionProductMainDto dto) {
        // 从入参中解析生产工单ID并校验
        if (dto == null) {
            return null;
        }
@@ -634,6 +649,7 @@
    @Override
    public ArrayList<Long> listMain(List<Long> idList) {
        // 查询主表ID集合
        return productionProductMainMapper.listMain(idList);
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionProductOutputServiceImpl.java
@@ -17,6 +17,7 @@
    @Override
    public IPage<ProductionProductOutputDto> listPageProductionProductOutputDto(Page page, ProductionProductOutputDto productionProductOutputDto) {
        // 分页查询生产产品出库
        return productionProductOutputMapper.listPageProductionProductOutputDto(page, productionProductOutputDto);
    }
}
src/main/java/com/ruoyi/production/service/impl/SalesLedgerProductionAccountingServiceImpl.java
@@ -15,6 +15,7 @@
    @Override
    public UserAccountDto getByUserId(UserProductionAccountingDto dto) {
        // 按用户查询生产核算信息
        if (dto == null || dto.getUserId() == null || dto.getDate() == null || dto.getDate().trim().isEmpty()) {
            return new UserAccountDto();
        }
src/main/java/com/ruoyi/technology/service/impl/TechnologyBomServiceImpl.java
@@ -108,12 +108,12 @@
    @Transactional(rollbackFor = Exception.class)
    public R update(TechnologyBom technologyBom) {
        if (technologyBom.getId() == null) {
            throw new ServiceException("BOM id is required");
            throw new ServiceException("BOM ID不能为空");
        }
        validateProductModel(technologyBom.getProductModelId());
        TechnologyBom oldBom = technologyBomMapper.selectById(technologyBom.getId());
        if (oldBom == null) {
            throw new ServiceException("BOM not found");
            throw new ServiceException("BOM不存在");
        }
        if (oldBom.getProductModelId() != null && !oldBom.getProductModelId().equals(technologyBom.getProductModelId())) {
            technologyRoutingMapper.updateProductModelByBomId(technologyBom.getProductModelId(), technologyBom.getId().longValue());
@@ -135,12 +135,12 @@
    @Transactional(rollbackFor = Exception.class)
    public boolean batchDelete(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            throw new ServiceException("Select at least one BOM");
            throw new ServiceException("请至少选择一个BOM");
        }
        List<TechnologyRouting> list = technologyRoutingMapper.selectList(Wrappers.<TechnologyRouting>lambdaQuery()
                .in(TechnologyRouting::getBomId, ids));
        if (!list.isEmpty()) {
            throw new ServiceException("BOM is referenced by routing");
            throw new ServiceException("BOM已被工艺路线引用,不能删除");
        }
        technologyBomStructureService.remove(Wrappers.<TechnologyBomStructure>lambdaQuery()
                .in(TechnologyBomStructure::getBomId, ids));
@@ -152,11 +152,11 @@
     */
    private void validateProductModel(Long productModelId) {
        if (productModelId == null) {
            throw new ServiceException("Product model is required");
            throw new ServiceException("产品规格ID不能为空");
        }
        ProductModel productModel = productModelService.getById(productModelId);
        if (productModel == null) {
            throw new ServiceException("Product model not found");
            throw new ServiceException("产品规格不存在");
        }
    }
src/main/java/com/ruoyi/technology/service/impl/TechnologyOperationParamServiceImpl.java
@@ -41,21 +41,21 @@
    public boolean saveTechnologyOperationParam(TechnologyOperationParam technologyOperationParam) {
        if (technologyOperationParam.getTechnologyOperationId() == null
                || technologyOperationMapper.selectById(technologyOperationParam.getTechnologyOperationId()) == null) {
            throw new ServiceException("Operation not found");
            throw new ServiceException("工序不存在");
        }
        if (technologyOperationParam.getTechnologyParamId() == null) {
            throw new ServiceException("Param is required");
            throw new ServiceException("参数ID不能为空");
        }
        TechnologyParam technologyParam = technologyParamMapper.selectById(technologyOperationParam.getTechnologyParamId());
        if (technologyParam == null) {
            throw new ServiceException("Param not found");
            throw new ServiceException("参数不存在");
        }
        boolean duplicate = technologyOperationParamMapper.selectCount(Wrappers.<TechnologyOperationParam>lambdaQuery()
                .eq(TechnologyOperationParam::getTechnologyOperationId, technologyOperationParam.getTechnologyOperationId())
                .eq(TechnologyOperationParam::getTechnologyParamId, technologyOperationParam.getTechnologyParamId())
                .ne(technologyOperationParam.getId() != null, TechnologyOperationParam::getId, technologyOperationParam.getId())) > 0;
        if (duplicate) {
            throw new ServiceException("Duplicate param in operation");
            throw new ServiceException("工序参数重复");
        }
        return this.saveOrUpdate(technologyOperationParam);
    }
src/main/java/com/ruoyi/technology/service/impl/TechnologyParamServiceImpl.java
@@ -2,18 +2,22 @@
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.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.technology.bean.dto.TechnologyParamDto;
import com.ruoyi.technology.bean.vo.TechnologyParamVo;
import com.ruoyi.technology.mapper.TechnologyOperationParamMapper;
import com.ruoyi.technology.mapper.TechnologyParamMapper;
import com.ruoyi.technology.pojo.TechnologyOperationParam;
import com.ruoyi.technology.pojo.TechnologyParam;
import com.ruoyi.technology.service.TechnologyParamService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -28,6 +32,7 @@
    private static final List<Integer> VALID_PARAM_TYPES = Arrays.asList(1, 2, 3, 4);
    private static final String PARAM_CODE_PREFIX = "PARAM_";
    private static final Byte DATE_PARAM_TYPE = (byte) 4;
    private final TechnologyOperationParamMapper technologyOperationParamMapper;
    /**
     * 分页查询基础参数并格式化日期类型展示。
@@ -161,10 +166,13 @@
     * 批量删除基础参数。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int deleteBaseParamByIds(Long[] ids) {
        if (ids == null || ids.length == 0) {
            throw new RuntimeException("删除ID不能为空");
        }
        return baseMapper.deleteBatchIds(Arrays.asList(ids));
        technologyOperationParamMapper.delete(Wrappers.<TechnologyOperationParam>lambdaQuery()
                .in(TechnologyOperationParam::getTechnologyParamId, Arrays.asList(ids)));
        return baseMapper.deleteByIds(Arrays.asList(ids));
    }
}