|
|
@@ -1,9 +1,12 @@
|
|
|
package com.etotem.cfc.service;
|
|
|
|
|
|
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
import com.etotem.cfc.entity.IndicatorDefinition;
|
|
|
import com.etotem.cfc.entity.IndicatorValue;
|
|
|
import com.etotem.cfc.mapper.IndicatorDefinitionMapper;
|
|
|
import com.etotem.cfc.mapper.IndicatorValueMapper;
|
|
|
+import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
import org.springframework.test.util.ReflectionTestUtils;
|
|
|
@@ -29,6 +32,12 @@ class IndicatorTrendServiceTest {
|
|
|
|
|
|
@BeforeEach
|
|
|
void setUp() {
|
|
|
+ // LambdaQueryWrapper.select(SFunction) 需要实体元数据(MP lambda cache)
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
|
|
+ IndicatorValue.class);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
|
|
+ IndicatorDefinition.class);
|
|
|
+
|
|
|
service = new IndicatorTrendService();
|
|
|
valueMapper = mock(IndicatorValueMapper.class);
|
|
|
ReflectionTestUtils.setField(service, "valueMapper", valueMapper);
|
|
|
@@ -205,4 +214,95 @@ class IndicatorTrendServiceTest {
|
|
|
assertEquals("指标C", items.get(2).get("name"), "deltaPct=null 恒定排最后");
|
|
|
assertNull(items.get(2).get("deltaPct"), "基数为 0 的百分比必须为 null");
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * quickLookup 的上一期必须从已加载的 rows 内存求解。
|
|
|
+ * 之前逐指标回查 indicator_values(resolvePrevious),40 个指标就是 40 次 SQL。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void quickLookup_resolvesPreviousInMemory_noExtraQueries() {
|
|
|
+ IndicatorDefinition d = def(1L, "双歧杆菌属", "higher_is_better", "%");
|
|
|
+ when(changeDefinitionMapper.selectList(any())).thenReturn(new ArrayList<>(Arrays.asList(d)));
|
|
|
+
|
|
|
+ // 三期观测:最新 12.8,上一期 10.5(percentageFloor 以下,Δ% 仍有效),首期 8.0
|
|
|
+ List<IndicatorValue> rows = new ArrayList<>();
|
|
|
+ rows.add(value(1L, "2026-08-01", "8.0"));
|
|
|
+ rows.add(value(1L, "2026-09-15", "10.5"));
|
|
|
+ rows.add(value(1L, "2026-10-01", "12.8"));
|
|
|
+ when(valueMapper.selectList(any())).thenReturn(rows);
|
|
|
+
|
|
|
+ Map<String, Object> data = service.quickLookup(3L, null, 1, 30, 2L, false, 8);
|
|
|
+ Map<?, ?> item = (Map<?, ?>) ((List<?>) data.get("items")).get(0);
|
|
|
+
|
|
|
+ // 上一期必须是 2026-09-15 的 10.5,而不是更早的 8.0
|
|
|
+ Map<?, ?> previous = (Map<?, ?>) item.get("previous");
|
|
|
+ assertNotNull(previous, "内存求解必须找到上一期");
|
|
|
+ assertEquals("2026-09-15", previous.get("reportDate"));
|
|
|
+ assertEquals("10.5", previous.get("value"));
|
|
|
+ // higher_is_better 从 10.5 涨到 12.8 → 改善
|
|
|
+ assertEquals("improved", ((Map<?, ?>) item.get("change")).get("trendCode"));
|
|
|
+ assertEquals("12.8", item.get("value"), "最新值必须是本期最新");
|
|
|
+ assertEquals(3, ((Number) item.get("pointsCount")).intValue());
|
|
|
+
|
|
|
+ // 一次 selectList 取全部观测,之后不再回查(无 N+1)
|
|
|
+ verify(valueMapper, times(1)).selectList(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 本期最新观测的日期为 null → 无法确定"更早",不猜上一期,趋势为 first_record */
|
|
|
+ @Test
|
|
|
+ void quickLookup_latestHasNullDate_noPrevious() {
|
|
|
+ IndicatorDefinition d = def(1L, "双歧杆菌属", "higher_is_better", "%");
|
|
|
+ when(changeDefinitionMapper.selectList(any())).thenReturn(new ArrayList<>(Arrays.asList(d)));
|
|
|
+
|
|
|
+ // 同定义的所有观测日期均为 null → 排序后最新的也是 null 日期
|
|
|
+ List<IndicatorValue> rows = new ArrayList<>();
|
|
|
+ rows.add(valueWithNullDate(1L, "12.8")); // null 日期
|
|
|
+ rows.add(valueWithNullDate(1L, "9.0")); // null 日期
|
|
|
+ when(valueMapper.selectList(any())).thenReturn(rows);
|
|
|
+
|
|
|
+ Map<String, Object> data = service.quickLookup(3L, null, 1, 30, 2L, false, 8);
|
|
|
+ Map<?, ?> item = (Map<?, ?>) ((List<?>) data.get("items")).get(0);
|
|
|
+
|
|
|
+ // 本期日期未知 → 无法确定"更早",不猜上一期
|
|
|
+ assertNull(item.get("previous"));
|
|
|
+ assertNull(item.get("latestReportDate"));
|
|
|
+ assertEquals("first_record", ((Map<?, ?>) item.get("change")).get("trendCode"),
|
|
|
+ "本期日期未知时按首次记录降级");
|
|
|
+ verify(valueMapper, times(1)).selectList(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** listSourceTypes 改为一次查询内存计算 periodCount/latestReportDate(原 1+2N) */
|
|
|
+ @Test
|
|
|
+ void listSourceTypes_computesPeriodCountAndLatestInOneQuery() {
|
|
|
+ List<IndicatorValue> rows = new ArrayList<>();
|
|
|
+ rows.add(sourceTypeRow("gut_flora", "2026-08-01"));
|
|
|
+ rows.add(sourceTypeRow("gut_flora", "2026-09-15"));
|
|
|
+ rows.add(sourceTypeRow("gut_flora", "2026-09-15")); // 同日 → 去重
|
|
|
+ rows.add(sourceTypeRow("gut_flora", "2026-10-01"));
|
|
|
+ rows.add(sourceTypeRow("physical_exam", "2026-07-01"));
|
|
|
+ rows.add(sourceTypeRow("physical_exam", "2026-08-02"));
|
|
|
+ when(valueMapper.selectList(any())).thenReturn(rows);
|
|
|
+
|
|
|
+ List<Map<String, Object>> out = service.listSourceTypes(3L);
|
|
|
+ assertEquals(2, out.size(), "两个 source_type 各一条");
|
|
|
+
|
|
|
+ Map<String, Object> gut = out.get(0);
|
|
|
+ assertEquals("gut_flora", gut.get("sourceType"));
|
|
|
+ assertEquals(3L, ((Number) gut.get("periodCount")).longValue(), "同日去重,3 个不同日期");
|
|
|
+ assertEquals("2026-10-01", gut.get("latestReportDate"));
|
|
|
+
|
|
|
+ Map<String, Object> physical = out.get(1);
|
|
|
+ assertEquals("physical_exam", physical.get("sourceType"));
|
|
|
+ assertEquals(2L, ((Number) physical.get("periodCount")).longValue());
|
|
|
+ assertEquals("2026-08-02", physical.get("latestReportDate"));
|
|
|
+
|
|
|
+ verify(valueMapper, times(1)).selectList(any()); // 不得按 sourceType 分别查库
|
|
|
+ }
|
|
|
+
|
|
|
+ private IndicatorValue sourceTypeRow(String sourceType, String date) {
|
|
|
+ IndicatorValue v = new IndicatorValue();
|
|
|
+ v.setSourceType(sourceType);
|
|
|
+ v.setReportDate(Date.valueOf(date));
|
|
|
+ return v;
|
|
|
+ }
|
|
|
}
|