package cn.iocoder.yudao.module.mes.service.safety; import cn.iocoder.yudao.framework.common.pojo.*; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.risk.SafetyRiskSourceSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.collect.MesCollectRealtimeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyRiskSourceDO; import cn.iocoder.yudao.module.mes.dal.mysql.collect.MesCollectRealtimeMapper; import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyRiskSourceMapper; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; import tools.jackson.core.type.TypeReference; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; /** * 安全生产 - 危险源监测。 * *

支持两种数据来源: *

    *
  1. 人工录入 currentValue:仅表示人工抄录的当前值;
  2. *
  3. 绑定设备(machineryId + metricKey):由数采实时表自动取值,复用现有设备采集,不引入新接口。
  4. *
*/ @Slf4j @Service public class SafetyRiskSourceService { @Resource private SafetyRiskSourceMapper mapper; @Resource private MesCollectRealtimeMapper realtimeMapper; public PageResult page(PageParam page, String name) { return mapper.selectPage(page, new LambdaQueryWrapperX().likeIfPresent(SafetyRiskSourceDO::getName,name).orderByDesc(SafetyRiskSourceDO::getId)); } @Transactional(rollbackFor=Exception.class) public Long save(SafetyRiskSourceSaveReqVO req) { SafetyRiskSourceDO old = req.getId()==null ? null : mapper.selectById(req.getId()); if (req.getId()!=null && old==null) throw invalidParamException("危险源不存在"); if (req.getMachineryId()!=null && (req.getMetricKey()==null || req.getMetricKey().isBlank())) throw invalidParamException("绑定设备后必须填写数采指标键"); if (req.getMachineryId()==null && req.getMetricKey()!=null && !req.getMetricKey().isBlank()) throw invalidParamException("填写数采指标键前请先绑定设备"); String status; try { status=SafetyRiskThreshold.evaluate(req.getCurrentValue(),req.getLowerLimit(),req.getUpperLimit()); } catch (IllegalArgumentException e) { throw invalidParamException(e.getMessage()); } var row=BeanUtils.toBean(req,SafetyRiskSourceDO.class); row.setRiskStatus(status); row.setObservedAt(req.getCurrentValue()==null ? null : old==null || !Objects.equals(old.getCurrentValue(),req.getCurrentValue()) ? LocalDateTime.now() : old.getObservedAt()); if (old==null) mapper.insert(row); else mapper.updateById(row); return row.getId(); } public void delete(Long id) { if (mapper.selectById(id)==null) throw invalidParamException("危险源不存在"); mapper.deleteById(id); } /** * 用设备采集数据刷新所有已绑定设备的危险源。 * *

取值顺序:data_json 中 metricKey 对应字段;未取到时置 UNKNOWN 并保留上次观测时间不变。 * * @return 本次刷新后处于 ALARM 状态的危险源 */ @Transactional(rollbackFor=Exception.class) public List refreshFromCollect() { List bound = mapper.selectList(new LambdaQueryWrapperX() .isNotNull(SafetyRiskSourceDO::getMachineryId)); List alarms = new ArrayList<>(); Map cache = new HashMap<>(); for (SafetyRiskSourceDO source : bound) { MesCollectRealtimeDO realtime = cache.computeIfAbsent(source.getMachineryId(), id -> realtimeMapper.selectOne(new LambdaQueryWrapperX() .eq(MesCollectRealtimeDO::getMachineryId, id) .orderByDesc(MesCollectRealtimeDO::getCollectTime).last("LIMIT 1"))); BigDecimal value = realtime == null ? null : extractMetric(realtime, source.getMetricKey()); String status; try { status = SafetyRiskThreshold.evaluate(value, source.getLowerLimit(), source.getUpperLimit()); } catch (IllegalArgumentException e) { log.warn("危险源[{}]阈值配置非法:{}", source.getName(), e.getMessage()); continue; } if (value == null) { // 取不到采集值:不覆盖历史观测时间,避免伪造数据 if (!"UNKNOWN".equals(source.getRiskStatus())) { var upd = new SafetyRiskSourceDO(); upd.setId(source.getId()); upd.setRiskStatus(status); mapper.updateById(upd); source.setRiskStatus(status); } continue; } var upd = new SafetyRiskSourceDO(); upd.setId(source.getId()); upd.setCurrentValue(value); upd.setObservedAt(realtime.getCollectTime() == null ? LocalDateTime.now() : realtime.getCollectTime()); upd.setRiskStatus(status); mapper.updateById(upd); source.setCurrentValue(value); source.setObservedAt(upd.getObservedAt()); source.setRiskStatus(status); if ("ALARM".equals(status)) alarms.add(source); } return alarms; } /** 从实时采集记录中提取指标值;优先 data_json,其次复用实时表的标准字段。 */ BigDecimal extractMetric(MesCollectRealtimeDO realtime, String metricKey) { if (metricKey == null || metricKey.isBlank() || realtime == null) return null; String json = realtime.getDataJson(); if (json != null && !json.isBlank()) { try { Map map = JsonUtils.parseObjectQuietly(json, new TypeReference>() {}); Object raw = map == null ? null : map.get(metricKey); if (raw != null) { if (raw instanceof Number n) return new BigDecimal(n.toString()); String text = raw.toString().trim(); if (!text.isEmpty()) return new BigDecimal(text); } } catch (Exception e) { log.debug("解析数采 data_json 失败,machineryId={}:{}", realtime.getMachineryId(), e.getMessage()); } } // 兼容:metricKey 直接取实时表字段(ot 运行时长等) return switch (metricKey) { case "ot" -> realtime.getOt(); case "sts" -> realtime.getSts() == null ? null : BigDecimal.valueOf(realtime.getSts()); case "opm" -> realtime.getOpm() == null ? null : BigDecimal.valueOf(realtime.getOpm()); default -> null; }; } }