Junjie
1 天以前 8027c8e2e0b5c559da612b187031dd6fd82d9bc7
src/main/java/com/zy/asrs/service/impl/WrkAnalysisServiceImpl.java
@@ -3,6 +3,7 @@
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.core.common.Cools;
@@ -15,6 +16,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
@@ -27,7 +30,8 @@
            WrkStsType.SETTLE_INBOUND.sts,
            WrkStsType.COMPLETE_OUTBOUND.sts,
            WrkStsType.SETTLE_OUTBOUND.sts,
            WrkStsType.COMPLETE_LOC_MOVE.sts
            WrkStsType.COMPLETE_LOC_MOVE.sts,
            WrkStsType.COMPLETE_CRN_MOVE.sts
    );
    private static final String MODE_TASK = "TASK";
    private static final String MODE_TIME = "TIME";
@@ -42,6 +46,7 @@
    private final BasCrnpErrLogService basCrnpErrLogService;
    private final BasDualCrnpErrLogService basDualCrnpErrLogService;
    private final BasRgvErrLogService basRgvErrLogService;
    private final BasStationErrLogService basStationErrLogService;
    private final BasStationService basStationService;
    private final BasWrkStatusService basWrkStatusService;
    private final WrkMastService wrkMastService;
@@ -50,6 +55,7 @@
                                  BasCrnpErrLogService basCrnpErrLogService,
                                  BasDualCrnpErrLogService basDualCrnpErrLogService,
                                  BasRgvErrLogService basRgvErrLogService,
                                  BasStationErrLogService basStationErrLogService,
                                  BasStationService basStationService,
                                  BasWrkStatusService basWrkStatusService,
                                  WrkMastService wrkMastService) {
@@ -57,6 +63,7 @@
        this.basCrnpErrLogService = basCrnpErrLogService;
        this.basDualCrnpErrLogService = basDualCrnpErrLogService;
        this.basRgvErrLogService = basRgvErrLogService;
        this.basStationErrLogService = basStationErrLogService;
        this.basStationService = basStationService;
        this.basWrkStatusService = basWrkStatusService;
        this.wrkMastService = wrkMastService;
@@ -68,7 +75,7 @@
        if (wrkMast == null || wrkMast.getWrkNo() == null) {
            return;
        }
        WrkAnalysis entity = this.getById(wrkMast.getWrkNo());
        WrkAnalysis entity = findActiveRecord(wrkMast.getWrkNo());
        Date now = new Date();
        if (entity == null) {
            entity = new WrkAnalysis();
@@ -85,9 +92,15 @@
        entity.setDualCrnFaultDurationMs(defaultLong(entity.getDualCrnFaultDurationMs()));
        entity.setRgvFaultCount(defaultInt(entity.getRgvFaultCount()));
        entity.setRgvFaultDurationMs(defaultLong(entity.getRgvFaultDurationMs()));
        entity.setStationFaultCount(defaultInt(entity.getStationFaultCount()));
        entity.setStationFaultDurationMs(defaultLong(entity.getStationFaultDurationMs()));
        entity.setMetricCompleteness(METRIC_PARTIAL);
        entity.setUpdateTime(now);
        this.saveOrUpdate(entity);
        if (entity.getId() == null) {
            this.save(entity);
        } else {
            this.updateById(entity);
        }
    }
    @Override
@@ -126,13 +139,11 @@
        if (wrkMast == null || wrkMast.getWrkNo() == null || !Objects.equals(wrkMast.getWrkSts(), WrkStsType.INBOUND_STATION_RUN.sts)) {
            return false;
        }
        WrkMast updateEntity = new WrkMast();
        updateEntity.setWrkNo(wrkMast.getWrkNo());
        updateEntity.setWrkSts(WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts);
        Date now = safeDate(operateTime);
        updateEntity.setIoTime(now);
        updateEntity.setModiTime(now);
        boolean updated = wrkMast.getWrkNo() != null && wrkMastService.update(updateEntity, new QueryWrapper<WrkMast>()
        boolean updated = wrkMast.getWrkNo() != null && wrkMastService.update(null, new UpdateWrapper<WrkMast>()
                .set("wrk_sts", WrkStsType.INBOUND_STATION_RUN_COMPLETE.sts)
                .set("io_time", now)
                .set("modi_time", now)
                .eq("wrk_no", wrkMast.getWrkNo())
                .eq("wrk_sts", WrkStsType.INBOUND_STATION_RUN.sts));
        if (!updated) {
@@ -185,7 +196,9 @@
        entity.setRgvNo(wrkMast.getRgvNo());
        entity.setFinalWrkSts(wrkMast.getWrkSts());
        entity.setUpdateTime(time);
        if (Objects.equals(wrkMast.getIoType(), WrkIoType.LOC_MOVE.id) && entity.getStationDurationMs() == null) {
        if ((Objects.equals(wrkMast.getIoType(), WrkIoType.LOC_MOVE.id)
                || Objects.equals(wrkMast.getIoType(), WrkIoType.CRN_MOVE.id))
                && entity.getStationDurationMs() == null) {
            entity.setStationDurationMs(0L);
        }
        this.updateById(entity);
@@ -211,6 +224,12 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void finishTask(WrkMast wrkMast, Date finishTime) {
        finishTask(wrkMast, finishTime, null);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void finishTask(WrkMast wrkMast, Date finishTime, Long wrkLogId) {
        if (wrkMast == null || wrkMast.getWrkNo() == null) {
            return;
        }
@@ -224,10 +243,13 @@
        if (entity.getAppeTime() != null) {
            entity.setTotalDurationMs(durationMs(entity.getAppeTime(), time));
        }
        if (Objects.equals(wrkMast.getIoType(), WrkIoType.LOC_MOVE.id) && entity.getStationDurationMs() == null) {
        if ((Objects.equals(wrkMast.getIoType(), WrkIoType.LOC_MOVE.id)
                || Objects.equals(wrkMast.getIoType(), WrkIoType.CRN_MOVE.id))
                && entity.getStationDurationMs() == null) {
            entity.setStationDurationMs(0L);
        }
        FaultSummary faultSummary = buildFaultSummary(wrkMast.getWrkNo(), time);
        entity.setWrkLogId(wrkLogId);
        FaultSummary faultSummary = buildFaultSummary(wrkMast.getWrkNo(), entity.getAppeTime(), time);
        entity.setHasFault(faultSummary.hasFault);
        entity.setFaultCount(faultSummary.totalCount);
        entity.setFaultDurationMs(faultSummary.totalDurationMs);
@@ -237,21 +259,25 @@
        entity.setDualCrnFaultDurationMs(faultSummary.dualDurationMs);
        entity.setRgvFaultCount(faultSummary.rgvCount);
        entity.setRgvFaultDurationMs(faultSummary.rgvDurationMs);
        entity.setStationFaultCount(faultSummary.stationCount);
        entity.setStationFaultDurationMs(faultSummary.stationDurationMs);
        entity.setMetricCompleteness(resolveMetricCompleteness(wrkMast, entity));
        entity.setUpdateTime(time);
        this.saveOrUpdate(entity);
        this.updateById(entity);
    }
    @Override
    public Map<Integer, WrkAnalysis> mapByWrkNos(Collection<Integer> wrkNos) {
        Map<Integer, WrkAnalysis> result = new LinkedHashMap<>();
        if (wrkNos == null || wrkNos.isEmpty()) {
    public Map<Long, WrkAnalysis> mapByWrkLogIds(Collection<Long> wrkLogIds) {
        Map<Long, WrkAnalysis> result = new LinkedHashMap<>();
        if (wrkLogIds == null || wrkLogIds.isEmpty()) {
            return result;
        }
        List<WrkAnalysis> list = this.listByIds(new LinkedHashSet<>(wrkNos));
        List<WrkAnalysis> list = this.list(new QueryWrapper<WrkAnalysis>()
                .in("wrk_log_id", new LinkedHashSet<>(wrkLogIds))
                .orderByAsc("wrk_log_id", "id"));
        for (WrkAnalysis item : list) {
            if (item != null && item.getWrkNo() != null) {
                result.put(item.getWrkNo(), item);
            if (item != null && item.getWrkLogId() != null) {
                result.put(item.getWrkLogId(), item);
            }
        }
        return result;
@@ -264,6 +290,7 @@
        ioTypes.add(option("1", "IN", "入库", WrkIoType.IN.id));
        ioTypes.add(option("2", "OUT", "出库", WrkIoType.OUT.id));
        ioTypes.add(option("3", "LOC_MOVE", "移库", WrkIoType.LOC_MOVE.id));
        ioTypes.add(option("4", "CRN_MOVE", "堆垛机移动", WrkIoType.CRN_MOVE.id));
        List<Map<String, Object>> timeFields = new ArrayList<>();
        timeFields.add(option(TIME_FIELD_FINISH, TIME_FIELD_FINISH, "完成时间", TIME_FIELD_FINISH));
        timeFields.add(option(TIME_FIELD_APPE, TIME_FIELD_APPE, "创建时间", TIME_FIELD_APPE));
@@ -302,11 +329,11 @@
        QueryWrapper<WrkMastLog> wrapper = buildHistoryLogWrapper(param);
        Page<WrkMastLog> page = wrkMastLogService.page(new Page<>(pageNo, pageSize), wrapper);
        List<WrkMastLog> logList = page.getRecords();
        List<Integer> wrkNos = logList.stream().map(WrkMastLog::getWrkNo).filter(Objects::nonNull).collect(Collectors.toList());
        Map<Integer, WrkAnalysis> analysisMap = mapByWrkNos(wrkNos);
        List<Long> logIds = logList.stream().map(WrkMastLog::getId).filter(Objects::nonNull).collect(Collectors.toList());
        Map<Long, WrkAnalysis> analysisMap = mapByWrkLogIds(logIds);
        List<Map<String, Object>> records = new ArrayList<>();
        for (WrkMastLog log : logList) {
            records.add(toListItem(log, analysisMap.get(log.getWrkNo())));
            records.add(toListItem(log, analysisMap.get(log.getId())));
        }
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("records", records);
@@ -326,13 +353,14 @@
        QueryWrapper<WrkAnalysis> wrapper = buildAnalysisWrapper(request, mode);
        List<WrkAnalysis> list = this.list(wrapper);
        list.sort(Comparator.comparing(WrkAnalysis::getFinishTime, Comparator.nullsLast(Date::compareTo))
                .thenComparing(WrkAnalysis::getWrkNo, Comparator.nullsLast(Integer::compareTo)));
                .thenComparing(WrkAnalysis::getWrkLogId, Comparator.nullsLast(Long::compareTo))
                .thenComparing(WrkAnalysis::getId, Comparator.nullsLast(Long::compareTo)));
        Collections.reverse(list);
        String timeField = TIME_FIELD_FINISH;
        if (TIME_FIELD_APPE.equalsIgnoreCase(request.getString("timeField"))) {
            timeField = TIME_FIELD_APPE;
        }
        return buildAnalysisResult(list, mode, timeField, request);
        return buildAnalysisResult(list, timeField);
    }
    private QueryWrapper<WrkMastLog> buildHistoryLogWrapper(Map<String, Object> param) {
@@ -366,7 +394,7 @@
        applyDeviceTypeFilter(wrapper, upperTrim(stringValue(param.get("deviceType"))));
        applyRange(wrapper, "appe_time", stringValue(param.get("appeTimeRange")));
        applyRange(wrapper, "modi_time", stringValue(param.get("finishTimeRange")));
        wrapper.orderByDesc("modi_time", "wrk_no");
        wrapper.orderByDesc("modi_time", "id");
        return wrapper;
    }
@@ -399,6 +427,11 @@
            wrapper.isNotNull("rgv_no");
        }
        if (MODE_TASK.equals(mode)) {
            List<Long> wrkLogIds = parseLongIds(request.get("wrkLogIds"));
            if (!wrkLogIds.isEmpty()) {
                wrapper.in("wrk_log_id", wrkLogIds);
                return wrapper;
            }
            List<Integer> wrkNos = parseWrkNos(request.get("wrkNos"));
            if (wrkNos.isEmpty()) {
                wrapper.eq("wrk_no", -1);
@@ -425,7 +458,7 @@
            return;
        }
        QueryWrapper<WrkAnalysis> analysisWrapper = new QueryWrapper<>();
        analysisWrapper.select("wrk_no");
        analysisWrapper.select("wrk_log_id");
        if (DEVICE_TYPE_CRN.equals(deviceType)) {
            analysisWrapper.isNotNull("crn_no");
        } else if (DEVICE_TYPE_DUAL_CRN.equals(deviceType)) {
@@ -435,21 +468,40 @@
        } else {
            return;
        }
        List<Integer> wrkNos = this.list(analysisWrapper).stream()
                .map(WrkAnalysis::getWrkNo)
        List<Long> wrkLogIds = this.list(analysisWrapper).stream()
                .map(WrkAnalysis::getWrkLogId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (wrkNos.isEmpty()) {
            wrapper.eq("wrk_no", -1);
        if (wrkLogIds.isEmpty()) {
            wrapper.eq("id", -1);
            return;
        }
        wrapper.in("wrk_no", wrkNos);
        wrapper.in("id", wrkLogIds);
    }
    private Map<String, Object> buildAnalysisResult(List<WrkAnalysis> list, String mode, String timeField, JSONObject request) {
    private Map<String, Object> buildAnalysisResult(List<WrkAnalysis> list, String timeField) {
        Map<String, Object> result = new LinkedHashMap<>();
        Map<String, Object> summary = new LinkedHashMap<>();
        Date taskStartTime = list.stream()
                .map(WrkAnalysis::getAppeTime)
                .filter(Objects::nonNull)
                .min(Date::compareTo)
                .orElse(null);
        Date taskEndTime = list.stream()
                .map(WrkAnalysis::getFinishTime)
                .filter(Objects::nonNull)
                .max(Date::compareTo)
                .orElse(null);
        summary.put("taskCount", list.size());
        summary.put("taskStartTime", taskStartTime);
        summary.put("taskStartTime$", formatDate(taskStartTime));
        summary.put("taskEndTime", taskEndTime);
        summary.put("taskEndTime$", formatDate(taskEndTime));
        summary.put("taskDurationMs", taskStartTime == null || taskEndTime == null ? null : durationMs(taskStartTime, taskEndTime));
        summary.put("avgTaskBeatDurationMs", list.isEmpty() || taskStartTime == null || taskEndTime == null
                ? null
                : durationMs(taskStartTime, taskEndTime) / list.size());
        summary.put("avgTaskPerHour", calculateAvgTaskPerHour(list.size(), summary.get("taskDurationMs")));
        summary.put("avgTotalDurationMs", average(list, item -> item.getTotalDurationMs() != null, WrkAnalysis::getTotalDurationMs));
        summary.put("avgStationDurationMs", average(list, item -> !METRIC_PARTIAL.equals(item.getMetricCompleteness()) && item.getStationDurationMs() != null, WrkAnalysis::getStationDurationMs));
        summary.put("avgCraneDurationMs", average(list, item -> !METRIC_PARTIAL.equals(item.getMetricCompleteness()) && item.getCraneDurationMs() != null, WrkAnalysis::getCraneDurationMs));
@@ -458,7 +510,7 @@
        summary.put("partialTaskCount", list.stream().filter(item -> METRIC_PARTIAL.equals(item.getMetricCompleteness())).count());
        result.put("summary", summary);
        result.put("durationCompare", buildDurationCompare(list));
        result.put("trend", buildTrend(list, mode, timeField, request));
        result.put("trend", buildTrend(list, timeField));
        result.put("faultPie", buildFaultPie(list));
        result.put("faultDuration", buildFaultDuration(list));
        result.put("detail", buildDetailRows(list));
@@ -473,21 +525,13 @@
                .collect(Collectors.toList());
    }
    private List<Map<String, Object>> buildTrend(List<WrkAnalysis> list, String mode, String timeField, JSONObject request) {
    private List<Map<String, Object>> buildTrend(List<WrkAnalysis> list, String timeField) {
        List<Map<String, Object>> trend = new ArrayList<>();
        if (list.isEmpty()) {
            return trend;
        }
        long startMs = Long.MAX_VALUE;
        long endMs = Long.MIN_VALUE;
        if (MODE_TIME.equals(mode)) {
            Long reqStart = parseLong(request.get("startTime"));
            Long reqEnd = parseLong(request.get("endTime"));
            if (reqStart != null && reqEnd != null) {
                startMs = reqStart;
                endMs = reqEnd;
            }
        }
        if (startMs == Long.MAX_VALUE || endMs == Long.MIN_VALUE) {
            for (WrkAnalysis item : list) {
                Date date = resolveBucketTime(item, timeField);
@@ -502,10 +546,10 @@
        if (startMs == Long.MAX_VALUE || endMs == Long.MIN_VALUE) {
            return trend;
        }
        boolean hourly = endMs - startMs <= 72L * 60L * 60L * 1000L;
        SimpleDateFormat keyFormat = new SimpleDateFormat(hourly ? "yyyy-MM-dd HH:00" : "yyyy-MM-dd");
        TrendBucketType bucketType = resolveTrendBucketType(startMs, endMs);
        SimpleDateFormat keyFormat = new SimpleDateFormat(resolveTrendBucketPattern(bucketType));
        keyFormat.setTimeZone(TimeZone.getDefault());
        Map<String, BucketAccumulator> bucketMap = new LinkedHashMap<>();
        Map<Long, BucketAccumulator> bucketMap = new LinkedHashMap<>();
        List<WrkAnalysis> sorted = new ArrayList<>(list);
        sorted.sort(Comparator.comparing(item -> resolveBucketTime(item, timeField), Comparator.nullsLast(Date::compareTo)));
        for (WrkAnalysis item : sorted) {
@@ -513,7 +557,8 @@
            if (bucketTime == null) {
                continue;
            }
            String key = keyFormat.format(bucketTime);
            Date bucketStart = truncateBucketTime(bucketTime, bucketType);
            long key = bucketStart.getTime();
            BucketAccumulator accumulator = bucketMap.computeIfAbsent(key, k -> new BucketAccumulator());
            accumulator.taskCount++;
            if (item.getTotalDurationMs() != null) {
@@ -529,9 +574,9 @@
                accumulator.craneDurationCount++;
            }
        }
        for (Map.Entry<String, BucketAccumulator> entry : bucketMap.entrySet()) {
        for (Map.Entry<Long, BucketAccumulator> entry : bucketMap.entrySet()) {
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("bucketLabel", entry.getKey());
            item.put("bucketLabel", keyFormat.format(new Date(entry.getKey())));
            item.put("taskCount", entry.getValue().taskCount);
            item.put("avgTotalDurationMs", entry.getValue().totalDurationCount == 0 ? null : entry.getValue().totalDurationMs / entry.getValue().totalDurationCount);
            item.put("avgStationDurationMs", entry.getValue().stationDurationCount == 0 ? null : entry.getValue().stationDurationMs / entry.getValue().stationDurationCount);
@@ -539,6 +584,41 @@
            trend.add(item);
        }
        return trend;
    }
    private TrendBucketType resolveTrendBucketType(long startMs, long endMs) {
        long spanMs = Math.max(0L, endMs - startMs);
        if (spanMs <= 6L * 60L * 60L * 1000L) {
            return TrendBucketType.MINUTE;
        }
        if (spanMs <= 72L * 60L * 60L * 1000L) {
            return TrendBucketType.HOUR;
        }
        return TrendBucketType.DAY;
    }
    private String resolveTrendBucketPattern(TrendBucketType bucketType) {
        if (bucketType == TrendBucketType.MINUTE) {
            return "yyyy-MM-dd HH:mm";
        }
        if (bucketType == TrendBucketType.HOUR) {
            return "yyyy-MM-dd HH:00";
        }
        return "yyyy-MM-dd";
    }
    private Date truncateBucketTime(Date time, TrendBucketType bucketType) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(time);
        if (bucketType == TrendBucketType.DAY) {
            calendar.set(Calendar.HOUR_OF_DAY, 0);
        }
        if (bucketType == TrendBucketType.DAY || bucketType == TrendBucketType.HOUR) {
            calendar.set(Calendar.MINUTE, 0);
        }
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        return calendar.getTime();
    }
    private List<Map<String, Object>> buildFaultPie(List<WrkAnalysis> list) {
@@ -551,11 +631,25 @@
    }
    private List<Map<String, Object>> buildFaultDuration(List<WrkAnalysis> list) {
        Map<String, Long> durationMap = new LinkedHashMap<>();
        for (WrkAnalysis item : list) {
            addDeviceFaultDuration(durationMap, "单堆垛机", item.getCrnNo(), item.getCrnFaultDurationMs());
            addDeviceFaultDuration(durationMap, "双工位堆垛机", item.getDualCrnNo(), item.getDualCrnFaultDurationMs());
            addDeviceFaultDuration(durationMap, "RGV", item.getRgvNo(), item.getRgvFaultDurationMs());
            addDeviceFaultDuration(durationMap, "输送站点", null, item.getStationFaultDurationMs());
        }
        List<Map<String, Object>> result = new ArrayList<>();
        result.add(slice("单堆垛机", list.stream().map(WrkAnalysis::getCrnFaultDurationMs).filter(Objects::nonNull).reduce(0L, Long::sum)));
        result.add(slice("双工位堆垛机", list.stream().map(WrkAnalysis::getDualCrnFaultDurationMs).filter(Objects::nonNull).reduce(0L, Long::sum)));
        result.add(slice("RGV", list.stream().map(WrkAnalysis::getRgvFaultDurationMs).filter(Objects::nonNull).reduce(0L, Long::sum)));
        durationMap.forEach((name, durationMs) -> result.add(slice(name, durationMs)));
        return result;
    }
    private void addDeviceFaultDuration(Map<String, Long> durationMap, String deviceLabel, Integer deviceNo, Long durationMs) {
        long value = defaultLong(durationMs);
        if (value <= 0L) {
            return;
        }
        String key = deviceNo == null ? deviceLabel : deviceLabel + deviceNo;
        durationMap.merge(key, value, Long::sum);
    }
    private List<Map<String, Object>> buildDetailRows(List<WrkAnalysis> list) {
@@ -564,6 +658,7 @@
    private Map<String, Object> toListItem(WrkMastLog log, WrkAnalysis analysis) {
        Map<String, Object> item = new LinkedHashMap<>();
        item.put("logId", log.getId());
        item.put("wrkNo", log.getWrkNo());
        item.put("wmsWrkNo", log.getWmsWrkNo());
        item.put("wrkSts", log.getWrkSts());
@@ -585,6 +680,9 @@
    private Map<String, Object> toDetailItem(WrkAnalysis item) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("id", item.getId());
        result.put("wrkLogId", item.getWrkLogId());
        result.put("rowKey", buildRowKey(item));
        result.put("wrkNo", item.getWrkNo());
        result.put("wmsWrkNo", item.getWmsWrkNo());
        result.put("ioType", item.getIoType());
@@ -613,25 +711,45 @@
        target.put("crnFaultDurationMs", analysis == null ? 0L : defaultLong(analysis.getCrnFaultDurationMs()));
        target.put("dualCrnFaultDurationMs", analysis == null ? 0L : defaultLong(analysis.getDualCrnFaultDurationMs()));
        target.put("rgvFaultDurationMs", analysis == null ? 0L : defaultLong(analysis.getRgvFaultDurationMs()));
        target.put("stationFaultDurationMs", analysis == null ? 0L : defaultLong(analysis.getStationFaultDurationMs()));
        target.put("metricCompleteness", analysis == null ? METRIC_PARTIAL : analysis.getMetricCompleteness());
    }
    private FaultSummary buildFaultSummary(Integer wrkNo, Date finishTime) {
    private FaultSummary buildFaultSummary(Integer wrkNo, Date taskStartTime, Date finishTime) {
        FaultSummary summary = new FaultSummary();
        if (wrkNo == null) {
            return summary;
        }
        List<BasCrnpErrLog> crnList = basCrnpErrLogService.list(new QueryWrapper<BasCrnpErrLog>().eq("wrk_no", wrkNo));
        List<BasDualCrnpErrLog> dualList = basDualCrnpErrLogService.list(new QueryWrapper<BasDualCrnpErrLog>().eq("wrk_no", wrkNo));
        List<BasRgvErrLog> rgvList = basRgvErrLogService.list(new QueryWrapper<BasRgvErrLog>().eq("task_no", wrkNo));
        List<BasCrnpErrLog> crnList = filterTaskFaultLogs(
                basCrnpErrLogService.list(new QueryWrapper<BasCrnpErrLog>().eq("wrk_no", wrkNo)),
                taskStartTime,
                finishTime
        );
        List<BasDualCrnpErrLog> dualList = filterTaskFaultLogs(
                basDualCrnpErrLogService.list(new QueryWrapper<BasDualCrnpErrLog>().eq("wrk_no", wrkNo)),
                taskStartTime,
                finishTime
        );
        List<BasRgvErrLog> rgvList = filterTaskFaultLogs(
                basRgvErrLogService.list(new QueryWrapper<BasRgvErrLog>().eq("task_no", wrkNo)),
                taskStartTime,
                finishTime
        );
        List<BasStationErrLog> stationList = filterTaskFaultLogs(
                basStationErrLogService.list(new QueryWrapper<BasStationErrLog>().eq("wrk_no", wrkNo)),
                taskStartTime,
                finishTime
        );
        summary.crnCount = crnList.size();
        summary.crnDurationMs = durationMs(crnList, finishTime);
        summary.dualCount = dualList.size();
        summary.dualDurationMs = durationMs(dualList, finishTime);
        summary.rgvCount = rgvList.size();
        summary.rgvDurationMs = durationMs(rgvList, finishTime);
        summary.totalCount = summary.crnCount + summary.dualCount + summary.rgvCount;
        summary.totalDurationMs = summary.crnDurationMs + summary.dualDurationMs + summary.rgvDurationMs;
        summary.stationCount = stationList.size();
        summary.stationDurationMs = durationMs(stationList, finishTime);
        summary.totalCount = summary.crnCount + summary.dualCount + summary.rgvCount + summary.stationCount;
        summary.totalDurationMs = summary.crnDurationMs + summary.dualDurationMs + summary.rgvDurationMs + summary.stationDurationMs;
        summary.hasFault = summary.totalCount > 0 ? 1 : 0;
        return summary;
    }
@@ -650,6 +768,9 @@
            } else if (item instanceof BasRgvErrLog) {
                startTime = ((BasRgvErrLog) item).getStartTime();
                endTime = ((BasRgvErrLog) item).getEndTime();
            } else if (item instanceof BasStationErrLog) {
                startTime = ((BasStationErrLog) item).getStartTime();
                endTime = ((BasStationErrLog) item).getEndTime();
            }
            if (startTime == null) {
                continue;
@@ -676,13 +797,13 @@
        if (wrkMast == null || wrkMast.getWrkNo() == null) {
            return null;
        }
        WrkAnalysis entity = this.getById(wrkMast.getWrkNo());
        WrkAnalysis entity = findActiveRecord(wrkMast.getWrkNo());
        if (entity != null) {
            syncBaseFields(entity, wrkMast);
            return entity;
        }
        initForTask(wrkMast);
        return this.getById(wrkMast.getWrkNo());
        return findActiveRecord(wrkMast.getWrkNo());
    }
    private void syncBaseFields(WrkAnalysis entity, WrkMast wrkMast) {
@@ -705,6 +826,28 @@
            return item.getAppeTime();
        }
        return item.getFinishTime();
    }
    private WrkAnalysis findActiveRecord(Integer wrkNo) {
        if (wrkNo == null) {
            return null;
        }
        return this.getOne(new QueryWrapper<WrkAnalysis>()
                .eq("wrk_no", wrkNo)
                .isNull("wrk_log_id")
                .isNull("finish_time")
                .orderByDesc("id")
                .last("limit 1"), false);
    }
    private String buildRowKey(WrkAnalysis item) {
        if (item.getWrkLogId() != null) {
            return "log-" + item.getWrkLogId();
        }
        if (item.getId() != null) {
            return "analysis-" + item.getId();
        }
        return "wrk-" + defaultInt(item.getWrkNo()) + "-" + defaultLong(item.getAppeTime() == null ? null : item.getAppeTime().getTime());
    }
    private Map<String, Object> option(String key, String code, String label, Object value) {
@@ -758,6 +901,9 @@
        if (Objects.equals(ioType, WrkIoType.LOC_MOVE.id)) {
            return "移库";
        }
        if (Objects.equals(ioType, WrkIoType.CRN_MOVE.id)) {
            return "堆垛机移动";
        }
        return String.valueOf(ioType);
    }
@@ -784,6 +930,19 @@
        return count == 0 ? null : total / count;
    }
    private Double calculateAvgTaskPerHour(int taskCount, Object taskDurationMsValue) {
        if (taskCount <= 0 || !(taskDurationMsValue instanceof Number)) {
            return null;
        }
        long taskDurationMs = ((Number) taskDurationMsValue).longValue();
        if (taskDurationMs <= 0L) {
            return null;
        }
        return BigDecimal.valueOf(taskCount * 3600000D / taskDurationMs)
                .setScale(2, RoundingMode.HALF_UP)
                .doubleValue();
    }
    private void applyRange(QueryWrapper<WrkMastLog> wrapper, String column, String rawValue) {
        if (Cools.isEmpty(rawValue) || !rawValue.contains("~")) {
            return;
@@ -794,6 +953,59 @@
        }
        wrapper.ge(column, DateUtils.convert(parts[0].trim()));
        wrapper.le(column, DateUtils.convert(parts[1].trim()));
    }
    private <T> List<T> filterTaskFaultLogs(List<T> source, Date taskStartTime, Date taskFinishTime) {
        if (source == null || source.isEmpty()) {
            return Collections.emptyList();
        }
        return source.stream()
                .filter(item -> overlapsTaskWindow(resolveFaultStartTime(item), resolveFaultEndTime(item), taskStartTime, taskFinishTime))
                .collect(Collectors.toList());
    }
    private Date resolveFaultStartTime(Object item) {
        if (item instanceof BasCrnpErrLog) {
            return ((BasCrnpErrLog) item).getStartTime();
        }
        if (item instanceof BasDualCrnpErrLog) {
            return ((BasDualCrnpErrLog) item).getStartTime();
        }
        if (item instanceof BasRgvErrLog) {
            return ((BasRgvErrLog) item).getStartTime();
        }
        if (item instanceof BasStationErrLog) {
            return ((BasStationErrLog) item).getStartTime();
        }
        return null;
    }
    private Date resolveFaultEndTime(Object item) {
        if (item instanceof BasCrnpErrLog) {
            return ((BasCrnpErrLog) item).getEndTime();
        }
        if (item instanceof BasDualCrnpErrLog) {
            return ((BasDualCrnpErrLog) item).getEndTime();
        }
        if (item instanceof BasRgvErrLog) {
            return ((BasRgvErrLog) item).getEndTime();
        }
        if (item instanceof BasStationErrLog) {
            return ((BasStationErrLog) item).getEndTime();
        }
        return null;
    }
    private boolean overlapsTaskWindow(Date eventStartTime, Date eventEndTime, Date taskStartTime, Date taskFinishTime) {
        long taskStart = taskStartTime == null ? Long.MIN_VALUE : taskStartTime.getTime();
        long taskEnd = taskFinishTime == null ? Long.MAX_VALUE : taskFinishTime.getTime();
        long eventStart = eventStartTime == null
                ? (eventEndTime == null ? Long.MIN_VALUE : eventEndTime.getTime())
                : eventStartTime.getTime();
        long eventEnd = eventEndTime == null
                ? (eventStartTime == null ? Long.MAX_VALUE : eventStartTime.getTime())
                : eventEndTime.getTime();
        return eventStart <= taskEnd && eventEnd >= taskStart;
    }
    private List<Integer> parseWrkNos(Object value) {
@@ -827,6 +1039,45 @@
        if (!Cools.isEmpty(text)) {
            for (String part : text.split(",")) {
                Integer parsed = parseInteger(part);
                if (parsed != null) {
                    result.add(parsed);
                }
            }
        }
        return result;
    }
    private List<Long> parseLongIds(Object value) {
        List<Long> result = new ArrayList<>();
        if (value == null) {
            return result;
        }
        if (value instanceof JSONArray) {
            JSONArray array = (JSONArray) value;
            for (int i = 0; i < array.size(); i++) {
                Long item = parseLong(array.get(i));
                if (item != null) {
                    result.add(item);
                }
            }
            return result;
        }
        if (value instanceof Collection) {
            for (Object item : (Collection<?>) value) {
                Long parsed = parseLong(item);
                if (parsed != null) {
                    result.add(parsed);
                }
            }
            return result;
        }
        String text = String.valueOf(value).trim();
        if (text.startsWith("[") && text.endsWith("]")) {
            return parseLongIds(JSONArray.parseArray(text));
        }
        if (!Cools.isEmpty(text)) {
            for (String part : text.split(",")) {
                Long parsed = parseLong(part);
                if (parsed != null) {
                    result.add(parsed);
                }
@@ -894,6 +1145,8 @@
        private long dualDurationMs;
        private int rgvCount;
        private long rgvDurationMs;
        private int stationCount;
        private long stationDurationMs;
    }
    private static class BucketAccumulator {
@@ -906,4 +1159,10 @@
        private long craneDurationCount;
    }
    private enum TrendBucketType {
        MINUTE,
        HOUR,
        DAY
    }
}