Quellcode durchsuchen

test(flow): 家庭成员管理/心愿兑换/规划师绑定流程测试

Xiaogang Liao vor 2 Monaten
Ursprung
Commit
5c8231ded0

+ 386 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/FamilyMemberManagementFlowTest.java

@@ -0,0 +1,386 @@
+package com.etotem.cfc.integration;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.family.FamilyMembersController;
+import com.etotem.cfc.dto.AddFamilyMemberDTO;
+import com.etotem.cfc.dto.FamilyMemberVO;
+import com.etotem.cfc.dto.SwitchMemberVO;
+import com.etotem.cfc.entity.RelationshipType;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.service.FamilyMemberService;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import org.junit.jupiter.api.*;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * ================================================================
+ * 场景:家庭成员管理全流程
+ * ================================================================
+ * 用户故事:作为家庭管理员(家长),我希望管理家庭成员,
+ *           以便正确维护家庭关系结构并授权其他成员使用。
+ *
+ * 流程步骤:
+ *   1. 管理员获取可用的关系类型列表
+ *   2. 管理员添加新家庭成员,指定关系类型
+ *   3. 管理员查看当前家庭成员列表
+ *   4. 管理员编辑成员信息(如修改关系类型)
+ *   5. 管理员切换到成员视角,体验成员端功能
+ *   6. 管理员移除不需要的成员
+ *
+ * 关键里程碑:
+ *   - [Milestone-1] 成功获取关系类型列表
+ *   - [Milestone-2] 新成员添加成功,获得成员ID
+ *   - [Milestone-3] 成员信息更新成功
+ *   - [Milestone-4] 成功切换到成员视角,获得切换凭证
+ *   - [Milestone-5] 成员移除成功
+ * ================================================================
+ */
+@SpringBootTest
+public class FamilyMemberManagementFlowTest {
+
+    @Resource
+    private FamilyMembersController familyMembersController;
+
+    @MockBean
+    private FamilyMemberService familyMemberService;
+
+    @MockBean
+    private FamilyMemberMapper familyMemberMapper;
+
+    @MockBean
+    private UserMapper userMapper;
+
+    // ========== 测试数据 ==========
+
+    private static final Long PARENT_USER_ID = 1001L;
+    private static final Long CHILD_MEMBER_ID = 2001L;
+    private static final Long FAMILY_ID = 3001L;
+    private static final String SWITCH_TOKEN = "switch_token_abc123";
+
+    // ========== 场景1:获取关系类型列表 ==========
+
+    /**
+     * 场景:管理员进入添加成员页面,需要选择关系类型
+     * 角色:家长(管理员)
+     * 触发条件:打开添加家庭成员页面
+     */
+    @Test
+    @DisplayName("[场景1] 获取关系类型列表 - 期望返回可用的关系类型")
+    void parentGetsRelationshipTypes_Success() {
+        // === Given:系统存在预置的关系类型 ===
+        List<RelationshipType> mockTypes = Arrays.asList(
+                createRelationshipType(1L, "父亲", "parent", 1),
+                createRelationshipType(2L, "母亲", "parent", 2),
+                createRelationshipType(3L, "儿子", "child", 3),
+                createRelationshipType(4L, "女儿", "child", 4),
+                createRelationshipType(5L, "爷爷", "elder", 5),
+                createRelationshipType(6L, "奶奶", "elder", 6)
+        );
+        when(familyMemberService.getRelationshipTypes()).thenReturn(mockTypes);
+
+        // === When:请求获取关系类型列表 ===
+        Result<List<RelationshipType>> result = familyMembersController.getRelationshipTypes();
+
+        // === Then:Milestone-1 - 返回完整的关系类型 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(6, result.getData().size());
+        verify(familyMemberService).getRelationshipTypes();
+    }
+
+    // ========== 场景2:添加新家庭成员 ==========
+
+    /**
+     * 场景:家长添加一个新的孩子成员到家庭
+     * 角色:家长(管理员)
+     * 触发条件:填写完成员信息并提交
+     */
+    @Test
+    @DisplayName("[场景2] 添加新家庭成员 - 期望成员添加成功并返回完整信息")
+    void parentAddsNewMember_Success() {
+        // === Given:家长填写了成员信息 ===
+        AddFamilyMemberDTO addDTO = new AddFamilyMemberDTO();
+        addDTO.setNickname("王小明");
+        addDTO.setGender("男");
+        addDTO.setBirthday("2015-06-15");
+        addDTO.setRelationshipTypeKey("child");
+        addDTO.setCapabilityRole("child");
+
+        FamilyMemberVO mockMember = createFamilyMemberVO(CHILD_MEMBER_ID, "王小明", "child");
+        when(familyMemberService.addMember(eq(PARENT_USER_ID), any(AddFamilyMemberDTO.class)))
+                .thenReturn(mockMember);
+
+        // === When:提交添加成员表单 ===
+        Result<FamilyMemberVO> result = familyMembersController.addMember(
+                createMockRequest(PARENT_USER_ID), addDTO);
+
+        // === Then:Milestone-2 - 成员添加成功 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertNotNull(result.getData().getId());
+        assertEquals("王小明", result.getData().getNickname());
+        assertEquals("child", result.getData().getRelationshipTypeKey());
+        verify(familyMemberService).addMember(eq(PARENT_USER_ID), any(AddFamilyMemberDTO.class));
+    }
+
+    /**
+     * 场景:家长添加成员时未填写必填信息(姓名)
+     * 角色:家长
+     * 触发条件:提交表单时 nickname 为空
+     */
+    @Test
+    @DisplayName("[场景2b] 添加成员时姓名为空 - 期望返回错误提示")
+    void parentAddsMemberWithEmptyName_Fails() {
+        // === Given:家长未填写成员姓名 ===
+        AddFamilyMemberDTO addDTO = new AddFamilyMemberDTO();
+        addDTO.setNickname(""); // 空姓名
+        addDTO.setRelationshipTypeKey("child");
+
+        when(familyMemberService.addMember(eq(PARENT_USER_ID), any(AddFamilyMemberDTO.class)))
+                .thenThrow(new IllegalArgumentException("姓名不能为空"));
+
+        // === When:提交添加成员表单 ===
+        Result<FamilyMemberVO> result = familyMembersController.addMember(
+                createMockRequest(PARENT_USER_ID), addDTO);
+
+        // === Then:返回错误信息 ===
+        assertEquals(500, result.getCode());
+        assertNotNull(result.getMessage());
+        assertTrue(result.getMessage().contains("姓名") || result.getMessage().contains("不能为空"));
+    }
+
+    // ========== 场景3:查看家庭成员列表 ==========
+
+    /**
+        * 场景:家长查看当前家庭的所有成员
+     * 角色:家长
+     * 触发条件:进入家庭成员管理页面
+     */
+    @Test
+    @DisplayName("[场景3] 查看家庭成员列表 - 期望返回所有成员及关系信息")
+    void parentViewsMemberList_Success() {
+        // === Given:家庭有多个成员 ===
+        List<FamilyMemberVO> mockMembers = Arrays.asList(
+                createFamilyMemberVO(1L, "张爸爸", "parent"),
+                createFamilyMemberVO(2L, "李妈妈", "parent"),
+                createFamilyMemberVO(CHILD_MEMBER_ID, "王小明", "child"),
+                createFamilyMemberVO(4L, "王爷爷", "elder")
+        );
+        when(familyMemberService.listMembers(PARENT_USER_ID)).thenReturn(mockMembers);
+
+        // === When:请求家庭成员列表 ===
+        Result<List<FamilyMemberVO>> result = familyMembersController.listMembers(
+                createMockRequest(PARENT_USER_ID));
+
+        // === Then:返回完整的成员列表 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(4, result.getData().size());
+        verify(familyMemberService).listMembers(PARENT_USER_ID);
+    }
+
+    // ========== 场景4:更新成员信息 ==========
+
+    /**
+     * 场景:家长需要修改成员的关系类型(如孩子长大后变更角色)
+     * 角色:家长
+     * 触发条件:编辑成员信息并保存
+     */
+    @Test
+    @DisplayName("[场景4] 更新成员关系类型 - 期望更新成功并返回新信息")
+    void parentUpdatesMemberRelationship_Success() {
+        // === Given:需要修改成员的关系类型 ===
+        Map<String, Object> updateRequest = new HashMap<>();
+        updateRequest.put("memberId", CHILD_MEMBER_ID);
+        updateRequest.put("nickname", "王小明");
+        updateRequest.put("relationshipTypeKey", "child"); // 保持不变或变更
+
+        FamilyMemberVO updatedMember = createFamilyMemberVO(CHILD_MEMBER_ID, "王小明", "child");
+        when(familyMemberService.updateMember(eq(PARENT_USER_ID), eq(CHILD_MEMBER_ID), any(AddFamilyMemberDTO.class)))
+                .thenReturn(updatedMember);
+
+        // === When:提交更新表单 ===
+        Result<FamilyMemberVO> result = familyMembersController.updateMember(
+                createMockRequest(PARENT_USER_ID), updateRequest);
+
+        // === Then:Milestone-3 - 更新成功 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(CHILD_MEMBER_ID, result.getData().getId());
+        verify(familyMemberService).updateMember(eq(PARENT_USER_ID), eq(CHILD_MEMBER_ID), any(AddFamilyMemberDTO.class));
+    }
+
+    // ========== 场景5:切换到成员视角 ==========
+
+    /**
+     * 场景:家长需要以孩子视角体验功能,验证孩子端显示是否正常
+     * 角色:家长(代理操作)
+     * 触发条件:选择成员并点击"切换视角"
+     */
+    @Test
+    @DisplayName("[场景5] 切换到成员视角 - 期望返回切换凭证并正确设置上下文")
+    void parentSwitchesToMemberView_Success() {
+        // === Given:选择了要切换的成员 ===
+        Map<String, Long> switchRequest = new HashMap<>();
+        switchRequest.put("memberId", CHILD_MEMBER_ID);
+
+        SwitchMemberVO mockSwitchVO = new SwitchMemberVO();
+        mockSwitchVO.setMemberId(CHILD_MEMBER_ID);
+        mockSwitchVO.setMemberName("王小明");
+        mockSwitchVO.setSwitchToken(SWITCH_TOKEN);
+        mockSwitchVO.setCurrentRole("child");
+
+        when(familyMemberService.switchToMember(PARENT_USER_ID, CHILD_MEMBER_ID))
+                .thenReturn(mockSwitchVO);
+
+        // === When:执行切换视角操作 ===
+        Result<SwitchMemberVO> result = familyMembersController.switchToMember(
+                createMockRequest(PARENT_USER_ID), switchRequest);
+
+        // === Then:Milestone-4 - 切换成功,获得切换凭证 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(CHILD_MEMBER_ID, result.getData().getMemberId());
+        assertEquals("王小明", result.getData().getMemberName());
+        assertNotNull(result.getData().getSwitchToken());
+        verify(familyMemberService).switchToMember(PARENT_USER_ID, CHILD_MEMBER_ID);
+    }
+
+    /**
+     * 场景:家长尝试切换到自己(不允许自己切换到自己)
+     * 角色:家长
+     * 触发条件:选择成员ID为自己
+     */
+    @Test
+    @DisplayName("[场景5b] 切换到本人视角 - 期望返回错误提示")
+    void parentSwitchesToSelf_Fails() {
+        // === Given:选择了自己的用户ID作为成员ID ===
+        Map<String, Long> switchRequest = new HashMap<>();
+        switchRequest.put("memberId", PARENT_USER_ID); // 自己的用户ID
+
+        when(familyMemberService.switchToMember(PARENT_USER_ID, PARENT_USER_ID))
+                .thenThrow(new RuntimeException("不能切换到自己的视角"));
+
+        // === When:执行切换操作 ===
+        Result<SwitchMemberVO> result = familyMembersController.switchToMember(
+                createMockRequest(PARENT_USER_ID), switchRequest);
+
+        // === Then:返回错误提示 ===
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("不能") || result.getMessage().contains("自己"));
+    }
+
+    // ========== 场景6:移除家庭成员 ==========
+
+    /**
+     * 场景:家庭成员关系结束,家长移除不再属于家庭的成员
+     * 角色:家长(管理员)
+     * 触发条件:确认移除操作
+     */
+    @Test
+    @DisplayName("[场景6] 移除家庭成员 - 期望成员从家庭中移除")
+    void parentRemovesMember_Success() {
+        // === Given:存在需要移除的成员 ===
+        Map<String, Long> kickRequest = new HashMap<>();
+        kickRequest.put("memberId", CHILD_MEMBER_ID);
+
+        doNothing().when(familyMemberService).kickMember(PARENT_USER_ID, CHILD_MEMBER_ID);
+
+        // === When:确认移除成员 ===
+        Result<String> result = familyMembersController.kickMember(
+                createMockRequest(PARENT_USER_ID), kickRequest);
+
+        // === Then:Milestone-5 - 移除成功 ===
+        assertEquals(200, result.getCode());
+        assertEquals("踢出成功", result.getData());
+        verify(familyMemberService).kickMember(PARENT_USER_ID, CHILD_MEMBER_ID);
+    }
+
+    /**
+     * 场景:家长尝试移除不存在的成员
+     * 角色:家长
+     * 触发条件:成员ID无效
+     */
+    @Test
+    @DisplayName("[场景6b] 移除不存在的成员 - 期望返回错误提示")
+    void parentRemovesNonExistentMember_Fails() {
+        // === Given:成员ID不存在 ===
+        Long nonExistentId = 99999L;
+        Map<String, Long> kickRequest = new HashMap<>();
+        kickRequest.put("memberId", nonExistentId);
+
+        doThrow(new RuntimeException("成员不存在")).when(familyMemberService)
+                .kickMember(PARENT_USER_ID, nonExistentId);
+
+        // === When:尝试移除成员 ===
+        Result<String> result = familyMembersController.kickMember(
+                createMockRequest(PARENT_USER_ID), kickRequest);
+
+        // === Then:返回错误提示 ===
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("不存在") || result.getMessage().contains("失败"));
+    }
+
+    /**
+     * 场景:非管理员尝试踢出成员
+     * 角色:普通家庭成员(非管理员)
+     * 触发条件:普通成员尝试执行踢人操作
+     */
+    @Test
+    @DisplayName("[场景6c] 普通成员尝试踢人 - 期望权限不足拒绝")
+    void nonAdminRemovesMember_Forbidden() {
+        // === Given:普通成员尝试踢人 ===
+        Long childUserId = 1002L; // 孩子账号
+        Map<String, Long> kickRequest = new HashMap<>();
+        kickRequest.put("memberId", 2003L);
+
+        doThrow(new RuntimeException("权限不足,只有管理员可以踢出成员"))
+                .when(familyMemberService).kickMember(childUserId, 2003L);
+
+        // === When:普通成员尝试踢人 ===
+        Result<String> result = familyMembersController.kickMember(
+                createMockRequest(childUserId), kickRequest);
+
+        // === Then:权限不足,拒绝操作 ===
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("权限") || result.getMessage().contains("管理员"));
+    }
+
+    // ========== Helper Methods ==========
+
+    private RelationshipType createRelationshipType(Long id, String typeName, String typeKey, Integer sortOrder) {
+        RelationshipType type = new RelationshipType();
+        type.setId(id);
+        type.setTypeName(typeName);
+        type.setTypeKey(typeKey);
+        type.setSortOrder(sortOrder);
+        type.setEnabled(true);
+        return type;
+    }
+
+    private FamilyMemberVO createFamilyMemberVO(Long id, String nickname, String relationshipTypeKey) {
+        FamilyMemberVO vo = new FamilyMemberVO();
+        vo.setId(id);
+        vo.setNickname(nickname);
+        vo.setRelationshipTypeKey(relationshipTypeKey);
+        vo.setGender("男");
+        vo.setBirthday("2015-06-15");
+        return vo;
+    }
+
+    private javax.servlet.http.HttpServletRequest createMockRequest(Long userId) {
+        javax.servlet.http.HttpServletRequest request = mock(javax.servlet.http.HttpServletRequest.class);
+        when(request.getAttribute("userId")).thenReturn(userId);
+        return request;
+    }
+}

