| | |
| | | package com.ruoyi.technology.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.ruoyi.basic.pojo.Product; |
| | | import com.ruoyi.basic.pojo.ProductModel; |
| | | import com.ruoyi.basic.service.IProductModelService; |
| | | import com.ruoyi.basic.service.IProductService; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.common.utils.poi.ExcelUtil; |
| | | import com.ruoyi.framework.web.domain.R; |
| | | import com.ruoyi.technology.bean.dto.BomImportDto; |
| | | import com.ruoyi.technology.bean.dto.TechnologyBomDto; |
| | | import com.ruoyi.technology.bean.vo.TechnologyBomStructureVo; |
| | | import com.ruoyi.technology.bean.vo.TechnologyBomVo; |
| | | import com.ruoyi.technology.mapper.TechnologyBomMapper; |
| | | import com.ruoyi.technology.mapper.TechnologyOperationMapper; |
| | | import com.ruoyi.technology.mapper.TechnologyRoutingMapper; |
| | | import com.ruoyi.technology.pojo.TechnologyBom; |
| | | import com.ruoyi.technology.pojo.TechnologyBomStructure; |
| | | import com.ruoyi.technology.pojo.TechnologyOperation; |
| | | import com.ruoyi.technology.pojo.TechnologyRouting; |
| | | import com.ruoyi.technology.service.TechnologyBomService; |
| | | import com.ruoyi.technology.service.TechnologyBomStructureService; |
| | | import jakarta.servlet.http.HttpServletResponse; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | /** |
| | | * <p> |
| | | * BOM表 服务实现类 |
| | | * </p> |
| | | * |
| | | * @author 芯导软件(江苏)有限公司 |
| | | * @since 2026-04-20 10:05:55 |
| | | */ |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | @Service |
| | | @RequiredArgsConstructor |
| | | public class TechnologyBomServiceImpl extends ServiceImpl<TechnologyBomMapper, TechnologyBom> implements TechnologyBomService { |
| | | |
| | | private final TechnologyBomMapper technologyBomMapper; |
| | | private final IProductModelService productModelService; |
| | | private final TechnologyBomStructureService technologyBomStructureService; |
| | | private final TechnologyRoutingMapper technologyRoutingMapper; |
| | | private final IProductService productService; |
| | | private final TechnologyOperationMapper technologyOperationMapper; |
| | | |
| | | /** |
| | | * 分页查询BOM列表。 |
| | | */ |
| | | @Override |
| | | public IPage<TechnologyBomVo> listPage(Page<TechnologyBomDto> page, TechnologyBomDto technologyBomDto) { |
| | | return technologyBomMapper.listPage(page, technologyBomDto); |
| | | } |
| | | |
| | | /** |
| | | * 根据规格查询BOM并转换为返回对象。 |
| | | */ |
| | | @Override |
| | | public List<TechnologyBomVo> listByModel(Long productModelId) { |
| | | List<TechnologyBom> list = this.list(Wrappers.<TechnologyBom>lambdaQuery() |
| | | .eq(TechnologyBom::getProductModelId, productModelId)); |
| | | List<TechnologyBomVo> result = new ArrayList<>(list.size()); |
| | | for (TechnologyBom item : list) { |
| | | TechnologyBomVo vo = new TechnologyBomVo(); |
| | | vo.setId(item.getId()); |
| | | vo.setProductModelId(item.getProductModelId()); |
| | | vo.setRemark(item.getRemark()); |
| | | vo.setVersion(item.getVersion()); |
| | | vo.setCreateTime(item.getCreateTime()); |
| | | vo.setUpdateTime(item.getUpdateTime()); |
| | | vo.setCreateUser(item.getCreateUser()); |
| | | vo.setUpdateUser(item.getUpdateUser()); |
| | | vo.setBomNo(item.getBomNo()); |
| | | vo.setDeptId(item.getDeptId()); |
| | | result.add(vo); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 新增BOM。BOM主表本身即代表成品规格,明细表不再写入成品根节点。 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public R add(TechnologyBom technologyBom) { |
| | | validateProductModel(technologyBom.getProductModelId()); |
| | | boolean saved = technologyBomMapper.insert(technologyBom) > 0; |
| | | if (!saved) { |
| | | return R.fail("Add BOM failed"); |
| | | } |
| | | technologyBom.setBomNo("BM." + String.format("%05d", technologyBom.getId())); |
| | | technologyBomMapper.updateById(technologyBom); |
| | | return R.ok(); |
| | | } |
| | | |
| | | /** |
| | | * 修改BOM。成品规格变化时同步刷新工艺路线的成品规格, |
| | | * 并清理与新成品规格冲突的物料行(成品不能作为自身物料),其余物料-工序关系保留。 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public R update(TechnologyBom technologyBom) { |
| | | if (technologyBom.getId() == null) { |
| | | throw new ServiceException("BOM ID不能为空"); |
| | | } |
| | | validateProductModel(technologyBom.getProductModelId()); |
| | | TechnologyBom oldBom = technologyBomMapper.selectById(technologyBom.getId()); |
| | | if (oldBom == null) { |
| | | throw new ServiceException("BOM不存在"); |
| | | } |
| | | if (oldBom.getProductModelId() != null && !oldBom.getProductModelId().equals(technologyBom.getProductModelId())) { |
| | | technologyRoutingMapper.updateProductModelByBomId(technologyBom.getProductModelId(), technologyBom.getId().longValue()); |
| | | technologyBomStructureService.remove(Wrappers.<TechnologyBomStructure>lambdaQuery() |
| | | .eq(TechnologyBomStructure::getBomId, technologyBom.getId().longValue()) |
| | | .eq(TechnologyBomStructure::getProductModelId, technologyBom.getProductModelId())); |
| | | } |
| | | if (technologyBom.getBomNo() == null) { |
| | | technologyBom.setBomNo(oldBom.getBomNo()); |
| | | } |
| | | technologyBomMapper.updateById(technologyBom); |
| | | return R.ok(); |
| | | } |
| | | |
| | | /** |
| | | * 删除BOM前校验是否已被工艺路线引用。 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public boolean batchDelete(List<Long> ids) { |
| | | if (ids == null || ids.isEmpty()) { |
| | | throw new ServiceException("请至少选择一个BOM"); |
| | | } |
| | | List<TechnologyRouting> list = technologyRoutingMapper.selectList(Wrappers.<TechnologyRouting>lambdaQuery() |
| | | .in(TechnologyRouting::getBomId, ids)); |
| | | if (!list.isEmpty()) { |
| | | throw new ServiceException("BOM已被工艺路线引用,不能删除"); |
| | | } |
| | | technologyBomStructureService.remove(Wrappers.<TechnologyBomStructure>lambdaQuery() |
| | | .in(TechnologyBomStructure::getBomId, ids)); |
| | | return this.removeBatchByIds(ids); |
| | | } |
| | | |
| | | /** |
| | | * 校验产品规格是否存在。 |
| | | */ |
| | | private void validateProductModel(Long productModelId) { |
| | | if (productModelId == null) { |
| | | throw new ServiceException("产品规格ID不能为空"); |
| | | } |
| | | ProductModel productModel = productModelService.getById(productModelId); |
| | | if (productModel == null) { |
| | | throw new ServiceException("产品规格不存在"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 导入扁平BOM:每行 = 成品 + 一个物料规格 + 消耗工序。 |
| | | * 同一次导入的所有行必须属于同一个成品;旧的父子/单位用量模板会被明确拒绝。 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public R uploadBom(MultipartFile file) { |
| | | ExcelUtil<BomImportDto> util = new ExcelUtil<>(BomImportDto.class); |
| | | List<BomImportDto> list; |
| | | try { |
| | | list = util.importExcel(file.getInputStream()); |
| | | } catch (Exception e) { |
| | | return R.fail("Excel解析失败"); |
| | | } |
| | | |
| | | if (list == null || list.isEmpty()) { |
| | | return R.fail("数据为空"); |
| | | } |
| | | |
| | | list.forEach(dto -> { |
| | | dto.setProductName(clean(dto.getProductName())); |
| | | dto.setProductSpec(clean(dto.getProductSpec())); |
| | | dto.setMaterialName(clean(dto.getMaterialName())); |
| | | dto.setMaterialSpec(clean(dto.getMaterialSpec())); |
| | | dto.setProcess(clean(dto.getProcess())); |
| | | }); |
| | | |
| | | boolean templateMatched = list.stream().anyMatch(dto -> StringUtils.isNotBlank(dto.getProductName()) |
| | | && StringUtils.isNotBlank(dto.getMaterialName())); |
| | | if (!templateMatched) { |
| | | return R.fail("BOM导入模板已更新为扁平结构,请使用最新模板(成品产品名称/规格、物料产品名称/规格、消耗工序)"); |
| | | } |
| | | |
| | | BomImportDto first = list.get(0); |
| | | ProductModel productModel = findModel(first.getProductName(), first.getProductSpec()); |
| | | TechnologyBom bom = new TechnologyBom(); |
| | | bom.setProductModelId(productModel.getId()); |
| | | bom.setVersion("1.0"); |
| | | bom.setRemark(first.getRemark()); |
| | | technologyBomMapper.insert(bom); |
| | | bom.setBomNo("BM." + String.format("%05d", bom.getId())); |
| | | technologyBomMapper.updateById(bom); |
| | | |
| | | Map<String, Long> operationIdByName = new HashMap<>(); |
| | | for (TechnologyOperation operation : technologyOperationMapper.selectList(null)) { |
| | | if (StringUtils.isNotBlank(operation.getName())) { |
| | | operationIdByName.putIfAbsent(operation.getName().trim(), operation.getId()); |
| | | } |
| | | } |
| | | |
| | | Set<String> relationKeys = new HashSet<>(); |
| | | for (int i = 0; i < list.size(); i++) { |
| | | BomImportDto dto = list.get(i); |
| | | int rowNo = i + 2; |
| | | if (StringUtils.isBlank(dto.getMaterialName())) { |
| | | throw new ServiceException("导入失败:第" + rowNo + "行缺少物料产品名称"); |
| | | } |
| | | if (StringUtils.isNotBlank(dto.getProductName()) |
| | | && !sameModel(dto, first)) { |
| | | throw new ServiceException("导入失败:第" + rowNo + "行的成品与第一行不一致,一次导入只能维护一个BOM"); |
| | | } |
| | | if (StringUtils.isBlank(dto.getProcess())) { |
| | | throw new ServiceException("导入失败:第" + rowNo + "行缺少消耗工序"); |
| | | } |
| | | Long operationId = operationIdByName.get(dto.getProcess()); |
| | | if (operationId == null) { |
| | | throw new ServiceException("导入失败:工序未维护[" + dto.getProcess() + "](第" + rowNo + "行)"); |
| | | } |
| | | |
| | | ProductModel materialModel = findModel(dto.getMaterialName(), dto.getMaterialSpec()); |
| | | if (materialModel.getId().equals(productModel.getId())) { |
| | | throw new ServiceException("导入失败:第" + rowNo + "行成品规格不能作为自身的物料"); |
| | | } |
| | | if (!relationKeys.add(materialModel.getId() + "#" + operationId)) { |
| | | throw new ServiceException("导入失败:第" + rowNo + "行物料与工序重复配置"); |
| | | } |
| | | |
| | | TechnologyBomStructure node = new TechnologyBomStructure(); |
| | | node.setBomId(bom.getId().longValue()); |
| | | node.setProductModelId(materialModel.getId()); |
| | | node.setOperationId(operationId); |
| | | technologyBomStructureService.save(node); |
| | | } |
| | | |
| | | return R.ok("BOM导入成功"); |
| | | } |
| | | |
| | | /** |
| | | * 导出扁平BOM:成品 + 物料 + 消耗工序。 |
| | | */ |
| | | @Override |
| | | public void exportBom(HttpServletResponse response, Long bomId) { |
| | | if (bomId == null) { |
| | | throw new ServiceException("BOM ID不能为空"); |
| | | } |
| | | TechnologyBom bom = technologyBomMapper.selectById(bomId); |
| | | if (bom == null) { |
| | | throw new ServiceException("BOM不存在"); |
| | | } |
| | | |
| | | List<TechnologyBomStructureVo> detailList = technologyBomStructureService.listByBomId(bomId); |
| | | if (detailList == null || detailList.isEmpty()) { |
| | | return; |
| | | } |
| | | |
| | | ProductModel productModel = productModelService.getById(bom.getProductModelId()); |
| | | String productName = null; |
| | | if (productModel != null && productModel.getProductId() != null) { |
| | | Product product = productService.getById(productModel.getProductId()); |
| | | productName = product == null ? null : product.getProductName(); |
| | | } |
| | | |
| | | List<BomImportDto> exportList = new ArrayList<>(detailList.size()); |
| | | for (TechnologyBomStructureVo detail : detailList) { |
| | | BomImportDto row = new BomImportDto(); |
| | | row.setProductName(productName); |
| | | row.setProductSpec(productModel == null ? null : productModel.getModel()); |
| | | row.setProductCode(productModel == null ? null : productModel.getProductCode()); |
| | | row.setMaterialName(detail.getProductName()); |
| | | row.setMaterialSpec(detail.getModel()); |
| | | row.setMaterialCode(detail.getProductCode()); |
| | | row.setProcess(detail.getOperationName()); |
| | | exportList.add(row); |
| | | } |
| | | |
| | | ExcelUtil<BomImportDto> util = new ExcelUtil<>(BomImportDto.class); |
| | | util.exportExcel(response, exportList, "BOM结构导出"); |
| | | } |
| | | |
| | | /** |
| | | * 复制BOM:复制成品规格与全部扁平物料-工序关系。 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public R copy(TechnologyBom technologyBom) { |
| | | if (technologyBom == null || technologyBom.getId() == null) { |
| | | throw new ServiceException("BOM ID不能为空"); |
| | | } |
| | | TechnologyBom oldTechnologyBom = technologyBomMapper.selectById(technologyBom.getId()); |
| | | if (oldTechnologyBom == null) { |
| | | throw new ServiceException("BOM不存在"); |
| | | } |
| | | validateProductModel(oldTechnologyBom.getProductModelId()); |
| | | |
| | | TechnologyBom newTechnologyBom = new TechnologyBom(); |
| | | newTechnologyBom.setProductModelId(oldTechnologyBom.getProductModelId()); |
| | | newTechnologyBom.setVersion("FZ" + oldTechnologyBom.getVersion()); |
| | | newTechnologyBom.setRemark(oldTechnologyBom.getRemark()); |
| | | boolean saved = technologyBomMapper.insert(newTechnologyBom) > 0; |
| | | if (!saved) { |
| | | return R.fail("Copy BOM failed"); |
| | | } |
| | | newTechnologyBom.setBomNo("BM." + String.format("%05d", newTechnologyBom.getId())); |
| | | technologyBomMapper.updateById(newTechnologyBom); |
| | | |
| | | List<TechnologyBomStructureVo> oldDetailList = |
| | | technologyBomStructureService.listByBomId(technologyBom.getId().longValue()); |
| | | if (oldDetailList != null && !oldDetailList.isEmpty()) { |
| | | List<TechnologyBomStructure> newDetailList = new ArrayList<>(oldDetailList.size()); |
| | | for (TechnologyBomStructureVo oldDetail : oldDetailList) { |
| | | TechnologyBomStructure newDetail = new TechnologyBomStructure(); |
| | | newDetail.setBomId(newTechnologyBom.getId().longValue()); |
| | | newDetail.setProductModelId(oldDetail.getProductModelId()); |
| | | newDetail.setOperationId(oldDetail.getOperationId()); |
| | | newDetailList.add(newDetail); |
| | | } |
| | | technologyBomStructureService.saveBatch(newDetailList); |
| | | } |
| | | return R.ok(); |
| | | } |
| | | |
| | | private boolean sameModel(BomImportDto row, BomImportDto first) { |
| | | return StringUtils.equals(row.getProductName(), first.getProductName()) |
| | | && StringUtils.equals(defaultBlank(row.getProductSpec()), defaultBlank(first.getProductSpec())); |
| | | } |
| | | |
| | | private String defaultBlank(String value) { |
| | | return value == null ? "" : value; |
| | | } |
| | | |
| | | private ProductModel findModel(String name, String spec) { |
| | | Product product = productService.getOne(new LambdaQueryWrapper<Product>() |
| | | .eq(Product::getProductName, name).last("limit 1")); |
| | | if (product == null) throw new ServiceException("产品未维护:" + name); |
| | | |
| | | ProductModel model = productModelService.getOne(new LambdaQueryWrapper<ProductModel>() |
| | | .eq(ProductModel::getProductId, product.getId()) |
| | | .eq(ProductModel::getModel, spec).last("limit 1")); |
| | | if (model == null) throw new ServiceException("规格未维护:" + name + "[" + spec + "]"); |
| | | return model; |
| | | } |
| | | |
| | | private String clean(String s) { |
| | | if (s == null) return null; |
| | | return s.replaceAll("[\\u00A0\\u3000]", "").trim(); |
| | | } |
| | | } |