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

test: 修补测试用例,全量204测试通过

- 修复 TeacherLoginTest(4用例):添加 @AfterEach cleanup() 清理测试手机号,
  防止唯一键冲突
- 修复 GuideApplicationServiceTest(2用例):添加 @MockBean
  GuideApplicationRecordMapper,避免 addRecord() 真实写入 DB
- 修复 AuthControllerTest(1用例):移除不必要的 doNothing() 调用
- 新增 5 个测试文件(AuthControllerTest 39用例, WishControllerTest 27用例,
  TaskControllerTest 19用例, FamilyControllerTest 10用例,
  StatsControllerTest 6用例)
- 更新 test-inventory.md 和 test-report.md 测试文档

Tests run: 204, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
Sisyphus 4 месяцев назад
Родитель
Сommit
4ae34331f0

+ 573 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/AuthControllerTest.java

@@ -0,0 +1,573 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.auth.AuthController;
+import com.etotem.cfc.dto.*;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.service.UserService;
+import com.etotem.cfc.service.VerificationCodeService;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+@SpringBootTest
+public class AuthControllerTest {
+
+    @Resource
+    private AuthController controller;
+
+    @MockBean
+    private UserService userService;
+
+    @MockBean
+    private VerificationCodeService verificationCodeService;
+
+    // ==================== sendCode ====================
+
+    @Test
+    public void sendCode_Success() {
+        SendCodeDTO dto = new SendCodeDTO();
+        dto.setPhone("13800138000");
+        dto.setType("login");
+
+        Result<Boolean> result = controller.sendCode(dto);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void sendCode_EmptyPhone() {
+        SendCodeDTO dto = new SendCodeDTO();
+        dto.setPhone("");
+        dto.setType("login");
+
+        Result<Boolean> result = controller.sendCode(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("手机号不能为空", result.getMessage());
+    }
+
+    @Test
+    public void sendCode_InvalidPhone() {
+        SendCodeDTO dto = new SendCodeDTO();
+        dto.setPhone("12345");
+        dto.setType("login");
+
+        Result<Boolean> result = controller.sendCode(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("手机号格式不正确", result.getMessage());
+    }
+
+    // ==================== phoneLogin ====================
+
+    @Test
+    public void phoneLogin_Success() {
+        PhoneLoginDTO dto = new PhoneLoginDTO();
+        dto.setPhone("13800138000");
+        dto.setCode("123456");
+        dto.setNickname("测试用户");
+
+        when(verificationCodeService.verifyCode("13800138000", "123456", "login")).thenReturn(true);
+        LoginResultDTO loginResult = new LoginResultDTO();
+        loginResult.setToken("mock-token");
+        loginResult.setUserId(1L);
+        loginResult.setRole("parent");
+        when(userService.phoneLogin(dto)).thenReturn(loginResult);
+
+        Result<LoginResultDTO> result = controller.phoneLogin(dto);
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals("mock-token", result.getData().getToken());
+    }
+
+    @Test
+    public void phoneLogin_EmptyParams() {
+        PhoneLoginDTO dto = new PhoneLoginDTO();
+        dto.setPhone(null);
+        dto.setCode(null);
+
+        Result<LoginResultDTO> result = controller.phoneLogin(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("手机号和验证码不能为空", result.getMessage());
+    }
+
+    @Test
+    public void phoneLogin_WrongCode() {
+        PhoneLoginDTO dto = new PhoneLoginDTO();
+        dto.setPhone("13800138000");
+        dto.setCode("000000");
+
+        when(verificationCodeService.verifyCode("13800138000", "000000", "login")).thenReturn(false);
+
+        Result<LoginResultDTO> result = controller.phoneLogin(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("验证码错误或已过期", result.getMessage());
+    }
+
+    @Test
+    public void phoneLogin_UserServiceException() {
+        PhoneLoginDTO dto = new PhoneLoginDTO();
+        dto.setPhone("13800138000");
+        dto.setCode("123456");
+
+        when(verificationCodeService.verifyCode("13800138000", "123456", "login")).thenReturn(true);
+        when(userService.phoneLogin(dto)).thenThrow(new RuntimeException("该账号不允许登录小程序"));
+
+        Result<LoginResultDTO> result = controller.phoneLogin(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("该账号不允许登录小程序", result.getMessage());
+    }
+
+    // ==================== wechatPhoneLogin ====================
+
+    @Test
+    public void wechatPhoneLogin_Success() {
+        WechatPhoneLoginDTO dto = new WechatPhoneLoginDTO();
+        dto.setCode("wx-code");
+        dto.setPhoneCode("phone-code");
+
+        LoginResultDTO loginResult = new LoginResultDTO();
+        loginResult.setToken("wechat-token");
+        loginResult.setUserId(2L);
+        when(userService.wechatPhoneLogin(dto)).thenReturn(loginResult);
+
+        Result<LoginResultDTO> result = controller.wechatPhoneLogin(dto);
+        assertEquals(200, result.getCode());
+        assertEquals("wechat-token", result.getData().getToken());
+    }
+
+    @Test
+    public void wechatPhoneLogin_EmptyParams() {
+        WechatPhoneLoginDTO dto = new WechatPhoneLoginDTO();
+        dto.setCode(null);
+        dto.setPhoneCode(null);
+
+        Result<LoginResultDTO> result = controller.wechatPhoneLogin(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("登录code和手机号code不能为空", result.getMessage());
+    }
+
+    // ==================== silentLogin ====================
+
+    @Test
+    public void silentLogin_Success() {
+        WechatLoginDTO dto = new WechatLoginDTO();
+        dto.setCode("silent-code");
+
+        LoginResultDTO loginResult = new LoginResultDTO();
+        loginResult.setToken("silent-token");
+        when(userService.silentLogin("silent-code")).thenReturn(loginResult);
+
+        Result<LoginResultDTO> result = controller.silentLogin(dto);
+        assertEquals(200, result.getCode());
+        assertEquals("silent-token", result.getData().getToken());
+    }
+
+    @Test
+    public void silentLogin_NoCode() {
+        WechatLoginDTO dto = new WechatLoginDTO();
+        dto.setCode(null);
+
+        Result<LoginResultDTO> result = controller.silentLogin(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("登录code不能为空", result.getMessage());
+    }
+
+    // ==================== autoLogin ====================
+
+    @Test
+    public void autoLogin_Success() {
+        WechatLoginDTO dto = new WechatLoginDTO();
+        dto.setOpenid("mock-openid");
+
+        LoginResultDTO loginResult = new LoginResultDTO();
+        loginResult.setToken("auto-token");
+        when(userService.autoLoginByOpenid("mock-openid")).thenReturn(loginResult);
+
+        Result<LoginResultDTO> result = controller.autoLogin(dto);
+        assertEquals(200, result.getCode());
+        assertEquals("auto-token", result.getData().getToken());
+    }
+
+    @Test
+    public void autoLogin_UserNotFound() {
+        WechatLoginDTO dto = new WechatLoginDTO();
+        dto.setOpenid("unknown-openid");
+
+        when(userService.autoLoginByOpenid("unknown-openid")).thenReturn(null);
+
+        Result<LoginResultDTO> result = controller.autoLogin(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("用户不存在,请先登录", result.getMessage());
+    }
+
+    @Test
+    public void autoLogin_NoOpenid() {
+        WechatLoginDTO dto = new WechatLoginDTO();
+        dto.setOpenid(null);
+
+        Result<LoginResultDTO> result = controller.autoLogin(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("openid不能为空", result.getMessage());
+    }
+
+    // ==================== registerWithIdCard ====================
+
+    @Test
+    public void registerWithIdCard_Success() {
+        RegisterWithIdCardDTO dto = new RegisterWithIdCardDTO();
+        dto.setPhone("13800138000");
+        dto.setCode("123456");
+        dto.setIdCard("110101199001011234");
+        dto.setRealName("张三");
+
+        LoginResultDTO loginResult = new LoginResultDTO();
+        loginResult.setToken("idcard-token");
+        when(userService.registerWithIdCard(dto)).thenReturn(loginResult);
+
+        Result<LoginResultDTO> result = controller.registerWithIdCard(dto);
+        assertEquals(200, result.getCode());
+    }
+
+    @Test
+    public void registerWithIdCard_EmptyPhone() {
+        RegisterWithIdCardDTO dto = new RegisterWithIdCardDTO();
+        dto.setPhone(null);
+        dto.setCode("123456");
+        dto.setIdCard("110101199001011234");
+        dto.setRealName("张三");
+
+        Result<LoginResultDTO> result = controller.registerWithIdCard(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("手机号和验证码不能为空", result.getMessage());
+    }
+
+    @Test
+    public void registerWithIdCard_InvalidIdCard() {
+        RegisterWithIdCardDTO dto = new RegisterWithIdCardDTO();
+        dto.setPhone("13800138000");
+        dto.setCode("123456");
+        dto.setIdCard("12345");
+        dto.setRealName("张三");
+
+        Result<LoginResultDTO> result = controller.registerWithIdCard(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("身份证号格式不正确", result.getMessage());
+    }
+
+    @Test
+    public void registerWithIdCard_EmptyRealName() {
+        RegisterWithIdCardDTO dto = new RegisterWithIdCardDTO();
+        dto.setPhone("13800138000");
+        dto.setCode("123456");
+        dto.setIdCard("110101199001011234");
+        dto.setRealName("");
+
+        Result<LoginResultDTO> result = controller.registerWithIdCard(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("真实姓名不能为空", result.getMessage());
+    }
+
+    // ==================== wechatLogin ====================
+
+    @Test
+    public void wechatLogin_Success() {
+        WechatLoginDTO dto = new WechatLoginDTO();
+        dto.setCode("wx-code");
+
+        LoginResultDTO loginResult = new LoginResultDTO();
+        loginResult.setToken("wx-token");
+        when(userService.wechatLogin(dto)).thenReturn(loginResult);
+
+        Result<LoginResultDTO> result = controller.wechatLogin(dto);
+        assertEquals(200, result.getCode());
+        assertEquals("wx-token", result.getData().getToken());
+    }
+
+    @Test
+    public void wechatLogin_Exception() {
+        WechatLoginDTO dto = new WechatLoginDTO();
+        dto.setCode("bad-code");
+
+        when(userService.wechatLogin(dto)).thenThrow(new RuntimeException("微信登录失败"));
+
+        Result<LoginResultDTO> result = controller.wechatLogin(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("微信登录失败", result.getMessage());
+    }
+
+    // ==================== getInfo ====================
+
+    @Test
+    public void getInfo_Success() {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getAttribute("userId")).thenReturn(1L);
+
+        User mockUser = new User();
+        mockUser.setId(1L);
+        mockUser.setNickname("测试用户");
+        when(userService.getUserInfo(1L)).thenReturn(mockUser);
+
+        Result<User> result = controller.getInfo(request);
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        // Password should be cleared
+        assertNull(result.getData().getPassword());
+    }
+
+    @Test
+    public void getInfo_NotLoggedIn() {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getAttribute("userId")).thenReturn(null);
+        when(request.getHeader("X-User-Id")).thenReturn(null);
+
+        Result<User> result = controller.getInfo(request);
+        assertEquals(500, result.getCode());
+        assertEquals("未登录", result.getMessage());
+    }
+
+    // ==================== setPassword ====================
+
+    @Test
+    public void setPassword_Success() {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getAttribute("userId")).thenReturn(1L);
+
+        SetPasswordDTO dto = new SetPasswordDTO();
+        dto.setPassword("newPass123");
+
+        when(userService.setPassword(1L, "newPass123")).thenReturn(true);
+
+        Result<Boolean> result = controller.setPassword(request, dto);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    // ==================== verifyPassword ====================
+
+    @Test
+    public void verifyPassword_Correct() {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getAttribute("userId")).thenReturn(1L);
+
+        VerifyPasswordDTO dto = new VerifyPasswordDTO();
+        dto.setPassword("correctPass");
+
+        when(userService.verifyPassword(1L, "correctPass")).thenReturn(true);
+
+        Result<Boolean> result = controller.verifyPassword(request, dto);
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void verifyPassword_Wrong() {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getAttribute("userId")).thenReturn(1L);
+
+        VerifyPasswordDTO dto = new VerifyPasswordDTO();
+        dto.setPassword("wrongPass");
+
+        when(userService.verifyPassword(1L, "wrongPass")).thenReturn(false);
+
+        Result<Boolean> result = controller.verifyPassword(request, dto);
+        assertFalse(result.getData());
+    }
+
+    // ==================== checkPhone ====================
+
+    @Test
+    public void checkPhone_Registered() {
+        CheckPhoneDTO dto = new CheckPhoneDTO();
+        dto.setPhone("13800138000");
+        when(userService.isPhoneRegistered("13800138000")).thenReturn(true);
+
+        Result<Boolean> result = controller.checkPhone(dto);
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void checkPhone_NotRegistered() {
+        CheckPhoneDTO dto = new CheckPhoneDTO();
+        dto.setPhone("13900139000");
+        when(userService.isPhoneRegistered("13900139000")).thenReturn(false);
+
+        Result<Boolean> result = controller.checkPhone(dto);
+        assertFalse(result.getData());
+    }
+
+    @Test
+    public void checkPhone_Invalid() {
+        CheckPhoneDTO dto = new CheckPhoneDTO();
+        dto.setPhone("123");
+
+        Result<Boolean> result = controller.checkPhone(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("手机号格式不正确", result.getMessage());
+    }
+
+    @Test
+    public void checkPhone_Empty() {
+        CheckPhoneDTO dto = new CheckPhoneDTO();
+        dto.setPhone("");
+
+        Result<Boolean> result = controller.checkPhone(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("手机号不能为空", result.getMessage());
+    }
+
+    // ==================== directRegister ====================
+
+    @Test
+    public void directRegister_Success() {
+        DirectRegisterDTO dto = new DirectRegisterDTO();
+        dto.setPhone("13800138000");
+        dto.setNickname("新用户");
+        dto.setRole("parent");
+
+        when(userService.isPhoneRegistered("13800138000")).thenReturn(false);
+        LoginResultDTO loginResult = new LoginResultDTO();
+        loginResult.setToken("reg-token");
+        loginResult.setUserId(3L);
+        when(userService.directRegister(dto)).thenReturn(loginResult);
+
+        Result<LoginResultDTO> result = controller.directRegister(dto);
+        assertEquals(200, result.getCode());
+        assertEquals("reg-token", result.getData().getToken());
+    }
+
+    @Test
+    public void directRegister_AlreadyRegistered() {
+        DirectRegisterDTO dto = new DirectRegisterDTO();
+        dto.setPhone("13800138000");
+        when(userService.isPhoneRegistered("13800138000")).thenReturn(true);
+
+        Result<LoginResultDTO> result = controller.directRegister(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("该手机号已注册,请使用验证码登录", result.getMessage());
+    }
+
+    @Test
+    public void directRegister_InvalidPhone() {
+        DirectRegisterDTO dto = new DirectRegisterDTO();
+        dto.setPhone("12345");
+
+        Result<LoginResultDTO> result = controller.directRegister(dto);
+        assertEquals(500, result.getCode());
+    }
+
+    // ==================== registerWithInvite ====================
+
+    @Test
+    public void registerWithInvite_Success() {
+        RegisterWithInviteCodeDTO dto = new RegisterWithInviteCodeDTO();
+        dto.setPhone("13800138000");
+        dto.setCode("123456");
+        dto.setInviteCode("INVITE123");
+        dto.setCodeId("code-id-1");
+
+        when(verificationCodeService.verifyCode("13800138000", "123456", "code-id-1")).thenReturn(true);
+        when(userService.isPhoneRegistered("13800138000")).thenReturn(false);
+        LoginResultDTO loginResult = new LoginResultDTO();
+        loginResult.setToken("invite-token");
+        when(userService.registerWithInviteCode(dto)).thenReturn(loginResult);
+
+        Result<LoginResultDTO> result = controller.registerWithInvite(dto);
+        assertEquals(200, result.getCode());
+        assertEquals("invite-token", result.getData().getToken());
+    }
+
+    @Test
+    public void registerWithInvite_EmptyPhone() {
+        RegisterWithInviteCodeDTO dto = new RegisterWithInviteCodeDTO();
+        dto.setPhone("");
+        dto.setInviteCode("INVITE123");
+        dto.setCode("123456");
+
+        Result<LoginResultDTO> result = controller.registerWithInvite(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("手机号不能为空", result.getMessage());
+    }
+
+    @Test
+    public void registerWithInvite_EmptyInviteCode() {
+        RegisterWithInviteCodeDTO dto = new RegisterWithInviteCodeDTO();
+        dto.setPhone("13800138000");
+        dto.setInviteCode("");
+        dto.setCode("123456");
+
+        Result<LoginResultDTO> result = controller.registerWithInvite(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("邀请码不能为空", result.getMessage());
+    }
+
+    @Test
+    public void registerWithInvite_WrongCode() {
+        RegisterWithInviteCodeDTO dto = new RegisterWithInviteCodeDTO();
+        dto.setPhone("13800138000");
+        dto.setCode("000000");
+        dto.setInviteCode("INVITE123");
+        dto.setCodeId("code-id-1");
+
+        when(verificationCodeService.verifyCode("13800138000", "000000", "code-id-1")).thenReturn(false);
+
+        Result<LoginResultDTO> result = controller.registerWithInvite(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("验证码错误或已过期", result.getMessage());
+    }
+
+    @Test
+    public void registerWithInvite_AlreadyRegistered() {
+        RegisterWithInviteCodeDTO dto = new RegisterWithInviteCodeDTO();
+        dto.setPhone("13800138000");
+        dto.setCode("123456");
+        dto.setInviteCode("INVITE123");
+        dto.setCodeId("code-id-1");
+
+        when(verificationCodeService.verifyCode("13800138000", "123456", "code-id-1")).thenReturn(true);
+        when(userService.isPhoneRegistered("13800138000")).thenReturn(true);
+
+        Result<LoginResultDTO> result = controller.registerWithInvite(dto);
+        assertEquals(500, result.getCode());
+        assertEquals("该手机号已注册,请使用验证码登录", result.getMessage());
+    }
+
+    // ==================== switchRole ====================
+
+    @Test
+    public void switchRole_Success() {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getAttribute("userId")).thenReturn(1L);
+
+        SwitchRoleDTO dto = new SwitchRoleDTO();
+        dto.setRole("teacher");
+
+        LoginResultDTO loginResult = new LoginResultDTO();
+        loginResult.setToken("switched-token");
+        loginResult.setRole("teacher");
+        when(userService.switchRoleForLogin(1L, "teacher")).thenReturn(loginResult);
+
+        Result<LoginResultDTO> result = controller.switchRole(request, dto);
+        assertEquals(200, result.getCode());
+        assertEquals("teacher", result.getData().getRole());
+    }
+
+    @Test
+    public void switchRole_NotLoggedIn() {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getAttribute("userId")).thenReturn(null);
+        when(request.getHeader("X-User-Id")).thenReturn(null);
+
+        SwitchRoleDTO dto = new SwitchRoleDTO();
+        dto.setRole("teacher");
+
+        Result<LoginResultDTO> result = controller.switchRole(request, dto);
+        assertEquals(500, result.getCode());
+        assertEquals("未登录", result.getMessage());
+    }
+}

+ 182 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/FamilyControllerTest.java

@@ -0,0 +1,182 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.family.FamilyController;
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.when;
+
+@SpringBootTest
+public class FamilyControllerTest {
+
+    @Resource
+    private FamilyController controller;
+
+    @MockBean
+    private FamilyMapper familyMapper;
+
+    @MockBean
+    private UserMapper userMapper;
+
+    private User mockUser(Long id, Long familyId, String role) {
+        User user = new User();
+        user.setId(id);
+        user.setFamilyId(familyId);
+        user.setRole(role);
+        user.setNickname("测试用户");
+        return user;
+    }
+
+    private Family mockFamily(Long id, String inviteCode) {
+        Family family = new Family();
+        family.setId(id);
+        family.setName("我的家庭");
+        family.setInviteCode(inviteCode);
+        return family;
+    }
+
+    // ==================== getInviteCode ====================
+
+    @Test
+    public void getInviteCode_Success() {
+        User user = mockUser(1L, 100L, "parent");
+        Family family = mockFamily(100L, "ABCD1234");
+
+        when(userMapper.selectById(1L)).thenReturn(user);
+        when(familyMapper.selectById(100L)).thenReturn(family);
+
+        Result<String> result = controller.getInviteCode(1L);
+        assertEquals(200, result.getCode());
+        assertEquals("ABCD1234", result.getData());
+    }
+
+    @Test
+    public void getInviteCode_UserNoFamily() {
+        User user = mockUser(1L, null, "parent");
+        when(userMapper.selectById(1L)).thenReturn(user);
+
+        Result<String> result = controller.getInviteCode(1L);
+        assertEquals(500, result.getCode());
+        assertEquals("用户没有家庭", result.getMessage());
+    }
+
+    @Test
+    public void getInviteCode_UserNotFound() {
+        when(userMapper.selectById(999L)).thenReturn(null);
+
+        Result<String> result = controller.getInviteCode(999L);
+        assertEquals(500, result.getCode());
+        assertEquals("用户没有家庭", result.getMessage());
+    }
+
+    @Test
+    public void getInviteCode_GenerateIfMissing() {
+        User user = mockUser(1L, 100L, "parent");
+        Family family = mockFamily(100L, null); // No invite code
+
+        when(userMapper.selectById(1L)).thenReturn(user);
+        when(familyMapper.selectById(100L)).thenReturn(family);
+
+        Result<String> result = controller.getInviteCode(1L);
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(8, result.getData().length());
+    }
+
+    // ==================== generateInviteCode ====================
+
+    @Test
+    public void generateNewInviteCode_Success() {
+        User user = mockUser(1L, 100L, "parent");
+        Family family = mockFamily(100L, "OLDCODE");
+
+        when(userMapper.selectById(1L)).thenReturn(user);
+        when(familyMapper.selectById(100L)).thenReturn(family);
+
+        Result<String> result = controller.generateInviteCode(1L);
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(8, result.getData().length());
+        // New code should be different from old one
+        assertNotEquals("OLDCODE", result.getData());
+    }
+
+    @Test
+    public void generateNewInviteCode_UserNoFamily() {
+        User user = mockUser(1L, null, "parent");
+        when(userMapper.selectById(1L)).thenReturn(user);
+
+        Result<String> result = controller.generateInviteCode(1L);
+        assertEquals(500, result.getCode());
+        assertEquals("用户没有家庭", result.getMessage());
+    }
+
+    // ==================== guideBindFamily ====================
+
+    @Test
+    public void guideBindFamily_Success() {
+        Map<String, String> params = new HashMap<>();
+        params.put("inviteCode", "ABCD1234");
+
+        User guide = mockUser(2L, 200L, "teacher");
+        guide.setTeacherFamilyIds("");
+        Family family = mockFamily(100L, "ABCD1234");
+        family.setId(100L);
+
+        when(userMapper.selectById(2L)).thenReturn(guide);
+        when(familyMapper.selectOne(any())).thenReturn(family);
+
+        Result<String> result = controller.guideBindFamily(2L, params);
+        assertEquals(200, result.getCode());
+        assertEquals("绑定成功", result.getData());
+    }
+
+    @Test
+    public void guideBindFamily_EmptyInviteCode() {
+        Map<String, String> params = new HashMap<>();
+        params.put("inviteCode", "");
+
+        Result<String> result = controller.guideBindFamily(1L, params);
+        assertEquals(500, result.getCode());
+        assertEquals("邀请码不能为空", result.getMessage());
+    }
+
+    @Test
+    public void guideBindFamily_NotTeacher() {
+        Map<String, String> params = new HashMap<>();
+        params.put("inviteCode", "ABCD1234");
+
+        User parent = mockUser(1L, 100L, "parent");
+        when(userMapper.selectById(1L)).thenReturn(parent);
+
+        Result<String> result = controller.guideBindFamily(1L, params);
+        assertEquals(500, result.getCode());
+        assertEquals("只有成长规划师才能使用此功能", result.getMessage());
+    }
+
+    @Test
+    public void guideBindFamily_InvalidCode() {
+        Map<String, String> params = new HashMap<>();
+        params.put("inviteCode", "INVALID");
+
+        User guide = mockUser(2L, 200L, "teacher");
+        when(userMapper.selectById(2L)).thenReturn(guide);
+        when(familyMapper.selectOne(any())).thenReturn(null);
+
+        Result<String> result = controller.guideBindFamily(2L, params);
+        assertEquals(500, result.getCode());
+        assertEquals("邀请码无效", result.getMessage());
+    }
+}

+ 148 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/StatsControllerTest.java

@@ -0,0 +1,148 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.stats.StatsController;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.TaskStatsService;
+import org.junit.jupiter.api.Test;
+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.when;
+
+@SpringBootTest
+public class StatsControllerTest {
+
+    @Resource
+    private StatsController controller;
+
+    @MockBean
+    private TaskStatsService taskStatsService;
+
+    @MockBean
+    private UserMapper userMapper;
+
+    @MockBean
+    private com.etotem.cfc.mapper.ChildMapper childMapper;
+
+    private User mockParentUser(Long id, Long familyId) {
+        User user = new User();
+        user.setId(id);
+        user.setFamilyId(familyId);
+        user.setRole("parent");
+        user.setNickname("家长用户");
+        return user;
+    }
+
+    // ==================== childCompletion ====================
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void childCompletion_Success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+        params.put("startDate", "2026-06-01");
+        params.put("endDate", "2026-06-09");
+
+        User user = mockParentUser(1L, 100L);
+        when(userMapper.selectById(1L)).thenReturn(user);
+
+        Map<String, Object> expectedStats = new LinkedHashMap<>();
+        expectedStats.put("totalTasks", 10);
+        expectedStats.put("completedTasks", 7);
+        expectedStats.put("pendingTasks", 3);
+        expectedStats.put("overdueTasks", 1);
+        expectedStats.put("completionRate", 70.0);
+
+        when(taskStatsService.getChildStats(any(), any(), any())).thenReturn(expectedStats);
+
+        Result<Map<String, Object>> result = controller.childCompletion(1L, params);
+        assertEquals(200, result.getCode());
+        assertEquals(7, result.getData().get("completedTasks"));
+        assertEquals(70.0, result.getData().get("completionRate"));
+    }
+
+    @Test
+    public void childCompletion_UserNotFound() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+
+        when(userMapper.selectById(999L)).thenReturn(null);
+
+        Result<Map<String, Object>> result = controller.childCompletion(999L, params);
+        assertEquals(500, result.getCode());
+        assertEquals("用户不存在", result.getMessage());
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void childCompletion_NoDates() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+
+        User user = mockParentUser(1L, 100L);
+        when(userMapper.selectById(1L)).thenReturn(user);
+
+        Map<String, Object> expectedStats = new LinkedHashMap<>();
+        expectedStats.put("totalTasks", 5);
+        expectedStats.put("completedTasks", 3);
+        expectedStats.put("completionRate", 60.0);
+
+        when(taskStatsService.getChildStats(any(), isNull(), isNull())).thenReturn(expectedStats);
+
+        Result<Map<String, Object>> result = controller.childCompletion(1L, params);
+        assertEquals(200, result.getCode());
+        assertEquals(3, result.getData().get("completedTasks"));
+    }
+
+    // ==================== familyCompletion ====================
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void familyCompletion_Success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("startDate", "2026-06-01");
+        params.put("endDate", "2026-06-09");
+
+        User user = mockParentUser(1L, 100L);
+        when(userMapper.selectById(1L)).thenReturn(user);
+
+        Map<String, Object> expectedStats = new LinkedHashMap<>();
+        expectedStats.put("totalTasks", 20);
+        expectedStats.put("completedTasks", 15);
+        expectedStats.put("completionRate", 75.0);
+
+        when(taskStatsService.getFamilyStats(eq(100L), any(), any())).thenReturn(expectedStats);
+
+        Result<Map<String, Object>> result = controller.familyCompletion(1L, params);
+        assertEquals(200, result.getCode());
+        assertEquals(15, result.getData().get("completedTasks"));
+    }
+
+    @Test
+    public void familyCompletion_UserNotFound() {
+        Map<String, Object> params = new HashMap<>();
+        when(userMapper.selectById(999L)).thenReturn(null);
+
+        Result<Map<String, Object>> result = controller.familyCompletion(999L, params);
+        assertEquals(500, result.getCode());
+        assertEquals("用户不存在", result.getMessage());
+    }
+
+    @Test
+    public void familyCompletion_NoFamily() {
+        Map<String, Object> params = new HashMap<>();
+        User user = mockParentUser(1L, null);
+        when(userMapper.selectById(1L)).thenReturn(user);
+
+        Result<Map<String, Object>> result = controller.familyCompletion(1L, params);
+        assertEquals(500, result.getCode());
+        assertEquals("未关联家庭", result.getMessage());
+    }
+}

+ 345 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/TaskControllerTest.java

@@ -0,0 +1,345 @@
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.task.TaskController;
+import com.etotem.cfc.dto.CompleteTaskDTO;
+import com.etotem.cfc.dto.CreateTaskDTO;
+import com.etotem.cfc.dto.TaskReviewDTO;
+import com.etotem.cfc.entity.MiniGame;
+import com.etotem.cfc.entity.Task;
+import com.etotem.cfc.service.MiniGameService;
+import com.etotem.cfc.service.TaskService;
+import org.junit.jupiter.api.Test;
+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.when;
+
+@SpringBootTest
+public class TaskControllerTest {
+
+    @Resource
+    private TaskController controller;
+
+    @MockBean
+    private TaskService taskService;
+
+    @MockBean
+    private MiniGameService miniGameService;
+
+    private Task mockTask(Long id, String status, Long childId) {
+        Task task = new Task();
+        task.setId(id);
+        task.setTitle("测试任务");
+        task.setDescription("任务描述");
+        task.setPoints(5);
+        task.setStatus(status);
+        task.setChildId(childId);
+        task.setFamilyId(100L);
+        task.setCreatorId(1L);
+        task.setExecutorType("child");
+        return task;
+    }
+
+    private MiniGame mockMiniGame(Long id, String code, String name) {
+        MiniGame game = new MiniGame();
+        game.setId(id);
+        game.setGameCode(code);
+        game.setGameName(name);
+        game.setDefaultPoints(5);
+        game.setStatus(1);
+        return game;
+    }
+
+    // ==================== getMinigameOptions ====================
+
+    @Test
+    public void getMinigameOptions_Success() {
+        List<MiniGame> games = Arrays.asList(
+            mockMiniGame(1L, "schulte", "舒尔特方格"),
+            mockMiniGame(2L, "1a2b", "猜数字")
+        );
+        when(miniGameService.getEnabledGames()).thenReturn(games);
+
+        Result<List<MiniGame>> result = controller.getMinigameOptions();
+        assertEquals(200, result.getCode());
+        assertEquals(2, result.getData().size());
+    }
+
+    @Test
+    public void getMinigameOptions_Empty() {
+        when(miniGameService.getEnabledGames()).thenReturn(new ArrayList<>());
+
+        Result<List<MiniGame>> result = controller.getMinigameOptions();
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData().isEmpty());
+    }
+
+    // ==================== createTask ====================
+
+    @Test
+    public void createTask_Success() {
+        CreateTaskDTO dto = new CreateTaskDTO();
+        dto.setTitle("每日阅读");
+        dto.setDescription("阅读30分钟");
+        dto.setPoints(5);
+        dto.setChildIds(Collections.singletonList(10L));
+
+        when(taskService.createTask(1L, dto)).thenReturn(100L);
+
+        Result<Long> result = controller.createTask(1L, dto);
+        assertEquals(200, result.getCode());
+        assertEquals(100L, result.getData());
+    }
+
+    // ==================== getTodayTasks ====================
+
+    @Test
+    public void getTodayTasks_Success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+
+        List<Task> tasks = Arrays.asList(
+            mockTask(1L, "pending", 10L),
+            mockTask(2L, "pending", 10L)
+        );
+        when(taskService.getTodayTasks(10L)).thenReturn(tasks);
+
+        Result<List<Task>> result = controller.getTodayTasks(params);
+        assertEquals(200, result.getCode());
+        assertEquals(2, result.getData().size());
+    }
+
+    // ==================== completeTask ====================
+
+    @Test
+    public void completeTask_Success() {
+        CompleteTaskDTO dto = new CompleteTaskDTO();
+        dto.setChildId(10L);
+        dto.setPhotoUrl("http://example.com/photo.jpg");
+
+        Map<String, Object> completionResult = new HashMap<>();
+        completionResult.put("pointsEarned", 5);
+        completionResult.put("newBalance", 100);
+        completionResult.put("needReview", false);
+
+        when(taskService.completeTask(1L, 10L, "http://example.com/photo.jpg")).thenReturn(completionResult);
+
+        Result<Map<String, Object>> result = controller.completeTask(1L, dto);
+        assertEquals(200, result.getCode());
+        assertEquals(5, result.getData().get("pointsEarned"));
+    }
+
+    // ==================== reviewTask ====================
+
+    @Test
+    public void reviewTask_Approved() {
+        TaskReviewDTO dto = new TaskReviewDTO();
+        dto.setApproved(true);
+        dto.setComment("完成得很好");
+
+        when(taskService.reviewTask(1L, dto)).thenReturn(true);
+
+        Result<Boolean> result = controller.reviewTask(1L, dto);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void reviewTask_Rejected() {
+        TaskReviewDTO dto = new TaskReviewDTO();
+        dto.setApproved(false);
+        dto.setComment("需要重做");
+
+        when(taskService.reviewTask(1L, dto)).thenReturn(true);
+
+        Result<Boolean> result = controller.reviewTask(1L, dto);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    // ==================== getTaskHistory ====================
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void getTaskHistory_Success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+        params.put("page", 1);
+        params.put("size", 10);
+
+        Page<Task> page = new Page<>(1, 10);
+        page.setRecords(Arrays.asList(
+            mockTask(1L, "completed", 10L),
+            mockTask(2L, "completed", 10L)
+        ));
+        page.setTotal(2);
+        page.setPages(1);
+
+        when(taskService.getTaskHistory(10L, 1, 10)).thenReturn(page);
+
+        Result<Page<Task>> result = controller.getTaskHistory(params);
+        assertEquals(200, result.getCode());
+        assertEquals(2, result.getData().getTotal());
+    }
+
+    @Test
+    public void getTaskHistory_DefaultPagination() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+
+        Page<Task> page = new Page<>(1, 10);
+        page.setRecords(new ArrayList<>());
+        page.setTotal(0);
+
+        when(taskService.getTaskHistory(10L, 1, 10)).thenReturn(page);
+
+        Result<Page<Task>> result = controller.getTaskHistory(params);
+        assertEquals(200, result.getCode());
+        assertEquals(0, result.getData().getTotal());
+    }
+
+    // ==================== getPendingReviewTasks ====================
+
+    @Test
+    public void getPendingReviewTasks_Success() {
+        List<Task> pendingTasks = Collections.singletonList(
+            mockTask(1L, "completed", 10L)
+        );
+        when(taskService.getPendingReviewTasks(1L)).thenReturn(pendingTasks);
+
+        Result<List<Task>> result = controller.getPendingReviewTasks(1L);
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().size());
+    }
+
+    // ==================== getTodayParentTasks ====================
+
+    @Test
+    public void getTodayParentTasks_Success() {
+        Task parentTask = mockTask(3L, "pending", null);
+        parentTask.setExecutorType("parent");
+        parentTask.setExecutorId(1L);
+
+        when(taskService.getTodayParentTasks(1L)).thenReturn(Collections.singletonList(parentTask));
+
+        Result<List<Task>> result = controller.getTodayParentTasks(1L);
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().size());
+        assertEquals("parent", result.getData().get(0).getExecutorType());
+    }
+
+    // ==================== completeParentTask ====================
+
+    @Test
+    public void completeParentTask_Success() {
+        Map<String, Object> completionResult = new HashMap<>();
+        completionResult.put("pointsEarned", 3);
+        completionResult.put("newBalance", 50);
+
+        when(taskService.completeParentTask(1L, 1L)).thenReturn(completionResult);
+
+        Result<Map<String, Object>> result = controller.completeParentTask(1L, 1L);
+        assertEquals(200, result.getCode());
+        assertEquals(3, result.getData().get("pointsEarned"));
+    }
+
+    // ==================== deleteTask ====================
+
+    @Test
+    public void deleteTask_Success() {
+        when(taskService.deleteTask(1L, 1L)).thenReturn(true);
+
+        Result<Boolean> result = controller.deleteTask(1L, 1L);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    // ==================== completeMinigameTask ====================
+
+    @Test
+    public void completeMinigameTask_Success() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("childId", 10L);
+        body.put("completionTime", 25);
+        body.put("score", 85);
+
+        Map<String, Object> gameResult = new HashMap<>();
+        gameResult.put("pointsEarned", 7);
+        gameResult.put("newBalance", 120);
+        gameResult.put("gameName", "舒尔特方格");
+        gameResult.put("needReview", false);
+
+        when(taskService.completeMinigameTask(1L, 10L, 25, 85)).thenReturn(gameResult);
+
+        Result<Map<String, Object>> result = controller.completeMinigameTask(1L, body);
+        assertEquals(200, result.getCode());
+        assertEquals(7, result.getData().get("pointsEarned"));
+    }
+
+    @Test
+    public void completeMinigameTask_NoScore() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("childId", 10L);
+
+        Map<String, Object> gameResult = new HashMap<>();
+        gameResult.put("pointsEarned", 5);
+        gameResult.put("newBalance", 100);
+
+        when(taskService.completeMinigameTask(eq(1L), eq(10L), isNull(), isNull())).thenReturn(gameResult);
+
+        Result<Map<String, Object>> result = controller.completeMinigameTask(1L, body);
+        assertEquals(200, result.getCode());
+    }
+
+    // ==================== batchComplete ====================
+
+    @Test
+    public void batchComplete_Success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("taskIds", Arrays.asList(1L, 2L, 3L));
+
+        // taskService.batchComplete is void, no return to mock
+        Result<Boolean> result = controller.batchComplete(params, 1L);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void batchComplete_EmptyIds() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("taskIds", new ArrayList<Long>());
+
+        Result<Boolean> result = controller.batchComplete(params, 1L);
+        assertEquals(500, result.getCode());
+        assertEquals("taskIds不能为空", result.getMessage());
+    }
+
+    // ==================== batchDelete ====================
+
+    @Test
+    public void batchDelete_Success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("taskIds", Arrays.asList(1L, 2L));
+
+        Result<Boolean> result = controller.batchDelete(params, 1L);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void batchDelete_EmptyIds() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("taskIds", new ArrayList<Long>());
+
+        Result<Boolean> result = controller.batchDelete(params, 1L);
+        assertEquals(500, result.getCode());
+        assertEquals("taskIds不能为空", result.getMessage());
+    }
+}

+ 360 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/WishControllerTest.java

@@ -0,0 +1,360 @@
+package com.etotem.cfc.controller;
+
+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 org.junit.jupiter.api.Test;
+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.when;
+
+@SpringBootTest
+public class WishControllerTest {
+
+    @Resource
+    private WishController controller;
+
+    @MockBean
+    private WishService wishService;
+
+    private WishDTO mockWishDTO(Long id, String status) {
+        WishDTO dto = new WishDTO();
+        dto.setId(id);
+        dto.setTitle("测试心愿");
+        dto.setDescription("心愿描述");
+        dto.setStatus(status);
+        dto.setStatusText("待定价");
+        dto.setChildId(10L);
+        dto.setFamilyId(100L);
+        dto.setChildName("测试孩子");
+        dto.setCreatedBy(1L);
+        dto.setCreatorType("child");
+        dto.setCreatedAt("2026-06-09 10:00:00");
+        return dto;
+    }
+
+    // ==================== createWish ====================
+
+    @Test
+    public void createWish_ByChild() {
+        CreateWishDTO dto = new CreateWishDTO();
+        dto.setTitle("新游戏机");
+        dto.setDescription("想要一个游戏机");
+
+        when(wishService.createByChild(1L, dto)).thenReturn(100L);
+
+        Result<Long> result = controller.createWish(1L, "child", dto);
+        assertEquals(200, result.getCode());
+        assertEquals(100L, result.getData());
+    }
+
+    @Test
+    public void createWish_ByParent() {
+        CreateWishDTO dto = new CreateWishDTO();
+        dto.setTitle("新书包");
+        dto.setChildId(10L);
+        dto.setPointsRequired(50);
+
+        when(wishService.createByParent(1L, dto)).thenReturn(101L);
+
+        Result<Long> result = controller.createWish(1L, "parent", dto);
+        assertEquals(200, result.getCode());
+        assertEquals(101L, result.getData());
+    }
+
+    @Test
+    public void createWish_Exception() {
+        CreateWishDTO dto = new CreateWishDTO();
+        dto.setTitle("");
+
+        when(wishService.createByChild(1L, dto))
+            .thenThrow(new RuntimeException("创建心愿失败"));
+
+        Result<Long> result = controller.createWish(1L, "child", dto);
+        assertEquals(500, result.getCode());
+    }
+
+    // ==================== setPrice ====================
+
+    @Test
+    public void setPrice_Success() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("pointsRequired", 100);
+
+        when(wishService.setPrice(1L, 1L, 100)).thenReturn(true);
+
+        Result<Boolean> result = controller.setPrice(1L, 1L, body);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void setPrice_InvalidPoints() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("pointsRequired", 0);
+
+        Result<Boolean> result = controller.setPrice(1L, 1L, body);
+        assertEquals(500, result.getCode());
+        assertEquals("请输入有效的积分值", result.getMessage());
+    }
+
+    @Test
+    public void setPrice_ServiceFailed() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("pointsRequired", 100);
+
+        when(wishService.setPrice(1L, 1L, 100)).thenReturn(false);
+
+        Result<Boolean> result = controller.setPrice(1L, 1L, body);
+        assertEquals(500, result.getCode());
+        assertEquals("定价失败,请检查心愿状态", result.getMessage());
+    }
+
+    // ==================== rejectWish ====================
+
+    @Test
+    public void rejectWish_Success() {
+        Map<String, String> body = new HashMap<>();
+        body.put("reason", "积分不够");
+
+        when(wishService.rejectWish(1L, 1L, "积分不够")).thenReturn(true);
+
+        Result<Boolean> result = controller.rejectWish(1L, 1L, body);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void rejectWish_Failed() {
+        Map<String, String> body = new HashMap<>();
+        body.put("reason", "不合理");
+
+        when(wishService.rejectWish(1L, 1L, "不合理")).thenReturn(false);
+
+        Result<Boolean> result = controller.rejectWish(1L, 1L, body);
+        assertEquals(500, result.getCode());
+        assertEquals("拒绝失败,请检查心愿状态", result.getMessage());
+    }
+
+    // ==================== requestExchange ====================
+
+    @Test
+    public void requestExchange_Success() {
+        when(wishService.requestExchange(1L, 10L)).thenReturn(true);
+
+        Result<Boolean> result = controller.requestExchange(1L, 10L);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void requestExchange_Failed() {
+        when(wishService.requestExchange(1L, 10L)).thenReturn(false);
+
+        Result<Boolean> result = controller.requestExchange(1L, 10L);
+        assertEquals(500, result.getCode());
+        assertEquals("申请兑换失败,请检查心愿状态", result.getMessage());
+    }
+
+    // ==================== approveExchange ====================
+
+    @Test
+    public void approveExchange_Approved() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("approved", true);
+
+        when(wishService.approveExchange(1L, 1L, true)).thenReturn(true);
+
+        Result<Boolean> result = controller.approveExchange(1L, 1L, body);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void approveExchange_Rejected() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("approved", false);
+
+        when(wishService.approveExchange(1L, 1L, false)).thenReturn(true);
+
+        Result<Boolean> result = controller.approveExchange(1L, 1L, body);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void approveExchange_MissingDecision() {
+        Map<String, Object> body = new HashMap<>();
+
+        Result<Boolean> result = controller.approveExchange(1L, 1L, body);
+        assertEquals(500, result.getCode());
+        assertEquals("请指定审批结果", result.getMessage());
+    }
+
+    // ==================== markFulfilled ====================
+
+    @Test
+    public void markFulfilled_Success() {
+        when(wishService.markFulfilled(1L, 1L)).thenReturn(true);
+
+        Result<Boolean> result = controller.markFulfilled(1L, 1L);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void markFulfilled_Failed() {
+        when(wishService.markFulfilled(1L, 1L)).thenReturn(false);
+
+        Result<Boolean> result = controller.markFulfilled(1L, 1L);
+        assertEquals(500, result.getCode());
+        assertEquals("标记失败,请检查心愿状态", result.getMessage());
+    }
+
+    // ==================== confirmReceived ====================
+
+    @Test
+    public void confirmReceived_Success() {
+        when(wishService.confirmReceived(1L, 10L)).thenReturn(true);
+
+        Result<Boolean> result = controller.confirmReceived(1L, 10L);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void confirmReceived_Failed() {
+        when(wishService.confirmReceived(1L, 10L)).thenReturn(false);
+
+        Result<Boolean> result = controller.confirmReceived(1L, 10L);
+        assertEquals(500, result.getCode());
+        assertEquals("确认失败,请检查心愿状态", result.getMessage());
+    }
+
+    // ==================== cancelWish ====================
+
+    @Test
+    public void cancelWish_Success() {
+        Map<String, String> body = new HashMap<>();
+        body.put("reason", "不想要了");
+
+        when(wishService.cancelWish(1L, 1L, "不想要了")).thenReturn(true);
+
+        Result<Boolean> result = controller.cancelWish(1L, 1L, body);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData());
+    }
+
+    @Test
+    public void cancelWish_Failed() {
+        Map<String, String> body = new HashMap<>();
+        body.put("reason", "已过期");
+
+        when(wishService.cancelWish(1L, 1L, "已过期")).thenReturn(false);
+
+        Result<Boolean> result = controller.cancelWish(1L, 1L, body);
+        assertEquals(500, result.getCode());
+        assertEquals("取消失败,请检查心愿状态", result.getMessage());
+    }
+
+    // ==================== getWishes ====================
+
+    @Test
+    public void getWishes_ChildView() {
+        List<WishDTO> wishes = Arrays.asList(
+            mockWishDTO(1L, "active"),
+            mockWishDTO(2L, "active")
+        );
+        when(wishService.getChildWishes(10L)).thenReturn(wishes);
+
+        Result<List<WishDTO>> result = controller.getWishes(10L, "child", null, null, null);
+        assertEquals(200, result.getCode());
+        assertEquals(2, result.getData().size());
+    }
+
+    @Test
+    public void getWishes_ParentViewByChild() {
+        List<WishDTO> wishes = Collections.singletonList(mockWishDTO(1L, "active"));
+        when(wishService.getChildWishes(10L)).thenReturn(wishes);
+
+        Result<List<WishDTO>> result = controller.getWishes(1L, "parent", 10L, null, null);
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().size());
+    }
+
+    @Test
+    public void getWishes_ParentViewByFamily() {
+        List<WishDTO> wishes = Arrays.asList(
+            mockWishDTO(1L, "active"),
+            mockWishDTO(2L, "draft")
+        );
+        when(wishService.getFamilyWishes(100L)).thenReturn(wishes);
+
+        Result<List<WishDTO>> result = controller.getWishes(1L, "parent", null, 100L, null);
+        assertEquals(200, result.getCode());
+        assertEquals(2, result.getData().size());
+    }
+
+    @Test
+    public void getWishes_MissingCondition() {
+        Result<List<WishDTO>> result = controller.getWishes(1L, "parent", null, null, null);
+        assertEquals(500, result.getCode());
+        assertEquals("请指定查询条件(childId或familyId)", result.getMessage());
+    }
+
+    // ==================== getPendingWishes ====================
+
+    @Test
+    public void getPendingWishes_Success() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("familyId", 100L);
+
+        List<WishDTO> pending = Arrays.asList(
+            mockWishDTO(3L, "draft"),
+            mockWishDTO(4L, "pending_exchange")
+        );
+        when(wishService.getParentPendingWishes(100L)).thenReturn(pending);
+
+        Result<List<WishDTO>> result = controller.getPendingWishes(1L, body);
+        assertEquals(200, result.getCode());
+        assertEquals(2, result.getData().size());
+    }
+
+    @Test
+    public void getPendingWishes_NoFamilyId() {
+        Map<String, Object> body = new HashMap<>();
+
+        Result<List<WishDTO>> result = controller.getPendingWishes(1L, body);
+        assertEquals(500, result.getCode());
+        assertEquals("请指定家庭ID", result.getMessage());
+    }
+
+    // ==================== getWishById ====================
+
+    @Test
+    public void getWishById_Success() {
+        WishDTO wish = mockWishDTO(1L, "active");
+        when(wishService.getWishById(1L)).thenReturn(wish);
+
+        Result<WishDTO> result = controller.getWishById(1L);
+        assertEquals(200, result.getCode());
+        assertEquals(1L, result.getData().getId());
+    }
+
+    @Test
+    public void getWishById_NotFound() {
+        when(wishService.getWishById(999L)).thenReturn(null);
+
+        Result<WishDTO> result = controller.getWishById(999L);
+        assertEquals(500, result.getCode());
+        assertEquals("心愿不存在", result.getMessage());
+    }
+}

+ 4 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/GuideApplicationServiceTest.java

@@ -3,6 +3,7 @@ package com.etotem.cfc.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.entity.GuideApplication;
 import com.etotem.cfc.mapper.GuideApplicationMapper;
+import com.etotem.cfc.mapper.GuideApplicationRecordMapper;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.mock.mockito.MockBean;
@@ -23,6 +24,9 @@ public class GuideApplicationServiceTest {
     @MockBean
     private GuideApplicationMapper guideApplicationMapper;
 
+    @MockBean
+    private GuideApplicationRecordMapper guideApplicationRecordMapper;
+
     private GuideApplication createApplication(Long id, Long userId, String type, String status) {
         GuideApplication app = new GuideApplication();
         app.setId(id);

+ 12 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/TeacherLoginTest.java

@@ -1,12 +1,15 @@
 package com.etotem.cfc.service;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import javax.annotation.Resource;
 
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.UserMapper;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.context.SpringBootTest;
 
+import java.util.Arrays;
 import java.util.Date;
 
 import static org.junit.jupiter.api.Assertions.*;
@@ -20,6 +23,15 @@ public class TeacherLoginTest {
     @Resource
     private UserMapper userMapper;
 
+    @AfterEach
+    public void cleanup() {
+        userMapper.delete(new LambdaQueryWrapper<User>()
+            .in(User::getPhone, Arrays.asList(
+                "13800000001", "13800000002", "13800000003",
+                "13800000004", "13800000005"))
+        );
+    }
+
     @Test
     public void testTeacherRoleLogin() {
         User teacher = new User();

+ 135 - 4
docs/系统测试/test-inventory.md

@@ -13,12 +13,17 @@
 | 团队管理 | GuideTeamControllerTest | 8 | P0 | ✅ 完善 |
 | 家庭任务管理 | GuideFamilyTaskControllerTest | 5 | P0-P1 | ✅ 扩充 |
 | 订单管理 | GuideOrderControllerTest | 5 | P0-P1 | ✅ 新增 |
-| 申请审核 | GuideApplicationServiceTest | 5 | P1 | ✅ 新增 |
+| 申请审核 | GuideApplicationServiceTest | 8 | P1 | ✅ 完善 |
 | 教师登录 | TeacherLoginTest | 4 | P0 | ✅ 完善 |
 | 集成测试 | GuideModuleIntegrationTest | 4 | P0 | ✅ 完善 |
-| 小游戏任务 | TaskServiceMinigameTest | - | P1 | 已有 |
-| 小游戏服务 | MiniGameServiceTest | - | P1 | 已有 |
-| **合计** | **14 文件** | **~82** | | |
+| 小游戏任务 | TaskServiceMinigameTest | 5 | P1 | 已有 |
+| 小游戏服务 | MiniGameServiceTest | 5 | P1 | 已有 |
+| 认证模块 | AuthControllerTest | 39 | P0 | ✅ 新增 |
+| 心愿模块 | WishControllerTest | 27 | P0 | ✅ 新增 |
+| 任务模块 | TaskControllerTest | 19 | P0 | ✅ 新增 |
+| 家庭模块 | FamilyControllerTest | 10 | P1 | ✅ 新增 |
+| 统计模块 | StatsControllerTest | 6 | P1 | ✅ 新增 |
+| **合计** | **24 文件** | **204** | **全通过** | ✅ |
 
 ## 需求 vs 测试映射
 
@@ -122,6 +127,132 @@
 | 审批拒绝 | POST /api/admin/guide/applications/{id}/reject | testRejectApplication | GuideApplicationServiceTest |
 | 审批不存在 | POST /api/admin/guide/applications/{id}/approve | testApprove_NotFound | GuideApplicationServiceTest |
 
+### 认证模块 (AuthController) — 新增 P0
+
+| 需求 | API | 测试用例 | 文件 |
+|------|-----|---------|------|
+| 发送验证码(成功) | POST /api/auth/send-code | sendCode_Success | AuthControllerTest |
+| 发送验证码(空手机号) | POST /api/auth/send-code | sendCode_EmptyPhone | AuthControllerTest |
+| 发送验证码(格式错误) | POST /api/auth/send-code | sendCode_InvalidPhone | AuthControllerTest |
+| 手机验证码登录(成功) | POST /api/auth/phone-login | phoneLogin_Success | AuthControllerTest |
+| 手机验证码登录(参数为空) | POST /api/auth/phone-login | phoneLogin_EmptyParams | AuthControllerTest |
+| 手机验证码登录(验证码错误) | POST /api/auth/phone-login | phoneLogin_WrongCode | AuthControllerTest |
+| 手机验证码登录(异常) | POST /api/auth/phone-login | phoneLogin_UserServiceException | AuthControllerTest |
+| 微信手机号登录(成功) | POST /api/auth/wechat-phone-login | wechatPhoneLogin_Success | AuthControllerTest |
+| 微信手机号登录(参数为空) | POST /api/auth/wechat-phone-login | wechatPhoneLogin_EmptyParams | AuthControllerTest |
+| 微信code登录(成功) | POST /api/auth/wechat-login | wechatLogin_Success | AuthControllerTest |
+| 微信code登录(异常) | POST /api/auth/wechat-login | wechatLogin_Exception | AuthControllerTest |
+| 静默登录(成功) | POST /api/auth/silent-login | silentLogin_Success | AuthControllerTest |
+| 静默登录(无code) | POST /api/auth/silent-login | silentLogin_NoCode | AuthControllerTest |
+| openid自动登录(成功) | POST /api/auth/auto-login | autoLogin_Success | AuthControllerTest |
+| openid自动登录(用户不存在) | POST /api/auth/auto-login | autoLogin_UserNotFound | AuthControllerTest |
+| openid自动登录(无openid) | POST /api/auth/auto-login | autoLogin_NoOpenid | AuthControllerTest |
+| 手机+身份证注册(成功) | POST /api/auth/register-with-idcard | registerWithIdCard_Success | AuthControllerTest |
+| 手机+身份证注册(空手机号) | POST /api/auth/register-with-idcard | registerWithIdCard_EmptyPhone | AuthControllerTest |
+| 手机+身份证注册(身份证格式错) | POST /api/auth/register-with-idcard | registerWithIdCard_InvalidIdCard | AuthControllerTest |
+| 手机+身份证注册(真实姓名为空) | POST /api/auth/register-with-idcard | registerWithIdCard_EmptyRealName | AuthControllerTest |
+| 当前用户信息(成功) | POST /api/auth/info | getInfo_Success | AuthControllerTest |
+| 当前用户信息(未登录) | POST /api/auth/info | getInfo_NotLoggedIn | AuthControllerTest |
+| 设置密码(成功) | POST /api/auth/set-password | setPassword_Success | AuthControllerTest |
+| 验证密码(正确) | POST /api/auth/verify-password | verifyPassword_Correct | AuthControllerTest |
+| 验证密码(错误) | POST /api/auth/verify-password | verifyPassword_Wrong | AuthControllerTest |
+| 检查手机号(已注册) | POST /api/auth/check-phone | checkPhone_Registered | AuthControllerTest |
+| 检查手机号(未注册) | POST /api/auth/check-phone | checkPhone_NotRegistered | AuthControllerTest |
+| 检查手机号(格式错误) | POST /api/auth/check-phone | checkPhone_Invalid | AuthControllerTest |
+| 检查手机号(为空) | POST /api/auth/check-phone | checkPhone_Empty | AuthControllerTest |
+| 直接注册(成功) | POST /api/auth/direct-register | directRegister_Success | AuthControllerTest |
+| 直接注册(已注册) | POST /api/auth/direct-register | directRegister_AlreadyRegistered | AuthControllerTest |
+| 直接注册(格式错误) | POST /api/auth/direct-register | directRegister_InvalidPhone | AuthControllerTest |
+| 邀请码注册(成功) | POST /api/auth/register-with-invite | registerWithInvite_Success | AuthControllerTest |
+| 邀请码注册(空手机号) | POST /api/auth/register-with-invite | registerWithInvite_EmptyPhone | AuthControllerTest |
+| 邀请码注册(空邀请码) | POST /api/auth/register-with-invite | registerWithInvite_EmptyInviteCode | AuthControllerTest |
+| 邀请码注册(验证码错误) | POST /api/auth/register-with-invite | registerWithInvite_WrongCode | AuthControllerTest |
+| 邀请码注册(手机已注册) | POST /api/auth/register-with-invite | registerWithInvite_AlreadyRegistered | AuthControllerTest |
+| 切换角色(成功) | POST /api/auth/switch-role | switchRole_Success | AuthControllerTest |
+| 切换角色(未登录) | POST /api/auth/switch-role | switchRole_NotLoggedIn | AuthControllerTest |
+
+### 心愿模块 (WishController) — 新增 P0
+
+| 需求 | API | 测试用例 | 文件 |
+|------|-----|---------|------|
+| 孩子创建心愿 | POST /api/wishes/create | createWish_ByChild | WishControllerTest |
+| 家长创建心愿 | POST /api/wishes/create | createWish_ByParent | WishControllerTest |
+| 创建心愿(异常) | POST /api/wishes/create | createWish_Exception | WishControllerTest |
+| 家长定价(成功) | PUT /api/wishes/{id}/set-price | setPrice_Success | WishControllerTest |
+| 家长定价(积分无效) | PUT /api/wishes/{id}/set-price | setPrice_InvalidPoints | WishControllerTest |
+| 家长定价(服务失败) | PUT /api/wishes/{id}/set-price | setPrice_ServiceFailed | WishControllerTest |
+| 家长拒绝心愿(成功) | PUT /api/wishes/{id}/reject | rejectWish_Success | WishControllerTest |
+| 家长拒绝心愿(失败) | PUT /api/wishes/{id}/reject | rejectWish_Failed | WishControllerTest |
+| 申请兑换(成功) | POST /api/wishes/{id}/exchange | requestExchange_Success | WishControllerTest |
+| 申请兑换(失败) | POST /api/wishes/{id}/exchange | requestExchange_Failed | WishControllerTest |
+| 审批兑换(批准) | PUT /api/wishes/{id}/approve-exchange | approveExchange_Approved | WishControllerTest |
+| 审批兑换(拒绝) | PUT /api/wishes/{id}/approve-exchange | approveExchange_Rejected | WishControllerTest |
+| 审批兑换(缺审批结果) | PUT /api/wishes/{id}/approve-exchange | approveExchange_MissingDecision | WishControllerTest |
+| 标记已购买(成功) | PUT /api/wishes/{id}/fulfill | markFulfilled_Success | WishControllerTest |
+| 标记已购买(失败) | PUT /api/wishes/{id}/fulfill | markFulfilled_Failed | WishControllerTest |
+| 孩子确认收货(成功) | PUT /api/wishes/{id}/confirm | confirmReceived_Success | WishControllerTest |
+| 孩子确认收货(失败) | PUT /api/wishes/{id}/confirm | confirmReceived_Failed | WishControllerTest |
+| 取消心愿(成功) | DELETE /api/wishes/{id} | cancelWish_Success | WishControllerTest |
+| 取消心愿(失败) | DELETE /api/wishes/{id} | cancelWish_Failed | WishControllerTest |
+| 心愿列表(孩子视角) | POST /api/wishes/list | getWishes_ChildView | WishControllerTest |
+| 心愿列表(家长指定孩子) | POST /api/wishes/list | getWishes_ParentViewByChild | WishControllerTest |
+| 心愿列表(家长查看家庭) | POST /api/wishes/list | getWishes_ParentViewByFamily | WishControllerTest |
+| 心愿列表(缺少条件) | POST /api/wishes/list | getWishes_MissingCondition | WishControllerTest |
+| 待处理列表(成功) | POST /api/wishes/pending | getPendingWishes_Success | WishControllerTest |
+| 待处理列表(缺家庭ID) | POST /api/wishes/pending | getPendingWishes_NoFamilyId | WishControllerTest |
+| 心愿详情(成功) | POST /api/wishes/{id} | getWishById_Success | WishControllerTest |
+| 心愿详情(不存在) | POST /api/wishes/{id} | getWishById_NotFound | WishControllerTest |
+
+### 任务模块 (TaskController) — 新增 P0
+
+| 需求 | API | 测试用例 | 文件 |
+|------|-----|---------|------|
+| 获取小游戏列表(有数据) | POST /api/tasks/minigame-options | getMinigameOptions_Success | TaskControllerTest |
+| 获取小游戏列表(空) | POST /api/tasks/minigame-options | getMinigameOptions_Empty | TaskControllerTest |
+| 创建任务(成功) | POST /api/tasks/create | createTask_Success | TaskControllerTest |
+| 获取今日任务 | POST /api/tasks/today | getTodayTasks_Success | TaskControllerTest |
+| 完成任务 | POST /api/tasks/{id}/complete | completeTask_Success | TaskControllerTest |
+| 审核任务(通过) | POST /api/tasks/{id}/review | reviewTask_Approved | TaskControllerTest |
+| 审核任务(拒绝) | POST /api/tasks/{id}/review | reviewTask_Rejected | TaskControllerTest |
+| 任务历史(有数据) | POST /api/tasks/history | getTaskHistory_Success | TaskControllerTest |
+| 任务历史(默认分页) | POST /api/tasks/history | getTaskHistory_DefaultPagination | TaskControllerTest |
+| 待审核任务 | POST /api/tasks/pending-review | getPendingReviewTasks_Success | TaskControllerTest |
+| 家长今日任务 | POST /api/tasks/today-parent | getTodayParentTasks_Success | TaskControllerTest |
+| 家长完成任务 | POST /api/tasks/{id}/complete-parent | completeParentTask_Success | TaskControllerTest |
+| 删除任务(成功) | DELETE /api/tasks/{id} | deleteTask_Success | TaskControllerTest |
+| 完成小游戏任务(含参数) | POST /api/tasks/{id}/complete-minigame | completeMinigameTask_Success | TaskControllerTest |
+| 完成小游戏任务(无分数) | POST /api/tasks/{id}/complete-minigame | completeMinigameTask_NoScore | TaskControllerTest |
+| 批量完成(成功) | POST /api/tasks/batch-complete | batchComplete_Success | TaskControllerTest |
+| 批量完成(空ID) | POST /api/tasks/batch-complete | batchComplete_EmptyIds | TaskControllerTest |
+| 批量删除(成功) | POST /api/tasks/batch-delete | batchDelete_Success | TaskControllerTest |
+| 批量删除(空ID) | POST /api/tasks/batch-delete | batchDelete_EmptyIds | TaskControllerTest |
+
+### 家庭模块 (FamilyController) — 新增 P1
+
+| 需求 | API | 测试用例 | 文件 |
+|------|-----|---------|------|
+| 获取邀请码(已有) | POST /api/family/invite-code | getInviteCode_Success | FamilyControllerTest |
+| 获取邀请码(无家庭) | POST /api/family/invite-code | getInviteCode_UserNoFamily | FamilyControllerTest |
+| 获取邀请码(用户不存在) | POST /api/family/invite-code | getInviteCode_UserNotFound | FamilyControllerTest |
+| 获取邀请码(自动生成) | POST /api/family/invite-code | getInviteCode_GenerateIfMissing | FamilyControllerTest |
+| 生成新邀请码(成功) | POST /api/family/invite-code/generate | generateNewInviteCode_Success | FamilyControllerTest |
+| 生成新邀请码(无家庭) | POST /api/family/invite-code/generate | generateNewInviteCode_UserNoFamily | FamilyControllerTest |
+| 规划师绑定家庭(成功) | POST /api/family/guide-bind | guideBindFamily_Success | FamilyControllerTest |
+| 规划师绑定家庭(空邀请码) | POST /api/family/guide-bind | guideBindFamily_EmptyInviteCode | FamilyControllerTest |
+| 规划师绑定家庭(非规划师) | POST /api/family/guide-bind | guideBindFamily_NotTeacher | FamilyControllerTest |
+| 规划师绑定家庭(无效邀请码) | POST /api/family/guide-bind | guideBindFamily_InvalidCode | FamilyControllerTest |
+
+### 统计模块 (StatsController) — 新增 P1
+
+| 需求 | API | 测试用例 | 文件 |
+|------|-----|---------|------|
+| 孩子完成率(有数据) | POST /api/stats/child-completion | childCompletion_Success | StatsControllerTest |
+| 孩子完成率(用户不存在) | POST /api/stats/child-completion | childCompletion_UserNotFound | StatsControllerTest |
+| 孩子完成率(无日期参数) | POST /api/stats/child-completion | childCompletion_NoDates | StatsControllerTest |
+| 家庭完成率(成功) | POST /api/stats/family-completion | familyCompletion_Success | StatsControllerTest |
+| 家庭完成率(用户不存在) | POST /api/stats/family-completion | familyCompletion_UserNotFound | StatsControllerTest |
+| 家庭完成率(未关联家庭) | POST /api/stats/family-completion | familyCompletion_NoFamily | StatsControllerTest |
+
 ## 测试数据 ID 分配
 
 | 服务 | ID范围 | 说明 |

+ 75 - 0
docs/系统测试/test-report.md

@@ -0,0 +1,75 @@
+# 后端测试报告
+
+**生成日期:** 2026-06-09
+**测试环境:** Spring Boot 2.7.18, JUnit 5, Mockito, MyBatis-Plus H2
+**测试范围:** 24 个测试文件, 204 个测试用例
+
+---
+
+## 测试结果
+
+| 指标 | 数值 |
+|------|------|
+| 测试总数 | 204 |
+| 通过 | 204 |
+| 失败 | 0 |
+| 错误 | 0 |
+| 跳过 | 0 |
+| 通过率 | **100%** |
+| 构建状态 | **BUILD SUCCESS** |
+
+---
+
+## 本次修复内容
+
+| 问题 | 测试类 | 根因 | 修复方案 |
+|------|--------|------|---------|
+| 手机号唯一键冲突 | TeacherLoginTest (4用例) | 历史运行残留数据导致 `uk_phone` 重复 | 添加 `@AfterEach cleanup()` 清理测试手机号 |
+| application_id 无默认值 | GuideApplicationServiceTest (2用例) | `GuideApplicationRecordMapper` 未 Mock,`addRecord()` 真实写入 DB 时 application_id 为 NULL | 添加 `@MockBean GuideApplicationRecordMapper` |
+
+---
+
+## 测试覆盖模块
+
+| 模块 | 测试类 | 用例数 | 优先级 |
+|------|--------|--------|--------|
+| 认证模块 | AuthControllerTest | 39 | P0 |
+| 任务模块 | TaskControllerTest | 19 | P0 |
+| 心愿模块 | WishControllerTest | 27 | P0 |
+| 家庭模块 | FamilyControllerTest | 10 | P1 |
+| 统计模块 | StatsControllerTest | 6 | P1 |
+| 团队管理 | GuideTeamControllerTest | 9 | P0 |
+| 指导师套餐 | GuidePackageServiceTest | 13 | P0-P1 |
+| 层级关系 | GuideHierarchyServiceTest | 10 | P0-P1 |
+| 家庭绑定管理 | GuideFamilyServiceTest | 7 | P0 |
+| 家庭任务管理 | GuideFamilyTaskControllerTest | 14 | P0-P1 |
+| 订单管理 | GuideOrderControllerTest | 8 | P0-P1 |
+| 申请审核 | GuideApplicationServiceTest | 8 | P1 |
+| 教师登录 | TeacherLoginTest | 4 | P0 |
+| 服务类型管理 | ServiceTypeServiceTest | 4 | P0-P1 |
+| 服务内容管理 | ServiceContentServiceTest | 6 | P0-P1 |
+| 套餐模板管理 | GuidePackageTemplateServiceTest | 6 | P0 |
+| 集成测试 | GuideModuleIntegrationTest | 4 | P0 |
+| 小游戏任务 | TaskServiceMinigameTest | 5 | P1 |
+| 小游戏服务 | MiniGameServiceTest | 5 | P1 |
+
+---
+
+## 测试覆盖 API
+
+总覆盖 API 端点约 **58 个**,涵盖:
+- 认证:手机验证码/微信登录/静默登录/自动登录/注册/切换角色 / 信息查询/密码管理
+- 任务:创建/完成/审核/历史/批量操作/小游戏任务
+- 心愿:创建/定价/审批/兑换/收货/取消/列表/详情
+- 家庭:邀请码/绑定规划师
+- 统计:孩子完成率/家庭完成率
+- 规划师:团队/订单/家庭绑定/套餐/层级/申请审核/服务类型/服务内容
+
+---
+
+## 测试统计
+
+```
+Tests run: 204, Failures: 0, Errors: 0, Skipped: 0
+[INFO] BUILD SUCCESS
+```