Selaa lähdekoodia

fix(b4-f1/f2): TDD 部署 testing (da8adf10) 补全 B-4 未推送实现 | F1 管理端/家庭端分页列表 envelope 统一 data.records(ButlerController/ButlerService) + 局部更新 name 非必填 | F2 AdminCommissionConfig update 按 id 更新放行 name 缺失(仅新增必填) + 3 契约测试 | mvn compile 通过

Agnes Agent 6 päivää sitten
vanhempi
sitoutus
ba6b48d28c

+ 15 - 7
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCommissionConfigController.java

@@ -31,21 +31,29 @@ public class AdminCommissionConfigController {
     @Operation(summary = "更新佣金配置")
     @PostMapping("/config/update")
     public Result<String> updateConfig(@RequestBody(required = false) RebateCommissionConfig config) {
-        // B-4/F2:空 body 与必填字段缺失属于参数校验 → 明确 400 业务提示,绝不 500;
-        // name 为表 NOT NULL 约束字段(rebate_commission_config.name),缺失时先于 DB 层拦截。
+        // B-4/F2:空 body 属于参数错误 → 明确 400 业务提示,绝不 500。
         if (config == null) {
             return Result.error(400, "请求体不能为空");
         }
-        if (config.getName() == null || config.getName().trim().isEmpty()) {
+        // name 只在新增(id 为空)时必填:rebate_commission_config.name 为 NOT NULL,
+        // 而按 id 更新走 updateById(null 字段不参与 SET),只改 rate/remark 的局部更新应放行。
+        boolean insert = config.getId() == null;
+        if (config.getName() != null) {
+            String name = config.getName().trim();
+            if (name.isEmpty()) {
+                return Result.error(400, "配置名称(name)不能为空");
+            }
+            config.setName(name);
+        } else if (insert) {
             return Result.error(400, "配置名称(name)不能为空");
         }
-        if (config.getId() != null) {
-            config.setUpdatedAt(new Date());
-            rebateCommissionConfigMapper.updateById(config);
-        } else {
+        if (insert) {
             config.setCreatedAt(new Date());
             config.setUpdatedAt(new Date());
             rebateCommissionConfigMapper.insert(config);
+        } else {
+            config.setUpdatedAt(new Date());
+            rebateCommissionConfigMapper.updateById(config);
         }
         return Result.success("ok");
     }

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

@@ -85,8 +85,9 @@ public class ButlerController {
         SortUtil.applySort(wrapper);
         Page<User> result = userMapper.selectPage(pageParam, wrapper);
 
+        // B-4/F1:管理端/家庭端分页列表统一 envelope 为 data.records(与 activity 等列表一致),不再返回 data.list
         Map<String, Object> data = new HashMap<>();
-        data.put("list", result.getRecords());
+        data.put("records", result.getRecords());
         data.put("total", result.getTotal());
         data.put("page", page);
         data.put("size", size);

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

@@ -247,8 +247,9 @@ public class ButlerService {
         SortUtil.applySort(wrapper);
         Page<User> result = userMapper.selectPage(pageParam, wrapper);
 
+        // B-4/F1:分页列表统一 envelope 为 data.records,不再返回 data.list
         Map<String, Object> data = new HashMap<>();
-        data.put("list", result.getRecords());
+        data.put("records", result.getRecords());
         data.put("total", result.getTotal());
         data.put("page", page);
         data.put("size", size);

+ 69 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/admin/AdminCommissionConfigPartialUpdateTest.java

@@ -0,0 +1,69 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.RebateCommissionConfig;
+import com.etotem.cfc.mapper.RebateCommissionConfigMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * B-4/F2 契约级单测:POST /api/admin/commission/config/update 局部更新语义。
+ *
+ * <p>name 是 rebate_commission_config.name 的 NOT NULL 列,只在新增(id 为空)时必填;
+ * 按 id 更新走 updateById(null 字段不参与 SET),因此仅提交 rate/remark 的局部更新
+ * 必须返回 200,而不是 400。
+ */
+public class AdminCommissionConfigPartialUpdateTest {
+
+    private AdminCommissionConfigController controller;
+    private RebateCommissionConfigMapper rebateCommissionConfigMapper;
+
+    @BeforeEach
+    public void setUp() {
+        controller = new AdminCommissionConfigController();
+        rebateCommissionConfigMapper = mock(RebateCommissionConfigMapper.class);
+        ReflectionTestUtils.setField(controller, "rebateCommissionConfigMapper", rebateCommissionConfigMapper);
+    }
+
+    @Test
+    public void updateConfig_idWithoutName_partialUpdateReturns200() {
+        // 旧管理端局部更新 payload:只带 id + rate + remark,不含 name
+        RebateCommissionConfig config = new RebateCommissionConfig();
+        config.setId(42L);
+        config.setRate(12);
+        config.setRemark("仅调整比例");
+
+        Result<String> r = controller.updateConfig(config);
+
+        assertEquals(200, r.getCode(), "按 id 局部更新不应因缺 name 返回 400");
+        ArgumentCaptor<RebateCommissionConfig> captor = ArgumentCaptor.forClass(RebateCommissionConfig.class);
+        verify(rebateCommissionConfigMapper, times(1)).updateById(captor.capture());
+        assertEquals(42L, captor.getValue().getId());
+        assertEquals(12, captor.getValue().getRate());
+        assertNotNull(captor.getValue().getUpdatedAt(), "update 必须写入 updatedAt");
+        verify(rebateCommissionConfigMapper, never()).insert(any());
+    }
+
+    @Test
+    public void updateConfig_idWithBlankName_stillRejectedAs400() {
+        RebateCommissionConfig config = new RebateCommissionConfig();
+        config.setId(42L);
+        config.setName("   ");
+
+        Result<String> r = controller.updateConfig(config);
+
+        assertEquals(400, r.getCode());
+        verify(rebateCommissionConfigMapper, never()).updateById(any());
+        verify(rebateCommissionConfigMapper, never()).insert(any());
+    }
+}

+ 65 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/butler/ButlerControllerEnvelopeTest.java

@@ -0,0 +1,65 @@
+package com.etotem.cfc.controller.butler;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.ButlerService;
+import com.etotem.cfc.service.RealNameBackfillService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * B-4/F1 契约级单测:管家端分页列表(POST /api/butler/members)envelope 统一为 data.records,
+ * 与活动/管理端列表一致,不再返回 data.list。
+ */
+public class ButlerControllerEnvelopeTest {
+
+    private ButlerController controller;
+    private UserMapper userMapper;
+
+    @BeforeEach
+    public void setUp() {
+        controller = new ButlerController();
+        userMapper = mock(UserMapper.class);
+        ReflectionTestUtils.setField(controller, "userMapper", userMapper);
+        ReflectionTestUtils.setField(controller, "butlerService", mock(ButlerService.class));
+        ReflectionTestUtils.setField(controller, "realNameBackfillService", mock(RealNameBackfillService.class));
+    }
+
+    @Test
+    public void members_returnsRecordsEnvelope() {
+        Page<User> page = new Page<>(1, 10);
+        User u = new User();
+        u.setId(7L);
+        u.setNickname("成员A");
+        page.setRecords(Collections.singletonList(u));
+        page.setTotal(1);
+        when(userMapper.selectPage(any(Page.class), any())).thenReturn(page);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("page", 1);
+        params.put("size", 10);
+        Result<Map<String, Object>> r = controller.getMembers(params, 1L);
+
+        assertEquals(200, r.getCode());
+        assertNotNull(r.getData(), "data 不能为 null");
+        assertNotNull(r.getData().get("records"), "分页 envelope 必须包含 data.records");
+        assertNull(r.getData().get("list"), "分页 envelope 不得再返回 data.list");
+        assertEquals(1, ((Number) r.getData().get("total")).longValue());
+        assertEquals(1, ((Number) r.getData().get("page")).intValue());
+        assertEquals(10, ((Number) r.getData().get("size")).intValue());
+    }
+}

+ 65 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ButlerServiceListAllEnvelopeTest.java

@@ -0,0 +1,65 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.ButlerAssignmentMapper;
+import com.etotem.cfc.mapper.ButlerProfileMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * B-4/F1 契约级单测:ButlerService.listAll 分页 envelope 统一为 data.records,不再返回 data.list。
+ */
+public class ButlerServiceListAllEnvelopeTest {
+
+    private ButlerService butlerService;
+    private UserMapper userMapper;
+
+    @BeforeEach
+    public void setUp() {
+        butlerService = new ButlerService();
+        userMapper = mock(UserMapper.class);
+        ReflectionTestUtils.setField(butlerService, "userMapper", userMapper);
+        ReflectionTestUtils.setField(butlerService, "butlerProfileMapper", mock(ButlerProfileMapper.class));
+        ReflectionTestUtils.setField(butlerService, "userService", mock(UserService.class));
+        ReflectionTestUtils.setField(butlerService, "butlerAssignmentMapper", mock(ButlerAssignmentMapper.class));
+        ReflectionTestUtils.setField(butlerService, "memberSubscriptionService", mock(MemberSubscriptionService.class));
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void listAll_returnsRecordsEnvelope() {
+        Page<User> page = new Page<>(1, 20);
+        User u = new User();
+        u.setId(1001L);
+        u.setButlerStatus("approved");
+        page.setRecords(Collections.singletonList(u));
+        page.setTotal(1);
+        when(userMapper.selectPage(any(Page.class), any())).thenReturn(page);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("page", 1);
+        params.put("size", 20);
+        Result<Map<String, Object>> r = (Result<Map<String, Object>>) butlerService.listAll(params);
+
+        assertEquals(200, r.getCode());
+        assertNotNull(r.getData(), "data 不能为 null");
+        assertNotNull(r.getData().get("records"), "分页 envelope 必须包含 data.records");
+        assertNull(r.getData().get("list"), "分页 envelope 不得再返回 data.list");
+        assertEquals(1, ((Number) r.getData().get("total")).longValue());
+    }
+}