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<LimsTrendPointDto> trend(LimsDataAnalysisQueryDto queryDto) {
|
return buildTrend(normalizeQuery(queryDto));
|
}
|
|
@Override
|
public List<LimsComparisonItemDto> comparison(LimsDataAnalysisQueryDto queryDto) {
|
return buildComparison(normalizeQuery(queryDto));
|
}
|
|
@Override
|
public List<LimsQualityDistributionItemDto> qualityDistribution(LimsDataAnalysisQueryDto queryDto) {
|
return buildQualityDistribution(normalizeQuery(queryDto));
|
}
|
|
private LimsAnalysisOverviewDto buildOverview(LimsDataAnalysisQueryDto query) {
|
QueryWrapper<DataCollection> totalWrapper = createDataCollectionFilter(query);
|
Long totalCollections = dataCollectionMapper.selectCount(totalWrapper);
|
|
QueryWrapper<DataCollection> abnormalWrapper = createDataCollectionFilter(query);
|
abnormalWrapper.eq("data_quality", "abnormal");
|
Long abnormalCollections = dataCollectionMapper.selectCount(abnormalWrapper);
|
|
QueryWrapper<DataCollection> qualifiedWrapper = createDataCollectionFilter(query);
|
qualifiedWrapper.eq("data_quality", "qualified");
|
Long qualifiedCollections = dataCollectionMapper.selectCount(qualifiedWrapper);
|
|
QueryWrapper<DataCollection> 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<Experiment>()
|
.eq("del_flag", "0")
|
.eq("experiment_status", "inProgress"));
|
|
Long warningMonitors = realtimeMonitorMapper.selectCount(new QueryWrapper<RealtimeMonitor>()
|
.eq("del_flag", "0")
|
.in("alert_status", Arrays.asList("warning", "alert")));
|
|
Long inStockSamples = sampleMapper.selectCount(new QueryWrapper<Sample>()
|
.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<LimsTrendPointDto> buildTrend(LimsDataAnalysisQueryDto query) {
|
String groupExpression = resolveTimeGroupExpression(query.getGranularity());
|
QueryWrapper<DataCollection> 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<Map<String, Object>> rawRows = dataCollectionMapper.selectMaps(wrapper);
|
Map<String, LimsTrendPointDto> trendMap = rawRows.stream().map(this::mapToTrendPoint)
|
.collect(Collectors.toMap(LimsTrendPointDto::getTime, item -> item, (a, b) -> b));
|
|
List<String> axis = buildTimeAxis(query);
|
List<LimsTrendPointDto> 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<LimsComparisonItemDto> buildComparison(LimsDataAnalysisQueryDto query) {
|
String dimensionColumn = resolveDimensionColumn(query.getDimension());
|
|
QueryWrapper<DataCollection> 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<LimsComparisonItemDto> 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<LimsQualityDistributionItemDto> buildQualityDistribution(LimsDataAnalysisQueryDto query) {
|
QueryWrapper<DataCollection> wrapper = createDataCollectionFilter(query);
|
wrapper.isNotNull("data_quality")
|
.ne("data_quality", "")
|
.select("data_quality AS category", "COUNT(1) AS point_count")
|
.groupBy("data_quality");
|
|
List<LimsQualityDistributionItemDto> 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<DataCollection> createDataCollectionFilter(LimsDataAnalysisQueryDto query) {
|
QueryWrapper<DataCollection> 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<String> buildTimeAxis(LimsDataAnalysisQueryDto query) {
|
List<String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> row, String key) {
|
if (row.containsKey(key)) {
|
return row.get(key);
|
}
|
for (Map.Entry<String, Object> 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;
|
}
|
}
|