+ 378 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/PlannerBindInviteFlowTest.java

@@ -0,0 +1,378 @@
+package com.etotem.cfc.integration;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.family.BindController;
+import com.etotem.cfc.controller.family.BindInviteController;
+import com.etotem.cfc.dto.BindGuideDTO;
+import com.etotem.cfc.entity.GuideFamily;
+import com.etotem.cfc.service.BindInviteService;
+import com.etotem.cfc.service.GuideFamilyService;
+import com.etotem.cfc.service.TeacherMessageService;
+import com.etotem.cfc.mapper.GuideFamilyMapper;
+import org.junit.jupiter.api.*;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * ================================================================
+ * 场景:规划师绑定邀请全流程
+ * ================================================================
+ * 用户故事:作为成长规划师,我希望邀请家长加入我的服务团队,
+ *           以便建立长期服务关系并管理我的客户家庭。
+ *
+ * 流程步骤:
+ *   1. 规划师登录系统,生成绑定邀请链接
+ *   2. 规划师将邀请链接发送给家长
+ *   3. 家长点击链接,查看邀请详情并确认
+ *   4. 家长授权绑定,成为规划师的客户
+ *   5. 规划师可在客户列表中看到新绑定的家庭
+ *
+ * 关键里程碑:
+ *   - [Milestone-1] 邀请链接生成成功,获得邀请凭证
+ *   - [Milestone-2] 邀请链接验证成功,展示邀请详情
+ *   - [Milestone-3] 家长确认绑定成功,与规划师建立关系
+ *   - [Milestone-4] 规划师客户列表更新,可以看到新家庭
+ *   - [Milestone-5] 解绑成功,关系终止
+ * ================================================================
+ */
+@SpringBootTest
+public class PlannerBindInviteFlowTest {
+
+    @Resource
+    private BindInviteController bindInviteController;
+
+    @Resource
+    private BindController bindController;
+
+    @MockBean
+    private BindInviteService bindInviteService;
+
+    @MockBean
+    private GuideFamilyService guideFamilyService;
+
+    @MockBean
+    private TeacherMessageService teacherMessageService;
+
+    @MockBean
+    private GuideFamilyMapper guideFamilyMapper;
+
+    // ========== 测试数据 ==========
+
+    private static final Long GUIDE_USER_ID = 4001L;
+    private static final Long PARENT_USER_ID = 1001L;
+    private static final Long FAMILY_ID = 3001L;
+    private static final String INVITE_TOKEN = "invite_abc123xyz";
+    private static final Long BINDING_ID = 1L;
+
+    // ========== 场景1:规划师生成邀请 ==========
+
+    /**
+     * 场景:规划师登录后生成一个绑定邀请
+     * 角色:成长规划师
+     * 触发条件:规划师点击"邀请家长"按钮
+     */
+    @Test
+    @DisplayName("[场景1] 规划师生成邀请链接 - 期望返回邀请凭证")
+    void guideGeneratesInvite_Success() {
+        // === Given:规划师请求生成邀请 ===
+        Map<String, Object> mockResult = new HashMap<>();
+        mockResult.put("token", INVITE_TOKEN);
+        mockResult.put("expireTime", "2026-07-15 23:59:59");
+        mockResult.put("guideName", "张规划师");
+        mockResult.put("guideAvatar", "https://example.com/avatar.jpg");
+
+        when(bindInviteService.generateInvite(GUIDE_USER_ID)).thenReturn(mockResult);
+
+        // === When:规划师点击生成邀请 ===
+        Result<Map<String, Object>> result = bindInviteController.generateInvite(GUIDE_USER_ID);
+
+        // === Then:Milestone-1 - 邀请生成成功 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(INVITE_TOKEN, result.getData().get("token"));
+        assertNotNull(result.getData().get("expireTime"));
+        verify(bindInviteService).generateInvite(GUIDE_USER_ID);
+    }
+
+    // ========== 场景2:家长验证邀请 ==========
+
+    /**
+     * 场景:家长点击邀请链接,系统验证邀请有效性
+     * 角色:家长
+     * 触发条件:家长打开邀请链接
+     */
+    @Test
+    @DisplayName("[场景2] 家长验证邀请链接 - 期望展示邀请详情")
+    void parentValidatesInvite_Success() {
+        // === Given:邀请链接有效 ===
+        Map<String, Object> params = new HashMap<>();
+        params.put("token", INVITE_TOKEN);
+
+        Map<String, Object> mockValidation = new HashMap<>();
+        mockValidation.put("valid", true);
+        mockValidation.put("guideId", GUIDE_USER_ID);
+        mockValidation.put("guideName", "张规划师");
+        mockValidation.put("guideAvatar", "https://example.com/avatar.jpg");
+        mockValidation.put("serviceTypes", Arrays.asList("学业规划", "心理辅导", "综合发展"));
+        mockValidation.put("expireTime", "2026-07-15 23:59:59");
+
+        when(bindInviteService.validateToken(INVITE_TOKEN)).thenReturn(mockValidation);
+
+        // === When:家长打开邀请链接 ===
+        Result<Map<String, Object>> result = bindInviteController.validateToken(params);
+
+        // === Then:Milestone-2 - 邀请验证成功,展示详情 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(true, result.getData().get("valid"));
+        assertEquals(GUIDE_USER_ID, result.getData().get("guideId"));
+        assertEquals("张规划师", result.getData().get("guideName"));
+        verify(bindInviteService).validateToken(INVITE_TOKEN);
+    }
+
+    /**
+     * 场景:家长使用过期或无效的邀请链接
+     * 角色:家长
+     * 触发条件:链接已过期或不存在
+     */
+    @Test
+    @DisplayName("[场景2b] 使用无效邀请链接 - 期望返回错误提示")
+    void parentValidatesInvalidInvite_Fails() {
+        // === Given:邀请链接无效 ===
+        Map<String, Object> params = new HashMap<>();
+        params.put("token", "invalid_token_123");
+
+        Map<String, Object> mockValidation = new HashMap<>();
+        mockValidation.put("valid", false);
+        mockValidation.put("error", "邀请链接已过期或不存在");
+
+        when(bindInviteService.validateToken("invalid_token_123")).thenReturn(mockValidation);
+
+        // === When:家长尝试使用无效链接 ===
+        Result<Map<String, Object>> result = bindInviteController.validateToken(params);
+
+        // === Then:返回错误信息 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(false, result.getData().get("valid"));
+        assertNotNull(result.getData().get("error"));
+    }
+
+    // ========== 场景3:家长确认绑定 ==========
+
+    /**
+     * 场景:家长查看邀请详情后,确认绑定该规划师
+     * 角色:家长
+     * 触发条件:家长点击"确认绑定"按钮
+     */
+    @Test
+    @DisplayName("[场景3] 家长确认绑定规划师 - 期望绑定成功")
+    void parentConfirmsBind_Success() {
+        // === Given:家长确认绑定规划师 ===
+        BindGuideDTO bindDTO = new BindGuideDTO();
+        bindDTO.setGuideId(GUIDE_USER_ID);
+        bindDTO.setServiceType("学业规划");
+        bindDTO.setServicePrice(29900L);
+
+        when(guideFamilyService.confirmBind(eq(GUIDE_USER_ID), eq(FAMILY_ID), eq("学业规划"), eq(29900L)))
+                .thenReturn(true);
+
+        // === When:家长点击"确认绑定" ===
+        Result<Boolean> result = bindController.acceptBind(
+                PARENT_USER_ID, FAMILY_ID, bindDTO);
+
+        // === Then:Milestone-3 - 绑定成功 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(guideFamilyService).confirmBind(eq(GUIDE_USER_ID), eq(FAMILY_ID), eq("学业规划"), eq(29900L));
+    }
+
+    /**
+     * 场景:家长尝试重复绑定同一个规划师
+     * 角色:家长
+     * 触发条件:已经绑定过该规划师,再次确认绑定
+     */
+    @Test
+    @DisplayName("[场景3b] 重复绑定同一规划师 - 期望返回错误提示")
+    void parentBindsSameGuideTwice_Fails() {
+        // === Given:已经绑定过该规划师 ===
+        BindGuideDTO bindDTO = new BindGuideDTO();
+        bindDTO.setGuideId(GUIDE_USER_ID);
+        bindDTO.setServiceType("学业规划");
+
+        when(guideFamilyService.confirmBind(eq(GUIDE_USER_ID), eq(FAMILY_ID), anyString(), anyLong()))
+                .thenThrow(new RuntimeException("该规划师已在服务中,无需重复绑定"));
+
+        // === When:家长尝试再次绑定 ===
+        Result<Boolean> result = bindController.acceptBind(
+                PARENT_USER_ID, FAMILY_ID, bindDTO);
+
+        // === Then:返回错误提示 ===
+        assertEquals(400, result.getCode());
+        assertTrue(result.getMessage().contains("已在服务中") || result.getMessage().contains("重复"));
+    }
+
+    // ========== 场景4:查看绑定规划师列表 ==========
+
+    /**
+     * 场景:家长查看已绑定的规划师列表
+     * 角色:家长
+     * 触发条件:家长进入"我的规划师"页面
+     */
+    @Test
+    @DisplayName("[场景4] 家长查看绑定规划师列表 - 期望返回已绑定的规划师")
+    void parentViewsBoundGuides_Success() {
+        // === Given:家庭已绑定多个规划师 ===
+        List<GuideFamily> mockBindings = Arrays.asList(
+                createGuideFamily(1L, GUIDE_USER_ID, "学业规划", 29900L, "active"),
+                createGuideFamily(2L, 4002L, "心理辅导", 19900L, "active")
+        );
+
+        when(guideFamilyService.getGuidesByFamily(FAMILY_ID)).thenReturn(mockBindings);
+
+        // === When:家长请求规划师列表 ===
+        Result<List<Map<String, Object>>> result = bindController.getBoundGuides(FAMILY_ID);
+
+        // === Then:Milestone-4 - 返回已绑定规划师列表 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(2, result.getData().size());
+        verify(guideFamilyService).getGuidesByFamily(FAMILY_ID);
+    }
+
+    // ========== 场景5:家长解除绑定 ==========
+
+    /**
+     * 场景:家长决定解除与某个规划师的绑定关系
+     * 角色:家长
+     * 触发条件:家长在规划师详情页点击"解除绑定"
+     */
+    @Test
+    @DisplayName("[场景5] 家长解除绑定规划师 - 期望解除成功并发送通知")
+    void parentUnbindsGuide_Success() {
+        // === Given:家长请求解除绑定 ===
+        Map<String, Object> unbindRequest = new HashMap<>();
+        unbindRequest.put("id", BINDING_ID);
+
+        GuideFamily unbindedGuide = createGuideFamily(BINDING_ID, GUIDE_USER_ID, "学业规划", 29900L, "unbound");
+        when(guideFamilyService.unbindAndGet(eq(BINDING_ID), eq(FAMILY_ID))).thenReturn(unbindGuide);
+        doNothing().when(teacherMessageService).sendMessage(eq(FAMILY_ID), eq(PARENT_USER_ID), eq(GUIDE_USER_ID), anyString());
+
+        // === When:家长确认解除绑定 ===
+        Result<Boolean> result = bindController.unbind(
+                PARENT_USER_ID, FAMILY_ID, unbindRequest);
+
+        // === Then:Milestone-5 - 解绑成功并通知规划师 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(guideFamilyService).unbindAndGet(eq(BINDING_ID), eq(FAMILY_ID));
+        verify(teacherMessageService).sendMessage(eq(FAMILY_ID), eq(PARENT_USER_ID), eq(GUIDE_USER_ID), anyString());
+    }
+
+    // ========== 场景6:规划师查看已绑定的家庭 ==========
+
+    /**
+     * 场景:规划师查看已绑定的家庭列表
+     * 角色:成长规划师
+     * 触发条件:规划师进入"我的客户"页面
+     */
+    @Test
+    @DisplayName("[场景6] 规划师查看绑定家庭列表 - 期望返回所有客户家庭")
+    void guideViewsBoundFamilies_Success() {
+        // === Given:规划师有多个绑定家庭 ===
+        List<GuideFamily> mockBindings = Arrays.asList(
+                createGuideFamily(1L, GUIDE_USER_ID, "active", FAMILY_ID, 1001L),
+                createGuideFamily(2L, GUIDE_USER_ID, "active", 3002L, 1002L)
+        );
+
+        when(guideFamilyService.getFamiliesByGuide(GUIDE_USER_ID)).thenReturn(mockBindings);
+
+        // === When:规划师请求客户列表 ===
+        Result<List<GuideFamily>> result = null; // 需要找对应的 Controller 方法
+        // 假设有 guideFamilyService.getFamiliesByGuide 方法
+
+        // === Then:返回所有已绑定家庭 ===
+        assertNotNull(result); // 实际需要对接真实 API
+    }
+
+    // ========== 场景7:完整绑定流程(端到端)============
+
+    /**
+     * 场景:规划师邀请 → 家长验证 → 家长确认绑定的完整流程
+     * 角色:规划师 → 家长
+     * 触发条件:完整的邀请绑定流程
+     */
+    @Test
+    @DisplayName("[场景7] 完整邀请绑定流程 - 期望全流程成功")
+    void completeInviteBindFlow_Success() {
+        // === Step 1:规划师生成邀请 ===
+        Map<String, Object> inviteResult = new HashMap<>();
+        inviteResult.put("token", INVITE_TOKEN);
+        inviteResult.put("expireTime", "2026-07-15 23:59:59");
+
+        when(bindInviteService.generateInvite(GUIDE_USER_ID)).thenReturn(inviteResult);
+
+        Result<Map<String, Object>> step1 = bindInviteController.generateInvite(GUIDE_USER_ID);
+        assertEquals(200, step1.getCode());
+        assertEquals(INVITE_TOKEN, step1.getData().get("token"));
+
+        // === Step 2:家长验证邀请 ===
+        Map<String, Object> validateParams = new HashMap<>();
+        validateParams.put("token", INVITE_TOKEN);
+
+        Map<String, Object> validationResult = new HashMap<>();
+        validationResult.put("valid", true);
+        validationResult.put("guideId", GUIDE_USER_ID);
+        validationResult.put("guideName", "张规划师");
+
+        when(bindInviteService.validateToken(INVITE_TOKEN)).thenReturn(validationResult);
+
+        Result<Map<String, Object>> step2 = bindInviteController.validateToken(validateParams);
+        assertEquals(200, step2.getCode());
+        assertEquals(true, step2.getData().get("valid"));
+
+        // === Step 3:家长确认绑定 ===
+        BindGuideDTO bindDTO = new BindGuideDTO();
+        bindDTO.setGuideId(GUIDE_USER_ID);
+        bindDTO.setServiceType("学业规划");
+        bindDTO.setServicePrice(29900L);
+
+        when(guideFamilyService.confirmBind(eq(GUIDE_USER_ID), eq(FAMILY_ID), eq("学业规划"), eq(29900L)))
+                .thenReturn(true);
+
+        Result<Boolean> step3 = bindController.acceptBind(PARENT_USER_ID, FAMILY_ID, bindDTO);
+        assertEquals(200, step3.getCode());
+        assertTrue(step3.getData());
+    }
+
+    // ========== Helper Methods ==========
+
+    private GuideFamily createGuideFamily(Long id, Long guideId, String serviceType, Long servicePrice, String status) {
+        GuideFamily gf = new GuideFamily();
+        gf.setId(id);
+        gf.setGuideId(guideId);
+        gf.setServiceType(serviceType);
+        gf.setServicePrice(servicePrice);
+        gf.setStatus(status);
+        gf.setBoundAt(new Date());
+        return gf;
+    }
+
+    private GuideFamily createGuideFamily(Long id, Long guideId, String status, Long familyId, Long userId) {
+        GuideFamily gf = new GuideFamily();
+        gf.setId(id);
+        gf.setGuideId(guideId);
+        gf.setFamilyId(familyId);
+        gf.setUserId(userId);
+        gf.setStatus(status);
+        gf.setBoundAt(new Date());
+        return gf;
+    }
+}

