xiaoyi
22 小时以前 8782476e9cb4480be5da588888d08022b7942d0c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
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);
    }
 
}