feat(WeightDeviceDataAlert): implement alert management functionality with CRUD operations and notification checks

This commit is contained in:
2026-01-14 20:07:11 +08:00
parent cd46e48d1c
commit 6b29e702cf
10 changed files with 667 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ import cn.lihongjie.coal.weightDevice.entity.WeightDeviceEntity;
import cn.lihongjie.coal.weightDevice.service.WeightDeviceService;
import cn.lihongjie.coal.weightDeviceData.entity.WeightDeviceDataEntity;
import cn.lihongjie.coal.weightDeviceData.service.WeightDeviceDataService;
import cn.lihongjie.coal.weightDeviceDataAlert.service.WeightDeviceDataAlertService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -69,6 +70,8 @@ public class WeightListener {
@Autowired WeightDeviceService weightDeviceService;
@Autowired WeightDeviceDataAlertService weightDeviceDataAlertService;
@Autowired
ConversionService conversionService;
@@ -598,6 +601,13 @@ public class WeightListener {
weightDeviceDataService.save(deviceData);
});
// 触发告警规则检查
try {
weightDeviceDataAlertService.checkAllAlertsAndNotify(device.getOrganizationId());
} catch (Exception e) {
log.error("Failed to check alerts for device {}: {}", device.getId(), e.getMessage(), e);
}
}
private Integer toBoolean(JsonNode node) {

View File

@@ -0,0 +1,61 @@
package cn.lihongjie.coal.weightDeviceDataAlert.controller;
import cn.lihongjie.coal.annotation.OrgScope;
import cn.lihongjie.coal.annotation.SysLog;
import cn.lihongjie.coal.base.dto.CommonQuery;
import cn.lihongjie.coal.base.dto.IdRequest;
import cn.lihongjie.coal.weightDeviceDataAlert.dto.CreateWeightDeviceDataAlertDto;
import cn.lihongjie.coal.weightDeviceDataAlert.dto.UpdateWeightDeviceDataAlertDto;
import cn.lihongjie.coal.weightDeviceDataAlert.dto.WeightDeviceDataAlertDto;
import cn.lihongjie.coal.weightDeviceDataAlert.service.WeightDeviceDataAlertService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/weightDeviceDataAlert")
@SysLog(module = "")
@Slf4j
@OrgScope
public class WeightDeviceDataAlertController {
@Autowired private WeightDeviceDataAlertService service;
@PostMapping("/create")
public WeightDeviceDataAlertDto create(@RequestBody CreateWeightDeviceDataAlertDto request) {
return this.service.create(request);
}
@PostMapping("/update")
public WeightDeviceDataAlertDto update(@RequestBody UpdateWeightDeviceDataAlertDto request) {
return this.service.update(request);
}
@PostMapping("/delete")
public Object delete(@RequestBody IdRequest request) {
this.service.delete(request);
return true;
}
@PostMapping("/getById")
public WeightDeviceDataAlertDto getById(@RequestBody IdRequest request) {
return this.service.getById(request.getId());
}
@PostMapping("/list")
public Page<WeightDeviceDataAlertDto> list(@RequestBody CommonQuery request) {
return this.service.list(request);
}
@PostMapping("/checkAndNotify")
@SysLog(action = "检查告警规则并发送通知")
public Object checkAndNotify() {
this.service.checkAllAlertsAndNotify();
return true;
}
}

View File

@@ -0,0 +1,34 @@
package cn.lihongjie.coal.weightDeviceDataAlert.dto;
import cn.lihongjie.coal.base.dto.OrgCommonDto;
import jakarta.persistence.ManyToOne;
import lombok.Data;
import org.hibernate.annotations.Comment;
import java.time.LocalDateTime;
@Data
public class CreateWeightDeviceDataAlertDto extends OrgCommonDto {
@Comment("查询条件JSON")
private String queryJson;
@Comment("预期吨数-jz")
private Double expectedTonnage;
@Comment("推送阈值 吨数")
private Double alertTonnageThreshold;
@Comment("截止日期")
private LocalDateTime endDate;
/**
* 关联的场景 用于推送消息
*/
@ManyToOne
private String scene;
}

View File

@@ -0,0 +1,34 @@
package cn.lihongjie.coal.weightDeviceDataAlert.dto;
import cn.lihongjie.coal.base.dto.OrgCommonDto;
import jakarta.persistence.ManyToOne;
import lombok.Data;
import org.hibernate.annotations.Comment;
import java.time.LocalDateTime;
@Data
public class UpdateWeightDeviceDataAlertDto extends OrgCommonDto {
@Comment("查询条件JSON")
private String queryJson;
@Comment("预期吨数-jz")
private Double expectedTonnage;
@Comment("推送阈值 吨数")
private Double alertTonnageThreshold;
@Comment("截止日期")
private LocalDateTime endDate;
/**
* 关联的场景 用于推送消息
*/
@ManyToOne
private String scene;
}

View File

@@ -0,0 +1,50 @@
package cn.lihongjie.coal.weightDeviceDataAlert.dto;
import cn.lihongjie.coal.base.dto.OrgCommonDto;
import cn.lihongjie.coal.dingtalkBotScene.entity.DingtalkBotSceneEntity;
import jakarta.persistence.ManyToOne;
import lombok.Data;
import org.hibernate.annotations.Comment;
import java.time.LocalDateTime;
@Data
public class WeightDeviceDataAlertDto extends OrgCommonDto {
@Comment("查询条件JSON")
private String queryJson;
@Comment("预期吨数-jz")
private Double expectedTonnage;
@Comment("实际吨数-jz")
private Double actualTonnage;
@Comment("进度 %")
private Double progressPercent;
@Comment("推送阈值 吨数")
private Double alertTonnageThreshold;
@Comment("上次提醒时间")
private LocalDateTime lastAlertTime;
@Comment("上次提醒进度")
private Double lastAlertProgressPercent;
@Comment("上次提醒吨数")
private Double lastAlertTonnage;
@Comment("截止日期")
private LocalDateTime endDate;
/**
* 关联的场景 用于推送消息
*/
@ManyToOne
private DingtalkBotSceneEntity scene;
}

View File

@@ -0,0 +1,72 @@
package cn.lihongjie.coal.weightDeviceDataAlert.entity;
import cn.lihongjie.coal.base.entity.OrgCommonEntity;
import cn.lihongjie.coal.dingtalkBotScene.entity.DingtalkBotSceneEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Data;
import org.hibernate.annotations.Comment;
import java.time.LocalDateTime;
@Data
@Entity
@Table(
indexes =
@jakarta.persistence.Index(
name = "idx_weightDeviceDataAlert_org_id",
columnList = "organization_id"))
public class WeightDeviceDataAlertEntity extends OrgCommonEntity {
@Comment("查询条件JSON")
private String queryJson;
@Comment("预期吨数-jz")
private Double expectedTonnage;
@Comment("实际吨数-jz")
private Double actualTonnage;
@Comment("进度 %")
private Double progressPercent;
@Comment("推送阈值 吨数")
private Double alertTonnageThreshold;
@Comment("上次提醒时间")
private LocalDateTime lastAlertTime;
@Comment("上次提醒进度")
private Double lastAlertProgressPercent;
@Comment("上次提醒吨数")
private Double lastAlertTonnage;
@Comment("截止日期")
private LocalDateTime endDate;
/**
* 关联的场景 用于推送消息
*/
@ManyToOne
private DingtalkBotSceneEntity scene;
}

View File

@@ -0,0 +1,23 @@
package cn.lihongjie.coal.weightDeviceDataAlert.mapper;
import cn.lihongjie.coal.base.mapper.BaseMapper;
import cn.lihongjie.coal.base.mapper.CommonEntityMapper;
import cn.lihongjie.coal.base.mapper.CommonMapper;
import cn.lihongjie.coal.weightDeviceDataAlert.dto.CreateWeightDeviceDataAlertDto;
import cn.lihongjie.coal.weightDeviceDataAlert.dto.UpdateWeightDeviceDataAlertDto;
import cn.lihongjie.coal.weightDeviceDataAlert.dto.WeightDeviceDataAlertDto;
import cn.lihongjie.coal.weightDeviceDataAlert.entity.WeightDeviceDataAlertEntity;
import org.mapstruct.Mapper;
import org.mapstruct.control.DeepClone;
@Mapper(
componentModel = org.mapstruct.MappingConstants.ComponentModel.SPRING,
uses = {CommonMapper.class, CommonEntityMapper.class},
mappingControl = DeepClone.class)
public interface WeightDeviceDataAlertMapper
extends BaseMapper<
WeightDeviceDataAlertEntity,
WeightDeviceDataAlertDto,
CreateWeightDeviceDataAlertDto,
UpdateWeightDeviceDataAlertDto> {}

View File

@@ -0,0 +1,16 @@
package cn.lihongjie.coal.weightDeviceDataAlert.repository;
import cn.lihongjie.coal.base.dao.BaseRepository;
import cn.lihongjie.coal.weightDeviceDataAlert.entity.WeightDeviceDataAlertEntity;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface WeightDeviceDataAlertRepository
extends BaseRepository<WeightDeviceDataAlertEntity> {
@Query("select false")
boolean isLinked(List<String> ids);
}

View File

@@ -0,0 +1,348 @@
package cn.lihongjie.coal.weightDeviceDataAlert.service;
import cn.lihongjie.coal.base.dto.CommonQuery;
import cn.lihongjie.coal.base.dto.IdRequest;
import cn.lihongjie.coal.base.service.BaseService;
import cn.lihongjie.coal.dbFunctions.DbFunctionService;
import cn.lihongjie.coal.dingtalkBot.entity.DingtalkBotEntity;
import cn.lihongjie.coal.dingtalkBot.service.DingtalkBotService;
import cn.lihongjie.coal.exception.BizException;
import cn.lihongjie.coal.weightDeviceData.entity.WeightDeviceDataEntity;
import cn.lihongjie.coal.weightDeviceData.repository.WeightDeviceDataRepository;
import cn.lihongjie.coal.weightDeviceDataAlert.dto.CreateWeightDeviceDataAlertDto;
import cn.lihongjie.coal.weightDeviceDataAlert.dto.UpdateWeightDeviceDataAlertDto;
import cn.lihongjie.coal.weightDeviceDataAlert.dto.WeightDeviceDataAlertDto;
import cn.lihongjie.coal.weightDeviceDataAlert.entity.WeightDeviceDataAlertEntity;
import cn.lihongjie.coal.weightDeviceDataAlert.mapper.WeightDeviceDataAlertMapper;
import cn.lihongjie.coal.weightDeviceDataAlert.repository.WeightDeviceDataAlertRepository;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.criteria.Predicate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
@Transactional
public class WeightDeviceDataAlertService
extends BaseService<WeightDeviceDataAlertEntity, WeightDeviceDataAlertRepository> {
@Autowired private WeightDeviceDataAlertRepository repository;
@Autowired private WeightDeviceDataAlertMapper mapper;
@Autowired private ConversionService conversionService;
@Autowired private DbFunctionService dbFunctionService;
@Autowired private WeightDeviceDataRepository weightDeviceDataRepository;
@Autowired private DingtalkBotService dingtalkBotService;
@Autowired private ObjectMapper objectMapper;
public WeightDeviceDataAlertDto create(CreateWeightDeviceDataAlertDto request) {
WeightDeviceDataAlertEntity entity = mapper.toEntity(request);
this.repository.save(entity);
return getById(entity.getId());
}
public WeightDeviceDataAlertDto update(UpdateWeightDeviceDataAlertDto request) {
WeightDeviceDataAlertEntity entity = this.repository.get(request.getId());
this.mapper.updateEntity(entity, request);
this.repository.save(entity);
return getById(entity.getId());
}
public void delete(IdRequest request) {
boolean linked = this.repository.isLinked(request.getIds());
if (linked) {
throw new BizException("数据已被关联,无法删除");
}
this.repository.deleteAllById(request.getIds());
}
public WeightDeviceDataAlertDto getById(String id) {
WeightDeviceDataAlertEntity entity = repository.get(id);
return mapper.toDto(entity);
}
public Page<WeightDeviceDataAlertDto> list(CommonQuery query) {
Page<WeightDeviceDataAlertEntity> page =
repository.findAll(
query.specification(conversionService),
PageRequest.of(
query.getPageNo(),
query.getPageSize(),
Sort.by(query.getOrders())));
return page.map(this.mapper::toDto);
}
/**
* 检查所有告警规则并发送通知
*/
public void checkAllAlertsAndNotify() {
checkAllAlertsAndNotify(null);
}
/**
* 检查指定机构的告警规则并发送通知
* @param organizationId 机构ID如果为null则检查所有机构
*/
public void checkAllAlertsAndNotify(String organizationId) {
if (organizationId != null) {
log.info("开始检查机构 {} 的称重数据告警规则", organizationId);
} else {
log.info("开始检查所有称重数据告警规则");
}
// 获取所有启用的告警规则,过滤掉已截止的规则
Specification<WeightDeviceDataAlertEntity> spec = (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
// 过滤机构
if (organizationId != null) {
predicates.add(criteriaBuilder.equal(root.get("organizationId"), organizationId));
}
// 过滤截止日期只查询未截止的规则endDate为null或endDate在当前时间之后
LocalDateTime now = LocalDateTime.now();
predicates.add(
criteriaBuilder.or(
criteriaBuilder.isNull(root.get("endDate")),
criteriaBuilder.greaterThan(root.get("endDate"), now)
)
);
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
List<WeightDeviceDataAlertEntity> alerts = repository.findAll(spec);
if (alerts.isEmpty()) {
log.info("没有找到任何有效的告警规则");
return;
}
int checkedCount = 0;
int notifiedCount = 0;
for (WeightDeviceDataAlertEntity alert : alerts) {
try {
checkedCount++;
boolean notified = checkAndNotify(alert);
if (notified) {
notifiedCount++;
}
} catch (Exception e) {
log.error("检查告警规则失败, 规则ID: {}, 错误: {}", alert.getId(), e.getMessage(), e);
}
}
log.info("告警规则检查完成, 共检查 {} 条规则, 发送 {} 条通知", checkedCount, notifiedCount);
}
/**
* 检查单个告警规则并发送通知
* @param alert 告警规则
* @return 是否发送了通知
*/
private boolean checkAndNotify(WeightDeviceDataAlertEntity alert) {
log.debug("检查告警规则: {}", alert.getName());
// 构建查询条件
Specification<WeightDeviceDataEntity> spec = buildSpecification(alert);
// 查询符合条件的称重数据
List<WeightDeviceDataEntity> dataList = weightDeviceDataRepository.findAll(spec);
if (dataList.isEmpty()) {
log.debug("规则 {} 没有找到匹配的数据", alert.getName());
return false;
}
// 计算总吨数(净重)
double totalTonnage = dataList.stream()
.mapToDouble(data -> data.getJz() != null ? data.getJz() : 0.0)
.sum();
// 计算进度百分比
Double expectedTonnage = alert.getExpectedTonnage();
double progressPercent = 0.0;
if (expectedTonnage != null && expectedTonnage > 0) {
progressPercent = (totalTonnage / expectedTonnage) * 100.0;
}
log.debug("规则 {}: 实际吨数={}, 预期吨数={}, 进度={}%",
alert.getName(), totalTonnage, expectedTonnage, String.format("%.2f", progressPercent));
// 更新实际吨数和进度
alert.setActualTonnage(totalTonnage);
alert.setProgressPercent(progressPercent);
// 判断是否需要发送通知
boolean shouldNotify = shouldSendNotification(alert, totalTonnage, progressPercent);
if (shouldNotify) {
sendNotification(alert, totalTonnage, progressPercent, dataList.size());
// 更新最后通知时间和吨数
alert.setLastAlertTime(LocalDateTime.now());
alert.setLastAlertTonnage(totalTonnage);
alert.setLastAlertProgressPercent(progressPercent);
}
// 保存更新
repository.save(alert);
return shouldNotify;
}
/**
* 构建查询规格
*/
private Specification<WeightDeviceDataEntity> buildSpecification(WeightDeviceDataAlertEntity alert) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// 机构过滤 - 必须使用规则的机构ID
if (alert.getOrganizationId() != null) {
predicates.add(cb.equal(root.get("organizationId"), alert.getOrganizationId()));
}
// 使用 queryJson 构建额外的查询条件
if (alert.getQueryJson() != null && !alert.getQueryJson().isEmpty()) {
try {
List<CommonQuery.QueryItem> queryItems = objectMapper.readValue(
alert.getQueryJson(),
new TypeReference<List<CommonQuery.QueryItem>>() {});
CommonQuery commonQuery = new CommonQuery();
commonQuery.setItems(new ArrayList<>(queryItems));
Specification<WeightDeviceDataEntity> spec =
(Specification<WeightDeviceDataEntity>) commonQuery.specification(conversionService);
if (spec != null) {
Predicate specPredicate = spec.toPredicate(root, query, cb);
if (specPredicate != null) {
predicates.add(specPredicate);
}
}
} catch (Exception e) {
log.error("解析查询条件JSON失败: {}", e.getMessage(), e);
}
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
/**
* 判断是否应该发送通知
*
* 通知逻辑:
* 1. 当实际吨数达到或超过 alertTonnageThreshold 时开始发送通知
* 2. 只要超过阈值后,每次吨数有变化就发送通知
* 3. 如果吨数与上次通知相同,不重复发送
*
* 例如预期1000吨阈值800吨
* - 实际吨数达到800吨时 → 发送通知
* - 实际吨数达到850吨时 → 发送通知
* - 实际吨数达到900吨时 → 发送通知
* - 实际吨数仍是900吨时 → 不发送(避免重复)
*/
private boolean shouldSendNotification(WeightDeviceDataAlertEntity alert, double currentTonnage, double currentProgress) {
// 如果没有配置阈值,不发送通知
Double threshold = alert.getAlertTonnageThreshold();
if (threshold == null || threshold <= 0) {
return false;
}
// 当前吨数未达到阈值,不发送通知
if (currentTonnage < threshold) {
return false;
}
// 如果从未发送过通知,且达到阈值,发送首次通知
if (alert.getLastAlertTonnage() == null) {
return true;
}
// 如果吨数与上次通知相同,不重复发送
return !(Math.abs(currentTonnage - alert.getLastAlertTonnage()) < 0.01);
// 吨数有变化且超过阈值,发送通知
}
/**
* 发送通知
*/
private void sendNotification(WeightDeviceDataAlertEntity alert, double tonnage, double progress, int recordCount) {
try {
// 构建通知数据
Map<String, Object> notificationData = new HashMap<>();
notificationData.put("alertId", alert.getId());
notificationData.put("alertName", alert.getName());
notificationData.put("actualTonnage", tonnage);
notificationData.put("expectedTonnage", alert.getExpectedTonnage());
notificationData.put("progressPercent", progress);
notificationData.put("recordCount", recordCount);
notificationData.put("lastAlertTonnage", alert.getLastAlertTonnage());
notificationData.put("lastAlertTime", alert.getLastAlertTime());
notificationData.put("organizationId", alert.getOrganizationId());
notificationData.put("sceneId", alert.getScene() != null ? alert.getScene().getId() : null);
notificationData.put("timestamp", LocalDateTime.now());
// 如果配置了钉钉场景,发送钉钉通知
if (alert.getScene() != null) {
List<DingtalkBotEntity> bots = alert.getScene().getDingtalkBots();
if (bots != null && !bots.isEmpty()) {
for (DingtalkBotEntity bot : bots) {
try {
dingtalkBotService.doSend(
bot,
alert.getScene().getDingtalkBotTemplate(),
notificationData);
log.info("钉钉通知发送成功: 规则={}, Bot={}", alert.getName(), bot.getName());
} catch (Exception e) {
log.error("发送钉钉通知失败: Bot={}, 错误={}", bot.getName(), e.getMessage(), e);
}
}
} else {
log.warn("规则 {} 配置了场景但没有可用的钉钉机器人", alert.getName());
}
}
log.info("发送告警通知: 规则={}, 实际吨数={}, 进度={}%, 记录数={}",
alert.getName(), String.format("%.2f", tonnage), String.format("%.2f", progress), recordCount);
} catch (Exception e) {
log.error("发送告警通知失败: {}", e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,19 @@
package scripts.dict
import cn.lihongjie.coal.base.dto.CommonQuery
import cn.lihongjie.coal.weightDeviceDataAlert.controller.WeightDeviceDataAlertController
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.context.ApplicationContext
ApplicationContext ioc = ioc
def controller = ioc.getBean(WeightDeviceDataAlertController.class)
def objectMapper = ioc.getBean(ObjectMapper.class) as ObjectMapper
return controller.list(params!=null ? objectMapper.convertValue(params, CommonQuery.class ) : new CommonQuery())