package cn.iocoder.yudao.module.mes.service.collect;
|
|
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.collect.vo.*;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.collect.*;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
|
import cn.iocoder.yudao.module.mes.dal.mysql.collect.*;
|
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper;
|
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectAlarmStatusEnum;
|
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectOperateModeEnum;
|
import cn.iocoder.yudao.module.mes.enums.collect.MesCollectRunStatusEnum;
|
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
|
import jakarta.annotation.Resource;
|
import org.springframework.stereotype.Service;
|
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import cn.iocoder.yudao.module.mes.framework.collect.config.MesCollectProperties;
|
import java.time.Duration;
|
import java.util.ArrayList;
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
|
import java.time.LocalDateTime;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.COLLECT_ALARM_ALREADY_CLEARED;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.COLLECT_ALARM_NOT_EXISTS;
|
|
/**
|
* MES 数采数据查询 Service 实现类
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
public class MesCollectQueryService {
|
|
/**
|
* 超过该时长未采集数据视为离线
|
*/
|
private static final long ONLINE_TIMEOUT_MINUTES = 5L;
|
|
@Resource
|
private MesDvMachineryMapper machineryMapper;
|
@Resource
|
private MesCollectRealtimeMapper realtimeMapper;
|
@Resource
|
private MesCollectRealtimeHistoryMapper realtimeHistoryMapper;
|
@Resource
|
private MesCollectCycleMapper cycleMapper;
|
@Resource
|
private MesCollectTechMapper techMapper;
|
@Resource
|
private MesCollectOperateLogMapper collectOperateLogMapper;
|
@Resource
|
private MesCollectAlarmMapper alarmMapper;
|
@Resource
|
private MesProWorkOrderMapper workOrderMapper;
|
@Resource
|
private MesCollectTaskResolver taskResolver;
|
@Resource
|
private MesMdWorkstationService workstationService;
|
@Resource
|
private MesCollectProperties collectProperties;
|
|
/** 基于保留的数采历史计算离线区间,首次采样之前不作推断。 */
|
public MesCollectOfflineResultRespVO getOfflineIntervals(String deviceCode, LocalDateTime start, LocalDateTime end) {
|
LocalDateTime now = LocalDateTime.now();
|
if (deviceCode == null || deviceCode.isBlank() || start == null || end == null || !start.isBefore(end)
|
|| end.isAfter(now) || Duration.between(start, end).compareTo(Duration.ofDays(7)) > 0) {
|
throw invalidParamException("请选择有效的设备和时间范围,单次最多查询七天,结束时间不能晚于当前时间");
|
}
|
if (start.isBefore(now.minusDays(collectProperties.getRetention().getRealtimeDays()))) {
|
throw invalidParamException("查询时间超出数采历史保留期,无法可靠计算停机记录");
|
}
|
var prior = realtimeHistoryMapper.selectOne(new LambdaQueryWrapperX<MesCollectRealtimeHistoryDO>()
|
.eq(MesCollectRealtimeHistoryDO::getDeviceCode, deviceCode)
|
.ge(MesCollectRealtimeHistoryDO::getCollectTime, now.minusDays(collectProperties.getRetention().getRealtimeDays()))
|
.le(MesCollectRealtimeHistoryDO::getCollectTime, start)
|
.orderByDesc(MesCollectRealtimeHistoryDO::getCollectTime).last("LIMIT 1"));
|
var history = realtimeHistoryMapper.selectList(new LambdaQueryWrapperX<MesCollectRealtimeHistoryDO>()
|
.eq(MesCollectRealtimeHistoryDO::getDeviceCode, deviceCode)
|
.gt(MesCollectRealtimeHistoryDO::getCollectTime, start)
|
.le(MesCollectRealtimeHistoryDO::getCollectTime, end)
|
.orderByAsc(MesCollectRealtimeHistoryDO::getCollectTime).last("LIMIT 100001"));
|
if (history.size() > 100000) throw invalidParamException("采样数据过多,请缩短查询时间范围");
|
List<LocalDateTime> samples = new ArrayList<>();
|
if (prior != null) samples.add(prior.getCollectTime());
|
history.forEach(item -> samples.add(item.getCollectTime()));
|
var intervals = CollectOfflinePolicy.calculate(samples, start, end, Duration.ofMinutes(ONLINE_TIMEOUT_MINUTES)).stream()
|
.map(item -> new MesCollectOfflineIntervalRespVO(item.start(), item.end(), Duration.between(item.start(), item.end()).getSeconds()))
|
.toList();
|
return new MesCollectOfflineResultRespVO(samples.isEmpty() ? null : samples.getFirst(), intervals);
|
}
|
|
/**
|
* 设备停机明细报表:按采集间隔推算每台设备的停机区间与合计。
|
*
|
* <p>与单设备离线查询保持同一口径(在线超时阈值、保留期、七天上限),
|
* 供 OEE 报表定位停机短板使用。
|
*/
|
public cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectDowntimeDetailRespVO
|
getDowntimeDetail(LocalDateTime start, LocalDateTime end) {
|
LocalDateTime now = LocalDateTime.now();
|
if (start == null || end == null || !start.isBefore(end) || end.isAfter(now)
|
|| Duration.between(start, end).compareTo(Duration.ofDays(31)) > 0) {
|
throw invalidParamException("请选择有效的时间范围,单次最多查询三十一天,结束时间不能晚于当前时间");
|
}
|
if (start.isBefore(now.minusDays(collectProperties.getRetention().getRealtimeDays()))) {
|
throw invalidParamException("查询时间超出数采历史保留期,无法可靠计算停机记录");
|
}
|
var result = new cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectDowntimeDetailRespVO();
|
result.setStartTime(start);
|
result.setEndTime(end);
|
result.setTimeoutMinutes((int) ONLINE_TIMEOUT_MINUTES);
|
long totalMinutes = Duration.between(start, end).toMinutes();
|
result.setTotalMinutes(totalMinutes);
|
|
var machineries = machineryMapper.selectListWithCollectDeviceCode();
|
List<cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectDowntimeDetailRespVO.Item> items = new ArrayList<>();
|
long sumDowntime = 0L;
|
for (var machinery : machineries) {
|
String deviceCode = machinery.getCollectDeviceCode();
|
var prior = realtimeHistoryMapper.selectOne(new LambdaQueryWrapperX<MesCollectRealtimeHistoryDO>()
|
.eq(MesCollectRealtimeHistoryDO::getDeviceCode, deviceCode)
|
.ge(MesCollectRealtimeHistoryDO::getCollectTime, now.minusDays(collectProperties.getRetention().getRealtimeDays()))
|
.le(MesCollectRealtimeHistoryDO::getCollectTime, start)
|
.orderByDesc(MesCollectRealtimeHistoryDO::getCollectTime).last("LIMIT 1"));
|
var history = realtimeHistoryMapper.selectList(new LambdaQueryWrapperX<MesCollectRealtimeHistoryDO>()
|
.eq(MesCollectRealtimeHistoryDO::getDeviceCode, deviceCode)
|
.gt(MesCollectRealtimeHistoryDO::getCollectTime, start)
|
.le(MesCollectRealtimeHistoryDO::getCollectTime, end)
|
.orderByAsc(MesCollectRealtimeHistoryDO::getCollectTime).last("LIMIT 100001"));
|
if (history.size() > 100000) throw invalidParamException("采样数据过多,请缩短查询时间范围");
|
List<LocalDateTime> samples = new ArrayList<>();
|
int count = 0;
|
if (prior != null) { samples.add(prior.getCollectTime()); count++; }
|
history.forEach(item -> samples.add(item.getCollectTime()));
|
count += history.size();
|
var intervals = CollectOfflinePolicy.calculate(samples, start, end, Duration.ofMinutes(ONLINE_TIMEOUT_MINUTES)).stream()
|
.map(i -> new cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectDowntimeDetailRespVO.Interval(
|
i.start(), i.end(), Duration.between(i.start(), i.end()).getSeconds()))
|
.toList();
|
long minutes = intervals.stream().mapToLong(i -> i.getSeconds() / 60).sum();
|
sumDowntime += minutes;
|
items.add(new cn.iocoder.yudao.module.mes.controller.admin.collect.vo.MesCollectDowntimeDetailRespVO.Item(
|
machinery.getId(), machinery.getCode(), machinery.getName(), deviceCode,
|
count, intervals.size(), minutes, intervals));
|
}
|
items.sort((a, b) -> Long.compare(b.getDowntimeMinutes(), a.getDowntimeMinutes()));
|
result.setItems(items);
|
result.setDowntimeMinutes(sumDowntime);
|
result.setDowntimeRate(totalMinutes > 0 && !items.isEmpty()
|
? Math.round(sumDowntime * 10000D / (totalMinutes * items.size())) / 100D : 0D);
|
return result;
|
}
|
|
/**
|
* 获取已配置数采编码的设备实时状态列表
|
*/
|
public List<MesCollectDeviceRespVO> getDeviceList() {
|
List<MesDvMachineryDO> machineries = machineryMapper.selectListWithCollectDeviceCode();
|
Map<Long, MesMdWorkstationDO> workstationMap = workstationService.getWorkstationMap(
|
machineries.stream().map(MesDvMachineryDO::getWorkstationId).filter(Objects::nonNull).toList());
|
return machineries.stream().map(machinery -> {
|
MesCollectDeviceRespVO vo = new MesCollectDeviceRespVO();
|
vo.setMachineryId(machinery.getId());
|
vo.setMachineryCode(machinery.getCode());
|
vo.setMachineryName(machinery.getName());
|
vo.setCollectDeviceCode(machinery.getCollectDeviceCode());
|
vo.setCollectEnabled(Boolean.TRUE.equals(machinery.getCollectEnabled()));
|
MesMdWorkstationDO workstation = workstationMap.get(machinery.getWorkstationId());
|
if (workstation != null) {
|
vo.setWorkstationId(workstation.getId());
|
vo.setWorkstationName(workstation.getName());
|
}
|
|
MesCollectRealtimeDO realtime = realtimeMapper.selectByDeviceCode(machinery.getCollectDeviceCode());
|
if (realtime != null) {
|
vo.setLastCollectTime(realtime.getCollectTime());
|
vo.setOnline(realtime.getCollectTime() != null
|
&& realtime.getCollectTime().isAfter(LocalDateTime.now().minusMinutes(ONLINE_TIMEOUT_MINUTES)));
|
vo.setSts(realtime.getSts());
|
vo.setStsName(MesCollectRunStatusEnum.getNameByStatus(realtime.getSts()));
|
vo.setOpm(realtime.getOpm());
|
vo.setOpmName(MesCollectOperateModeEnum.getNameByMode(realtime.getOpm()));
|
vo.setOt(realtime.getOt());
|
vo.setTemps(realtime.getTemps());
|
// 网关格式报文的结构化字段为空,前端「当前数据」列直接展示原始 JSON
|
vo.setDataJson(realtime.getDataJson());
|
} else {
|
vo.setOnline(false);
|
}
|
|
MesProTaskDO task = taskResolver.resolve(machinery.getWorkstationId());
|
if (task != null) {
|
vo.setTaskId(task.getId());
|
vo.setTaskCode(task.getCode());
|
vo.setTaskName(task.getName());
|
vo.setWorkOrderId(task.getWorkOrderId());
|
MesProWorkOrderDO workOrder = task.getWorkOrderId() != null
|
? workOrderMapper.selectById(task.getWorkOrderId()) : null;
|
if (workOrder != null) {
|
vo.setWorkOrderCode(workOrder.getCode());
|
}
|
}
|
vo.setActiveAlarmCount(alarmMapper.selectActiveCountByDeviceCode(machinery.getCollectDeviceCode()));
|
return vo;
|
}).toList();
|
}
|
|
/**
|
* 获取指定数采设备的实时状态快照
|
*/
|
public MesCollectRealtimeRespVO getRealtime(String deviceCode) {
|
MesCollectRealtimeDO realtime = realtimeMapper.selectByDeviceCode(deviceCode);
|
if (realtime == null) {
|
return null;
|
}
|
MesCollectRealtimeRespVO vo = BeanUtils.toBean(realtime, MesCollectRealtimeRespVO.class);
|
vo.setStsName(MesCollectRunStatusEnum.getNameByStatus(realtime.getSts()));
|
vo.setOpmName(MesCollectOperateModeEnum.getNameByMode(realtime.getOpm()));
|
return vo;
|
}
|
|
/**
|
* 分页查询实时状态历史
|
*/
|
public PageResult<MesCollectRealtimeRespVO> getRealtimeHistoryPage(MesCollectRealtimeHistoryPageReqVO reqVO) {
|
PageResult<MesCollectRealtimeHistoryDO> pageResult = realtimeHistoryMapper.selectPage(reqVO);
|
PageResult<MesCollectRealtimeRespVO> result = BeanUtils.toBean(pageResult, MesCollectRealtimeRespVO.class);
|
result.getList().forEach(vo -> {
|
vo.setStsName(MesCollectRunStatusEnum.getNameByStatus(vo.getSts()));
|
vo.setOpmName(MesCollectOperateModeEnum.getNameByMode(vo.getOpm()));
|
});
|
return result;
|
}
|
|
/**
|
* 分页查询周期过程数据
|
*/
|
public PageResult<MesCollectCycleRespVO> getCyclePage(MesCollectCyclePageReqVO reqVO) {
|
return BeanUtils.toBean(cycleMapper.selectPage(reqVO), MesCollectCycleRespVO.class);
|
}
|
|
/**
|
* 获取单条周期过程数据
|
*/
|
public MesCollectCycleRespVO getCycle(Long id) {
|
MesCollectCycleDO cycle = cycleMapper.selectById(id);
|
return cycle == null ? null : BeanUtils.toBean(cycle, MesCollectCycleRespVO.class);
|
}
|
|
/**
|
* 获取指定数采设备的最新工艺参数快照
|
*/
|
public MesCollectTechRespVO getTech(String deviceCode) {
|
MesCollectTechDO tech = techMapper.selectByDeviceCode(deviceCode);
|
return tech == null ? null : BeanUtils.toBean(tech, MesCollectTechRespVO.class);
|
}
|
|
/**
|
* 分页查询参数变更日志
|
*/
|
public PageResult<MesCollectOperateLogRespVO> getOperateLogPage(MesCollectOperateLogPageReqVO reqVO) {
|
return BeanUtils.toBean(collectOperateLogMapper.selectPage(reqVO), MesCollectOperateLogRespVO.class);
|
}
|
|
/**
|
* 分页查询报警记录
|
*/
|
public PageResult<MesCollectAlarmRespVO> getAlarmPage(MesCollectAlarmPageReqVO reqVO) {
|
PageResult<MesCollectAlarmDO> pageResult = alarmMapper.selectPage(reqVO);
|
PageResult<MesCollectAlarmRespVO> result = BeanUtils.toBean(pageResult, MesCollectAlarmRespVO.class);
|
result.getList().forEach(vo -> vo.setStatusName(MesCollectAlarmStatusEnum.ACTIVE.getStatus().equals(vo.getStatus())
|
? MesCollectAlarmStatusEnum.ACTIVE.getName() : MesCollectAlarmStatusEnum.CLEARED.getName()));
|
return result;
|
}
|
|
/**
|
* 人工确认消除报警
|
*/
|
public void ackAlarm(Long id) {
|
MesCollectAlarmDO alarm = alarmMapper.selectById(id);
|
if (alarm == null) {
|
throw exception(COLLECT_ALARM_NOT_EXISTS);
|
}
|
if (MesCollectAlarmStatusEnum.CLEARED.getStatus().equals(alarm.getStatus())) {
|
throw exception(COLLECT_ALARM_ALREADY_CLEARED);
|
}
|
MesCollectAlarmDO updateObj = new MesCollectAlarmDO();
|
updateObj.setId(id);
|
updateObj.setStatus(MesCollectAlarmStatusEnum.CLEARED.getStatus());
|
updateObj.setClearTime(LocalDateTime.now());
|
alarmMapper.updateById(updateObj);
|
}
|
|
}
|