package cn.iocoder.yudao.module.mes.controller.admin.dv.statistics; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.mes.controller.admin.dv.statistics.vo.MesDvOeeRespVO; import cn.iocoder.yudao.module.mes.controller.admin.dv.statistics.vo.MesDvStatisticsRespVO; import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO; import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryTypeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.dv.maintenrecord.MesDvMaintenRecordDO; import cn.iocoder.yudao.module.mes.dal.dataobject.dv.repair.MesDvRepairDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO; import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper; import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryTypeMapper; import cn.iocoder.yudao.module.mes.dal.mysql.dv.maintenrecord.MesDvMaintenRecordMapper; import cn.iocoder.yudao.module.mes.dal.mysql.dv.repair.MesDvRepairMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper; import cn.iocoder.yudao.module.mes.enums.dv.MesDvMachineryStatusEnum; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - MES 设备统计") @RestController @RequestMapping("/mes/dv/statistics") @Validated public class MesDvStatisticsController { @Resource private MesDvMachineryMapper machineryMapper; @Resource private MesDvMachineryTypeMapper machineryTypeMapper; @Resource private MesDvRepairMapper repairMapper; @Resource private MesDvMaintenRecordMapper maintenRecordMapper; @Resource private MesProTaskMapper taskMapper; @GetMapping("/summary") @Operation(summary = "设备统计汇总(台账/维修/保养/类型分布)") @PreAuthorize("@ss.hasPermission('mes:dv-machinery:query')") public CommonResult getSummary() { MesDvStatisticsRespVO vo = new MesDvStatisticsRespVO(); // 1. 设备台账统计 Long total = machineryMapper.selectCount(); Long producing = machineryMapper.selectCount(new LambdaQueryWrapperX() .eq(MesDvMachineryDO::getStatus, MesDvMachineryStatusEnum.PRODUCING.getStatus())); Long stopped = machineryMapper.selectCount(new LambdaQueryWrapperX() .eq(MesDvMachineryDO::getStatus, MesDvMachineryStatusEnum.STOP.getStatus())); Long maintaining = machineryMapper.selectCount(new LambdaQueryWrapperX() .eq(MesDvMachineryDO::getStatus, MesDvMachineryStatusEnum.MAINTENANCE.getStatus())); vo.setTotalMachinery(total); vo.setProducingCount(producing); vo.setStoppedCount(stopped); vo.setMaintainingCount(maintaining); vo.setFaultRate(total != null && total > 0 ? (double) (stopped + maintaining) / total * 100 : 0D); // 2. 维修统计 vo.setTotalRepair(repairMapper.selectCount()); vo.setFinishedRepair(repairMapper.selectCount(new LambdaQueryWrapperX() .eq(MesDvRepairDO::getStatus, cn.iocoder.yudao.module.mes.enums.dv.MesDvRepairStatusEnum.FINISHED.getStatus()))); vo.setProcessingRepair((vo.getTotalRepair() != null ? vo.getTotalRepair() : 0L) - (vo.getFinishedRepair() != null ? vo.getFinishedRepair() : 0L)); // 3. 保养统计 vo.setTotalMainten(maintenRecordMapper.selectCount()); vo.setFinishedMainten(maintenRecordMapper.selectCount(new LambdaQueryWrapperX() .eq(MesDvMaintenRecordDO::getStatus, cn.iocoder.yudao.module.mes.enums.dv.MesDvMaintenRecordStatusEnum.SUBMITTED.getStatus()))); // 4. 按设备类型分布 List machineryList = machineryMapper.selectList(); Map typeCountMap = new HashMap<>(); for (MesDvMachineryDO machinery : machineryList) { if (machinery.getMachineryTypeId() == null) { continue; } typeCountMap.merge(machinery.getMachineryTypeId(), 1L, Long::sum); } List typeList = machineryTypeMapper.selectList(); Map typeNameMap = new HashMap<>(); for (MesDvMachineryTypeDO type : typeList) { typeNameMap.put(type.getId(), type.getName()); } List typeDistribution = new ArrayList<>(); typeCountMap.forEach((typeId, count) -> { MesDvStatisticsRespVO.TypeItem item = new MesDvStatisticsRespVO.TypeItem(); item.setMachineryTypeId(typeId); item.setMachineryTypeName(typeNameMap.get(typeId)); item.setCount(count); typeDistribution.add(item); }); vo.setTypeDistribution(typeDistribution); return success(vo); } @GetMapping("/oee") @Operation(summary = "设备 OEE 统计(稼动率×性能开动率×良品率)") @PreAuthorize("@ss.hasPermission('mes:dv-machinery:query')") public CommonResult getOee( @RequestParam(value = "startTime", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime, @RequestParam(value = "endTime", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime) { // 1. 查询统计区间内任务(按完成时间过滤;不传区间则统计全部) List allTasks = taskMapper.selectList(new LambdaQueryWrapperX() .betweenIfPresent(MesProTaskDO::getFinishDate, new LocalDateTime[]{startTime, endTime})); // 2. 整体 OEE MesDvOeeRespVO vo = new MesDvOeeRespVO(); OeeCalc overall = calcOee(allTasks); vo.setOverallOee(overall.oee); vo.setOverallAvailability(overall.availability); vo.setOverallPerformance(overall.performance); vo.setOverallQuality(overall.quality); vo.setTaskCount((long) allTasks.size()); // 3. 按设备聚合:任务按工作站分组,设备通过 workstationId 关联工作站 Map> tasksByWorkstation = new HashMap<>(); for (MesProTaskDO task : allTasks) { if (task.getWorkstationId() == null) { continue; } tasksByWorkstation.computeIfAbsent(task.getWorkstationId(), k -> new ArrayList<>()).add(task); } List items = new ArrayList<>(); for (MesDvMachineryDO machinery : machineryMapper.selectList()) { if (machinery.getWorkstationId() == null) { continue; // 未绑定工作站,无法关联任务 } List tasks = tasksByWorkstation.getOrDefault(machinery.getWorkstationId(), Collections.emptyList()); OeeCalc calc = calcOee(tasks); MesDvOeeRespVO.MachineryOee item = new MesDvOeeRespVO.MachineryOee(); item.setMachineryId(machinery.getId()); item.setMachineryCode(machinery.getCode()); item.setMachineryName(machinery.getName()); item.setOee(calc.oee); item.setAvailability(calc.availability); item.setPerformance(calc.performance); item.setQuality(calc.quality); item.setTaskCount((long) tasks.size()); item.setPlannedQuantity(calc.plannedQuantity); item.setProducedQuantity(calc.producedQuantity); items.add(item); } vo.setItems(items); return success(vo); } private static OeeCalc calcOee(List tasks) { double totalPlannedHours = 0, totalActualHours = 0; BigDecimal totalPlannedQty = BigDecimal.ZERO, totalProducedQty = BigDecimal.ZERO, totalQualifyQty = BigDecimal.ZERO; for (MesProTaskDO task : tasks) { if (task.getDuration() != null) { totalPlannedHours += task.getDuration() * 8D; // 1 工作日 = 8 小时 } if (task.getFirstFeedbackTime() != null && task.getLastFeedbackTime() != null && task.getLastFeedbackTime().isAfter(task.getFirstFeedbackTime())) { totalActualHours += Duration.between(task.getFirstFeedbackTime(), task.getLastFeedbackTime()).toMinutes() / 60D; } if (task.getQuantity() != null) { totalPlannedQty = totalPlannedQty.add(task.getQuantity()); } if (task.getProducedQuantity() != null) { totalProducedQty = totalProducedQty.add(task.getProducedQuantity()); } if (task.getQualifyQuantity() != null) { totalQualifyQty = totalQualifyQty.add(task.getQualifyQuantity()); } } OeeCalc calc = new OeeCalc(); // 稼动率 = 实际报工时长 / 计划时长(无计划时长时按 1 处理) calc.availability = percent(totalPlannedHours > 0 ? Math.min(1D, totalActualHours / totalPlannedHours) : 1D); // 性能开动率 = 实际产量 / 计划产量 calc.performance = totalPlannedQty.compareTo(BigDecimal.ZERO) > 0 ? percent(Math.min(1D, totalProducedQty.divide(totalPlannedQty, 4, RoundingMode.HALF_UP).doubleValue())) : 0D; // 良品率 = 合格品 / 实际产量(无产量时按 1 处理) calc.quality = totalProducedQty.compareTo(BigDecimal.ZERO) > 0 ? percent(Math.min(1D, totalQualifyQty.divide(totalProducedQty, 4, RoundingMode.HALF_UP).doubleValue())) : 100D; // OEE(%) = 稼动率(%) × 性能(%) × 良品率(%) / 10000,保留两位小数 calc.oee = Math.round(calc.availability * calc.performance * calc.quality / 10000D * 100D) / 100D; calc.plannedQuantity = totalPlannedQty; calc.producedQuantity = totalProducedQty; return calc; } /** 比例(0-1) 转百分比,保留两位小数 */ private static double percent(double ratio) { return Math.round(ratio * 10000D) / 100D; } private static class OeeCalc { private Double availability; private Double performance; private Double quality; private Double oee; private BigDecimal plannedQuantity; private BigDecimal producedQuantity; } }