tuqin
6 天以前 6e43a9e4daa56a07fe605d6bec1370f36bd5fbfc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
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());
    }
 
}