Explorar el Código

fix(product): 批量上下架 productIds 强转 List<Long> 导致 Integer->Long ClassCastException

- 根因:@RequestBody Map<String,Object> 中 JSON 数字数组经 Jackson 反序列化为 List<Integer>,
  旧写法 (List<Long>) 强转为擦除强转,运行期 for (Long id) 元素转型抛 ClassCastException
- 修复:逐元素 ParamUtils.getLong 安全解析(兼容 Integer/Long/数字字符串);shelve 兼容布尔/字符串
- 新增回归测试 AdminProductControllerBatchShelveTest(真实 Jackson 反序列化报文),修复前红/修复后绿
Xiaogang Liao hace 1 semana
padre
commit
1afdb92631

+ 27 - 6
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductController.java

@@ -136,12 +136,33 @@ if (params.get("distributionSystemId") != null) {
     @Operation(summary = "批量商品上下架")
     @PostMapping("/batch-shelve")
     public Result<String> batchShelve(@RequestBody Map<String, Object> params) {
-        List<Long> productIds = (List<Long>) params.get("productIds");
-        Boolean shelve = params.get("shelve") != null
-                ? (Boolean) params.get("shelve")
-                : null;
-
-        if (productIds == null || productIds.isEmpty()) {
+        // Jackson 把 Map<String,Object> 里的 JSON 数字数组反序列化为 List<Integer>,
+        // 直接 (List<Long>) 强转是擦除强转(编译通过),到服务层 for (Long id : list)
+        // 才会在运行期对元素做 Integer -> Long 转型并抛 ClassCastException。
+        // 这里逐元素走 ParamUtils.getLong(Number / 数字字符串均安全解析)。
+        List<Long> productIds = new java.util.ArrayList<>();
+        Object rawIds = params.get("productIds");
+        if (rawIds instanceof List) {
+            for (Object item : (List<?>) rawIds) {
+                Long id = ParamUtils.getLong(item);
+                if (id != null) {
+                    productIds.add(id);
+                }
+            }
+        }
+        // 同理:shelve 若被前端意外送成字符串 "true"/"false" 会 ClassCastException,
+        // 用 Number/String 归一;布尔值直接读。
+        Object shelveObj = params.get("shelve");
+        Boolean shelve;
+        if (shelveObj instanceof Boolean) {
+            shelve = (Boolean) shelveObj;
+        } else if (shelveObj instanceof String && !((String) shelveObj).isEmpty()) {
+            shelve = Boolean.parseBoolean(((String) shelveObj).trim());
+        } else {
+            shelve = null;
+        }
+
+        if (productIds.isEmpty()) {
             return Result.error("productIds不能为空");
         }
         if (shelve == null) {

+ 117 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/admin/AdminProductControllerBatchShelveTest.java

@@ -0,0 +1,117 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.ProductService;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+/**
+ * 批量商品上下架(/api/admin/product/batch-shelve)类型安全回归测试。
+ *
+ * 故障:批量下架时抛 ClassCastException: Integer cannot be cast to Long。
+ * 根因:@RequestBody Map<String, Object> 使 Jackson 把 JSON 数字数组反序列化为
+ *      List<Integer>;控制器旧写法 (List<Long>) params.get("productIds") 为擦除强转,
+ *      编译通过,到服务层 for (Long id : list) 才在运行期对元素做 Integer->Long 转换而失败。
+ *
+ * 约定:productIds 必须是 Number 或数字字符串均可;解析后的 List<Long> 交给服务层。
+ * 用 ObjectMapper 反序列化真实 JSON 复现线上报文(与控制器收到的对象一致)。
+ */
+public class AdminProductControllerBatchShelveTest {
+
+    private static final ObjectMapper OM = new ObjectMapper();
+
+    private AdminProductController controller;
+    private ProductService productService;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        controller = new AdminProductController();
+        productService = mock(ProductService.class);
+        ReflectionTestUtils.setField(controller, "productService", productService);
+    }
+
+    @Test
+    public void batchShelve_jsonIntegerIds_convertedToLong() throws Exception {
+        // 复现线上报文:Jackson 对 JSON 数字产出 Integer
+        @SuppressWarnings("unchecked")
+        Map<String, Object> params = OM.readValue(
+                "{\"productIds\":[1,2,3],\"shelve\":false}", Map.class);
+        @SuppressWarnings("unchecked")
+        List<?> raw = (List<?>) params.get("productIds");
+        assertEquals(Integer.class, raw.get(0).getClass(),
+                "前置条件:Jackson 将 JSON 数字反序列化为 Integer(即本缺陷的诱因)");
+
+        when(productService.batchAdminShelve(any(), eq(false)))
+                .thenReturn(Result.success("批量下架成功"));
+
+        Result<String> result = controller.batchShelve(params);
+
+        assertEquals(200, result.getCode());
+        assertEquals("批量下架成功", result.getData());
+
+        ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class);
+        verify(productService, times(1)).batchAdminShelve(captor.capture(), eq(false));
+        for (Object id : captor.getValue()) {
+            assertEquals(Long.class, id.getClass(),
+                    "传给服务层的元素必须是 Long,实际为 " + id.getClass());
+        }
+        assertEquals(3, captor.getValue().size());
+    }
+
+    @Test
+    public void batchShelve_largeValueAndStringNumberAccepted() throws Exception {
+        // 大数值(超出 Integer 范围,Jackson 产出 Long)与字符串数字都应安全解析
+        @SuppressWarnings("unchecked")
+        Map<String, Object> params = OM.readValue(
+                "{\"productIds\":[3000000000,\"42\"],\"shelve\":true}", Map.class);
+
+        when(productService.batchAdminShelve(any(), eq(true)))
+                .thenReturn(Result.success("批量上架成功"));
+
+        Result<String> result = controller.batchShelve(params);
+
+        assertEquals(200, result.getCode());
+        verify(productService, times(1))
+                .batchAdminShelve(eq(java.util.Arrays.asList(3000000000L, 42L)), eq(true));
+    }
+
+    @Test
+    public void batchShelve_missingOrEmptyIds_returnsCleanError() throws Exception {
+        // Result.error(msg) 约定:body code=500(HTTP 200)。要点是干净的业务错误,
+        // 而不是抛 ClassCastException / 崩溃。
+        @SuppressWarnings("unchecked")
+        Map<String, Object> absent = OM.readValue("{\"shelve\":false}", Map.class);
+        Result<String> r1 = controller.batchShelve(absent);
+        assertEquals(500, r1.getCode());
+        assertEquals("productIds不能为空", r1.getMessage());
+
+        @SuppressWarnings("unchecked")
+        Map<String, Object> empty = OM.readValue("{\"productIds\":[],\"shelve\":false}", Map.class);
+        Result<String> r2 = controller.batchShelve(empty);
+        assertEquals(500, r2.getCode());
+        assertEquals("productIds不能为空", r2.getMessage());
+
+        verify(productService, never()).batchAdminShelve(any(), any());
+    }
+
+    @Test
+    public void batchShelve_missingShelve_returnsCleanError() throws Exception {
+        @SuppressWarnings("unchecked")
+        Map<String, Object> params = OM.readValue("{\"productIds\":[1]}", Map.class);
+        Result<String> result = controller.batchShelve(params);
+        assertEquals(500, result.getCode());
+        assertEquals("shelve不能为空", result.getMessage());
+        verify(productService, never()).batchAdminShelve(any(), any());
+    }
+}