liu
20 小时以前 f57a9de2dbe69e7f867b39c25abdab58b401a2df
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
package cn.iocoder.yudao.module.mes.service.dv.telemetry;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.mes.controller.admin.dv.telemetry.vo.MesDvTelemetryCheckSummaryRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.telemetry.vo.MesDvTelemetryDeviceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.telemetry.vo.MesDvTelemetryLatestReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.telemetry.vo.MesDvTelemetryPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.telemetry.vo.MesDvTelemetryPullRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.telemetry.MesDvTelemetryDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.telemetry.MesDvTelemetryMapper;
import cn.iocoder.yudao.module.mes.framework.config.MesTelemetryProperties;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
 
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_TELEMETRY_PULL_ERROR;
 
/**
 * MES 设备数采遥测数据 Service 实现类
 *
 * @author 超级管理员
 */
@Service
@Slf4j
public class MesDvTelemetryServiceImpl implements MesDvTelemetryService {
 
    static final long MAX_FUTURE_SKEW_MINUTES = 5;
 
    @Resource
    private MesDvTelemetryMapper telemetryMapper;
    @Resource
    private MesTelemetryProperties telemetryProperties;
 
    @Override
    public PageResult<MesDvTelemetryDO> getTelemetryPage(MesDvTelemetryPageReqVO pageReqVO) {
        return telemetryMapper.selectPage(pageReqVO);
    }
 
    @Override
    public List<MesDvTelemetryDO> getLatestTelemetry(MesDvTelemetryLatestReqVO reqVO) {
        // 取最近一批数据,按设备+信号去重保留最新,用于实时监控视图
        List<MesDvTelemetryDO> list = telemetryMapper.selectLatest(reqVO, 5000);
        Map<String, MesDvTelemetryDO> latestMap = new LinkedHashMap<>();
        LocalDateTime queryTime = LocalDateTime.now();
        for (MesDvTelemetryDO row : list) {
            // 双重防护:即使数据库中已有未来时间的脏数据,也不能作为实时数据返回
            if (!isAcceptableTelemetryTime(row.getTelemetryDataTime(), queryTime)) {
                continue;
            }
            String key = StrUtil.blankToDefault(row.getTbDeviceId(), "")
                    + ":" + StrUtil.blankToDefault(row.getParamKeyName(), "");
            latestMap.putIfAbsent(key, row);
        }
        return new ArrayList<>(latestMap.values());
    }
 
    @Override
    public List<MesDvTelemetryCheckSummaryRespVO> getCheckSummary(MesDvTelemetryLatestReqVO reqVO) {
        List<MesDvTelemetryDO> list = getLatestTelemetry(reqVO);
        Map<String, MesDvTelemetryCheckSummaryRespVO> deviceMap = new LinkedHashMap<>();
        for (MesDvTelemetryDO row : list) {
            String key = StrUtil.blankToDefault(row.getTbDeviceId(), "");
            MesDvTelemetryCheckSummaryRespVO vo = deviceMap.get(key);
            if (vo == null) {
                vo = new MesDvTelemetryCheckSummaryRespVO();
                vo.setTbDeviceId(row.getTbDeviceId());
                vo.setDeviceName(row.getDeviceName());
                vo.setTotalCount(0);
                vo.setNormalCount(0);
                vo.setAnomalyCount(0);
                deviceMap.put(key, vo);
            }
            vo.setTotalCount(vo.getTotalCount() + 1);
            if (Boolean.TRUE.equals(row.getWhetherAnomaly())) {
                vo.setAnomalyCount(vo.getAnomalyCount() + 1);
            } else {
                vo.setNormalCount(vo.getNormalCount() + 1);
            }
        }
        List<MesDvTelemetryCheckSummaryRespVO> result = new ArrayList<>(deviceMap.values());
        for (MesDvTelemetryCheckSummaryRespVO vo : result) {
            if (vo.getTotalCount() != null && vo.getTotalCount() > 0) {
                BigDecimal rate = BigDecimal.valueOf(vo.getAnomalyCount() * 100.0 / vo.getTotalCount());
                vo.setAnomalyRate(rate.setScale(1, RoundingMode.HALF_UP));
            } else {
                vo.setAnomalyRate(BigDecimal.ZERO);
            }
        }
        return result;
    }
 
    @Override
    public List<MesDvTelemetryDeviceRespVO> getDeviceList() {
        List<MesDvTelemetryDeviceRespVO> result = new ArrayList<>();
        for (MesDvTelemetryDO row : telemetryMapper.selectDeviceList()) {
            MesDvTelemetryDeviceRespVO vo = new MesDvTelemetryDeviceRespVO();
            vo.setTbDeviceId(row.getTbDeviceId());
            vo.setDeviceName(row.getDeviceName());
            result.add(vo);
        }
        return result;
    }
 
