package com.ruoyi.lims.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.ruoyi.lims.dto.*; import com.ruoyi.lims.mapper.DataCollectionMapper; import com.ruoyi.lims.mapper.ExperimentMapper; import com.ruoyi.lims.mapper.RealtimeMonitorMapper; import com.ruoyi.lims.mapper.SampleMapper; import com.ruoyi.lims.pojo.DataCollection; import com.ruoyi.lims.pojo.Experiment; import com.ruoyi.lims.pojo.RealtimeMonitor; import com.ruoyi.lims.pojo.Sample; import com.ruoyi.lims.service.LimsDataAnalysisService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Transactional(rollbackFor = Exception.class) public class LimsDataAnalysisServiceImpl implements LimsDataAnalysisService { private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter HOUR_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00"); private final DataCollectionMapper dataCollectionMapper; private final ExperimentMapper experimentMapper; private final RealtimeMonitorMapper realtimeMonitorMapper; private final SampleMapper sampleMapper; @Override public LimsDataAnalysisDashboardDto dashboard(LimsDataAnalysisQueryDto queryDto) { LimsDataAnalysisQueryDto query = normalizeQuery(queryDto); LimsDataAnalysisDashboardDto dashboardDto = new LimsDataAnalysisDashboardDto(); dashboardDto.setStartTime(query.getStartTime()); dashboardDto.setEndTime(query.getEndTime()); dashboardDto.setGranularity(query.getGranularity()); dashboardDto.setDimension(query.getDimension()); dashboardDto.setOverview(buildOverview(query)); dashboardDto.setTrend(buildTrend(query)); dashboardDto.setComparison(buildComparison(query)); dashboardDto.setQualityDistribution(buildQualityDistribution(query)); return dashboardDto; } @Override public LimsAnalysisOverviewDto overview(LimsDataAnalysisQueryDto queryDto) { return buildOverview(normalizeQuery(queryDto)); } @Override public List trend(LimsDataAnalysisQueryDto queryDto) { return buildTrend(normalizeQuery(queryDto)); } @Override public List comparison(LimsDataAnalysisQueryDto queryDto) { return buildComparison(normalizeQuery(queryDto)); } @Override public List qualityDistribution(LimsDataAnalysisQueryDto queryDto) { return buildQualityDistribution(normalizeQuery(queryDto)); } private LimsAnalysisOverviewDto buildOverview(LimsDataAnalysisQueryDto query) { QueryWrapper totalWrapper = createDataCollectionFilter(query); Long totalCollections = dataCollectionMapper.selectCount(totalWrapper); QueryWrapper abnormalWrapper = createDataCollectionFilter(query); abnormalWrapper.eq("data_quality", "abnormal"); Long abnormalCollections = dataCollectionMapper.selectCount(abnormalWrapper); QueryWrapper qualifiedWrapper = createDataCollectionFilter(query); qualifiedWrapper.eq("data_quality", "qualified"); Long qualifiedCollections = dataCollectionMapper.selectCount(qualifiedWrapper); QueryWrapper todayWrapper = new QueryWrapper<>(); todayWrapper.eq("del_flag", "0") .ge("collection_time", LocalDateTime.now().minusHours(24)) .le("collection_time", LocalDateTime.now()); Long todayCollections = dataCollectionMapper.selectCount(todayWrapper); Long inProgressExperiments = experimentMapper.selectCount(new QueryWrapper() .eq("del_flag", "0") .eq("experiment_status", "inProgress")); Long warningMonitors = realtimeMonitorMapper.selectCount(new QueryWrapper() .eq("del_flag", "0") .in("alert_status", Arrays.asList("warning", "alert"))); Long inStockSamples = sampleMapper.selectCount(new QueryWrapper() .eq("del_flag", "0") .eq("sample_status", "inStock")); LimsAnalysisOverviewDto dto = new LimsAnalysisOverviewDto(); dto.setTotalCollections(defaultLong(totalCollections)); dto.setTodayCollections(defaultLong(todayCollections)); dto.setAbnormalCollections(defaultLong(abnormalCollections)); dto.setInProgressExperiments(defaultLong(inProgressExperiments)); dto.setWarningMonitors(defaultLong(warningMonitors)); dto.setInStockSamples(defaultLong(inStockSamples)); dto.setQualifiedRate(calculateRate(qualifiedCollections, totalCollections)); return dto; } private List buildTrend(LimsDataAnalysisQueryDto query) { String groupExpression = resolveTimeGroupExpression(query.getGranularity()); QueryWrapper wrapper = createDataCollectionFilter(query); wrapper.select( groupExpression + " AS time_key", "COUNT(1) AS point_count", "AVG(collection_value) AS avg_value", "MAX(collection_value) AS max_value", "MIN(collection_value) AS min_value" ) .groupBy(groupExpression) .orderByAsc("time_key"); List> rawRows = dataCollectionMapper.selectMaps(wrapper); Map trendMap = rawRows.stream().map(this::mapToTrendPoint) .collect(Collectors.toMap(LimsTrendPointDto::getTime, item -> item, (a, b) -> b)); List axis = buildTimeAxis(query); List result = new ArrayList<>(); for (String key : axis) { LimsTrendPointDto point = trendMap.get(key); if (point == null) { point = new LimsTrendPointDto(); point.setTime(key); point.setPointCount(0L); point.setAvgValue(BigDecimal.ZERO); point.setMaxValue(BigDecimal.ZERO); point.setMinValue(BigDecimal.ZERO); } result.add(point); } return result; } private List buildComparison(LimsDataAnalysisQueryDto query) { String dimensionColumn = resolveDimensionColumn(query.getDimension()); QueryWrapper wrapper = createDataCollectionFilter(query); wrapper.isNotNull(dimensionColumn) .ne(dimensionColumn, "") .select( dimensionColumn + " AS dimension_value", "COUNT(1) AS point_count", "AVG(collection_value) AS avg_value", "MAX(collection_value) AS max_value", "MIN(collection_value) AS min_value" ) .groupBy(dimensionColumn); List result = dataCollectionMapper.selectMaps(wrapper).stream() .map(this::mapToComparisonItem) .sorted(Comparator.comparing(LimsComparisonItemDto::getPointCount, Comparator.nullsLast(Long::compareTo)).reversed()) .collect(Collectors.toList()); int limit = query.getTopN() == null ? 10 : query.getTopN(); if (result.size() > limit) { return result.subList(0, limit); } return result; } private List buildQualityDistribution(LimsDataAnalysisQueryDto query) { QueryWrapper wrapper = createDataCollectionFilter(query); wrapper.isNotNull("data_quality") .ne("data_quality", "") .select("data_quality AS category", "COUNT(1) AS point_count") .groupBy("data_quality"); List result = dataCollectionMapper.selectMaps(wrapper).stream() .map(this::mapToQualityDistributionItem) .sorted(Comparator.comparing(LimsQualityDistributionItemDto::getPointCount, Comparator.nullsLast(Long::compareTo)).reversed()) .collect(Collectors.toList()); long total = result.stream().map(LimsQualityDistributionItemDto::getPointCount).filter(Objects::nonNull).mapToLong(Long::longValue).sum(); for (LimsQualityDistributionItemDto item : result) { item.setRatio(calculateRate(item.getPointCount(), total)); } return result; } private QueryWrapper createDataCollectionFilter(LimsDataAnalysisQueryDto query) { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("del_flag", "0") .ge("collection_time", query.getStartTime()) .le("collection_time", query.getEndTime()); if (StringUtils.hasText(query.getDataType())) { wrapper.eq("data_type", query.getDataType().trim()); } if (StringUtils.hasText(query.getDeviceCode())) { wrapper.eq("device_code", query.getDeviceCode().trim()); } return wrapper; } private String resolveTimeGroupExpression(String granularity) { if ("hour".equalsIgnoreCase(granularity)) { return "DATE_FORMAT(collection_time, '%Y-%m-%d %H:00:00')"; } return "DATE_FORMAT(collection_time, '%Y-%m-%d')"; } private String resolveDimensionColumn(String dimension) { if ("deviceName".equalsIgnoreCase(dimension)) { return "device_name"; } if ("deviceCode".equalsIgnoreCase(dimension)) { return "device_code"; } return "data_type"; } private List buildTimeAxis(LimsDataAnalysisQueryDto query) { List axis = new ArrayList<>(); if ("hour".equalsIgnoreCase(query.getGranularity())) { LocalDateTime cursor = query.getStartTime().withMinute(0).withSecond(0).withNano(0); LocalDateTime end = query.getEndTime().withMinute(0).withSecond(0).withNano(0); while (!cursor.isAfter(end)) { axis.add(cursor.format(HOUR_FORMATTER)); cursor = cursor.plusHours(1); } return axis; } LocalDate cursor = query.getStartTime().toLocalDate(); LocalDate end = query.getEndTime().toLocalDate(); while (!cursor.isAfter(end)) { axis.add(cursor.format(DAY_FORMATTER)); cursor = cursor.plusDays(1); } return axis; } private LimsTrendPointDto mapToTrendPoint(Map row) { LimsTrendPointDto point = new LimsTrendPointDto(); point.setTime(asString(readIgnoreCase(row, "time_key"))); point.setPointCount(defaultLong(asLong(readIgnoreCase(row, "point_count")))); point.setAvgValue(scale(asBigDecimal(readIgnoreCase(row, "avg_value")))); point.setMaxValue(scale(asBigDecimal(readIgnoreCase(row, "max_value")))); point.setMinValue(scale(asBigDecimal(readIgnoreCase(row, "min_value")))); return point; } private LimsComparisonItemDto mapToComparisonItem(Map row) { LimsComparisonItemDto item = new LimsComparisonItemDto(); item.setDimensionValue(asString(readIgnoreCase(row, "dimension_value"))); item.setPointCount(defaultLong(asLong(readIgnoreCase(row, "point_count")))); item.setAvgValue(scale(asBigDecimal(readIgnoreCase(row, "avg_value")))); item.setMaxValue(scale(asBigDecimal(readIgnoreCase(row, "max_value")))); item.setMinValue(scale(asBigDecimal(readIgnoreCase(row, "min_value")))); return item; } private LimsQualityDistributionItemDto mapToQualityDistributionItem(Map row) { LimsQualityDistributionItemDto item = new LimsQualityDistributionItemDto(); item.setCategory(asString(readIgnoreCase(row, "category"))); item.setPointCount(defaultLong(asLong(readIgnoreCase(row, "point_count")))); item.setRatio(BigDecimal.ZERO); return item; } private Object readIgnoreCase(Map row, String key) { if (row.containsKey(key)) { return row.get(key); } for (Map.Entry entry : row.entrySet()) { if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(key)) { return entry.getValue(); } } return null; } private BigDecimal calculateRate(Number part, Number total) { if (part == null || total == null || total.longValue() == 0L) { return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); } return BigDecimal.valueOf(part.longValue()) .multiply(BigDecimal.valueOf(100)) .divide(BigDecimal.valueOf(total.longValue()), 2, RoundingMode.HALF_UP); } private BigDecimal scale(BigDecimal value) { if (value == null) { return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); } return value.setScale(2, RoundingMode.HALF_UP); } private String asString(Object value) { return value == null ? "" : String.valueOf(value); } private Long asLong(Object value) { if (value == null) { return 0L; } if (value instanceof Number) { return ((Number) value).longValue(); } return Long.parseLong(String.valueOf(value)); } private BigDecimal asBigDecimal(Object value) { if (value == null) { return BigDecimal.ZERO; } if (value instanceof BigDecimal) { return (BigDecimal) value; } if (value instanceof Number) { return BigDecimal.valueOf(((Number) value).doubleValue()); } return new BigDecimal(String.valueOf(value)); } private Long defaultLong(Long value) { return value == null ? 0L : value; } private LimsDataAnalysisQueryDto normalizeQuery(LimsDataAnalysisQueryDto source) { LimsDataAnalysisQueryDto query = new LimsDataAnalysisQueryDto(); if (source != null) { query.setStartTime(source.getStartTime()); query.setEndTime(source.getEndTime()); query.setDataType(source.getDataType()); query.setDeviceCode(source.getDeviceCode()); query.setGranularity(source.getGranularity()); query.setDimension(source.getDimension()); query.setTopN(source.getTopN()); } LocalDateTime now = LocalDateTime.now().withSecond(0).withNano(0); if (query.getStartTime() == null && query.getEndTime() == null) { query.setEndTime(now); query.setStartTime(now.minusDays(6).withHour(0).withMinute(0)); } else if (query.getStartTime() == null) { query.setStartTime(query.getEndTime().minusDays(6).withHour(0).withMinute(0)); } else if (query.getEndTime() == null) { query.setEndTime(query.getStartTime().plusDays(6).withHour(23).withMinute(59)); } if (query.getStartTime().isAfter(query.getEndTime())) { LocalDateTime temp = query.getStartTime(); query.setStartTime(query.getEndTime()); query.setEndTime(temp); } if (!"hour".equalsIgnoreCase(query.getGranularity()) && !"day".equalsIgnoreCase(query.getGranularity())) { query.setGranularity("day"); } else { query.setGranularity(query.getGranularity().toLowerCase(Locale.ROOT)); } if (!"deviceName".equalsIgnoreCase(query.getDimension()) && !"deviceCode".equalsIgnoreCase(query.getDimension()) && !"dataType".equalsIgnoreCase(query.getDimension())) { query.setDimension("dataType"); } else if ("devicename".equalsIgnoreCase(query.getDimension())) { query.setDimension("deviceName"); } else if ("devicecode".equalsIgnoreCase(query.getDimension())) { query.setDimension("deviceCode"); } else { query.setDimension("dataType"); } if (query.getTopN() == null || query.getTopN() <= 0) { query.setTopN(10); } else if (query.getTopN() > 50) { query.setTopN(50); } return query; } }