package cn.iocoder.yudao.module.mes.service.safety;
|
|
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.dashboard.MesSafetyDashboardRespVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.dashboard.MesSafetyDashboardRespVO.*;
|
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.function.Function;
|
import java.util.stream.Collectors;
|
|
/**
|
* 安全生产 - 可视化看板 Service 实现
|
*
|
* 数据量在当前规模下较小,采用全量查询 + 内存聚合,避免为每张统计写一套 SQL
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@Validated
|
public class MesSafetyDashboardServiceImpl implements MesSafetyDashboardService {
|
|
@Resource
|
private MesSafetyRiskMapper riskMapper;
|
|
@Resource
|
private MesSafetyRiskUnitMapper riskUnitMapper;
|
|
@Resource
|
private MesSafetyHazardMapper hazardMapper;
|
|
@Resource
|
private MesSafetyWorkTicketMapper ticketMapper;
|
|
@Resource
|
private MesSafetyTrainingMapper trainingMapper;
|
|
@Resource
|
private MesSafetyTrainingAttendeeMapper attendeeMapper;
|
|
@Resource
|
private MesSafetyCertificateMapper certificateMapper;
|
|
@Resource
|
private MesMdWorkshopMapper workshopMapper;
|
|
/** Top N */
|
private static final int TOP_N = 10;
|
/** 最近动态条数 */
|
private static final int RECENT_N = 5;
|
/** 证书即将到期阈值 */
|
private static final long EXPIRING_SOON_DAYS = 30L;
|
|
@Override
|
public MesSafetyDashboardRespVO getOverview() {
|
MesSafetyDashboardRespVO vo = new MesSafetyDashboardRespVO();
|
LocalDate today = LocalDate.now();
|
LocalDateTime now = LocalDateTime.now();
|
|
// 车间名称映射(多个统计共用)
|
Map<Long, String> workshopNameMap = loadWorkshopNames();
|
|
// ========== 风险 ==========
|
List<MesSafetyRiskDO> risks = riskMapper.selectAllList();
|
vo.setRiskTotal((long) risks.size());
|
vo.setRiskLevelStats(buildRiskLevelStats(risks));
|
vo.setRiskByWorkshop(buildRiskByWorkshop(risks, workshopNameMap));
|
|
// ========== 隐患 ==========
|
List<MesSafetyHazardDO> hazards = hazardMapper.selectAllList();
|
vo.setHazardStats(buildHazardStats(hazards, today));
|
vo.setHazardByWorkshop(buildHazardByWorkshop(hazards, workshopNameMap));
|
|
// ========== 作业票 ==========
|
List<MesSafetyWorkTicketDO> tickets = ticketMapper.selectAllList();
|
vo.setTicketStats(buildTicketStats(tickets, now));
|
|
// ========== 培训 ==========
|
List<MesSafetyTrainingDO> trainings = trainingMapper.selectAllList();
|
vo.setTrainingStats(buildTrainingStats(trainings));
|
|
// ========== 证书 ==========
|
List<MesSafetyCertificateDO> certificates = certificateMapper.selectAllList();
|
vo.setCertificateStats(buildCertificateStats(certificates, today));
|
|
// ========== 最近动态 ==========
|
vo.setRecentHazards(buildRecentHazards(hazards));
|
vo.setRecentTickets(buildRecentTickets(tickets));
|
|
return vo;
|
}
|
|
// ==================== 风险 ====================
|
|
private List<NameCount> buildRiskLevelStats(List<MesSafetyRiskDO> risks) {
|
// 1重大 2较大 3一般 4低
|
String[] levelNames = {"重大风险", "较大风险", "一般风险", "低风险"};
|
Map<Integer, Long> countMap = risks.stream()
|
.filter(r -> r.getRiskLevel() != null)
|
.collect(Collectors.groupingBy(MesSafetyRiskDO::getRiskLevel, Collectors.counting()));
|
List<NameCount> list = new ArrayList<>(4);
|
for (int i = 1; i <= 4; i++) {
|
list.add(new NameCount(levelNames[i - 1], countMap.getOrDefault(i, 0L), i));
|
}
|
return list;
|
}
|
|
private List<NameCount> buildRiskByWorkshop(List<MesSafetyRiskDO> risks, Map<Long, String> workshopNameMap) {
|
// 风险本身没有车间,经风险单元的 workshopId 归属
|
Set<Long> unitIds = risks.stream().map(MesSafetyRiskDO::getUnitId)
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
if (unitIds.isEmpty()) {
|
return Collections.emptyList();
|
}
|
Map<Long, MesSafetyRiskUnitDO> unitMap = riskUnitMapper.selectByIds(unitIds).stream()
|
.collect(Collectors.toMap(MesSafetyRiskUnitDO::getId, Function.identity(), (a, b) -> a));
|
Map<Long, Long> countMap = new HashMap<>();
|
for (MesSafetyRiskDO risk : risks) {
|
MesSafetyRiskUnitDO unit = unitMap.get(risk.getUnitId());
|
if (unit == null || unit.getWorkshopId() == null) {
|
continue;
|
}
|
countMap.merge(unit.getWorkshopId(), 1L, Long::sum);
|
}
|
return toTopList(countMap, workshopNameMap);
|
}
|
|
// ==================== 隐患 ====================
|
|
private HazardStats buildHazardStats(List<MesSafetyHazardDO> hazards, LocalDate today) {
|
HazardStats stats = new HazardStats();
|
long total = hazards.size();
|
long pending = 0, rectifying = 0, toCheck = 0, closed = 0, overdue = 0;
|
for (MesSafetyHazardDO h : hazards) {
|
Integer s = h.getStatus();
|
if (MesSafetyHazardDO.STATUS_PENDING.equals(s)) pending++;
|
else if (MesSafetyHazardDO.STATUS_RECTIFYING.equals(s)) rectifying++;
|
else if (MesSafetyHazardDO.STATUS_TO_CHECK.equals(s)) toCheck++;
|
else if (MesSafetyHazardDO.STATUS_CLOSED.equals(s)) closed++;
|
if (h.getDeadline() != null
|
&& !MesSafetyHazardDO.STATUS_CLOSED.equals(s)
|
&& h.getDeadline().isBefore(today)) {
|
overdue++;
|
}
|
}
|
stats.setTotal(total);
|
stats.setPending(pending);
|
stats.setRectifying(rectifying);
|
stats.setToCheck(toCheck);
|
stats.setClosed(closed);
|
stats.setOverdue(overdue);
|
stats.setClosedRate(total > 0
|
? BigDecimal.valueOf(closed * 100.0 / total).setScale(1, RoundingMode.HALF_UP)
|
: BigDecimal.ZERO);
|
return stats;
|
}
|
|
private List<NameCount> buildHazardByWorkshop(List<MesSafetyHazardDO> hazards, Map<Long, String> workshopNameMap) {
|
Map<Long, Long> countMap = new HashMap<>();
|
for (MesSafetyHazardDO h : hazards) {
|
if (h.getWorkshopId() != null) {
|
countMap.merge(h.getWorkshopId(), 1L, Long::sum);
|
}
|
}
|
return toTopList(countMap, workshopNameMap);
|
}
|
|
// ==================== 作业票 ====================
|
|
private TicketStats buildTicketStats(List<MesSafetyWorkTicketDO> tickets, LocalDateTime now) {
|
TicketStats stats = new TicketStats();
|
long pending = 0, approved = 0, finished = 0, archived = 0, rejected = 0, overdue = 0;
|
for (MesSafetyWorkTicketDO t : tickets) {
|
Integer s = t.getStatus();
|
if (MesSafetyWorkTicketDO.STATUS_PENDING.equals(s)) pending++;
|
else if (MesSafetyWorkTicketDO.STATUS_APPROVED.equals(s)) approved++;
|
else if (MesSafetyWorkTicketDO.STATUS_FINISHED.equals(s)) finished++;
|
else if (MesSafetyWorkTicketDO.STATUS_ARCHIVED.equals(s)) archived++;
|
else if (MesSafetyWorkTicketDO.STATUS_REJECTED.equals(s)) rejected++;
|
if (MesSafetyWorkTicketDO.STATUS_APPROVED.equals(s)
|
&& t.getPlanEndTime() != null
|
&& t.getPlanEndTime().isBefore(now)) {
|
overdue++;
|
}
|
}
|
stats.setTotal((long) tickets.size());
|
stats.setPending(pending);
|
stats.setApproved(approved);
|
stats.setFinished(finished);
|
stats.setArchived(archived);
|
stats.setRejected(rejected);
|
stats.setOverdue(overdue);
|
return stats;
|
}
|
|
// ==================== 培训 ====================
|
|
private TrainingStats buildTrainingStats(List<MesSafetyTrainingDO> trainings) {
|
TrainingStats stats = new TrainingStats();
|
long finished = 0, ongoing = 0, planned = 0;
|
long attendeeTotal = 0;
|
BigDecimal scoreSum = BigDecimal.ZERO;
|
int scoredCount = 0;
|
for (MesSafetyTrainingDO t : trainings) {
|
Integer s = t.getStatus();
|
if (MesSafetyTrainingDO.STATUS_FINISHED.equals(s)) finished++;
|
else if (MesSafetyTrainingDO.STATUS_ONGOING.equals(s)) ongoing++;
|
else if (MesSafetyTrainingDO.STATUS_PLANNED.equals(s)) planned++;
|
if (t.getAttendeeCount() != null) {
|
attendeeTotal += t.getAttendeeCount();
|
}
|
if (t.getAvgScore() != null) {
|
scoreSum = scoreSum.add(t.getAvgScore());
|
scoredCount++;
|
}
|
}
|
stats.setTotal((long) trainings.size());
|
stats.setFinished(finished);
|
stats.setOngoing(ongoing);
|
stats.setPlanned(planned);
|
stats.setAttendeeTotal(attendeeTotal);
|
stats.setAvgScore(scoredCount > 0
|
? scoreSum.divide(BigDecimal.valueOf(scoredCount), 1, RoundingMode.HALF_UP)
|
: null);
|
return stats;
|
}
|
|
// ==================== 证书 ====================
|
|
private CertificateStats buildCertificateStats(List<MesSafetyCertificateDO> certificates, LocalDate today) {
|
CertificateStats stats = new CertificateStats();
|
long valid = 0, expired = 0, expiringSoon = 0;
|
for (MesSafetyCertificateDO c : certificates) {
|
boolean isValid = MesSafetyCertificateDO.STATUS_VALID.equals(c.getStatus());
|
if (isValid) {
|
valid++;
|
}
|
if (c.getExpireDate() != null) {
|
long days = java.time.temporal.ChronoUnit.DAYS.between(today, c.getExpireDate());
|
if (days < 0) {
|
expired++;
|
} else if (days <= EXPIRING_SOON_DAYS) {
|
expiringSoon++;
|
}
|
}
|
}
|
stats.setTotal((long) certificates.size());
|
stats.setValid(valid);
|
stats.setExpired(expired);
|
stats.setExpiringSoon(expiringSoon);
|
return stats;
|
}
|
|
// ==================== 最近动态 ====================
|
|
private List<RecentHazard> buildRecentHazards(List<MesSafetyHazardDO> hazards) {
|
return hazards.stream()
|
.sorted(Comparator.comparing(MesSafetyHazardDO::getId, Comparator.nullsLast(Comparator.reverseOrder())))
|
.limit(RECENT_N)
|
.map(h -> {
|
RecentHazard r = new RecentHazard();
|
r.setId(h.getId());
|
r.setCode(h.getCode());
|
r.setName(h.getName());
|
r.setHazardLevel(h.getHazardLevel());
|
r.setStatus(h.getStatus());
|
r.setFoundDate(h.getFoundDate());
|
return r;
|
})
|
.collect(Collectors.toList());
|
}
|
|
private List<RecentTicket> buildRecentTickets(List<MesSafetyWorkTicketDO> tickets) {
|
return tickets.stream()
|
.sorted(Comparator.comparing(MesSafetyWorkTicketDO::getId, Comparator.nullsLast(Comparator.reverseOrder())))
|
.limit(RECENT_N)
|
.map(t -> {
|
RecentTicket r = new RecentTicket();
|
r.setId(t.getId());
|
r.setCode(t.getCode());
|
r.setTicketType(t.getTicketType());
|
r.setWorkLocation(t.getWorkLocation());
|
r.setStatus(t.getStatus());
|
r.setCreateTime(t.getCreateTime());
|
return r;
|
})
|
.collect(Collectors.toList());
|
}
|
|
// ==================== 工具 ====================
|
|
private Map<Long, String> loadWorkshopNames() {
|
return workshopMapper.selectList().stream()
|
.collect(Collectors.toMap(MesMdWorkshopDO::getId, MesMdWorkshopDO::getName, (a, b) -> a));
|
}
|
|
private List<NameCount> toTopList(Map<Long, Long> countMap, Map<Long, String> nameMap) {
|
return countMap.entrySet().stream()
|
.sorted(Map.Entry.<Long, Long>comparingByValue().reversed())
|
.limit(TOP_N)
|
.map(e -> new NameCount(
|
nameMap.getOrDefault(e.getKey(), "未知车间"),
|
e.getValue(),
|
e.getKey().intValue()))
|
.collect(Collectors.toList());
|
}
|
|
}
|