    @Override
    public MesDvTelemetryPullRespVO pullTelemetry() {
        // 1. 校验配置
        if (StrUtil.isBlank(telemetryProperties.getBaseUrl())) {
            throw exception(DV_TELEMETRY_PULL_ERROR, "未配置数采服务地址 yudao.mes.telemetry.base-url");
        }
        String url = telemetryProperties.getBaseUrl() + telemetryProperties.getPath();
        // 2. 调用外部数采接口
        String responseText;
        try {
            responseText = HttpUtils.get(url, null);
        } catch (Exception e) {
            log.error("[pullTelemetry][调用数采接口失败,url({})]", url, e);
            throw exception(DV_TELEMETRY_PULL_ERROR, e.getMessage());
        }
        if (StrUtil.isBlank(responseText)) {
            throw exception(DV_TELEMETRY_PULL_ERROR, "数采接口返回为空");
        }
        // 3. 解析响应
        Map<?, ?> response = JsonUtils.parseObject(responseText, Map.class);
        if (response == null || !"200".equals(String.valueOf(response.get("code")))) {
            log.error("[pullTelemetry][数采接口返回异常,url({}),响应({})]", url, responseText);
            throw exception(DV_TELEMETRY_PULL_ERROR, "数采接口返回异常");
        }
        List<?> dataList = (List<?>) response.get("data");
        // 4. 无数据时返回空结果,不视为错误
        MesDvTelemetryPullRespVO result = new MesDvTelemetryPullRespVO();
        LocalDateTime pullTime = LocalDateTime.now();
        result.setPullTime(pullTime);
        if (CollUtil.isEmpty(dataList)) {
            result.setRecordCount(0);
            result.setDeviceCount(0);
            return result;
        }
        // 5. 转换并批量入库
        List<MesDvTelemetryDO> saveList = new ArrayList<>(dataList.size());
        for (Object rowObj : dataList) {
            if (!(rowObj instanceof Map)) {
                continue;
            }
            MesDvTelemetryDO data = convert((Map<?, ?>) rowObj);
            if (!isAcceptableTelemetryTime(data.getTelemetryDataTime(), pullTime)) {
                log.warn("[pullTelemetry][忽略异常遥测时间,deviceId({}),telemetryDataTime({}),pullTime({})]",
                        data.getTbDeviceId(), data.getTelemetryDataTime(), pullTime);
                continue;
            }
            data.setPullTime(pullTime);
            saveList.add(data);
        }
        if (CollUtil.isNotEmpty(saveList)) {
            telemetryMapper.insertBatch(saveList);
        }
        // 6. 返回统计
        result.setRecordCount(saveList.size());
        result.setDeviceCount((int) saveList.stream()
                .map(MesDvTelemetryDO::getTbDeviceId).distinct().count());
        return result;
    }
 
    private MesDvTelemetryDO convert(Map<?, ?> row) {
        MesDvTelemetryDO data = new MesDvTelemetryDO();
        data.setTbDeviceId(StrUtil.toStringOrNull(row.get("tbDeviceId")));
        data.setDeviceName(StrUtil.toStringOrNull(row.get("deviceName")));
        data.setParamName(StrUtil.toStringOrNull(row.get("paramName")));
        data.setParamKeyName(StrUtil.toStringOrNull(row.get("paramKeyName")));
        data.setStandardValue(StrUtil.toStringOrNull(row.get("standardValue")));
        data.setTimelyValue(StrUtil.toStringOrNull(row.get("timelyValue")));
        data.setAvgValue(StrUtil.toStringOrNull(row.get("avgValue")));
        data.setMaxValue(StrUtil.toStringOrNull(row.get("maxValue")));
        data.setMinValue(StrUtil.toStringOrNull(row.get("minValue")));
        data.setWhetherAnomaly(parseBoolean(row.get("whetherAnomaly")));
        data.setTelemetryDataTime(parseTime(row.get("telemetryDataTime")));
        data.setBillNo(StrUtil.toStringOrNull(row.get("billNo")));
        data.setShiftName(StrUtil.toStringOrNull(row.get("shiftName")));
        return data;
    }
 
    private Boolean parseBoolean(Object value) {
        if (value == null) {
            return null;
        }
        String str = String.valueOf(value).trim();
        if ("1".equals(str) || "true".equalsIgnoreCase(str) || "Y".equalsIgnoreCase(str)) {
            return Boolean.TRUE;
        }
        if ("0".equals(str) || "false".equalsIgnoreCase(str) || "N".equalsIgnoreCase(str)) {
            return Boolean.FALSE;
        }
        return null;
    }
 
    private LocalDateTime parseTime(Object value) {
        if (value == null || StrUtil.isBlank(String.valueOf(value))) {
            return null;
        }
        try {
            return DateUtil.parse(String.valueOf(value)).toLocalDateTime();
        } catch (Exception e) {
            log.warn("[parseTime][遥测时间解析失败,value({})]", value);
            return null;
        }
    }
 
    /**
     * 遥测时间必须存在,且不能明显晚于本次拉取/查询时间。
     * 保留 5 分钟容差,兼容设备时钟与应用服务器之间的轻微偏差。
     */
    static boolean isAcceptableTelemetryTime(LocalDateTime telemetryTime, LocalDateTime referenceTime) {
        return telemetryTime != null && referenceTime != null
                && !telemetryTime.isAfter(referenceTime.plusMinutes(MAX_FUTURE_SKEW_MINUTES));
    }
 
}