package cn.iocoder.yudao.module.mes.service.em.alert;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.util.StrUtil;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
import cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo.MesEmAlertHandleReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo.MesEmAlertPageReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo.MesEmAlertRespVO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.em.alert.MesEmAlertDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.em.meter.MesEmMeterDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.em.quota.MesEmQuotaDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.em.record.MesEmRecordDO;
|
import cn.iocoder.yudao.module.mes.dal.mysql.em.alert.MesEmAlertMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.em.meter.MesEmMeterMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.em.quota.MesEmQuotaMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.em.record.MesEmRecordMapper;
|
import cn.iocoder.yudao.module.mes.enums.em.*;
|
import jakarta.annotation.Resource;
|
import org.springframework.stereotype.Service;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.time.format.DateTimeFormatter;
|
import java.util.Collections;
|
import java.util.Comparator;
|
import java.util.List;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.EM_ALERT_ALREADY_HANDLED;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.EM_ALERT_NOT_EXISTS;
|
|
/**
|
* MES 能耗预警 Service 实现类
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@Validated
|
public class MesEmAlertServiceImpl implements MesEmAlertService {
|
|
private static final BigDecimal HUNDRED = new BigDecimal("100");
|
private static final BigDecimal DEFAULT_MOM_RATIO = new BigDecimal("30");
|
private static final BigDecimal DEFAULT_YOY_RATIO = new BigDecimal("30");
|
private static final DateTimeFormatter DATE_NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
|
|
@Resource
|
private MesEmAlertMapper alertMapper;
|
@Resource
|
private MesEmRecordMapper recordMapper;
|
@Resource
|
private MesEmMeterMapper meterMapper;
|
@Resource
|
private MesEmQuotaMapper quotaMapper;
|
|
@Override
|
public void checkRecord(MesEmRecordDO record) {
|
MesEmMeterDO meter = meterMapper.selectById(record.getMeterId());
|
checkQuota(record, MesEmQuotaPeriodEnum.DAY);
|
checkQuota(record, MesEmQuotaPeriodEnum.MONTH);
|
checkMomentum(record);
|
checkLeak(record, meter);
|
}
|
|
/**
|
* 定额预警:日定额比较当日用量,月定额比较月初至当日累计用量
|
*/
|
private void checkQuota(MesEmRecordDO record, MesEmQuotaPeriodEnum period) {
|
List<MesEmQuotaDO> quotas = quotaMapper.selectListByObject(record.getEnergyType(), period.getPeriod(),
|
record.getWorkshopId(), record.getLineId(), record.getMachineryId());
|
MesEmQuotaDO quota = mostSpecificQuota(quotas);
|
if (quota == null) {
|
return;
|
}
|
LocalDate begin = MesEmQuotaPeriodEnum.MONTH.equals(period)
|
? record.getRecordDate().withDayOfMonth(1) : record.getRecordDate();
|
BigDecimal actual = recordMapper.sumUsage(record.getEnergyType(), quota.getObjectType(),
|
quota.getObjectId(), begin, record.getRecordDate());
|
BigDecimal quotaValue = quota.getQuotaValue() == null ? BigDecimal.ZERO : quota.getQuotaValue();
|
BigDecimal warnRatio = defaultIfNull(quota.getWarnRatio(), new BigDecimal("10"));
|
BigDecimal alarmRatio = defaultIfNull(quota.getAlarmRatio(), new BigDecimal("20"));
|
BigDecimal warnThreshold = quotaValue.multiply(BigDecimal.ONE.add(
|
warnRatio.divide(HUNDRED, 6, RoundingMode.HALF_UP)));
|
BigDecimal alarmThreshold = quotaValue.multiply(BigDecimal.ONE.add(
|
alarmRatio.divide(HUNDRED, 6, RoundingMode.HALF_UP)));
|
|
Long objectId = quota.getObjectId() == null ? 0L : quota.getObjectId();
|
if (actual.compareTo(warnThreshold) <= 0) {
|
// 用量回落到阈值内时,撤销尚未处理的预警
|
deleteIfPending(0L, MesEmAlertTypeEnum.QUOTA.getType(), record.getRecordDate(), objectId);
|
return;
|
}
|
BigDecimal exceedRate = rate(actual, quotaValue);
|
String unit = StrUtil.blankToDefault(record.getUnit(), StrUtil.blankToDefault(quota.getUnit(), ""));
|
String content = StrUtil.format("{}{} {} 用量 {} {},定额 {} {},超出 {}%", period.getName(),
|
quota.getObjectName(), MesEmEnergyTypeEnum.getName(record.getEnergyType()),
|
stripZeros(actual), unit, stripZeros(quotaValue), unit,
|
exceedRate == null ? "-" : stripZeros(exceedRate));
|
MesEmAlertDO alert = buildAlert(record, 0L, "", quota.getObjectName(),
|
MesEmAlertTypeEnum.QUOTA, actual.compareTo(alarmThreshold) > 0
|
? MesEmAlertLevelEnum.SERIOUS : MesEmAlertLevelEnum.WARN,
|
quota.getObjectType(), objectId, actual, quotaValue, unit, content);
|
upsertAlert(alert);
|
}
|
|
/**
|
* 环比 / 同比异常预警:与上日、去年同日比较
|
*/
|
private void checkMomentum(MesEmRecordDO record) {
|
List<MesEmQuotaDO> quotas = quotaMapper.selectListByObject(record.getEnergyType(), null,
|
record.getWorkshopId(), record.getLineId(), record.getMachineryId());
|
MesEmQuotaDO quota = mostSpecificQuota(quotas);
|
BigDecimal momRatio = quota == null ? DEFAULT_MOM_RATIO : defaultIfNull(quota.getMomWarnRatio(), DEFAULT_MOM_RATIO);
|
BigDecimal yoyRatio = quota == null ? DEFAULT_YOY_RATIO : defaultIfNull(quota.getYoyWarnRatio(), DEFAULT_YOY_RATIO);
|
BigDecimal current = defaultIfNull(record.getUsageAmount(), BigDecimal.ZERO);
|
String unit = StrUtil.blankToDefault(record.getUnit(), "");
|
|
// 环比:昨日
|
LocalDate prevDate = record.getRecordDate().minusDays(1);
|
BigDecimal previous = recordMapper.sumMeterUsage(record.getMeterId(), prevDate, prevDate);
|
checkMomentumItem(record, current, previous, momRatio, unit,
|
MesEmAlertTypeEnum.MOM, "环比", prevDate);
|
|
// 同比:去年同日
|
LocalDate lastYearDate = record.getRecordDate().minusYears(1);
|
BigDecimal lastYear = recordMapper.sumMeterUsage(record.getMeterId(), lastYearDate, lastYearDate);
|
checkMomentumItem(record, current, lastYear, yoyRatio, unit,
|
MesEmAlertTypeEnum.YOY, "同比", lastYearDate);
|
}
|
|
private void checkMomentumItem(MesEmRecordDO record, BigDecimal current, BigDecimal base,
|
BigDecimal thresholdRatio, String unit, MesEmAlertTypeEnum alertType,
|
String label, LocalDate baseDate) {
|
if (base == null || base.compareTo(BigDecimal.ZERO) <= 0) {
|
deleteIfPending(record.getMeterId(), alertType.getType(), record.getRecordDate(), 0L);
|
return;
|
}
|
BigDecimal changeRate = rate(current, base);
|
if (changeRate == null || changeRate.compareTo(thresholdRatio) <= 0) {
|
deleteIfPending(record.getMeterId(), alertType.getType(), record.getRecordDate(), 0L);
|
return;
|
}
|
MesEmAlertLevelEnum level = changeRate.compareTo(thresholdRatio.multiply(new BigDecimal("2"))) > 0
|
? MesEmAlertLevelEnum.SERIOUS : MesEmAlertLevelEnum.WARN;
|
String content = StrUtil.format("{} {} {} 用量 {} {},较 {} 的 {} {} 上升 {}%,超过{}异常阈值 {}%",
|
record.getMeterName(), MesEmEnergyTypeEnum.getName(record.getEnergyType()), label,
|
stripZeros(current), unit, baseDate, label, stripZeros(base), stripZeros(changeRate),
|
label, stripZeros(thresholdRatio));
|
MesEmAlertDO alert = buildAlert(record, record.getMeterId(), record.getMeterCode(), record.getMeterName(),
|
alertType, level, resolveObjectType(record), resolveObjectId(record),
|
current, base, unit, content);
|
upsertAlert(alert);
|
}
|
|
/**
|
* 疑似跑冒滴漏预警:当日用量超过计量点配置的异常上限
|
*/
|
private void checkLeak(MesEmRecordDO record, MesEmMeterDO meter) {
|
BigDecimal threshold = meter == null ? null : meter.getLeakThreshold();
|
BigDecimal current = defaultIfNull(record.getUsageAmount(), BigDecimal.ZERO);
|
if (threshold == null || current.compareTo(threshold) <= 0) {
|
deleteIfPending(record.getMeterId(), MesEmAlertTypeEnum.LEAK.getType(), record.getRecordDate(), 0L);
|
return;
|
}
|
MesEmAlertLevelEnum level = current.compareTo(threshold.multiply(new BigDecimal("1.5"))) > 0
|
? MesEmAlertLevelEnum.SERIOUS : MesEmAlertLevelEnum.WARN;
|
String unit = StrUtil.blankToDefault(record.getUnit(), "");
|
String content = StrUtil.format("{} 当日用量 {} {},超过日用量异常上限 {} {},疑似跑冒滴漏",
|
record.getMeterName(), stripZeros(current), unit, stripZeros(threshold), unit);
|
MesEmAlertDO alert = buildAlert(record, record.getMeterId(), record.getMeterCode(), record.getMeterName(),
|
MesEmAlertTypeEnum.LEAK, level, resolveObjectType(record), resolveObjectId(record),
|
current, threshold, unit, content);
|
upsertAlert(alert);
|
}
|
|
private MesEmAlertDO buildAlert(MesEmRecordDO record, Long meterId, String meterCode, String meterName,
|
MesEmAlertTypeEnum alertType, MesEmAlertLevelEnum alertLevel,
|
Integer objectType, Long objectId,
|
BigDecimal actualValue, BigDecimal thresholdValue,
|
String unit, String content) {
|
MesEmAlertDO alert = new MesEmAlertDO();
|
alert.setAlertNo(buildAlertNo(record.getRecordDate(), meterId, alertType.getType(), objectId));
|
alert.setMeterId(meterId);
|
alert.setMeterCode(meterCode);
|
alert.setMeterName(meterName);
|
alert.setEnergyType(record.getEnergyType());
|
alert.setAlertType(alertType.getType());
|
alert.setAlertLevel(alertLevel.getLevel());
|
alert.setAlertDate(record.getRecordDate());
|
alert.setObjectType(objectType);
|
alert.setObjectId(objectId);
|
alert.setObjectName(meterName);
|
alert.setActualValue(actualValue);
|
alert.setThresholdValue(thresholdValue);
|
alert.setUnit(unit);
|
alert.setContent(content);
|
alert.setStatus(MesEmAlertStatusEnum.PENDING.getStatus());
|
return alert;
|
}
|
|
private void upsertAlert(MesEmAlertDO alert) {
|
MesEmAlertDO existing = alertMapper.selectByUnique(alert.getMeterId(), alert.getAlertType(),
|
alert.getAlertDate(), alert.getObjectId());
|
if (existing == null) {
|
alertMapper.insert(alert);
|
return;
|
}
|
alert.setId(existing.getId());
|
alert.setAlertNo(existing.getAlertNo());
|
alert.setStatus(existing.getStatus());
|
alertMapper.updateById(alert);
|
}
|
|
private void deleteIfPending(Long meterId, Integer alertType, LocalDate alertDate, Long objectId) {
|
MesEmAlertDO existing = alertMapper.selectByUnique(meterId, alertType, alertDate, objectId);
|
if (existing != null && MesEmAlertStatusEnum.PENDING.getStatus().equals(existing.getStatus())) {
|
alertMapper.deleteById(existing.getId());
|
}
|
}
|
|
private String buildAlertNo(LocalDate date, Long meterId, Integer alertType, Long objectId) {
|
return "AL" + date.format(DATE_NO_FORMATTER) + "-" + meterId + "-" + alertType + "-" + objectId;
|
}
|
|
private MesEmQuotaDO mostSpecificQuota(List<MesEmQuotaDO> quotas) {
|
if (CollUtil.isEmpty(quotas)) {
|
return null;
|
}
|
return quotas.stream()
|
.max(Comparator.comparing(MesEmQuotaDO::getObjectType)
|
.thenComparing(MesEmQuotaDO::getId))
|
.orElse(null);
|
}
|
|
private Integer resolveObjectType(MesEmRecordDO record) {
|
if (record.getMachineryId() != null) {
|
return MesEmQuotaObjectTypeEnum.MACHINERY.getType();
|
}
|
if (record.getLineId() != null) {
|
return MesEmQuotaObjectTypeEnum.LINE.getType();
|
}
|
if (record.getWorkshopId() != null) {
|
return MesEmQuotaObjectTypeEnum.WORKSHOP.getType();
|
}
|
return MesEmQuotaObjectTypeEnum.FACTORY.getType();
|
}
|
|
private Long resolveObjectId(MesEmRecordDO record) {
|
if (record.getMachineryId() != null) {
|
return record.getMachineryId();
|
}
|
if (record.getLineId() != null) {
|
return record.getLineId();
|
}
|
if (record.getWorkshopId() != null) {
|
return record.getWorkshopId();
|
}
|
return 0L;
|
}
|
|
private BigDecimal defaultIfNull(BigDecimal value, BigDecimal defaultValue) {
|
return value == null ? defaultValue : value;
|
}
|
|
/**
|
* 计算变化率(%),基数为 0 时返回 null
|
*/
|
private BigDecimal rate(BigDecimal current, BigDecimal base) {
|
if (base == null || base.compareTo(BigDecimal.ZERO) == 0) {
|
return null;
|
}
|
return current.subtract(base).multiply(HUNDRED).divide(base, 2, RoundingMode.HALF_UP);
|
}
|
|
private String stripZeros(BigDecimal value) {
|
if (value == null) {
|
return "0";
|
}
|
return value.stripTrailingZeros().toPlainString();
|
}
|
|
@Override
|
public int checkRange(LocalDate begin, LocalDate end) {
|
List<MesEmRecordDO> records = recordMapper.selectListByDateRange(begin, end);
|
records.forEach(this::checkRecord);
|
return records.size();
|
}
|
|
@Override
|
public void deleteMeterAlerts(Long meterId, LocalDate alertDate) {
|
alertMapper.deleteByMeterIdAndDate(meterId, alertDate);
|
}
|
|
@Override
|
public MesEmAlertDO getAlert(Long id) {
|
return alertMapper.selectById(id);
|
}
|
|
@Override
|
public PageResult<MesEmAlertDO> getAlertPage(MesEmAlertPageReqVO pageReqVO) {
|
return alertMapper.selectPage(pageReqVO);
|
}
|
|
@Override
|
public List<MesEmAlertRespVO> getAlertRespVOList(List<MesEmAlertDO> list) {
|
if (CollUtil.isEmpty(list)) {
|
return Collections.emptyList();
|
}
|
return BeanUtils.toBean(list, MesEmAlertRespVO.class, vo -> {
|
vo.setEnergyTypeName(MesEmEnergyTypeEnum.getName(vo.getEnergyType()));
|
vo.setAlertTypeName(MesEmAlertTypeEnum.getName(vo.getAlertType()));
|
vo.setAlertLevelName(MesEmAlertLevelEnum.getName(vo.getAlertLevel()));
|
vo.setObjectTypeName(MesEmQuotaObjectTypeEnum.getName(vo.getObjectType()));
|
vo.setStatusName(MesEmAlertStatusEnum.getName(vo.getStatus()));
|
});
|
}
|
|
@Override
|
public void handleAlert(MesEmAlertHandleReqVO handleReqVO) {
|
MesEmAlertDO alert = alertMapper.selectById(handleReqVO.getId());
|
if (alert == null) {
|
throw exception(EM_ALERT_NOT_EXISTS);
|
}
|
if (MesEmAlertStatusEnum.HANDLED.getStatus().equals(alert.getStatus())) {
|
throw exception(EM_ALERT_ALREADY_HANDLED);
|
}
|
alertMapper.updateById(new MesEmAlertDO().setId(alert.getId())
|
.setStatus(MesEmAlertStatusEnum.HANDLED.getStatus())
|
.setHandleUserId(SecurityFrameworkUtils.getLoginUserId())
|
.setHandleUserName(SecurityFrameworkUtils.getLoginUserNickname())
|
.setHandleTime(LocalDateTime.now())
|
.setHandleRemark(handleReqVO.getHandleRemark()));
|
}
|
|
}
|