doc/lims-data-analysis-front-integration.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,150 @@ # LIMS æ°æ®åæä¸å±ç¤º - å端èè°ææ¡£ ## 1. 模åè¯´æ æ¬æ¬¡å¨ `lims` æ¨¡åæ°å¢äºâæ°æ®åæä¸å±ç¤ºâè½åï¼è¦çï¼ - æ°æ®è¶å¿åæï¼æå¤©/æå°æ¶ï¼ - æ°æ®æ¯è¾åæï¼ææ°æ®ç±»å/设å¤ï¼ - æ°æ®è´¨éåå¸ç»è®¡ - æ¦è§ææ çæ¿ï¼æ»éãå¼å¸¸ãåæ ¼çãé¢è¦çï¼ æ¥å£ç»ä¸åç¼ï¼`/lims/dataAnalysis` --- ## 2. é´æä¸éç¨çº¦å® 1. é´ææ¹å¼ï¼æ²¿ç¨ç³»ç»ç°æ Tokenï¼`Authorization` 请æ±å¤´ï¼ã 2. è¿åæ ¼å¼ï¼`AjaxResult` ```json { "code": 200, "msg": "æä½æå", "data": {} } ``` 3. æ¶é´åæ°æ ¼å¼ï¼`yyyy-MM-dd HH:mm:ss` 4. é»è®¤æ¶é´èå´ï¼è¥ `startTime/endTime` é½ä¸ä¼ ï¼åé»è®¤æè¿ 7 天ã --- ## 3. æ¥è¯¢åæ°ï¼éç¨ï¼ | åæ° | ç±»å | å¿ å¡« | 说æ | |---|---|---|---| | startTime | string | å¦ | å¼å§æ¶é´ï¼æ ¼å¼ï¼`yyyy-MM-dd HH:mm:ss` | | endTime | string | å¦ | ç»ææ¶é´ï¼æ ¼å¼ï¼`yyyy-MM-dd HH:mm:ss` | | dataType | string | å¦ | æ°æ®ç±»åï¼`temperature/humidity/pressure/flow/concentration` | | deviceCode | string | å¦ | 设å¤ç¼å· | | granularity | string | å¦ | è¶å¿ç²åº¦ï¼`day/hour`ï¼é»è®¤ `day` | | dimension | string | å¦ | æ¯è¾ç»´åº¦ï¼`dataType/deviceName/deviceCode`ï¼é»è®¤ `dataType` | | topN | number | å¦ | æ¯è¾åæè¿åæ¡æ°ï¼é»è®¤ `10`ï¼æå¤§ `50` | --- ## 4. æ¥å£æ¸ å | æ¥å£ | æ¹æ³ | 说æ | |---|---|---| | `/lims/dataAnalysis/dashboard` | GET | 䏿¬¡è¿åçæ¿å ¨é¨æ°æ®ï¼æ¨èï¼ | | `/lims/dataAnalysis/overview` | GET | ä» è¿åæ¦è§ææ | | `/lims/dataAnalysis/trend` | GET | ä» è¿åè¶å¿æ°æ® | | `/lims/dataAnalysis/comparison` | GET | ä» è¿åæ¯è¾åææ°æ® | | `/lims/dataAnalysis/qualityDistribution` | GET | ä» è¿åè´¨éå叿°æ® | --- ## 5. 详ç»è¿åç»æ ### 5.1 `GET /lims/dataAnalysis/dashboard` `data` ç»æï¼ ```json { "startTime": "2026-05-16 00:00:00", "endTime": "2026-05-22 23:59:59", "granularity": "day", "dimension": "dataType", "overview": { "totalCollections": 1200, "todayCollections": 210, "abnormalCollections": 38, "qualifiedRate": 94.17, "inProgressExperiments": 6, "warningMonitors": 3, "inStockSamples": 168 }, "trend": [ { "time": "2026-05-16", "pointCount": 145, "avgValue": 23.65, "maxValue": 29.40, "minValue": 20.10 } ], "comparison": [ { "dimensionValue": "temperature", "pointCount": 560, "avgValue": 24.31, "maxValue": 33.20, "minValue": 18.90 } ], "qualityDistribution": [ { "category": "qualified", "pointCount": 1130, "ratio": 94.17 }, { "category": "abnormal", "pointCount": 38, "ratio": 3.17 }, { "category": "pending", "pointCount": 32, "ratio": 2.67 } ] } ``` åæ®µè§£éï¼ - `overview.totalCollections`ï¼æ¶é´èå´å ééæ»æ¡æ° - `overview.todayCollections`ï¼æè¿ 24 å°æ¶ééæ¡æ° - `overview.qualifiedRate`ï¼åæ ¼çï¼ç¾åæ¯ï¼ - `trend`ï¼æçº¿å¾æ°æ®æº - `comparison`ï¼æ±ç¶å¾/æ¨ªåæ¡å½¢å¾æ°æ®æº - `qualityDistribution`ï¼é¥¼å¾æ°æ®æº --- ## 6. å端è°ç¨å»ºè®® 1. 页é¢åå§åä¼å è°ç¨ `dashboard`ï¼åå°è¯·æ±æ¬¡æ°ã 2. ç¨æ·åæ¢çéæ¡ä»¶ï¼æ¶é´ã设å¤ãæ°æ®ç±»åï¼åï¼éæ°è¯·æ± `dashboard`ã 3. è¥é¡µé¢ååºå è½½ï¼å¯åå«è° `overview/trend/comparison/qualityDistribution`ã 4. `trend` å·²è¡¥é½æ¶é´è½´ç©ºæ¡£ä½ï¼æ æ°æ®è¿å 0ï¼ï¼å¯ç´æ¥ç»å¶è¿ç»æçº¿ã --- ## 7. è°ç¨ç¤ºä¾ ```bash curl -G 'http://localhost:8080/lims/dataAnalysis/dashboard' \ --data-urlencode 'startTime=2026-05-16 00:00:00' \ --data-urlencode 'endTime=2026-05-22 23:59:59' \ --data-urlencode 'granularity=day' \ --data-urlencode 'dimension=dataType' \ --data-urlencode 'topN=10' \ -H 'Authorization: Bearer <token>' ``` src/main/java/com/ruoyi/lims/controller/LimsDataAnalysisController.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,51 @@ package com.ruoyi.lims.controller; import com.ruoyi.framework.web.domain.AjaxResult; import com.ruoyi.lims.dto.LimsDataAnalysisQueryDto; import com.ruoyi.lims.service.LimsDataAnalysisService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @AllArgsConstructor @RequestMapping("/lims/dataAnalysis") @Api(value = "LimsDataAnalysis", tags = "æ°æ®åæä¸å±ç¤º") public class LimsDataAnalysisController { private final LimsDataAnalysisService limsDataAnalysisService; @GetMapping("/dashboard") @ApiOperation("æ°æ®åæçæ¿") public AjaxResult dashboard(LimsDataAnalysisQueryDto queryDto) { return AjaxResult.success(limsDataAnalysisService.dashboard(queryDto)); } @GetMapping("/overview") @ApiOperation("æ°æ®åææ¦è§") public AjaxResult overview(LimsDataAnalysisQueryDto queryDto) { return AjaxResult.success(limsDataAnalysisService.overview(queryDto)); } @GetMapping("/trend") @ApiOperation("æ°æ®è¶å¿åæ") public AjaxResult trend(LimsDataAnalysisQueryDto queryDto) { return AjaxResult.success(limsDataAnalysisService.trend(queryDto)); } @GetMapping("/comparison") @ApiOperation("æ°æ®æ¯è¾åæ") public AjaxResult comparison(LimsDataAnalysisQueryDto queryDto) { return AjaxResult.success(limsDataAnalysisService.comparison(queryDto)); } @GetMapping("/qualityDistribution") @ApiOperation("æ°æ®è´¨éåå¸") public AjaxResult qualityDistribution(LimsDataAnalysisQueryDto queryDto) { return AjaxResult.success(limsDataAnalysisService.qualityDistribution(queryDto)); } } src/main/java/com/ruoyi/lims/dto/LimsAnalysisOverviewDto.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,34 @@ package com.ruoyi.lims.dto; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.math.BigDecimal; @Data @ApiModel(description = "LIMSæ°æ®åææ¦è§") public class LimsAnalysisOverviewDto { @ApiModelProperty(value = "ééæ»æ¡æ°") private Long totalCollections; @ApiModelProperty(value = "æè¿24å°æ¶ééæ¡æ°") private Long todayCollections; @ApiModelProperty(value = "å¼å¸¸æ°æ®æ¡æ°") private Long abnormalCollections; @ApiModelProperty(value = "æ°æ®åæ ¼ç(%)") private BigDecimal qualifiedRate; @ApiModelProperty(value = "è¿è¡ä¸å®éªæ°") private Long inProgressExperiments; @ApiModelProperty(value = "é¢è¦çæ§æ°") private Long warningMonitors; @ApiModelProperty(value = "å¨åºæ ·åæ°") private Long inStockSamples; } src/main/java/com/ruoyi/lims/dto/LimsComparisonItemDto.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,28 @@ package com.ruoyi.lims.dto; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.math.BigDecimal; @Data @ApiModel(description = "LIMSæ¯è¾åæé¡¹") public class LimsComparisonItemDto { @ApiModelProperty(value = "维度å¼") private String dimensionValue; @ApiModelProperty(value = "æ°æ®ç¹æ°é") private Long pointCount; @ApiModelProperty(value = "å¹³åå¼") private BigDecimal avgValue; @ApiModelProperty(value = "æå¤§å¼") private BigDecimal maxValue; @ApiModelProperty(value = "æå°å¼") private BigDecimal minValue; } src/main/java/com/ruoyi/lims/dto/LimsDataAnalysisDashboardDto.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,38 @@ package com.ruoyi.lims.dto; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.time.LocalDateTime; import java.util.List; @Data @ApiModel(description = "LIMSæ°æ®åæçæ¿") public class LimsDataAnalysisDashboardDto { @ApiModelProperty(value = "åæå¼å§æ¶é´") private LocalDateTime startTime; @ApiModelProperty(value = "åæç»ææ¶é´") private LocalDateTime endTime; @ApiModelProperty(value = "è¶å¿ç²åº¦") private String granularity; @ApiModelProperty(value = "æ¯è¾ç»´åº¦") private String dimension; @ApiModelProperty(value = "æ¦è§æ°æ®") private LimsAnalysisOverviewDto overview; @ApiModelProperty(value = "è¶å¿æ°æ®") private List<LimsTrendPointDto> trend; @ApiModelProperty(value = "æ¯è¾æ°æ®") private List<LimsComparisonItemDto> comparison; @ApiModelProperty(value = "è´¨éå叿°æ®") private List<LimsQualityDistributionItemDto> qualityDistribution; } src/main/java/com/ruoyi/lims/dto/LimsDataAnalysisQueryDto.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,37 @@ package com.ruoyi.lims.dto; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; @Data @ApiModel(description = "LIMSæ°æ®åææ¥è¯¢åæ°") public class LimsDataAnalysisQueryDto { @ApiModelProperty(value = "å¼å§æ¶é´ï¼æ ¼å¼ï¼yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime startTime; @ApiModelProperty(value = "ç»ææ¶é´ï¼æ ¼å¼ï¼yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime endTime; @ApiModelProperty(value = "æ°æ®ç±»åï¼temperature/humidity/pressure/flow/concentration") private String dataType; @ApiModelProperty(value = "设å¤ç¼å·") private String deviceCode; @ApiModelProperty(value = "è¶å¿ç²åº¦ï¼day/hourï¼é»è®¤day") private String granularity; @ApiModelProperty(value = "æ¯è¾ç»´åº¦ï¼dataType/deviceName/deviceCodeï¼é»è®¤dataType") private String dimension; @ApiModelProperty(value = "æ¯è¾åæè¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§50") private Integer topN; } src/main/java/com/ruoyi/lims/dto/LimsQualityDistributionItemDto.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,22 @@ package com.ruoyi.lims.dto; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.math.BigDecimal; @Data @ApiModel(description = "LIMSè´¨éåå¸é¡¹") public class LimsQualityDistributionItemDto { @ApiModelProperty(value = "æ°æ®è´¨éåç±»") private String category; @ApiModelProperty(value = "æ°é") private Long pointCount; @ApiModelProperty(value = "å æ¯(%)") private BigDecimal ratio; } src/main/java/com/ruoyi/lims/dto/LimsTrendPointDto.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,28 @@ package com.ruoyi.lims.dto; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.math.BigDecimal; @Data @ApiModel(description = "LIMSè¶å¿åæç¹") public class LimsTrendPointDto { @ApiModelProperty(value = "æ¶é´ç»´åº¦å¼") private String time; @ApiModelProperty(value = "æ°æ®ç¹æ°é") private Long pointCount; @ApiModelProperty(value = "å¹³åå¼") private BigDecimal avgValue; @ApiModelProperty(value = "æå¤§å¼") private BigDecimal maxValue; @ApiModelProperty(value = "æå°å¼") private BigDecimal minValue; } src/main/java/com/ruoyi/lims/service/LimsDataAnalysisService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,19 @@ package com.ruoyi.lims.service; import com.ruoyi.lims.dto.*; import java.util.List; public interface LimsDataAnalysisService { LimsDataAnalysisDashboardDto dashboard(LimsDataAnalysisQueryDto queryDto); LimsAnalysisOverviewDto overview(LimsDataAnalysisQueryDto queryDto); List<LimsTrendPointDto> trend(LimsDataAnalysisQueryDto queryDto); List<LimsComparisonItemDto> comparison(LimsDataAnalysisQueryDto queryDto); List<LimsQualityDistributionItemDto> qualityDistribution(LimsDataAnalysisQueryDto queryDto); } src/main/java/com/ruoyi/lims/service/impl/LimsDataAnalysisServiceImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,387 @@ 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; } } src/main/resources/application-dev.yml
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,222 @@ # è¯å¯¼-ä»å¨ç©æµç³»ç»é¡¹ç®ç¸å ³é ç½® inspur: appId: a3b0e3f1-7210-4ed0-8eed-c6c443e04e36 appSecret: 7aab6b10061962e861ab69596eec6c037daffd2b56cdf518df8554ff56daa9c8 ruoyi: # åç§° name: RuoYi # çæ¬ version: 3.8.9 # çæå¹´ä»½ copyrightYear: 2025 # æä»¶è·¯å¾ 示ä¾ï¼ Windowsé ç½®D:/ruoyi/uploadPathï¼Linuxé ç½® /home/ruoyi/uploadPathï¼ profile: /center-lims/mis/file # è·åipå°åå¼å ³ addressEnabled: false # éªè¯ç ç±»å math æ°åè®¡ç® char å符éªè¯ captchaType: math # å¼åç¯å¢é ç½® server: # æå¡å¨çHTTP端å£ï¼é»è®¤ä¸º8080 port: 8080 servlet: # åºç¨ç访é®è·¯å¾ context-path: / tomcat: # tomcatçURIç¼ç uri-encoding: UTF-8 # è¿æ¥æ°æ»¡åçæéæ°ï¼é»è®¤ä¸º100 accept-count: 1000 threads: # tomcatæå¤§çº¿ç¨æ°ï¼é»è®¤ä¸º200 max: 800 # Tomcatå¯å¨åå§åççº¿ç¨æ°ï¼é»è®¤å¼10 min-spare: 100 # æ¥å¿é ç½® logging: level: com.ruoyi: warn org.springframework: warn minio: endpoint: http://114.132.189.42/ port: 7019 secure: false accessKey: admin secretKey: 12345678 preview-expiry: 24 # é¢è§å°åé»è®¤24å°æ¶ default-bucket: uploadPath # ç¨æ·é ç½® user: password: # å¯ç æå¤§éè¯¯æ¬¡æ° maxRetryCount: 5 # å¯ç é宿¶é´ï¼é»è®¤10åéï¼ lockTime: 10 # Springé ç½® spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: # ä¸»åºæ°æ®æº master: url: jdbc:mysql://127.0.0.1:3306/lims-ruoyi?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 123456 # ä»åºæ°æ®æº slave: # 仿°æ®æºå¼å ³/é»è®¤å ³é enabled: false url: username: password: # åå§è¿æ¥æ° initialSize: 5 # æå°è¿æ¥æ± æ°é minIdle: 10 # æå¤§è¿æ¥æ± æ°é maxActive: 20 # é ç½®è·åè¿æ¥çå¾ è¶ æ¶çæ¶é´ maxWait: 60000 # é ç½®è¿æ¥è¶ æ¶æ¶é´ connectTimeout: 30000 # é ç½®ç½ç»è¶ æ¶æ¶é´ socketTimeout: 60000 # é ç½®é´éå¤ä¹ æè¿è¡ä¸æ¬¡æ£æµï¼æ£æµéè¦å ³éç空é²è¿æ¥ï¼å使¯æ¯«ç§ timeBetweenEvictionRunsMillis: 60000 # é ç½®ä¸ä¸ªè¿æ¥å¨æ± 䏿å°çåçæ¶é´ï¼å使¯æ¯«ç§ minEvictableIdleTimeMillis: 300000 # é ç½®ä¸ä¸ªè¿æ¥å¨æ± 䏿大çåçæ¶é´ï¼å使¯æ¯«ç§ maxEvictableIdleTimeMillis: 900000 # é ç½®æ£æµè¿æ¥æ¯å¦ææ validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true # 设置ç½ååï¼ä¸å¡«åå 许ææè®¿é® allow: url-pattern: /druid/* # æ§å¶å°ç®¡çç¨æ·ååå¯ç login-username: ruoyi login-password: 123456 filter: stat: enabled: true # æ ¢SQLè®°å½ log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true # èµæºä¿¡æ¯ messages: # å½é åèµæºæä»¶è·¯å¾ basename: i18n/messages # æä»¶ä¸ä¼ servlet: multipart: # å个æä»¶å¤§å° max-file-size: 1GB # 设置æ»ä¸ä¼ çæä»¶å¤§å° max-request-size: 2GB # æå¡æ¨¡å devtools: restart: # çé¨ç½²å¼å ³ enabled: false # redis é ç½® redis: # å°å host: 127.0.0.1 # host: 172.17.0.1 # 端å£ï¼é»è®¤ä¸º6379 port: 6379 # æ°æ®åºç´¢å¼ database: 0 # å¯ç password: # password: 123456 # è¿æ¥è¶ æ¶æ¶é´ timeout: 10s lettuce: pool: # è¿æ¥æ± ä¸çæå°ç©ºé²è¿æ¥ min-idle: 0 # è¿æ¥æ± ä¸çæå¤§ç©ºé²è¿æ¥ max-idle: 8 # è¿æ¥æ± çæå¤§æ°æ®åºè¿æ¥æ° max-active: 8 # #è¿æ¥æ± æå¤§é»å¡çå¾ æ¶é´ï¼ä½¿ç¨è´å¼è¡¨ç¤ºæ²¡æéå¶ï¼ max-wait: -1ms # tokené ç½® token: # 令çèªå®ä¹æ è¯ header: Authorization # 令çå¯é¥ secret: abcdefghijklmnopqrstuvwxyz # ä»¤çæææï¼é»è®¤30åéï¼ expireTime: 450 # MyBatis Plusé ç½® mybatis-plus: # æç´¢æå®å å«å æ ¹æ®èªå·±çé¡¹ç®æ¥ typeAliasesPackage: com.ruoyi.**.pojo # é ç½®mapperçæ«æï¼æ¾å°ææçmapper.xmlæ å°æä»¶ mapperLocations: classpath*:mapper/**/*Mapper.xml # å è½½å ¨å±çé ç½®æä»¶ configLocation: classpath:mybatis/mybatis-config.xml global-config: enable-sql-runner: true db-config: id-type: auto # PageHelperå页æä»¶ pagehelper: helperDialect: mysql supportMethodsArguments: true params: count=countSql # Swaggeré ç½® swagger: # æ¯å¦å¼å¯swagger enabled: false # 请æ±åç¼ pathMapping: /dev-api # 鲿¢XSSæ»å» xss: # è¿æ»¤å¼å ³ enabled: true # æé¤é¾æ¥ï¼å¤ä¸ªç¨éå·åéï¼ excludes: /system/notice # å¹é 龿¥ urlPatterns: /system/*,/monitor/*,/tool/* # 代ç çæ gen: # ä½è author: ruoyi # é»è®¤çæå è·¯å¾ system éæ¹æèªå·±ç模ååç§° å¦ system monitor tool packageName: com.ruoyi.project.system # èªå¨å»é¤è¡¨åç¼ï¼é»è®¤æ¯true autoRemovePre: false # 表åç¼ï¼çæç±»åä¸ä¼å å«è¡¨åç¼ï¼å¤ä¸ªç¨éå·åéï¼ tablePrefix: sys_ # æ¯å¦å è®¸çææä»¶è¦çå°æ¬å°ï¼èªå®ä¹è·¯å¾ï¼ï¼é»è®¤ä¸å 许 allowOverwrite: false file: temp-dir: /center-lims/mis/file/temp/uploads upload-dir: /center-lims/mis/file/prod/uploads