package cn.iocoder.yudao.module.mes.service.pro.route;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.util.BooleanUtil;
|
import cn.hutool.core.util.ObjUtil;
|
import cn.hutool.core.util.StrUtil;
|
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteImportExcelVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteImportRespVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRoutePageReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteSaveReqVO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessOutputDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
|
import cn.iocoder.yudao.module.mes.dal.mysql.pro.route.MesProRouteMapper;
|
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
|
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
|
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
|
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
|
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
|
import cn.iocoder.yudao.module.mdm.enums.MdmItemTypeEnum;
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
import jakarta.annotation.Resource;
|
import org.springframework.context.annotation.Lazy;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.util.ArrayList;
|
import java.util.Collection;
|
import java.util.Collections;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Set;
|
import java.util.Comparator;
|
import java.util.Arrays;
|
import java.util.Objects;
|
import java.util.stream.Collectors;
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
|
|
/**
|
* MES 工艺路线 Service 实现类
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@Validated
|
public class MesProRouteServiceImpl implements MesProRouteService {
|
|
@Resource
|
private MesProRouteMapper routeMapper;
|
|
@Resource
|
@Lazy
|
private MesProRouteProcessService routeProcessService;
|
@Resource
|
@Lazy
|
private MesProRouteProductService routeProductService;
|
@Resource
|
@Lazy
|
private MesProRouteProductBomService routeProductBomService;
|
@Resource
|
@Lazy
|
private MesMdItemService itemService;
|
@Resource
|
private MesMdItemTypeService itemTypeService;
|
@Resource
|
private MesWmWarehouseAreaService warehouseAreaService;
|
@Resource
|
private MdmItemApi mdmItemApi;
|
|
@Override
|
public Long createRoute(MesProRouteSaveReqVO createReqVO) {
|
// 1. 校验编码唯一性
|
validateRouteCodeUnique(null, createReqVO.getCode());
|
// 2. 插入
|
MesProRouteDO route = BeanUtils.toBean(createReqVO, MesProRouteDO.class);
|
route.setStatus(CommonStatusEnum.DISABLE.getStatus());
|
routeMapper.insert(route);
|
return route.getId();
|
}
|
|
@Override
|
public void updateRoute(MesProRouteSaveReqVO updateReqVO) {
|
// 1.1 校验存在
|
validateRouteExists(updateReqVO.getId());
|
// 1.2 校验编码唯一性
|
validateRouteCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
|
|
// 2. 更新
|
MesProRouteDO updateObj = BeanUtils.toBean(updateReqVO, MesProRouteDO.class);
|
routeMapper.updateById(updateObj);
|
}
|
|
@Override
|
public void updateRouteStatus(Long id, Integer status) {
|
// 1.1 校验存在
|
MesProRouteDO route = validateRouteExists(id);
|
// 1.2 启用时的校验
|
if (CommonStatusEnum.ENABLE.getStatus().equals(status)) {
|
validateRouteEnable(id);
|
}
|
|
// 2. 更新状态
|
routeMapper.updateById(new MesProRouteDO().setId(id).setStatus(status));
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void deleteRoute(Long id) {
|
// 1.1 校验存在
|
validateRouteExists(id);
|
// 1.2 已启用的工艺路线,不允许删除
|
validateRouteNotEnable(id);
|
|
// 2.1 级联删除
|
routeProcessService.deleteRouteProcessByRouteId(id);
|
routeProductService.deleteRouteProductByRouteId(id);
|
routeProductBomService.deleteRouteProductBomByRouteId(id);
|
// 2.2 删除工艺路线
|
routeMapper.deleteById(id);
|
}
|
|
@Override
|
public MesProRouteDO validateRouteExists(Long id) {
|
MesProRouteDO route = routeMapper.selectById(id);
|
if (route == null) {
|
throw exception(PRO_ROUTE_NOT_EXISTS);
|
}
|
return route;
|
}
|
|
private void validateRouteCodeUnique(Long id, String code) {
|
MesProRouteDO route = routeMapper.selectByCode(code);
|
if (route == null) {
|
return;
|
}
|
if (ObjUtil.notEqual(route.getId(), id)) {
|
throw exception(PRO_ROUTE_CODE_DUPLICATE);
|
}
|
}
|
|
/**
|
* 启用工艺路线时的校验
|
*/
|
private void validateRouteEnable(Long routeId) {
|
// 1. 必须有工序
|
List<MesProRouteProcessDO> processList = routeProcessService.getRouteProcessListByRouteId(routeId);
|
if (CollUtil.isEmpty(processList)) {
|
throw exception(PRO_ROUTE_ENABLE_NO_PROCESS);
|
}
|
// 2. 必须有关键工序
|
boolean hasKeyProcess = processList.stream().anyMatch(MesProRouteProcessDO::getKeyFlag);
|
if (BooleanUtil.isFalse(hasKeyProcess)) {
|
throw exception(PRO_ROUTE_ENABLE_NO_KEY_PROCESS);
|
}
|
// 3. 所有产品必须配置了 BOM 消耗
|
List<MesProRouteProductDO> productList = routeProductService.getRouteProductListByRouteId(routeId);
|
for (MesProRouteProductDO product : productList) {
|
MesMdItemDO item = itemService.validateItemExists(product.getItemId());
|
if (product.getWarehouseId() == null || product.getLocationId() == null || product.getAreaId() == null) {
|
throw exception(PRO_ROUTE_PRODUCT_WAREHOUSE_REQUIRED, item.getName());
|
}
|
warehouseAreaService.validateWarehouseAreaExists(product.getWarehouseId(),
|
product.getLocationId(), product.getAreaId());
|
List<MesProRouteProductBomDO> bomList = routeProductBomService
|
.getRouteProductBomList(routeId, null, product.getItemId());
|
if (CollUtil.isEmpty(bomList)) {
|
throw exception(PRO_ROUTE_ENABLE_PRODUCT_NO_BOM, item.getName());
|
}
|
}
|
validateProcessOutputs(processList, productList);
|
}
|
|
private void validateProcessOutputs(List<MesProRouteProcessDO> processList,
|
List<MesProRouteProductDO> productList) {
|
if (CollUtil.isEmpty(productList)) {
|
throw exception(PRO_ROUTE_LAST_OUTPUT_INVALID, processList.get(processList.size() - 1).getProcessId());
|
}
|
Set<Long> boundItemIds = productList.stream().map(MesProRouteProductDO::getItemId).collect(Collectors.toSet());
|
Set<Long> finishedItemIds = productList.stream()
|
.map(MesProRouteProductDO::getItemId)
|
.filter(itemId -> isItemUnderCategory(itemId, "FG"))
|
.collect(Collectors.toSet());
|
MesProRouteProcessDO lastProcess = processList.stream()
|
.max(Comparator.comparing(MesProRouteProcessDO::getSort)).orElseThrow();
|
for (MesProRouteProcessDO process : processList) {
|
Set<Long> outputIds = parseOutputItemIds(process.getOutputItemId());
|
boolean last = process.getId().equals(lastProcess.getId());
|
if (CollUtil.isEmpty(outputIds)) {
|
// 中间工序不一定形成可入库的独立半成品,允许不配置产出;末道工序必须产出成品。
|
if (last) {
|
throw exception(PRO_ROUTE_LAST_OUTPUT_INVALID, process.getProcessId());
|
}
|
continue;
|
}
|
for (Long outputId : outputIds) {
|
if (!boundItemIds.contains(outputId)) {
|
MesMdItemDO item = itemService.validateItemExists(outputId);
|
throw exception(PRO_ROUTE_PROCESS_OUTPUT_NOT_BOUND, item.getName());
|
}
|
}
|
if (last) {
|
if (finishedItemIds.isEmpty() || !outputIds.equals(finishedItemIds)) {
|
throw exception(PRO_ROUTE_LAST_OUTPUT_INVALID, process.getProcessId());
|
}
|
} else if (outputIds.stream().anyMatch(itemId -> !isItemUnderCategory(itemId, "SF"))) {
|
throw exception(PRO_ROUTE_MIDDLE_OUTPUT_INVALID, process.getProcessId());
|
}
|
}
|
// 4. 末道工序成品产出必须按产品归属,避免“通用”产出串到其它产品的工单
|
List<MesProRouteProcessOutputDO> lastOutputs = routeProcessService
|
.getRouteProcessOutputListByRouteProcessIds(Collections.singletonList(lastProcess.getId()));
|
if (CollUtil.isNotEmpty(lastOutputs)) {
|
boolean attributionInvalid = lastOutputs.stream().anyMatch(output ->
|
output.getProductId() == null || !output.getProductId().equals(output.getItemId()));
|
Set<Long> attributedProducts = lastOutputs.stream()
|
.map(MesProRouteProcessOutputDO::getProductId)
|
.filter(Objects::nonNull)
|
.collect(Collectors.toSet());
|
if (attributionInvalid || !attributedProducts.containsAll(finishedItemIds)) {
|
throw exception(PRO_ROUTE_LAST_OUTPUT_ATTRIBUTION_REQUIRED, lastProcess.getProcessId());
|
}
|
}
|
}
|
|
private Set<Long> parseOutputItemIds(String value) {
|
if (StrUtil.isBlank(value)) {
|
return Collections.emptySet();
|
}
|
return Arrays.stream(value.split(",")).map(String::trim).filter(StrUtil::isNotBlank)
|
.map(Long::valueOf).collect(Collectors.toSet());
|
}
|
|
private boolean isItemUnderCategory(Long itemId, String rootCode) {
|
MesMdItemDO item = itemService.validateItemExists(itemId);
|
MesMdItemTypeDO type = itemTypeService.getItemType(item.getItemTypeId());
|
while (type != null) {
|
if ("SF".equalsIgnoreCase(type.getCode())
|
|| (type.getName() != null && type.getName().contains("半成品"))) {
|
return "SF".equals(rootCode);
|
}
|
if ("FG".equalsIgnoreCase(type.getCode()) || isFinishedCategoryName(type.getName())) {
|
return "FG".equals(rootCode);
|
}
|
if (type.getParentId() == null || MesMdItemTypeDO.PARENT_ID_ROOT.equals(type.getParentId())) {
|
break;
|
}
|
type = itemTypeService.getItemType(type.getParentId());
|
}
|
// MES 分类未使用 FG/SF 编码时,按 MDM 主数据的物料类型判定。
|
CommonResult<MdmItemRespDTO> mdmResult = mdmItemApi.getItemByCode(item.getCode());
|
if (mdmResult == null || !mdmResult.isSuccess() || mdmResult.getData() == null) {
|
return false;
|
}
|
MdmItemRespDTO mdmItem = mdmResult.getData();
|
Integer mdmType = mdmItem.getItemType();
|
// 历史同步可能把分类 ID 存在 itemType,不能把分类 2/3 误判为成品/半成品。
|
if (Objects.equals(mdmItem.getCategoryId(), mdmType == null ? null : mdmType.longValue())) {
|
return false;
|
}
|
return "FG".equals(rootCode) ? MdmItemTypeEnum.PRODUCT.getType().equals(mdmType)
|
: "SF".equals(rootCode) && MdmItemTypeEnum.SEMI_FINISHED.getType().equals(mdmType);
|
}
|
|
private boolean isFinishedCategoryName(String name) {
|
return name != null && !name.contains("半成品") && !name.contains("非成品")
|
&& (name.contains("产成品") || name.contains("成品"));
|
}
|
|
@Override
|
public MesProRouteDO getRoute(Long id) {
|
return routeMapper.selectById(id);
|
}
|
|
@Override
|
public PageResult<MesProRouteDO> getRoutePage(MesProRoutePageReqVO pageReqVO) {
|
return routeMapper.selectPage(pageReqVO);
|
}
|
|
@Override
|
public List<MesProRouteDO> getRouteListByStatus(Integer status) {
|
return routeMapper.selectListByStatus(status);
|
}
|
|
@Override
|
public void validateRouteNotEnable(Long routeId) {
|
MesProRouteDO route = routeMapper.selectById(routeId);
|
if (route != null && CommonStatusEnum.ENABLE.getStatus().equals(route.getStatus())) {
|
throw exception(PRO_ROUTE_IS_ENABLE);
|
}
|
}
|
|
@Override
|
public List<MesProRouteDO> getRouteList(Collection<Long> ids) {
|
if (CollUtil.isEmpty(ids)) {
|
return Collections.emptyList();
|
}
|
return routeMapper.selectByIds(ids);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public MesProRouteImportRespVO importRouteList(List<MesProRouteImportExcelVO> importList,
|
boolean updateSupport) {
|
// 1. 校验非空
|
if (CollUtil.isEmpty(importList)) {
|
throw exception(PRO_ROUTE_IMPORT_LIST_IS_EMPTY);
|
}
|
|
// 2. 逐行处理
|
MesProRouteImportRespVO respVO = MesProRouteImportRespVO.builder()
|
.createCodes(new ArrayList<>()).updateCodes(new ArrayList<>())
|
.failureCodes(new LinkedHashMap<>()).build();
|
AtomicInteger index = new AtomicInteger(1);
|
importList.forEach(importItem -> {
|
int currentIndex = index.getAndIncrement();
|
String key = StrUtil.blankToDefault(importItem.getCode(), "第 " + currentIndex + " 行");
|
if (StrUtil.isBlank(importItem.getCode())) {
|
respVO.getFailureCodes().put(key, "工艺路线编码不能为空");
|
return;
|
}
|
if (StrUtil.isBlank(importItem.getName())) {
|
respVO.getFailureCodes().put(key, "工艺路线名称不能为空");
|
return;
|
}
|
// 2.1 判断:创建 or 更新
|
MesProRouteDO existRoute = routeMapper.selectByCode(importItem.getCode());
|
if (existRoute == null) {
|
// 2.1.1 新增(与手工创建一致:默认禁用,通过启用接口校验后启用)
|
MesProRouteDO route = BeanUtils.toBean(importItem, MesProRouteDO.class);
|
route.setId(null);
|
route.setStatus(CommonStatusEnum.DISABLE.getStatus());
|
routeMapper.insert(route);
|
respVO.getCreateCodes().add(importItem.getCode());
|
} else if (updateSupport) {
|
// 2.1.2 更新(状态由启用/禁用接口单独维护,导入不修改状态)
|
MesProRouteDO updateObj = BeanUtils.toBean(importItem, MesProRouteDO.class);
|
updateObj.setId(existRoute.getId());
|
updateObj.setCode(existRoute.getCode());
|
routeMapper.updateById(updateObj);
|
respVO.getUpdateCodes().add(importItem.getCode());
|
} else {
|
respVO.getFailureCodes().put(key, "工艺路线编码已存在");
|
}
|
});
|
return respVO;
|
}
|
|
}
|