+ 431 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/WishExchangeFlowTest.java

@@ -0,0 +1,431 @@
+package com.etotem.cfc.integration;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.wish.WishController;
+import com.etotem.cfc.dto.CreateWishDTO;
+import com.etotem.cfc.dto.WishDTO;
+import com.etotem.cfc.service.WishService;
+import com.etotem.cfc.service.PointsService;
+import com.etotem.cfc.mapper.WishMapper;
+import com.etotem.cfc.entity.Wish;
+import org.junit.jupiter.api.*;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * ================================================================
+ * 场景:心愿兑换审批全流程
+ * ================================================================
+ * 用户故事:作为孩子,我希望创建心愿并通过积累积分兑换,
+ *           以获得家长的认可和实际的奖励物品。
+ *
+ * 流程步骤:
+ *   1. 孩子创建心愿(如"想要一套科学实验套装")
+ *   2. 家长查看心愿并设定所需积分(如 500 积分)
+ *   3. 孩子通过完成任务积累积分后,申请兑换心愿
+ *   4. 家长审批通过,系统自动扣除积分
+ *   5. 家长购买物品后标记"已购买"
+ *   6. 孩子确认收货,心愿流程完成
+ *
+ * 关键里程碑:
+ *   - [Milestone-1] 心愿创建成功,获得心愿ID
+ *   - [Milestone-2] 家长定价成功,心愿状态变更为"已定价"
+ *   - [Milestone-3] 兑换申请提交,状态变更为"兑换中"
+ *   - [Milestone-4] 家长审批通过,积分立即扣减
+ *   - [Milestone-5] 标记已购买,等待孩子确认
+ *   - [Milestone-6] 孩子确认收货,心愿完成
+ * ================================================================
+ */
+@SpringBootTest
+public class WishExchangeFlowTest {
+
+    @Resource
+    private WishController wishController;
+
+    @MockBean
+    private WishService wishService;
+
+    @MockBean
+    private PointsService pointsService;
+
+    @MockBean
+    private WishMapper wishMapper;
+
+    // ========== 测试数据 ==========
+
+    private static final Long CHILD_USER_ID = 2001L;
+    private static final Long PARENT_USER_ID = 1001L;
+    private static final Long FAMILY_ID = 3001L;
+    private static final Long WISH_ID = 1L;
+    private static final String WISH_TITLE = "科学实验套装";
+    private static final Integer POINTS_REQUIRED = 500;
+
+    // ========== 场景1:孩子创建心愿 ==========
+
+    /**
+     * 场景:孩子创建一个新的心愿
+     * 角色:孩子
+     * 触发条件:孩子点击"创建心愿"并填写表单
+     */
+    @Test
+    @DisplayName("[场景1] 孩子创建心愿 - 期望心愿创建成功")
+    void childCreatesWish_Success() {
+        // === Given:孩子填写了心愿信息 ===
+        CreateWishDTO createDTO = new CreateWishDTO();
+        createDTO.setTitle(WISH_TITLE);
+        createDTO.setDescription("包含50个实验的科学实验套装");
+        createDTO.setImageUrl("https://example.com/science-kit.jpg");
+        createDTO.setLinkUrl("https://shop.example.com/science-kit");
+        createDTO.setEstimatedPrice(29900L); // 299元
+
+        when(wishService.createByChild(eq(CHILD_USER_ID), any(CreateWishDTO.class)))
+                .thenReturn(WISH_ID);
+
+        // === When:孩子提交创建心愿表单 ===
+        Result<Long> result = wishController.createWish(
+                CHILD_USER_ID, "child", createDTO);
+
+        // === Then:Milestone-1 - 心愿创建成功 ===
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(WISH_ID, result.getData());
+        verify(wishService).createByChild(eq(CHILD_USER_ID), any(CreateWishDTO.class));
+    }
+
+    // ========== 场景2:家长定价 ==========
+
+    /**
+     * 场景:家长为孩子的心愿设定所需积分
+     * 角色:家长
+     * 触发条件:家长打开心愿详情,决定设定多少积分可兑换
+     */
+    @Test
+    @DisplayName("[场景2] 家长设定心愿积分 - 期望积分设定成功且状态更新")
+    void parentSetsWishPrice_Success() {
+        // === Given:家长决定设定 500 积分 ===
+        Map<String, Object> priceRequest = new HashMap<>();
+        priceRequest.put("pointsRequired", POINTS_REQUIRED);
+
+        when(wishService.setPrice(eq(WISH_ID), eq(PARENT_USER_ID), eq(POINTS_REQUIRED)))
+                .thenReturn(true);
+
+        // === When:家长提交定价 ===
+        Result<Boolean> result = wishController.setPrice(
+                WISH_ID, PARENT_USER_ID, priceRequest);
+
+        // === Then:Milestone-2 - 定价成功 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(wishService).setPrice(eq(WISH_ID), eq(PARENT_USER_ID), eq(POINTS_REQUIRED));
+    }
+
+    /**
+     * 场景:家长设定的积分值为负数或零
+     * 角色:家长
+     * 触发条件:表单校验未通过
+     */
+    @Test
+    @DisplayName("[场景2b] 家长设定无效积分值 - 期望返回错误提示")
+    void parentSetsInvalidPrice_Fails() {
+        // === Given:设定了无效的积分值 ===
+        Map<String, Object> priceRequest = new HashMap<>();
+        priceRequest.put("pointsRequired", 0); // 无效值
+
+        // === When:尝试提交定价 ===
+        Result<Boolean> result = wishController.setPrice(
+                WISH_ID, PARENT_USER_ID, priceRequest);
+
+        // === Then:返回错误提示 ===
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("有效") || result.getMessage().contains("大于0"));
+    }
+
+    // ========== 场景3:孩子申请兑换 ==========
+
+    /**
+     * 场景:孩子积累足够积分后,申请兑换心愿
+     * 角色:孩子
+     * 触发条件:孩子点击心愿的"申请兑换"按钮
+     */
+    @Test
+    @DisplayName("[场景3] 孩子申请兑换心愿 - 期望兑换申请提交成功")
+    void childRequestsExchange_Success() {
+        // === Given:孩子已积累足够积分 ===
+        when(wishService.requestExchange(eq(WISH_ID), eq(CHILD_USER_ID)))
+                .thenReturn(true);
+
+        // === When:孩子点击"申请兑换" ===
+        Result<Boolean> result = wishController.requestExchange(WISH_ID, CHILD_USER_ID);
+
+        // === Then:Milestone-3 - 兑换申请提交成功 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(wishService).requestExchange(eq(WISH_ID), eq(CHILD_USER_ID));
+    }
+
+    /**
+         * 场景:孩子积分不足时尝试兑换
+     * 角色:孩子
+     * 触发条件:孩子积分不足以兑换该心愿
+     */
+    @Test
+    @DisplayName("[场景3b] 积分不足时申请兑换 - 期望返回错误提示")
+    void childRequestsExchangeWithInsufficientPoints_Fails() {
+        // === Given:孩子积分不足 ===
+        when(wishService.requestExchange(eq(WISH_ID), eq(CHILD_USER_ID)))
+                .thenThrow(new RuntimeException("积分不足,当前积分:200,需要:500"));
+
+        // === When:孩子尝试兑换 ===
+        Result<Boolean> result = wishController.requestExchange(WISH_ID, CHILD_USER_ID);
+
+        // === Then:返回积分不足提示 ===
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("积分不足"));
+    }
+
+    // ========== 场景4:家长审批兑换 ==========
+
+    /**
+     * 场景:家长审批通过孩子的兑换申请,积分立即扣除
+     * 角色:家长
+     * 触发条件:家长查看兑换申请并点击"批准"
+     */
+    @Test
+    @DisplayName("[场景4] 家长审批通过兑换 - 期望审批成功且积分扣除")
+    void parentApprovesExchange_Success() {
+        // === Given:家长同意孩子的兑换申请 ===
+        Map<String, Object> approveRequest = new HashMap<>();
+        approveRequest.put("approved", true);
+
+        when(wishService.approveExchange(eq(WISH_ID), eq(PARENT_USER_ID), eq(true)))
+                .thenReturn(true);
+        when(pointsService.deductForWish(eq(CHILD_USER_ID), eq(POINTS_REQUIRED)))
+                .thenReturn(true);
+
+        // === When:家长点击"批准" ===
+        Result<Boolean> result = wishController.approveExchange(
+                WISH_ID, PARENT_USER_ID, approveRequest);
+
+        // === Then:Milestone-4 - 审批通过 + Milestone-5 积分扣减 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(wishService).approveExchange(eq(WISH_ID), eq(PARENT_USER_ID), eq(true));
+    }
+
+    /**
+     * 场景:家长拒绝孩子的兑换申请
+     * 角色:家长
+     * 触发条件:家长认为该心愿不合适,点击"拒绝"
+     */
+    @Test
+    @DisplayName("[场景4b] 家长拒绝兑换申请 - 期望心愿状态回退")
+    void parentRejectsExchange_Success() {
+        // === Given:家长拒绝兑换申请 ===
+        Map<String, Object> approveRequest = new HashMap<>();
+        approveRequest.put("approved", false);
+        approveRequest.put("reason", "本月预算已用完,下个月再兑换好吗?");
+
+        when(wishService.approveExchange(eq(WISH_ID), eq(PARENT_USER_ID), eq(false)))
+                .thenReturn(true);
+
+        // === When:家长点击"拒绝" ===
+        Result<Boolean> result = wishController.approveExchange(
+                WISH_ID, PARENT_USER_ID, approveRequest);
+
+        // === Then:拒绝成功,心愿状态回退 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(wishService).approveExchange(eq(WISH_ID), eq(PARENT_USER_ID), eq(false));
+    }
+
+    // ========== 场景5:标记已购买 ==========
+
+    /**
+     * 场景:家长购买物品后标记心愿为"已购买"
+     * 角色:家长
+     * 触发条件:家长完成购买操作
+     */
+    @Test
+    @DisplayName("[场景5] 家长标记心愿已购买 - 期望状态变更为待收货")
+    void parentMarksFulfilled_Success() {
+        // === Given:家长已完成购买 ===
+        when(wishService.markFulfilled(eq(WISH_ID), eq(PARENT_USER_ID)))
+                .thenReturn(true);
+
+        // === When:家长点击"已购买" ===
+        Result<Boolean> result = wishController.markFulfilled(WISH_ID, PARENT_USER_ID);
+
+        // === Then:Milestone-5 - 标记成功 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(wishService).markFulfilled(eq(WISH_ID), eq(PARENT_USER_ID));
+    }
+
+    // ========== 场景6:孩子确认收货 ==========
+
+    /**
+     * 场景:孩子收到物品后确认收货,心愿流程完成
+     * 角色:孩子
+     * 触发条件:孩子收到实物并确认无误
+     */
+    @Test
+    @DisplayName("[场景6] 孩子确认收货 - 期望心愿流程完成")
+    void childConfirmsReceived_Success() {
+        // === Given:孩子已收到物品 ===
+        when(wishService.confirmReceived(eq(WISH_ID), eq(CHILD_USER_ID)))
+                .thenReturn(true);
+
+        // === When:孩子点击"确认收货" ===
+        Result<Boolean> result = wishController.confirmReceived(WISH_ID, CHILD_USER_ID);
+
+        // === Then:Milestone-6 - 心愿完成 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(wishService).confirmReceived(eq(WISH_ID), eq(CHILD_USER_ID));
+    }
+
+    // ========== 场景7:完整心愿流程(端到端)============
+
+    /**
+     * 场景:完整的心愿从创建到完成的整个生命周期
+     * 角色:孩子 → 家长 → 孩子
+     * 触发条件:心愿的完整业务流程
+     */
+    @Test
+    @DisplayName("[场景7] 完整心愿生命周期 - 期望全流程成功")
+    void completeWishLifecycle_Success() {
+        // === Step 1:孩子创建心愿 ===
+        CreateWishDTO createDTO = new CreateWishDTO();
+        createDTO.setTitle("一本科学杂志");
+        createDTO.setEstimatedPrice(3000L);
+
+        Long newWishId = 2L;
+        when(wishService.createByChild(eq(CHILD_USER_ID), any(CreateWishDTO.class)))
+                .thenReturn(newWishId);
+
+        Result<Long> createResult = wishController.createWish(
+                CHILD_USER_ID, "child", createDTO);
+        assertEquals(200, createResult.getCode());
+        assertEquals(newWishId, createResult.getData());
+
+        // === Step 2:家长定价 ===
+        Map<String, Object> priceRequest = new HashMap<>();
+        priceRequest.put("pointsRequired", 100);
+
+        when(wishService.setPrice(eq(newWishId), eq(PARENT_USER_ID), eq(100)))
+                .thenReturn(true);
+
+        Result<Boolean> priceResult = wishController.setPrice(
+                newWishId, PARENT_USER_ID, priceRequest);
+        assertEquals(200, priceResult.getCode());
+        assertTrue(priceResult.getData());
+
+        // === Step 3:孩子申请兑换 ===
+        when(wishService.requestExchange(eq(newWishId), eq(CHILD_USER_ID)))
+                .thenReturn(true);
+
+        Result<Boolean> exchangeResult = wishController.requestExchange(newWishId, CHILD_USER_ID);
+        assertEquals(200, exchangeResult.getCode());
+        assertTrue(exchangeResult.getData());
+
+        // === Step 4:家长审批通过 ===
+        Map<String, Object> approveRequest = new HashMap<>();
+        approveRequest.put("approved", true);
+
+        when(wishService.approveExchange(eq(newWishId), eq(PARENT_USER_ID), eq(true)))
+                .thenReturn(true);
+
+        Result<Boolean> approveResult = wishController.approveExchange(
+                newWishId, PARENT_USER_ID, approveRequest);
+        assertEquals(200, approveResult.getCode());
+        assertTrue(approveResult.getData());
+
+        // === Step 5:家长标记已购买 ===
+        when(wishService.markFulfilled(eq(newWishId), eq(PARENT_USER_ID)))
+                .thenReturn(true);
+
+        Result<Boolean> fulfillResult = wishController.markFulfilled(newWishId, PARENT_USER_ID);
+        assertEquals(200, fulfillResult.getCode());
+        assertTrue(fulfillResult.getData());
+
+        // === Step 6:孩子确认收货 ===
+        when(wishService.confirmReceived(eq(newWishId), eq(CHILD_USER_ID)))
+                .thenReturn(true);
+
+        Result<Boolean> confirmResult = wishController.confirmReceived(newWishId, CHILD_USER_ID);
+        assertEquals(200, confirmResult.getCode());
+        assertTrue(confirmResult.getData());
+    }
+
+    // ========== 场景8:家长拒绝心愿 ==========
+
+    /**
+     * 场景:家长认为某个心愿不合适,直接拒绝
+     * 角色:家长
+     * 触发条件:家长点击"拒绝"并填写原因
+     */
+    @Test
+    @DisplayName("[场景8] 家长直接拒绝心愿 - 期望心愿被关闭")
+    void parentRejectsWish_Success() {
+        // === Given:家长决定拒绝该心愿 ===
+        Map<String, String> rejectRequest = new HashMap<>();
+        rejectRequest.put("reason", "这套书家里已经有了,换个其他的吧");
+
+        when(wishService.rejectWish(eq(WISH_ID), eq(PARENT_USER_ID), anyString()))
+                .thenReturn(true);
+
+        // === When:家长点击"拒绝" ===
+        Result<Boolean> result = wishController.rejectWish(
+                WISH_ID, PARENT_USER_ID, rejectRequest);
+
+        // === Then:心愿被关闭 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(wishService).rejectWish(eq(WISH_ID), eq(PARENT_USER_ID), anyString());
+    }
+
+    // ========== 场景9:孩子取消心愿 ==========
+
+    /**
+     * 场景:孩子改变主意,主动取消心愿
+     * 角色:孩子
+     * 触发条件:孩子点击"取消心愿"
+     */
+    @Test
+    @DisplayName("[场景9] 孩子取消心愿 - 期望心愿被取消")
+    void childCancelsWish_Success() {
+        // === Given:孩子改变主意 ===
+        Map<String, String> cancelRequest = new HashMap<>();
+        cancelRequest.put("reason", "想了想还是不要了");
+
+        when(wishService.cancelWish(eq(WISH_ID), eq(CHILD_USER_ID), anyString()))
+                .thenReturn(true);
+
+        // === When:孩子点击"取消" ===
+        Result<Boolean> result = wishController.cancelWish(
+                WISH_ID, CHILD_USER_ID, cancelRequest);
+
+        // === Then:心愿被取消 ===
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+        verify(wishService).cancelWish(eq(WISH_ID), eq(CHILD_USER_ID), anyString());
+    }
+
+    // ========== Helper Methods ==========
+
+    private WishDTO createWishDTO(Long id, String title, String status) {
+        WishDTO dto = new WishDTO();
+        dto.setId(id);
+        dto.setTitle(title);
+        dto.setStatus(status);
+        dto.setPointsRequired(POINTS_REQUIRED);
+        return dto;
+    }
+}