Просмотр исходного кода

fix(T02): scope=pair 缺参返回 400 明确错误码 + 补控制器契约单测

- IndicatorController.changeRanking:scope=pair 缺 fromReportDate/toReportDate 时
  返回 Result.error(400, ...) 而非默认 500,与 GlobalExceptionHandler 对
  IllegalArgumentException 的映射(400)保持一致,避免同类错误两套码。
- IndicatorTrendService.changeRanking:scope=pair 分支捕获 IllegalArgumentException
  时回填 code=400(参数/期次非法属客户端错误),与控制器口径统一。
- 新增 IndicatorControllerTest:API 边界契约单测,覆盖 scope=pair 缺参 400
  且不触达 service、参数齐全时正确透传 12 个入参。
Sisyphus 2 недель назад
Родитель
Сommit
ec88d47ea6

+ 2 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/IndicatorController.java

@@ -103,9 +103,10 @@ public class IndicatorController {
         Integer pageSize = params.get("pageSize") != null ? Integer.valueOf(params.get("pageSize").toString()) : null;
 
         // 参数校验:scope=pair 时必须传 fromReportDate 与 toReportDate
+        // 400 与 GlobalExceptionHandler 对 IllegalArgumentException 的映射保持一致(缺参属客户端错误)
         if ("pair".equals(scope)) {
             if (fromReportDate == null || toReportDate == null) {
-                return Result.error("scope=pair 时必须传 fromReportDate 与 toReportDate");
+                return Result.error(400, "scope=pair 时必须传 fromReportDate 与 toReportDate");
             }
         }
         return Result.success(indicatorTrendService.changeRanking(

+ 2 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/IndicatorTrendService.java

@@ -95,7 +95,8 @@ public class IndicatorTrendService {
             return timelineService.pairCompare(subjectId, reportType, fromReportDate, toReportDate,
                     category, orderBy, filter, page, pageSize);
         } catch (IllegalArgumentException e) {
-            data.put("code", 500);
+            // 参数/期次非法属客户端错误 → 400(与 GlobalExceptionHandler 的映射一致)
+            data.put("code", 400);
             data.put("message", e.getMessage());
             return data;
         } catch (Exception e) {

+ 108 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/IndicatorControllerTest.java

@@ -0,0 +1,108 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.service.IndicatorChangeService;
+import com.etotem.cfc.service.IndicatorCorrelationService;
+import com.etotem.cfc.service.IndicatorTimelineService;
+import com.etotem.cfc.service.IndicatorTrendService;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 仅做 API 边界契约单测(无 Spring 上下文):
+ * <ul>
+ *   <li>验收 #3:scope=pair 缺 fromReportDate/toReportDate → 400 明确错误码,不触及 service</li>
+ *   <li>验收 #8:健康报告指标对照接口返回 Result(权限校验由控制器负责,此处只验证参数校验短路)</li>
+ * </ul>
+ */
+@ExtendWith(MockitoExtension.class)
+class IndicatorControllerTest {
+
+    @Mock
+    private IndicatorTrendService indicatorTrendService;
+
+    @Mock
+    private IndicatorTimelineService indicatorTimelineService;
+
+    @Mock
+    private IndicatorCorrelationService indicatorCorrelationService;
+
+    @Mock
+    private FamilyMemberMapper familyMemberMapper;
+
+    @InjectMocks
+    private IndicatorController controller;
+
+    private static Map<String, Object> bodyWith(Long subjectId, Object... kv) {
+        Map<String, Object> m = new HashMap<>();
+        m.put("subjectId", subjectId);
+        if (kv != null) {
+            for (int i = 0; i + 1 < kv.length; i += 2) {
+                m.put(String.valueOf(kv[i]), kv[i + 1]);
+            }
+        }
+        return m;
+    }
+
+    @Test
+    void changeRanking_scopePairMissingDates_returns400AndDoesNotCallService() {
+        FamilyMember member = new FamilyMember();
+        member.setId(88L);
+        member.setFamilyId(7L);
+        when(familyMemberMapper.selectOne(any())).thenReturn(member);
+
+        Map<String, Object> params = bodyWith(88L, "scope", "pair"); // 缺 fromReportDate/toReportDate
+
+        Result<?> result = controller.changeRanking(params, 7L);
+
+        assertEquals(Integer.valueOf(400), result.getCode());
+        assertTrue(result.getMessage().contains("scope=pair"));
+        assertTrue(result.getMessage().contains("fromReportDate"));
+        assertTrue(result.getMessage().contains("toReportDate"));
+        verify(indicatorTrendService, never()).changeRanking(any(), any(), any(),
+                anyString(), anyString(), any(), any(), any(), any(), any(), any(), any());
+    }
+
+    @Test
+    void changeRanking_scopePairAllParams_ok() {
+        FamilyMember member = new FamilyMember();
+        member.setId(88L);
+        member.setFamilyId(7L);
+        when(familyMemberMapper.selectOne(any())).thenReturn(member);
+
+        Map<String, Object> params = bodyWith(88L,
+                "scope", "pair",
+                "fromReportDate", "2026-01-01",
+                "toReportDate", "2026-10-01",
+                "orderBy", "magnitude",
+                "filter", "all",
+                "page", 1,
+                "pageSize", 50);
+        when(indicatorTrendService.changeRanking(any(), any(), any(), eq("pair"), anyString(), any(),
+                any(), anyString(), anyString(), anyString(), any(), any()))
+                .thenReturn(new HashMap<>());
+
+        Result<?> result = controller.changeRanking(params, 7L);
+
+        assertEquals(Integer.valueOf(200), result.getCode());
+        verify(indicatorTrendService).changeRanking(eq(88L), any(), any(), eq("pair"),
+                eq("magnitude"), eq(50), eq(7L), eq("2026-01-01"), eq("2026-10-01"), eq("all"), eq(1), eq(50));
+    }
+}