xiaoyi
6 天以前 154ea8973b5b2498c2a30044b3445a8adfad8634
feat(mes): 移植 OEE 分析功能(需求18 OEE分析)

## 背景
需求表第18条「设备管理模块缺少 OEE 分析」,长辉原本没有 OEE 统计能力。
从新高电力移植设备统计/ OEE 模块。

## 内容
- MesDvStatisticsController:设备统计汇总 /summary + OEE 统计 /oee
- OEE = 稼动率 × 性能开动率 × 良品率
- 稼动率 = 实际报工时长 / 计划时长
- 性能开动率 = 实际产量 / 计划产量
- 良品率 = 合格数 / 实际产量
- 支持按时间区间过滤(基于任务完成时间)
- MesDvOeeRespVO / MesDvStatisticsRespVO:对应响应 VO

## 验证(本地)
后端编译通过;接口实测 /oee code=0,overallOee=0.14%、taskCount=67;
带区间查询 taskCount=58;/summary 设备总数15。
已添加3个文件
342 ■■■■■ 文件已修改
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/statistics/MesDvStatisticsController.java 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/statistics/vo/MesDvOeeRespVO.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/statistics/vo/MesDvStatisticsRespVO.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/statistics/MesDvStatisticsController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,218 @@
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.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 = "设备统计汇总(台账/ç»´ä¿®/保养/类型分布)")
    public CommonResult<MesDvStatisticsRespVO> getSummary() {
        MesDvStatisticsRespVO vo = new MesDvStatisticsRespVO();
        // 1. è®¾å¤‡å°è´¦ç»Ÿè®¡
        Long total = machineryMapper.selectCount();
        Long producing = machineryMapper.selectCount(new LambdaQueryWrapperX<MesDvMachineryDO>()
                .eq(MesDvMachineryDO::getStatus, MesDvMachineryStatusEnum.PRODUCING.getStatus()));
        Long stopped = machineryMapper.selectCount(new LambdaQueryWrapperX<MesDvMachineryDO>()
                .eq(MesDvMachineryDO::getStatus, MesDvMachineryStatusEnum.STOP.getStatus()));
        Long maintaining = machineryMapper.selectCount(new LambdaQueryWrapperX<MesDvMachineryDO>()
                .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<MesDvRepairDO>()
                .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<MesDvMaintenRecordDO>()
                .eq(MesDvMaintenRecordDO::getStatus, cn.iocoder.yudao.module.mes.enums.dv.MesDvMaintenRecordStatusEnum.SUBMITTED.getStatus())));
        // 4. æŒ‰è®¾å¤‡ç±»åž‹åˆ†å¸ƒ
        List<MesDvMachineryDO> machineryList = machineryMapper.selectList();
        Map<Long, Long> typeCountMap = new HashMap<>();
        for (MesDvMachineryDO machinery : machineryList) {
            if (machinery.getMachineryTypeId() == null) {
                continue;
            }
            typeCountMap.merge(machinery.getMachineryTypeId(), 1L, Long::sum);
        }
        List<MesDvMachineryTypeDO> typeList = machineryTypeMapper.selectList();
        Map<Long, String> typeNameMap = new HashMap<>();
        for (MesDvMachineryTypeDO type : typeList) {
            typeNameMap.put(type.getId(), type.getName());
        }
        List<MesDvStatisticsRespVO.TypeItem> 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 ç»Ÿè®¡ï¼ˆç¨¼åŠ¨çŽ‡Ã—æ€§èƒ½å¼€åŠ¨çŽ‡Ã—è‰¯å“çŽ‡ï¼‰")
    public CommonResult<MesDvOeeRespVO> 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<MesProTaskDO> allTasks = taskMapper.selectList(new LambdaQueryWrapperX<MesProTaskDO>()
                .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<Long, List<MesProTaskDO>> tasksByWorkstation = new HashMap<>();
        for (MesProTaskDO task : allTasks) {
            if (task.getWorkstationId() == null) {
                continue;
            }
            tasksByWorkstation.computeIfAbsent(task.getWorkstationId(), k -> new ArrayList<>()).add(task);
        }
        List<MesDvOeeRespVO.MachineryOee> items = new ArrayList<>();
        for (MesDvMachineryDO machinery : machineryMapper.selectList()) {
            if (machinery.getWorkstationId() == null) {
                continue; // æœªç»‘定工作站,无法关联任务
            }
            List<MesProTaskDO> 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<MesProTaskDO> 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;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/statistics/vo/MesDvOeeRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.statistics.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - MES è®¾å¤‡ OEE ç»Ÿè®¡ Response VO")
@Data
public class MesDvOeeRespVO {
    @Schema(description = "整体 OEE(%)")
    private Double overallOee;
    @Schema(description = "整体稼动率(%)")
    private Double overallAvailability;
    @Schema(description = "整体性能开动率(%)")
    private Double overallPerformance;
    @Schema(description = "整体良品率(%)")
    private Double overallQuality;
    @Schema(description = "统计任务总数")
    private Long taskCount;
    @Schema(description = "按设备 OEE æ˜Žç»†")
    private List<MachineryOee> items;
    @Schema(description = "设备 OEE æ˜Žç»†é¡¹")
    @Data
    public static class MachineryOee {
        @Schema(description = "设备编号")
        private Long machineryId;
        @Schema(description = "设备编码")
        private String machineryCode;
        @Schema(description = "设备名称")
        private String machineryName;
        @Schema(description = "OEE(%)")
        private Double oee;
        @Schema(description = "稼动率(%)")
        private Double availability;
        @Schema(description = "性能开动率(%)")
        private Double performance;
        @Schema(description = "良品率(%)")
        private Double quality;
        @Schema(description = "关联任务数")
        private Long taskCount;
        @Schema(description = "计划产量")
        private java.math.BigDecimal plannedQuantity;
        @Schema(description = "实际产量")
        private java.math.BigDecimal producedQuantity;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/statistics/vo/MesDvStatisticsRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.statistics.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - MES è®¾å¤‡ç»Ÿè®¡ Response VO")
@Data
public class MesDvStatisticsRespVO {
    @Schema(description = "设备总数")
    private Long totalMachinery;
    @Schema(description = "生产中设备数")
    private Long producingCount;
    @Schema(description = "停机设备数")
    private Long stoppedCount;
    @Schema(description = "维护中设备数")
    private Long maintainingCount;
    @Schema(description = "设备故障率(停机+维护 / æ€»æ•° * 100,%)")
    private Double faultRate;
    @Schema(description = "维修单总数")
    private Long totalRepair;
    @Schema(description = "维修中/待验收单数")
    private Long processingRepair;
    @Schema(description = "已确认维修单数")
    private Long finishedRepair;
    @Schema(description = "保养记录总数")
    private Long totalMainten;
    @Schema(description = "已完成保养记录数")
    private Long finishedMainten;
    @Schema(description = "按设备类型分布")
    private List<TypeItem> typeDistribution;
    @Schema(description = "设备类型分布项")
    @Data
    public static class TypeItem {
        @Schema(description = "设备类型编号")
        private Long machineryTypeId;
        @Schema(description = "设备类型名称")
        private String machineryTypeName;
        @Schema(description = "设备数量")
        private Long count;
    }
}