package cn.iocoder.yudao.module.mes.service.safety;
|
|
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.statistics.MesSafetyStatisticsReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.statistics.MesSafetyStatisticsRespVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.statistics.MesSafetyStatisticsRespVO.GroupStat;
|
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.statistics.MesSafetyStatisticsRespVO.Summary;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkshopDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.*;
|
import cn.iocoder.yudao.module.mes.dal.mysql.md.workstation.MesMdWorkshopMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.safety.*;
|
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.util.*;
|
import java.util.stream.Collectors;
|
|
/**
|
* 安全生产 - 统计分析 Service 实现
|
*
|
* 数据规模较小,采用全量查询 + 内存过滤分组,便于灵活组合筛选条件
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@Validated
|
public class MesSafetyStatisticsServiceImpl implements MesSafetyStatisticsService {
|
|
@Resource
|
private MesSafetyHazardMapper hazardMapper;
|
|
@Resource
|
private MesSafetyWorkTicketMapper ticketMapper;
|
|
@Resource
|
private MesSafetyTrainingMapper trainingMapper;
|
|
@Resource
|
private MesSafetyCertificateMapper certificateMapper;
|
|
@Resource
|
private MesMdWorkshopMapper workshopMapper;
|
|
/** 字典标签(与 SQL 中 system_dict_data 保持一致) */
|
private static final String[] HAZARD_LEVEL_NAMES = {null, "一般隐患", "重大隐患"};
|
private static final String[] HAZARD_SOURCE_NAMES = {null, "日常排查", "专项检查", "上级检查", "员工上报", "其他"};
|
private static final String[] TICKET_TYPE_NAMES = {null, "动火作业", "受限空间作业", "高处作业", "吊装作业",
|
"临时用电作业", "断路作业", "盲板抽堵作业", "设备检修作业"};
|
private static final String[] TICKET_STATUS_NAMES = {null, null, null, null, null, null, null, null, null, null,
|
"已批准"}; // 仅占位,实际用 switch
|
private static final String[] TRAINING_TYPE_NAMES = {null, "三级安全教育", "专项培训", "复训", "日常安全教育", "其他"};
|
private static final String[] CERT_TYPE_NAMES = {null, "特种作业操作证", "安全管理人员证", "主要负责人证", "其他资格证"};
|
|
@Override
|
public MesSafetyStatisticsRespVO getStatistics(MesSafetyStatisticsReqVO reqVO) {
|
LocalDate today = LocalDate.now();
|
MesSafetyStatisticsRespVO vo = new MesSafetyStatisticsRespVO();
|
|
// 车间名称映射
|
Map<Long, String> workshopNameMap = workshopMapper.selectList().stream()
|
.collect(Collectors.toMap(MesMdWorkshopDO::getId, MesMdWorkshopDO::getName, (a, b) -> a));
|
|
// ========== 隐患 ==========
|
List<MesSafetyHazardDO> hazards = hazardMapper.selectAllList().stream()
|
.filter(h -> inRange(h.getCreateTime(), reqVO))
|
.filter(h -> matchWorkshop(h.getWorkshopId(), reqVO))
|
.collect(Collectors.toList());
|
|
Summary summary = new Summary();
|
long hazardClosed = hazards.stream()
|
.filter(h -> MesSafetyHazardDO.STATUS_CLOSED.equals(h.getStatus())).count();
|
long hazardOverdue = hazards.stream()
|
.filter(h -> h.getDeadline() != null
|
&& !MesSafetyHazardDO.STATUS_CLOSED.equals(h.getStatus())
|
&& h.getDeadline().isBefore(today))
|
.count();
|
summary.setHazardTotal((long) hazards.size());
|
summary.setHazardClosed(hazardClosed);
|
summary.setHazardOverdue(hazardOverdue);
|
summary.setHazardClosedRate(rate(hazardClosed, hazards.size()));
|
|
// 隐患按车间
|
Map<Long, List<MesSafetyHazardDO>> byWorkshop = hazards.stream()
|
.filter(h -> h.getWorkshopId() != null)
|
.collect(Collectors.groupingBy(MesSafetyHazardDO::getWorkshopId));
|
vo.setHazardByWorkshop(byWorkshop.entrySet().stream()
|
.map(e -> buildStat(workshopNameMap.getOrDefault(e.getKey(), "未知车间"),
|
e.getKey().intValue(), e.getValue(),
|
h -> MesSafetyHazardDO.STATUS_CLOSED.equals(h.getStatus())))
|
.sorted(Comparator.comparing(GroupStat::getTotal).reversed())
|
.collect(Collectors.toList()));
|
|
// 隐患按等级
|
vo.setHazardByLevel(groupByInteger(hazards, MesSafetyHazardDO::getHazardLevel, HAZARD_LEVEL_NAMES,
|
h -> MesSafetyHazardDO.STATUS_CLOSED.equals(h.getStatus())));
|
// 隐患按来源
|
vo.setHazardBySource(groupByInteger(hazards, MesSafetyHazardDO::getSource, HAZARD_SOURCE_NAMES,
|
h -> MesSafetyHazardDO.STATUS_CLOSED.equals(h.getStatus())));
|
|
// ========== 作业票 ==========
|
List<MesSafetyWorkTicketDO> tickets = ticketMapper.selectAllList().stream()
|
.filter(t -> inRange(t.getCreateTime(), reqVO))
|
.filter(t -> matchWorkshop(t.getWorkshopId(), reqVO))
|
.collect(Collectors.toList());
|
long ticketArchived = tickets.stream()
|
.filter(t -> MesSafetyWorkTicketDO.STATUS_ARCHIVED.equals(t.getStatus())).count();
|
long ticketRejected = tickets.stream()
|
.filter(t -> MesSafetyWorkTicketDO.STATUS_REJECTED.equals(t.getStatus())).count();
|
summary.setTicketTotal((long) tickets.size());
|
summary.setTicketArchived(ticketArchived);
|
summary.setTicketRejected(ticketRejected);
|
|
vo.setTicketByType(groupByInteger(tickets, MesSafetyWorkTicketDO::getTicketType, TICKET_TYPE_NAMES,
|
t -> MesSafetyWorkTicketDO.STATUS_ARCHIVED.equals(t.getStatus())));
|
vo.setTicketByStatus(groupByInteger(tickets, MesSafetyWorkTicketDO::getStatus, null,
|
t -> MesSafetyWorkTicketDO.STATUS_ARCHIVED.equals(t.getStatus())));
|
|
// ========== 培训 ==========
|
List<MesSafetyTrainingDO> trainings = trainingMapper.selectAllList().stream()
|
.filter(t -> inRange(t.getCreateTime(), reqVO))
|
.collect(Collectors.toList());
|
long attendeeTotal = trainings.stream()
|
.mapToLong(t -> t.getAttendeeCount() == null ? 0 : t.getAttendeeCount()).sum();
|
BigDecimal scoreSum = BigDecimal.ZERO;
|
int scoredCount = 0;
|
for (MesSafetyTrainingDO t : trainings) {
|
if (t.getAvgScore() != null) {
|
scoreSum = scoreSum.add(t.getAvgScore());
|
scoredCount++;
|
}
|
}
|
summary.setTrainingTotal((long) trainings.size());
|
summary.setTrainingAttendee(attendeeTotal);
|
summary.setTrainingAvgScore(scoredCount > 0
|
? scoreSum.divide(BigDecimal.valueOf(scoredCount), 1, RoundingMode.HALF_UP) : null);
|
|
vo.setTrainingByType(groupByInteger(trainings, MesSafetyTrainingDO::getTrainingType, TRAINING_TYPE_NAMES,
|
t -> MesSafetyTrainingDO.STATUS_FINISHED.equals(t.getStatus())));
|
|
// ========== 证书 ==========
|
List<MesSafetyCertificateDO> certificates = certificateMapper.selectAllList().stream()
|
.filter(c -> inRange(c.getCreateTime(), reqVO))
|
.collect(Collectors.toList());
|
long certExpired = certificates.stream()
|
.filter(c -> c.getExpireDate() != null && c.getExpireDate().isBefore(today))
|
.count();
|
summary.setCertificateTotal((long) certificates.size());
|
summary.setCertificateExpired(certExpired);
|
|
vo.setCertificateByType(groupByInteger(certificates, MesSafetyCertificateDO::getCertType, CERT_TYPE_NAMES,
|
c -> MesSafetyCertificateDO.STATUS_VALID.equals(c.getStatus())));
|
|
vo.setSummary(summary);
|
return vo;
|
}
|
|
// ==================== 工具 ====================
|
|
/**
|
* 通用分组统计:按 Integer 字段分组,输出名称/数量/已完成/完成率
|
*/
|
private <T> List<GroupStat> groupByInteger(List<T> list,
|
java.util.function.Function<T, Integer> keyFn,
|
String[] nameArray,
|
java.util.function.Predicate<T> finishedFn) {
|
Map<Integer, List<T>> grouped = list.stream()
|
.filter(item -> keyFn.apply(item) != null)
|
.collect(Collectors.groupingBy(keyFn));
|
return grouped.entrySet().stream()
|
.map(e -> {
|
Integer key = e.getKey();
|
String name = resolveName(key, nameArray);
|
return buildStat(name, key, e.getValue(), finishedFn);
|
})
|
.sorted(Comparator.comparing(GroupStat::getTotal).reversed())
|
.collect(Collectors.toList());
|
}
|
|
private String resolveName(Integer key, String[] nameArray) {
|
if (nameArray != null && key >= 0 && key < nameArray.length && nameArray[key] != null) {
|
return nameArray[key];
|
}
|
// 状态类字典没有通用名称数组,直接按已知值映射
|
return switch (key) {
|
case 0 -> "待处理";
|
case 10 -> "处理中";
|
case 20 -> "已完成";
|
case 30 -> "已归档";
|
case 90 -> "已驳回";
|
default -> "其他(" + key + ")";
|
};
|
}
|
|
private <T> GroupStat buildStat(String name, Integer code, List<T> items,
|
java.util.function.Predicate<T> finishedFn) {
|
GroupStat stat = new GroupStat();
|
stat.setName(name);
|
stat.setCode(code);
|
stat.setTotal((long) items.size());
|
long finished = items.stream().filter(finishedFn).count();
|
stat.setFinished(finished);
|
stat.setFinishRate(rate(finished, items.size()));
|
return stat;
|
}
|
|
private BigDecimal rate(long part, long total) {
|
if (total <= 0) {
|
return BigDecimal.ZERO;
|
}
|
return BigDecimal.valueOf(part * 100.0 / total).setScale(1, RoundingMode.HALF_UP);
|
}
|
|
/**
|
* 时间范围过滤(按创建时间,含首尾两天)
|
*/
|
private boolean inRange(LocalDateTime createTime, MesSafetyStatisticsReqVO reqVO) {
|
if (reqVO.getStartDate() == null && reqVO.getEndDate() == null) {
|
return true;
|
}
|
if (createTime == null) {
|
return false;
|
}
|
if (reqVO.getStartDate() != null && createTime.isBefore(reqVO.getStartDate().atStartOfDay())) {
|
return false;
|
}
|
if (reqVO.getEndDate() != null && createTime.isAfter(reqVO.getEndDate().atTime(23, 59, 59))) {
|
return false;
|
}
|
return true;
|
}
|
|
private boolean matchWorkshop(Long workshopId, MesSafetyStatisticsReqVO reqVO) {
|
return reqVO.getWorkshopId() == null || Objects.equals(workshopId, reqVO.getWorkshopId());
|
}
|
|
}
|