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)); } }