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; 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 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 listPage(Page page, TechnologyBomDto technologyBomDto) { return technologyBomMapper.listPage(page, technologyBomDto); } /** * 根据规格查询BOM并转换为返回对象。 */ @Override public List listByModel(Long productModelId) { List list = this.list(Wrappers.lambdaQuery() .eq(TechnologyBom::getProductModelId, productModelId)); List 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.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 ids) { if (ids == null || ids.isEmpty()) { throw new ServiceException("请至少选择一个BOM"); } List list = technologyRoutingMapper.selectList(Wrappers.lambdaQuery() .in(TechnologyRouting::getBomId, ids)); if (!list.isEmpty()) { throw new ServiceException("BOM已被工艺路线引用,不能删除"); } technologyBomStructureService.remove(Wrappers.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 util = new ExcelUtil<>(BomImportDto.class); List 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 operationIdByName = new HashMap<>(); for (TechnologyOperation operation : technologyOperationMapper.selectList(null)) { if (StringUtils.isNotBlank(operation.getName())) { operationIdByName.putIfAbsent(operation.getName().trim(), operation.getId()); } } Set 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 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 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 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 oldDetailList = technologyBomStructureService.listByBomId(technologyBom.getId().longValue()); if (oldDetailList != null && !oldDetailList.isEmpty()) { List 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() .eq(Product::getProductName, name).last("limit 1")); if (product == null) throw new ServiceException("产品未维护:" + name); ProductModel model = productModelService.getOne(new LambdaQueryWrapper() .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(); } }