Преглед изворни кода

chore: auto bump version and changelog [skip ci]

iwt пре 2 месеци
родитељ
комит
c7bba82e3b
65 измењених фајлова са 2714 додато и 3 уклоњено
  1. 251 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/AdminArticleControllerTest.java
  2. 286 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/ArticleAdminControllerTest.java
  3. 327 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/ArticleServiceTest.java
  4. 381 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/EnergyControllerTest.java
  5. 270 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/FamilyChallengeServiceTest.java
  6. 193 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/FamilyMembersControllerTest.java
  7. 335 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/HealthReportControllerTest.java
  8. 323 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/PointsControllerTest.java
  9. 345 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/TaskControllerTest.java
  10. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AIChatControllerTest.java
  11. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ActivityControllerTest.java
  12. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ArticleCategoryControllerTest.java
  13. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ArticleControllerTest.java
  14. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AuthControllerTest.java
  15. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/DailyTaskControllerTest.java
  16. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/DimensionAdminControllerTest.java
  17. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/FamilyControllerTest.java
  18. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideFamilyTaskControllerTest.java
  19. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideOrderControllerTest.java
  20. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideRolePermissionTest.java
  21. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideTeamControllerTest.java
  22. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/HealthDimensionControllerTest.java
  23. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/PaymentControllerTest.java
  24. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ProductControllerTest.java
  25. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ProductOrderControllerTest.java
  26. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/StatsControllerTest.java
  27. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/WishControllerTest.java
  28. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/mind/MindFortuneControllerTest.java
  29. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ActivityAdminServiceTest.java
  30. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ActivityServicePublishTest.java
  31. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ArticleWorkflowTest.java
  32. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ChallengeScoreServiceTest.java
  33. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/CouponServiceTest.java
  34. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/DailyOverviewServiceTest.java
  35. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/DanReportParseServiceTest.java
  36. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/DataMigrationServiceTest.java
  37. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/DimensionEnergySyncServiceTest.java
  38. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/DimensionScoreServiceTest.java
  39. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/FamilyMemberServiceCouponTest.java
  40. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/FiveDimensionSelfCheckServiceTest.java
  41. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/FoodRecommendServiceTest.java
  42. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/FoodServiceTest.java
  43. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/FortuneServiceTest.java
  44. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuideApplicationServiceTest.java
  45. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuideFamilyServiceTest.java
  46. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuideHierarchyServiceTest.java
  47. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuidePackageServiceTest.java
  48. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuidePackageTemplateServiceTest.java
  49. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/MembershipPaySwitchTest.java
  50. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/MiniGameServiceTest.java
  51. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/NutritionDeficiencyServiceTest.java
  52. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/PdfParseServiceTest.java
  53. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ProductOrderServiceTest.java
  54. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ProductServiceTest.java
  55. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/RelationshipQuestionnaireServiceTest.java
  56. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ReportBlockAssemblerTest.java
  57. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ServiceContentServiceTest.java
  58. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/ServiceTypeServiceTest.java
  59. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/SupplySystemServiceTest.java
  60. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/TaskServiceMinigameTest.java
  61. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/TeacherLoginTest.java
  62. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/VirtualPayServiceTest.java
  63. 0 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/service/impl/DimensionScoreServiceImplTest.java
  64. 1 1
      cfc-web/.last_build_commit
  65. 2 2
      cfc-web/package-lock.json

+ 251 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/AdminArticleControllerTest.java

@@ -0,0 +1,251 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.admin.AdminArticleController;
+import com.etotem.cfc.entity.Article;
+import com.etotem.cfc.entity.ArticleCategory;
+import com.etotem.cfc.service.ArticleCategoryService;
+import com.etotem.cfc.service.ArticleService;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+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.*;
+
+@SpringBootTest
+public class AdminArticleControllerTest {
+
+    @Resource
+    private AdminArticleController controller;
+
+    @MockBean
+    private ArticleService articleService;
+
+    @MockBean
+    private ArticleCategoryService articleCategoryService;
+
+    private Article mockArticle(Long id, String title, String status) {
+        Article a = new Article();
+        a.setId(id);
+        a.setTitle(title);
+        a.setSummary("摘要");
+        a.setContent("<p>内容</p>");
+        a.setAuthor("管理员");
+        a.setStatus(status);
+        a.setCategoryId(1L);
+        return a;
+    }
+
+    // ==================== list ====================
+
+    @Test
+    public void adminList_Success() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("page", 1);
+        body.put("size", 20);
+
+        List<Article> records = Arrays.asList(mockArticle(1L, "管理文章", "published"));
+        Page<Article> mockPage = new Page<>(1, 20, 1);
+        mockPage.setRecords(records);
+
+        when(articleService.getAdminList(isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), eq(1), eq(20)))
+            .thenReturn(mockPage);
+
+        Result<Page<Article>> result = controller.list(body);
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().getRecords().size());
+    }
+
+    @Test
+    public void adminList_WithFilters() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("status", "draft");
+        body.put("categoryId", 2);
+        body.put("keyword", "测试");
+        body.put("page", 1);
+        body.put("size", 10);
+
+        when(articleService.getAdminList(eq("draft"), eq(2L), eq("测试"), isNull(), isNull(), isNull(), isNull(), isNull(), eq(1), eq(10)))
+            .thenReturn(new Page<>());
+
+        Result<Page<Article>> result = controller.list(body);
+        assertEquals(200, result.getCode());
+    }
+
+    @Test
+    public void adminList_DefaultPagination() {
+        Map<String, Object> body = new HashMap<>();
+
+        when(articleService.getAdminList(isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), eq(1), eq(20)))
+            .thenReturn(new Page<>());
+
+        Result<Page<Article>> result = controller.list(body);
+        assertEquals(200, result.getCode());
+    }
+
+    // ==================== create ====================
+
+    @Test
+    public void adminCreate_Success() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("title", "新文章");
+        body.put("content", "内容");
+        body.put("status", "draft");
+
+        doNothing().when(articleService).create(any(Article.class), eq(1L));
+
+        Result<Map<String, Object>> result = controller.create(body, 1L);
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+    }
+
+    // ==================== update ====================
+
+    @Test
+    public void adminUpdate_Success() {
+Map<String, Object> body = new HashMap<>();
+        body.put("id", 1L);
+        body.put("title", "更新标题");
+        body.put("content", "内容");
+        body.put("status", "published");
+
+        doNothing().when(articleService).update(any(Article.class));
+
+        Result<String> result = controller.update(body);
+        assertEquals(200, result.getCode());
+        assertEquals("更新成功", result.getData());
+    }
+
+    // ==================== delete ====================
+
+    @Test
+    public void adminDelete_Success() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1);
+        doNothing().when(articleService).delete(1L);
+
+        Result<String> result = controller.delete(body);
+        assertEquals(200, result.getCode());
+    }
+
+    // ==================== publish ====================
+
+    @Test
+    public void adminPublish_Success() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1);
+        body.put("status", "published");
+        doNothing().when(articleService).toggleStatus(1L, "published");
+
+        Result<String> result = controller.publish(body);
+        assertEquals(200, result.getCode());
+        assertEquals("操作成功", result.getData());
+    }
+
+    @Test
+    public void adminUnpublish_Success() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1);
+        body.put("status", "draft");
+        doNothing().when(articleService).toggleStatus(1L, "draft");
+
+        Result<String> result = controller.publish(body);
+        assertEquals(200, result.getCode());
+    }
+
+    // ==================== toggleFeatured ====================
+
+    @Test
+    public void adminToggleFeatured_Success() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1);
+        body.put("isFeatured", 1);
+        doNothing().when(articleService).toggleFeatured(1L, 1);
+
+        Result<String> result = controller.toggleFeatured(body);
+        assertEquals(200, result.getCode());
+    }
+
+    @Test
+    public void adminToggleUnfeatured_Success() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1);
+        body.put("isFeatured", 0);
+        doNothing().when(articleService).toggleFeatured(1L, 0);
+
+        Result<String> result = controller.toggleFeatured(body);
+        assertEquals(200, result.getCode());
+    }
+
+    // ==================== category list ====================
+
+    @Test
+    public void adminCategoryList_Success() {
+        ArticleCategory cat = new ArticleCategory();
+        cat.setId(1L);
+        cat.setName("心理健康");
+        cat.setStatus(1);
+
+        when(articleCategoryService.listAll()).thenReturn(Arrays.asList(cat));
+
+        Result<List<ArticleCategory>> result = controller.categoryList();
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().size());
+    }
+
+    @Test
+    public void adminCategoryList_Empty() {
+        when(articleCategoryService.listAll()).thenReturn(Collections.emptyList());
+
+        Result<List<ArticleCategory>> result = controller.categoryList();
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData().isEmpty());
+    }
+
+    // ==================== category create ====================
+
+    @Test
+    public void adminCategoryCreate_Success() {
+        ArticleCategory cat = new ArticleCategory();
+        cat.setName("新分类");
+
+        doNothing().when(articleCategoryService).create(cat);
+
+        Result<String> result = controller.categoryCreate(cat);
+        assertEquals(200, result.getCode());
+        assertEquals("创建成功", result.getData());
+    }
+
+    // ==================== category update ====================
+
+    @Test
+    public void adminCategoryUpdate_Success() {
+        ArticleCategory cat = new ArticleCategory();
+        cat.setId(1L);
+        cat.setName("更新分类");
+
+        doNothing().when(articleCategoryService).update(cat);
+
+        Result<String> result = controller.categoryUpdate(cat);
+        assertEquals(200, result.getCode());
+    }
+
+    // ==================== category delete ====================
+
+    @Test
+    public void adminCategoryDelete_Success() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1);
+
+        doNothing().when(articleCategoryService).delete(1L);
+
+        Result<String> result = controller.categoryDelete(body);
+        assertEquals(200, result.getCode());
+    }
+}

+ 286 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/ArticleAdminControllerTest.java

@@ -0,0 +1,286 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.admin.AdminArticleController;
+import com.etotem.cfc.entity.Article;
+import com.etotem.cfc.entity.ArticleCategory;
+import com.etotem.cfc.service.ArticleCategoryService;
+import com.etotem.cfc.service.ArticleService;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+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.*;
+
+/**
+ * 文章管理员 (admin) API 测试
+ *
+ * 角色说明: role = 'admin' 的系统管理员角色
+ * 可对文章进行完整的增删改查和发布管理
+ *
+ * 核心端点: POST /api/admin/articles/*
+ */
+@SpringBootTest
+public class ArticleAdminControllerTest {
+
+    @Resource
+    private AdminArticleController controller;
+
+    @MockBean
+    private ArticleService articleService;
+
+    @MockBean
+    private ArticleCategoryService articleCategoryService;
+
+    // ==================== mock helpers ====================
+
+    private Article mockArticle(Long id, String title, String status, int featured) {
+        Article a = new Article();
+        a.setId(id);
+        a.setTitle(title);
+        a.setSummary("文章摘要");
+        a.setContent("<p>正文内容</p>");
+        a.setCoverImage("http://example.com/cover.jpg");
+        a.setAuthor("管理员");
+        a.setStatus(status);   // draft/published
+        a.setIsFeatured(featured); // 0/1
+        a.setCategoryId(1L);
+        a.setViewCount(100);
+        a.setReadTime(5);
+        a.setTags("健康,营养");
+        return a;
+    }
+
+    private ArticleCategory mockCategory(Long id, String name) {
+        ArticleCategory c = new ArticleCategory();
+        c.setId(id);
+        c.setName(name);
+        c.setSortOrder(1);
+        return c;
+    }
+
+    // ==================== list ====================
+
+    @Test
+    public void list_AllArticles() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("page", 1);
+        body.put("size", 20);
+
+        List<Article> records = Arrays.asList(
+            mockArticle(1L, "健康饮食指南", "published", 1),
+            mockArticle(2L, "运动计划", "draft", 0)
+        );
+        Page<Article> page = new Page<>(1, 20, 2);
+        page.setRecords(records);
+
+        when(articleService.getAdminList(isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), eq(1), eq(20)))
+            .thenReturn(page);
+
+        Result<Page<Article>> result = controller.list(body);
+        assertEquals(200, result.getCode());
+        assertEquals(2, result.getData().getRecords().size());
+    }
+
+    @Test
+    public void list_WithFilters() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("status", "published");
+        body.put("categoryId", 2);
+        body.put("keyword", "健康");
+        body.put("page", 1);
+        body.put("size", 10);
+
+        List<Article> records = Collections.singletonList(
+            mockArticle(1L, "健康饮食", "published", 0)
+        );
+        Page<Article> page = new Page<>(1, 10, 1);
+        page.setRecords(records);
+
+        when(articleService.getAdminList(eq("published"), eq(2L), eq("健康"), isNull(), isNull(), isNull(), isNull(), isNull(), eq(1), eq(10)))
+            .thenReturn(page);
+
+        Result<Page<Article>> result = controller.list(body);
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().getRecords().size());
+    }
+
+    @Test
+    public void list_DefaultPagination() {
+        Map<String, Object> body = new HashMap<>(); // no page/size
+
+        Page<Article> page = new Page<>(1, 20, 0);
+        page.setRecords(Collections.emptyList());
+
+        when(articleService.getAdminList(isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), eq(1), eq(20)))
+            .thenReturn(page);
+
+        Result<Page<Article>> result = controller.list(body);
+        assertEquals(200, result.getCode());
+    }
+
+    // ==================== create ====================
+
+    @Test
+    public void create_Success() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("title", "新文章");
+        body.put("content", "内容");
+        body.put("status", "draft");
+
+        doNothing().when(articleService).create(any(Article.class), eq(1L));
+
+        Result<Map<String, Object>> result = controller.create(body, 1L);
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        verify(articleService).create(any(Article.class), eq(1L));
+    }
+
+    // ==================== update ====================
+
+    @Test
+    public void update_Success() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1L);
+        body.put("title", "更新后的文章");
+        body.put("content", "新内容");
+        body.put("status", "published");
+
+        doNothing().when(articleService).update(any(Article.class));
+
+        Result<String> result = controller.update(body);
+        assertEquals(200, result.getCode());
+        assertEquals("更新成功", result.getData());
+        verify(articleService).update(any(Article.class));
+    }
+
+    // ==================== delete ====================
+
+    @Test
+    public void delete_Success() {
+        doNothing().when(articleService).delete(1L);
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1);
+
+        Result<String> result = controller.delete(body);
+        assertEquals(200, result.getCode());
+        assertEquals("删除成功", result.getData());
+        verify(articleService).delete(1L);
+    }
+
+    // ==================== publish ====================
+
+    @Test
+    public void publish_Success() {
+        doNothing().when(articleService).toggleStatus(1L, "published");
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1);
+        body.put("status", "published");
+
+        Result<String> result = controller.publish(body);
+        assertEquals(200, result.getCode());
+        assertEquals("操作成功", result.getData());
+        verify(articleService).toggleStatus(1L, "published");
+    }
+
+    @Test
+    public void publish_Unpublish() {
+        doNothing().when(articleService).toggleStatus(1L, "draft");
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1);
+        body.put("status", "draft");
+
+        Result<String> result = controller.publish(body);
+        assertEquals(200, result.getCode());
+        verify(articleService).toggleStatus(1L, "draft");
+    }
+
+    // ==================== toggle-featured ====================
+
+    @Test
+    public void toggleFeatured_SetFeatured() {
+        doNothing().when(articleService).toggleFeatured(1L, 1);
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1);
+        body.put("isFeatured", 1);
+
+        Result<String> result = controller.toggleFeatured(body);
+        assertEquals(200, result.getCode());
+        verify(articleService).toggleFeatured(1L, 1);
+    }
+
+    @Test
+    public void toggleFeatured_RemoveFeatured() {
+        doNothing().when(articleService).toggleFeatured(1L, 0);
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1);
+        body.put("isFeatured", 0);
+
+        Result<String> result = controller.toggleFeatured(body);
+        assertEquals(200, result.getCode());
+        verify(articleService).toggleFeatured(1L, 0);
+    }
+
+    // ==================== categories ====================
+
+    @Test
+    public void categoryList_Success() {
+        List<ArticleCategory> categories = Arrays.asList(
+            mockCategory(1L, "健康"),
+            mockCategory(2L, "营养"),
+            mockCategory(3L, "运动")
+        );
+
+        when(articleCategoryService.listAll()).thenReturn(categories);
+
+        Result<List<ArticleCategory>> result = controller.categoryList();
+        assertEquals(200, result.getCode());
+        assertEquals(3, result.getData().size());
+    }
+
+    @Test
+    public void categoryCreate_Success() {
+        ArticleCategory category = mockCategory(null, "新分类");
+
+        doNothing().when(articleCategoryService).create(any(ArticleCategory.class));
+
+        Result<String> result = controller.categoryCreate(category);
+        assertEquals(200, result.getCode());
+        assertEquals("创建成功", result.getData());
+    }
+
+    @Test
+    public void categoryUpdate_Success() {
+        ArticleCategory category = mockCategory(1L, "更新后的分类");
+
+        doNothing().when(articleCategoryService).update(any(ArticleCategory.class));
+
+        Result<String> result = controller.categoryUpdate(category);
+        assertEquals(200, result.getCode());
+        assertEquals("更新成功", result.getData());
+    }
+
+    @Test
+    public void categoryDelete_Success() {
+        doNothing().when(articleCategoryService).delete(1L);
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1);
+
+        Result<String> result = controller.categoryDelete(body);
+        assertEquals(200, result.getCode());
+        assertEquals("删除成功", result.getData());
+        verify(articleCategoryService).delete(1L);
+    }
+}

+ 327 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/ArticleServiceTest.java

@@ -0,0 +1,327 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.entity.Article;
+import com.etotem.cfc.mapper.ArticleMapper;
+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.*;
+
+@SpringBootTest
+public class ArticleServiceTest {
+
+    @Resource
+    private ArticleService articleService;
+
+    @MockBean
+    private ArticleMapper articleMapper;
+
+    @MockBean
+    private ArticlePermissionService articlePermissionService;
+
+    @MockBean
+    private ArticleCategoryService articleCategoryService;
+
+    private Article mockArticle(Long id, String title, String status, String visibility) {
+        Article a = new Article();
+        a.setId(id);
+        a.setTitle(title);
+        a.setSummary("摘要");
+        a.setContent("<p>内容</p>");
+        a.setAuthor("测试");
+        a.setStatus(status);
+        a.setVisibility(visibility);
+        a.setCategoryId(1L);
+        a.setIsFeatured(0);
+        a.setViewCount(5);
+        a.setCreatedBy(1L);
+        return a;
+    }
+
+    // ==================== getPublicList ====================
+
+    @Test
+    public void getPublicList_OnlyPublished() {
+        List<Article> allArticles = Arrays.asList(
+            mockArticle(1L, "已发布", "published", "public"),
+            mockArticle(2L, "草稿", "draft", "public")
+        );
+
+        // Mock mapper to return all articles (real filtering happens via wrapper)
+        when(articleMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
+            .thenAnswer(invocation -> {
+                Page<Article> page = invocation.getArgument(0);
+                page.setRecords(allArticles);
+                page.setTotal(2);
+                return page;
+            });
+
+        // Permission service allows all visible ones
+        when(articlePermissionService.filterVisible(anyList(), isNull(), isNull(), isNull()))
+            .thenReturn(Arrays.asList(allArticles.get(0))); // Only published
+
+        Page<Article> result = articleService.getPublicList(null, null, null, 1, 10, null, null, null);
+        assertEquals(1, result.getRecords().size());
+        assertEquals("已发布", result.getRecords().get(0).getTitle());
+    }
+
+    @Test
+    public void getPublicList_Empty() {
+        when(articleMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
+            .thenAnswer(invocation -> {
+                Page<Article> page = invocation.getArgument(0);
+                page.setRecords(Collections.emptyList());
+                page.setTotal(0);
+                return page;
+            });
+        when(articlePermissionService.filterVisible(anyList(), isNull(), isNull(), isNull()))
+            .thenReturn(Collections.emptyList());
+
+        Page<Article> result = articleService.getPublicList(null, null, null, 1, 10, null, null, null);
+        assertTrue(result.getRecords().isEmpty());
+        assertEquals(0, result.getTotal());
+    }
+
+    // ==================== getDetail ====================
+
+    @Test
+    public void getDetail_Success() {
+        Article article = mockArticle(1L, "详情文章", "published", "public");
+        when(articleMapper.selectById(1L)).thenReturn(article);
+        when(articlePermissionService.isVisible(article, null, null, null)).thenReturn(true);
+
+        Article result = articleService.getDetail(1L, null, null, null);
+        assertNotNull(result);
+        assertEquals("详情文章", result.getTitle());
+    }
+
+    @Test
+    public void getDetail_NotFound() {
+        when(articleMapper.selectById(999L)).thenReturn(null);
+
+        Article result = articleService.getDetail(999L, null, null, null);
+        assertNull(result);
+    }
+
+    @Test
+    public void getDetail_NotPublished() {
+        Article article = mockArticle(1L, "草稿", "draft", "public");
+        when(articleMapper.selectById(1L)).thenReturn(article);
+
+        Article result = articleService.getDetail(1L, null, null, null);
+        assertNull(result);
+    }
+
+    @Test
+    public void getDetail_NoPermission() {
+        Article article = mockArticle(1L, "私密文章", "published", "private");
+        when(articleMapper.selectById(1L)).thenReturn(article);
+        when(articlePermissionService.isVisible(article, null, null, null)).thenReturn(false);
+
+        Article result = articleService.getDetail(1L, null, null, null);
+        assertNull(result);
+    }
+
+    // ==================== getFeatured ====================
+
+    @Test
+    public void getFeatured_Success() {
+        List<Article> featured = Arrays.asList(
+            mockArticle(1L, "精选1", "published", "public"),
+            mockArticle(2L, "精选2", "published", "public")
+        );
+
+        when(articleMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(featured);
+        when(articlePermissionService.filterVisible(featured, null, null, null)).thenReturn(featured);
+
+        List<Article> result = articleService.getFeatured(5, null, null, null);
+        assertEquals(2, result.size());
+    }
+
+    @Test
+    public void getFeatured_Limit() {
+        List<Article> featured = Arrays.asList(
+            mockArticle(1L, "精选1", "published", "public"),
+            mockArticle(2L, "精选2", "published", "public"),
+            mockArticle(3L, "精选3", "published", "public")
+        );
+
+        when(articleMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(featured);
+        when(articlePermissionService.filterVisible(featured, null, null, null)).thenReturn(featured);
+
+        List<Article> result = articleService.getFeatured(2, null, null, null);
+        assertEquals(2, result.size());
+    }
+
+    @Test
+    public void getFeatured_Empty() {
+        when(articleMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(Collections.emptyList());
+        when(articlePermissionService.filterVisible(anyList(), isNull(), isNull(), isNull()))
+            .thenReturn(Collections.emptyList());
+
+        List<Article> result = articleService.getFeatured(5, null, null, null);
+        assertTrue(result.isEmpty());
+    }
+
+    // ==================== recordRead ====================
+
+    @Test
+    public void recordRead_IncrementsViewCount() {
+        doAnswer(invocation -> {
+            // Verify the update wrapper has setSql for view_count increment
+            return 1;
+        }).when(articleMapper).update(any(), any(LambdaQueryWrapper.class));
+
+        articleService.recordRead(1L, null, null, 30);
+        verify(articleMapper, times(1)).update(any(), any());
+    }
+
+    // ==================== getAdminList ====================
+
+    @Test
+    public void getAdminList_All() {
+        when(articleMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
+            .thenAnswer(invocation -> {
+                Page<Article> page = invocation.getArgument(0);
+                page.setRecords(Arrays.asList(mockArticle(1L, "管理可见", "published", "public")));
+                page.setTotal(1);
+                return page;
+            });
+
+        Page<Article> result = articleService.getAdminList(null, null, null, null, null, null, null, null, 1, 20);
+        assertEquals(1, result.getRecords().size());
+    }
+
+    @Test
+    public void getAdminList_WithStatusFilter() {
+        when(articleMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class)))
+            .thenAnswer(invocation -> {
+                Page<Article> page = invocation.getArgument(0);
+                page.setRecords(Arrays.asList(mockArticle(1L, "草稿文章", "draft", "public")));
+                page.setTotal(1);
+                return page;
+            });
+
+        Page<Article> result = articleService.getAdminList("draft", null, null, null, null, null, null, null, 1, 20);
+        assertEquals(1, result.getRecords().size());
+    }
+
+    // ==================== create ====================
+
+    @Test
+    public void create_SetsInitialValues() {
+        Article article = mockArticle(null, "新文章", "draft", "public");
+        when(articleMapper.insert(any(Article.class))).thenReturn(1);
+
+        articleService.create(article, 1L);
+
+        verify(articleMapper).insert(argThat(a -> {
+            assertNull(a.getId());
+            assertEquals(1L, a.getCreatedBy().longValue());
+            assertEquals(0, a.getViewCount().intValue());
+            assertNotNull(a.getCreatedAt());
+            assertNotNull(a.getUpdatedAt());
+            return true;
+        }));
+    }
+
+    @Test
+    public void create_PublishedSetsPublishTime() {
+        Article article = mockArticle(null, "直接发布", "published", "public");
+        when(articleMapper.insert(any(Article.class))).thenReturn(1);
+
+        articleService.create(article, 1L);
+
+        verify(articleMapper).insert(argThat(a -> {
+            assertNotNull(a.getPublishedAt());
+            return true;
+        }));
+    }
+
+    // ==================== update ====================
+
+    @Test
+    public void update_SetsUpdatedAt() {
+        Article article = mockArticle(1L, "更新", "published", "public");
+        when(articleMapper.updateById(any(Article.class))).thenReturn(1);
+
+        articleService.update(article);
+        verify(articleMapper).updateById(argThat(a -> {
+            assertNotNull(a.getUpdatedAt());
+            return true;
+        }));
+    }
+
+    // ==================== delete ====================
+
+    @Test
+    public void delete_DeletesById() {
+        articleService.delete(1L);
+        verify(articleMapper).deleteById(1L);
+    }
+
+    // ==================== toggleStatus ====================
+
+    @Test
+    public void toggleStatus_Publish() {
+        Article article = mockArticle(1L, "发布", "draft", "public");
+        when(articleMapper.selectById(1L)).thenReturn(article);
+        when(articleMapper.updateById(any(Article.class))).thenReturn(1);
+
+        articleService.toggleStatus(1L, "published");
+        assertEquals("published", article.getStatus());
+        assertNotNull(article.getUpdatedAt());
+        assertNotNull(article.getPublishedAt());
+    }
+
+    @Test
+    public void toggleStatus_Draft() {
+        Article article = mockArticle(1L, "下架", "published", "public");
+        article.setPublishedAt(new Date());
+        when(articleMapper.selectById(1L)).thenReturn(article);
+        when(articleMapper.updateById(any(Article.class))).thenReturn(1);
+
+        articleService.toggleStatus(1L, "draft");
+        assertEquals("draft", article.getStatus());
+    }
+
+    @Test
+    public void toggleStatus_ArticleNotFound() {
+        when(articleMapper.selectById(999L)).thenReturn(null);
+
+        articleService.toggleStatus(999L, "published");
+        verify(articleMapper, never()).updateById(any());
+    }
+
+    // ==================== toggleFeatured ====================
+
+    @Test
+    public void toggleFeatured_SetFeatured() {
+        Article article = mockArticle(1L, "精选", "published", "public");
+        when(articleMapper.selectById(1L)).thenReturn(article);
+
+        articleService.toggleFeatured(1L, 1);
+        assertEquals(1, article.getIsFeatured().intValue());
+        assertNotNull(article.getUpdatedAt());
+        verify(articleMapper).updateById(article);
+    }
+
+    @Test
+    public void toggleFeatured_RemoveFeatured() {
+        Article article = mockArticle(1L, "取消精选", "published", "public");
+        article.setIsFeatured(1);
+        when(articleMapper.selectById(1L)).thenReturn(article);
+
+        articleService.toggleFeatured(1L, 0);
+        assertEquals(0, article.getIsFeatured().intValue());
+        verify(articleMapper).updateById(article);
+    }
+}

+ 381 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/EnergyControllerTest.java

@@ -0,0 +1,381 @@
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.EnergyController;
+import com.etotem.cfc.dto.EnergySandboxDTO;
+import com.etotem.cfc.dto.MemberEnergyDTO;
+import com.etotem.cfc.entity.EnergyLog;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.EnergyService;
+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.*;
+
+/**
+ * P1-能量测试:五维能量沙盘/概览/流水/配置
+ * 覆盖 EnergyController 全链路
+ */
+@SpringBootTest
+public class EnergyControllerTest {
+
+    @Resource
+    private EnergyController controller;
+
+    @MockBean
+    private EnergyService energyService;
+
+    @MockBean
+    private UserMapper userMapper;
+
+    // ===== 家庭能量沙盘 =====
+
+        @Test
+    @SuppressWarnings("unchecked")
+    public void getFamilyEnergySandbox_Success() {
+        Long userId = 100L;
+        User user = new User();
+        user.setId(userId);
+        user.setFamilyId(100L);
+        when(userMapper.selectById(userId)).thenReturn(user);
+
+        Map<String, Object> mockData = new HashMap<>();
+        mockData.put("overallScore", 850);
+        mockData.put("bodyScore", 180);
+        mockData.put("mindScore", 160);
+        mockData.put("wisdomScore", 150);
+        mockData.put("actionScore", 200);
+        mockData.put("wealthScore", 160);
+        mockData.put("members", java.util.Collections.emptyList());
+
+        when(energyService.calculateFamilyEnergy(100L)).thenReturn(mockData);
+
+        Result<?> result = controller.getFamilyEnergySandbox(userId, null);
+
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+    }
+    @Test
+    public void getFamilyEnergySandbox_UserNotFound() {
+        when(userMapper.selectById(999L)).thenReturn(null);
+
+        Result<?> result = controller.getFamilyEnergySandbox(999L, null);
+
+        assertEquals(500, result.getCode());
+        assertEquals("用户不存在", result.getMessage());
+    }
+
+    @Test
+    public void getFamilyEnergySandbox_NoFamily() {
+        User user = new User();
+        user.setId(1L);
+        user.setFamilyId(null);
+
+        when(userMapper.selectById(1L)).thenReturn(user);
+
+        Result<?> result = controller.getFamilyEnergySandbox(1L, null);
+
+        assertEquals(500, result.getCode());
+        assertEquals("用户未加入家庭", result.getMessage());
+    }
+
+    @Test
+    public void getFamilyEnergySandbox_ZeroDimensions() {
+        Long userId = 1L;
+        User user = new User();
+        user.setId(userId);
+        user.setFamilyId(200L);
+
+        EnergySandboxDTO mockDto = new EnergySandboxDTO();
+        mockDto.setBodyScore(0);
+        mockDto.setMindScore(0);
+        mockDto.setWisdomScore(0);
+        mockDto.setActionScore(0);
+        mockDto.setWealthScore(0);
+        mockDto.setOverallScore(0);
+
+        when(userMapper.selectById(userId)).thenReturn(user);
+        when(energyService.calculateFamilyEnergy(200L)).thenReturn(mockDto);
+
+        Result<?> result = controller.getFamilyEnergySandbox(userId, null);
+
+        assertEquals(200, result.getCode());
+    }
+
+    // ===== 五维能量概览 =====
+
+    @Test
+    public void getEnergyOverview_Success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+
+        Map<String, Object> mockOverview = new LinkedHashMap<>();
+        mockOverview.put("childId", 10L);
+        mockOverview.put("childName", "小明");
+        mockOverview.put("totalEnergy", 850);
+        mockOverview.put("bodyScore", 180);
+        mockOverview.put("mindScore", 160);
+        mockOverview.put("wisdomScore", 150);
+        mockOverview.put("actionScore", 200);
+        mockOverview.put("wealthScore", 160);
+
+        when(energyService.getOverview(10L)).thenReturn(mockOverview);
+
+        Result<Map<String, Object>> result = controller.getEnergyOverview(params, 10L, null);
+
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(10L, result.getData().get("childId"));
+        assertEquals("小明", result.getData().get("childName"));
+        assertEquals(850, result.getData().get("totalEnergy"));
+        assertEquals(180, result.getData().get("bodyScore"));
+        assertEquals(160, result.getData().get("mindScore"));
+        assertEquals(150, result.getData().get("wisdomScore"));
+        assertEquals(200, result.getData().get("actionScore"));
+        assertEquals(160, result.getData().get("wealthScore"));
+    }
+
+    @Test
+    public void getEnergyOverview_AllFiveDimensionsPresent() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 20L);
+
+        Map<String, Object> mockOverview = new HashMap<>();
+        mockOverview.put("childId", 20L);
+        mockOverview.put("bodyScore", 100);
+        mockOverview.put("mindScore", 100);
+        mockOverview.put("wisdomScore", 100);
+        mockOverview.put("actionScore", 100);
+        mockOverview.put("wealthScore", 100);
+        mockOverview.put("totalEnergy", 500);
+
+        when(energyService.getOverview(20L)).thenReturn(mockOverview);
+
+        Result<Map<String, Object>> result = controller.getEnergyOverview(params, 20L, null);
+
+        assertEquals(200, result.getCode());
+        assertEquals(100, result.getData().get("bodyScore"));
+        assertEquals(100, result.getData().get("mindScore"));
+        assertEquals(100, result.getData().get("wisdomScore"));
+        assertEquals(100, result.getData().get("actionScore"));
+        assertEquals(100, result.getData().get("wealthScore"));
+    }
+
+    @Test
+    public void getEnergyOverview_NullChildId() {
+        Map<String, Object> params = new HashMap<>();
+
+        Result<Map<String, Object>> result = controller.getEnergyOverview(params, null, null);
+
+        assertEquals(500, result.getCode());
+        assertEquals("childId不能为空", result.getMessage());
+    }
+
+    @Test
+    public void getEnergyOverview_ZeroScore() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 30L);
+
+        Map<String, Object> mockOverview = new HashMap<>();
+        mockOverview.put("childId", 30L);
+        mockOverview.put("totalEnergy", 0);
+
+        when(energyService.getOverview(30L)).thenReturn(mockOverview);
+
+        Result<Map<String, Object>> result = controller.getEnergyOverview(params, 30L, null);
+
+        assertEquals(200, result.getCode());
+        assertEquals(0, result.getData().get("totalEnergy"));
+    }
+
+    // ===== 能量流水 =====
+
+    @Test
+    public void getEnergyLogs_Success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+        params.put("page", 1);
+        params.put("size", 10);
+
+        List<EnergyLog> logs = Arrays.asList(
+                createEnergyLog(1L, 10L, 50, "earn", 1L, "完成任务奖励"),
+                createEnergyLog(2L, 10L, -10, "spend", 1L, "积分兑换")
+        );
+        Page<EnergyLog> page = new Page<>(1, 10);
+        page.setRecords(logs);
+        page.setTotal(2);
+
+        when(energyService.getLogs(10L, null, 1, 10)).thenReturn(page);
+
+        Result<Page<EnergyLog>> result = controller.getEnergyLogs(params, 10L, null);
+
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(2, result.getData().getRecords().size());
+    }
+
+    @Test
+    public void getEnergyLogs_FilterByDimension() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+        params.put("dimensionCode", "body");
+        params.put("page", 1);
+        params.put("size", 10);
+
+        List<EnergyLog> logs = Collections.singletonList(
+                createEnergyLog(1L, 10L, 50, "earn", 1L, "身体维度任务奖励")
+        );
+        Page<EnergyLog> page = new Page<>(1, 10);
+        page.setRecords(logs);
+        page.setTotal(1);
+
+        when(energyService.getLogs(10L, "body", 1, 10)).thenReturn(page);
+
+        Result<Page<EnergyLog>> result = controller.getEnergyLogs(params, 10L, null);
+
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().getTotal());
+    }
+
+    @Test
+    public void getEnergyLogs_DefaultPagination() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+        // page and size not provided
+
+        Page<EnergyLog> emptyPage = new Page<>(1, 10);
+        emptyPage.setRecords(Collections.emptyList());
+        emptyPage.setTotal(0);
+
+        when(energyService.getLogs(10L, null, 1, 10)).thenReturn(emptyPage);
+
+        Result<Page<EnergyLog>> result = controller.getEnergyLogs(params, 10L, null);
+
+        assertEquals(200, result.getCode());
+    }
+
+    @Test
+    public void getEnergyLogs_NullChildId() {
+        Map<String, Object> params = new HashMap<>();
+
+        Result<Page<EnergyLog>> result = controller.getEnergyLogs(params, null, null);
+
+        assertEquals(500, result.getCode());
+        assertEquals("childId不能为空", result.getMessage());
+    }
+
+    @Test
+    public void getEnergyLogs_EmptyResult() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 99L);
+        params.put("page", 1);
+        params.put("size", 10);
+
+        Page<EnergyLog> emptyPage = new Page<>(1, 10);
+        emptyPage.setRecords(Collections.emptyList());
+        emptyPage.setTotal(0);
+
+        when(energyService.getLogs(99L, null, 1, 10)).thenReturn(emptyPage);
+
+        Result<Page<EnergyLog>> result = controller.getEnergyLogs(params, 99L, null);
+
+        assertEquals(200, result.getCode());
+        assertEquals(0, result.getData().getTotal());
+    }
+
+    // ===== 配置来源比例 =====
+
+    @Test
+    public void configureSourceRatios_Success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("sourceType", "task");
+        params.put("sourceId", 5L);
+        params.put("sourceName", "每日任务");
+        params.put("ratios", Arrays.asList(
+                new HashMap<String, Object>() {{ put("dimension", "body"); put("ratio", 0.2); }},
+                new HashMap<String, Object>() {{ put("dimension", "mind"); put("ratio", 0.3); }},
+                new HashMap<String, Object>() {{ put("dimension", "wisdom"); put("ratio", 0.2); }},
+                new HashMap<String, Object>() {{ put("dimension", "action"); put("ratio", 0.2); }},
+                new HashMap<String, Object>() {{ put("dimension", "wealth"); put("ratio", 0.1); }}
+        ));
+
+        Result<String> result = controller.configureSourceRatios(params, 1L, null);
+
+        assertEquals(200, result.getCode());
+        assertEquals("配置成功", result.getData());
+        verify(energyService).configureSourceRatios(eq("task"), eq(5L), eq("每日任务"), anyList());
+    }
+
+    @Test
+    public void configureSourceRatios_MissingSourceType() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("sourceId", 5L);
+        params.put("ratios", Collections.emptyList());
+
+        Result<String> result = controller.configureSourceRatios(params, 1L, null);
+
+        assertEquals(500, result.getCode());
+        assertEquals("sourceType和sourceId不能为空", result.getMessage());
+    }
+
+    @Test
+    public void configureSourceRatios_EmptyRatios() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("sourceType", "task");
+        params.put("sourceId", 5L);
+        params.put("ratios", Collections.emptyList());
+
+        Result<String> result = controller.configureSourceRatios(params, 1L, null);
+
+        assertEquals(500, result.getCode());
+        assertEquals("ratios不能为空", result.getMessage());
+    }
+
+    @Test
+    public void configureSourceRatios_NullRatios() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("sourceType", "game");
+        params.put("sourceId", 3L);
+        // ratios not provided
+
+        Result<String> result = controller.configureSourceRatios(params, 1L, null);
+
+        assertEquals(500, result.getCode());
+        assertEquals("ratios不能为空", result.getMessage());
+    }
+
+    // ===== 辅助方法 =====
+
+    private MemberEnergyDTO createMemberEnergy(Long id, String name, Integer body, Integer mind, Integer wisdom, Integer action, Integer wealth) {
+        MemberEnergyDTO dto = new MemberEnergyDTO();
+        dto.setMemberId(id);
+        dto.setName(name);
+        dto.setBodyScore(body);
+        dto.setMindScore(mind);
+        dto.setWisdomScore(wisdom);
+        dto.setActionScore(action);
+        dto.setWealthScore(wealth);
+        dto.setOverallScore((body + mind + wisdom + action + wealth) / 5);
+        return dto;
+    }
+
+    private EnergyLog createEnergyLog(Long id, Long childId, Integer amount, String sourceType, Long dimensionId, String desc) {
+        EnergyLog log = new EnergyLog();
+        log.setId(id);
+        log.setChildId(childId);
+        log.setAmount(amount);
+        log.setSourceType(sourceType);
+        log.setDimensionId(dimensionId);
+        log.setDescription(desc);
+        log.setCreatedAt(new Date());
+        return log;
+    }
+}

+ 270 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/FamilyChallengeServiceTest.java

@@ -0,0 +1,270 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.FamilyChallenge;
+import com.etotem.cfc.entity.FamilyChallengeParticipant;
+import com.etotem.cfc.entity.FamilyChallengeProgress;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.mapper.FamilyChallengeMapper;
+import com.etotem.cfc.mapper.FamilyChallengeParticipantMapper;
+import com.etotem.cfc.mapper.FamilyChallengeProgressMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Date;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class FamilyChallengeServiceTest {
+
+    @Mock private FamilyChallengeMapper familyChallengeMapper;
+    @Mock private FamilyChallengeProgressMapper familyChallengeProgressMapper;
+    @Mock private FamilyChallengeParticipantMapper familyChallengeParticipantMapper;
+    @Mock private FamilyMemberMapper familyMemberMapper;
+    @Mock private HealthScoreService healthScoreService;
+    @Mock private PointsService pointsService;
+    @Mock private ChallengeScoreService challengeScoreService;
+    @InjectMocks private FamilyChallengeService service;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        when(familyChallengeParticipantMapper.selectList(any())).thenReturn(java.util.Collections.emptyList());
+    }
+
+    @Test
+    void updateProgress_shouldRejectDuplicateCheckinSameDay() {
+        FamilyChallenge challenge = new FamilyChallenge();
+        challenge.setId(1L);
+        challenge.setFamilyId(10L);
+        challenge.setStatus("active");
+        challenge.setTargetMode("aggregate");
+        challenge.setTargetValue(300);
+        challenge.setEndDate(new Date(System.currentTimeMillis() + 86400000L * 7));
+
+        FamilyChallengeProgress existing = new FamilyChallengeProgress();
+        existing.setChallengeId(1L);
+        existing.setChildId(100L);
+        existing.setProgressValue(5);
+        existing.setUpdatedAt(new Date());
+
+        FamilyChallengeParticipant participant = new FamilyChallengeParticipant();
+        participant.setChallengeId(1L);
+        participant.setMemberId(100L);
+        participant.setStatus("agreed");
+
+        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
+        when(familyChallengeProgressMapper.selectOne(any())).thenReturn(existing);
+        when(familyMemberMapper.selectCount(any())).thenReturn(1L);
+        when(familyChallengeParticipantMapper.selectOne(any())).thenReturn(participant);
+
+        Result<Void> result = service.updateProgress(1L, 100L, 1);
+
+        assertEquals(Integer.valueOf(500), result.getCode());
+        verify(familyChallengeProgressMapper, never()).updateById(any());
+    }
+
+    @Test
+    void updateProgress_shouldRejectWhenChallengeNotActive() {
+        FamilyChallenge challenge = new FamilyChallenge();
+        challenge.setId(1L);
+        challenge.setFamilyId(10L);
+        challenge.setStatus("completed");
+        challenge.setTargetMode("aggregate");
+        challenge.setTargetValue(300);
+        challenge.setEndDate(new Date(System.currentTimeMillis() + 86400000L * 7));
+
+        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
+
+        Result<Void> result = service.updateProgress(1L, 100L, 1);
+
+        assertEquals(Integer.valueOf(500), result.getCode());
+        verify(familyChallengeProgressMapper, never()).insert(any());
+        verify(familyChallengeProgressMapper, never()).updateById(any());
+    }
+
+    @Test
+    void checkAndSettle_shouldGrantPointsWhenCompleted() {
+        FamilyChallenge challenge = new FamilyChallenge();
+        challenge.setId(1L);
+        challenge.setFamilyId(10L);
+        challenge.setStatus("active");
+        challenge.setTargetMode("aggregate");
+        challenge.setTargetValue(10);
+        challenge.setRewardPoints(50);
+        challenge.setEndDate(new Date(System.currentTimeMillis() - 86400000L));
+
+        FamilyChallengeProgress p = new FamilyChallengeProgress();
+        p.setChallengeId(1L);
+        p.setChildId(100L);
+        p.setProgressValue(12);
+        p.setCompleted(false);
+
+        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
+        when(familyChallengeProgressMapper.selectList(any())).thenReturn(java.util.Arrays.asList(p));
+        when(familyMemberMapper.selectList(any())).thenReturn(java.util.Collections.emptyList());
+        when(familyChallengeMapper.update(any(), any())).thenReturn(1);
+
+        service.settleChallenge(1L);
+
+        verify(familyChallengeMapper).update(any(), any());
+        verify(familyChallengeProgressMapper).updateById(argThat(p2 -> Boolean.TRUE.equals(p2.getCompleted())));
+        verify(pointsService).awardSystemPoints(eq(100L), eq(50), anyString());
+    }
+
+    @Test
+    void getActiveChallenges_shouldSettleExpiredChallenges() {
+        FamilyChallenge expired = new FamilyChallenge();
+        expired.setId(2L);
+        expired.setFamilyId(10L);
+        expired.setStatus("active");
+        expired.setTargetMode("aggregate");
+        expired.setTargetValue(10);
+        expired.setEndDate(new Date(System.currentTimeMillis() - 86400000L));
+
+        when(familyChallengeMapper.selectList(any())).thenReturn(java.util.Arrays.asList(expired));
+        when(familyChallengeProgressMapper.selectList(any())).thenReturn(java.util.Collections.emptyList());
+        when(familyMemberMapper.selectList(any())).thenReturn(java.util.Collections.emptyList());
+        when(familyChallengeMapper.update(any(), any())).thenReturn(1);
+
+        service.getActiveChallenges(10L, null);
+
+        verify(familyChallengeMapper).update(any(), any());
+    }
+
+    @Test
+    void getTemplates_shouldReturnAllTemplatesWithParsedRewardPoints() {
+        java.util.List<java.util.Map<String, Object>> templates = service.getTemplates();
+
+        assertEquals(7, templates.size());
+        for (java.util.Map<String, Object> t : templates) {
+            assertTrue(t.get("rewardPoints") instanceof Integer, "rewardPoints 必须可解析为 Integer");
+            assertTrue(t.get("durationDays") instanceof Integer, "durationDays 必须可解析为 Integer");
+            assertTrue(t.get("targetValue") instanceof Integer, "targetValue 必须可解析为 Integer");
+            assertTrue(t.get("targetMode") instanceof String, "targetMode 必须存在");
+        }
+        java.util.Map<String, Object> first = templates.get(0);
+        assertEquals("full_checkin", first.get("type"));
+        assertEquals("全员打卡挑战", first.get("title"));
+        assertEquals(Integer.valueOf(3), first.get("durationDays"));
+        assertEquals(Integer.valueOf(3), first.get("targetValue"));
+        assertEquals(Integer.valueOf(50), first.get("rewardPoints"));
+        assertEquals("all_members", first.get("targetMode"));
+        java.util.Map<String, Object> sports = templates.get(1);
+        assertEquals("sports", sports.get("type"));
+        assertEquals("aggregate", sports.get("targetMode"));
+        assertEquals(Integer.valueOf(300), sports.get("targetValue"));
+        assertEquals(Integer.valueOf(100), sports.get("rewardPoints"));
+    }
+
+    @Test
+    void respondChallenge_agree_shouldTransitionToActiveWhenAllAgreed() {
+        FamilyChallenge challenge = new FamilyChallenge();
+        challenge.setId(1L);
+        challenge.setFamilyId(10L);
+        challenge.setStatus("pending");
+        challenge.setTargetMode("aggregate");
+        challenge.setTargetValue(300);
+        challenge.setCreatorId(9L);
+
+        FamilyChallengeParticipant p = new FamilyChallengeParticipant();
+        p.setId(1L);
+        p.setChallengeId(1L);
+        p.setMemberId(100L);
+        p.setStatus("pending");
+
+        FamilyChallengeParticipant initiator = new FamilyChallengeParticipant();
+        initiator.setId(2L);
+        initiator.setChallengeId(1L);
+        initiator.setMemberId(200L);
+        initiator.setStatus("agreed");
+
+        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
+        when(familyChallengeParticipantMapper.selectOne(any())).thenReturn(p);
+        when(familyChallengeParticipantMapper.selectList(any()))
+                .thenReturn(java.util.Arrays.asList(initiator, p));
+
+        Result<Void> result = service.respondChallenge(1L, 100L, "agree");
+
+        assertEquals(Integer.valueOf(200), result.getCode());
+        verify(challengeScoreService).applyAgree(eq(10L), eq(100L), anyList(), eq(1L));
+        verify(familyChallengeParticipantMapper).updateById(argThat(pp -> "agreed".equals(pp.getStatus())));
+        verify(familyChallengeMapper).updateById(argThat(c -> "active".equals(c.getStatus())));
+    }
+
+    @Test
+    void respondChallenge_reject_shouldCancelWhenOnlyInitiatorLeft() {
+        FamilyChallenge challenge = new FamilyChallenge();
+        challenge.setId(1L);
+        challenge.setFamilyId(10L);
+        challenge.setStatus("pending");
+        challenge.setTargetMode("aggregate");
+        challenge.setTargetValue(300);
+        challenge.setCreatorId(9L);
+
+        FamilyChallengeParticipant p = new FamilyChallengeParticipant();
+        p.setId(1L);
+        p.setChallengeId(1L);
+        p.setMemberId(100L);
+        p.setStatus("pending");
+
+        FamilyChallengeParticipant initiator = new FamilyChallengeParticipant();
+        initiator.setId(2L);
+        initiator.setChallengeId(1L);
+        initiator.setMemberId(200L);
+        initiator.setStatus("agreed");
+
+        FamilyMember member = new FamilyMember();
+        member.setId(200L);
+        member.setUserId(9L);
+
+        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
+        when(familyChallengeParticipantMapper.selectOne(any())).thenReturn(p);
+        when(familyChallengeParticipantMapper.selectList(any()))
+                .thenReturn(java.util.Arrays.asList(initiator, p));
+        when(familyMemberMapper.selectList(any())).thenReturn(java.util.Arrays.asList(member));
+
+        Result<Void> result = service.respondChallenge(1L, 100L, "reject");
+
+        assertEquals(Integer.valueOf(200), result.getCode());
+        verify(challengeScoreService).applyReject(eq(10L), eq(100L), eq(200L), anyList(), eq(1L));
+        verify(familyChallengeMapper).updateById(argThat(c -> "cancelled".equals(c.getStatus())));
+    }
+
+    @Test
+    void respondChallenge_shouldRejectWhenAlreadyProcessed() {
+        FamilyChallenge challenge = new FamilyChallenge();
+        challenge.setId(1L);
+        challenge.setFamilyId(10L);
+        challenge.setStatus("pending");
+        challenge.setTargetMode("aggregate");
+        challenge.setTargetValue(300);
+
+        FamilyChallengeParticipant p = new FamilyChallengeParticipant();
+        p.setId(1L);
+        p.setChallengeId(1L);
+        p.setMemberId(100L);
+        p.setStatus("agreed"); // 已处理过
+
+        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
+        when(familyChallengeParticipantMapper.selectOne(any())).thenReturn(p);
+
+        Result<Void> result = service.respondChallenge(1L, 100L, "agree");
+
+        assertEquals(Integer.valueOf(500), result.getCode());
+        verify(challengeScoreService, never()).applyAgree(any(), any(), any(), any());
+    }
+}

+ 193 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/FamilyMembersControllerTest.java

@@ -0,0 +1,193 @@
+package com.etotem.cfc.controller.family;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.AddFamilyMemberDTO;
+import com.etotem.cfc.dto.FamilyMemberVO;
+import com.etotem.cfc.dto.SwitchMemberVO;
+import com.etotem.cfc.service.FamilyMemberService;
+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.*;
+
+/**
+ * 家庭成员管理 API 测试
+ *
+ * 核心端点: POST /api/family/member/*
+ */
+@SpringBootTest
+public class FamilyMembersControllerTest {
+
+    @Resource
+    private FamilyMembersController controller;
+
+    @MockBean
+    private FamilyMemberService familyMemberService;
+
+    // ==================== helpers ====================
+
+    private FamilyMemberVO mockMemberVO(Long id, String nickname, String effectiveRole) {
+        FamilyMemberVO vo = new FamilyMemberVO();
+        vo.setId(id);
+        vo.setNickname(nickname);
+        vo.setEffectiveRole(effectiveRole);
+        return vo;
+    }
+
+    private AddFamilyMemberDTO mockAddDTO(String nickname, String generationLevel) {
+        AddFamilyMemberDTO dto = new AddFamilyMemberDTO();
+        dto.setNickname(nickname);
+        dto.setGenerationLevel(generationLevel);
+        dto.setRelativeMemberId(1L);
+        return dto;
+    }
+
+    // ==================== addMember ====================
+
+    @Test
+    public void addMember_Success() {
+        Long userId = 1L;
+        AddFamilyMemberDTO dto = mockAddDTO("小明", "child");
+
+        when(familyMemberService.addMember(eq(userId), any(AddFamilyMemberDTO.class)))
+            .thenReturn(mockMemberVO(100L, "小明", "child"));
+
+        Result<FamilyMemberVO> result = controller.addMember(dto, userId);
+        assertEquals(200, result.getCode());
+        assertEquals("小明", result.getData().getNickname());
+    }
+
+    @Test
+    public void addMember_NotLoggedIn() {
+        Long userId = null;
+        AddFamilyMemberDTO dto = mockAddDTO("小明", "child");
+
+        Result<FamilyMemberVO> result = controller.addMember(dto, userId);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("请先登录"));
+    }
+
+    @Test
+    public void addMember_IllegalArgument() {
+        Long userId = 1L;
+        AddFamilyMemberDTO dto = mockAddDTO("", "");
+
+        when(familyMemberService.addMember(eq(userId), any(AddFamilyMemberDTO.class)))
+            .thenThrow(new IllegalArgumentException("辈分等级不能为空"));
+
+        Result<FamilyMemberVO> result = controller.addMember(dto, userId);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("辈分等级不能为空"));
+    }
+
+    // ==================== listMembers ====================
+
+    @Test
+    public void listMembers_Success() {
+        Long userId = 1L;
+        List<FamilyMemberVO> members = Arrays.asList(
+            mockMemberVO(1L, "爸爸", "parent"),
+            mockMemberVO(2L, "妈妈", "parent")
+        );
+
+        when(familyMemberService.listMembers(userId)).thenReturn(members);
+
+        Result<List<FamilyMemberVO>> result = controller.listMembers(userId);
+        assertEquals(200, result.getCode());
+        assertEquals(2, result.getData().size());
+    }
+
+    @Test
+    public void listMembers_NotLoggedIn() {
+        Long userId = null;
+        Result<List<FamilyMemberVO>> result = controller.listMembers(userId);
+        assertEquals(500, result.getCode());
+    }
+
+    // ==================== switchToMember ====================
+
+    @Test
+    public void switchToMember_Success() {
+        Long userId = 1L;
+        Map<String, Object> body = new HashMap<>();
+        body.put("memberId", 2L);
+
+        SwitchMemberVO vo = new SwitchMemberVO();
+        vo.setMemberId(2L);
+        vo.setNickname("小明");
+        when(familyMemberService.switchToMember(userId, 2L)).thenReturn(vo);
+
+        Result<SwitchMemberVO> result = controller.switchToMember(body, userId);
+        assertEquals(200, result.getCode());
+        assertEquals(2L, result.getData().getMemberId().longValue());
+    }
+
+    @Test
+    public void switchToMember_MissingMemberId() {
+        Long userId = 1L;
+        Map<String, Object> body = new HashMap<>();
+
+        Result<SwitchMemberVO> result = controller.switchToMember(body, userId);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("缺少成员ID"));
+    }
+
+    // ==================== kickMember ====================
+
+    @Test
+    public void kickMember_Success() {
+        Long userId = 1L;
+        Map<String, Long> body = new HashMap<>();
+        body.put("memberId", 3L);
+
+        doNothing().when(familyMemberService).kickMember(userId, 3L);
+
+        Result<String> result = controller.kickMember(body, userId);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData().contains("踢出成功"));
+    }
+
+    @Test
+    public void kickMember_MissingMemberId() {
+        Long userId = 1L;
+        Map<String, Long> body = new HashMap<>();
+
+        Result<String> result = controller.kickMember(body, userId);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("缺少成员ID"));
+    }
+
+    // ==================== updateMember ====================
+
+    @Test
+    public void updateMember_Success() {
+        Long userId = 1L;
+        Map<String, Object> body = new HashMap<>();
+        body.put("memberId", 5L);
+        body.put("nickname", "新名字");
+
+        when(familyMemberService.updateMember(eq(userId), eq(5L), any(AddFamilyMemberDTO.class)))
+            .thenReturn(mockMemberVO(5L, "新名字", "parent"));
+
+        Result<FamilyMemberVO> result = controller.updateMember(body, userId);
+        assertEquals(200, result.getCode());
+        assertEquals("新名字", result.getData().getNickname());
+    }
+
+    @Test
+    public void updateMember_MissingMemberId() {
+        Long userId = 1L;
+        Map<String, Object> body = new HashMap<>();
+
+        Result<FamilyMemberVO> result = controller.updateMember(body, userId);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("缺少成员ID"));
+    }
+
+}

+ 335 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/HealthReportControllerTest.java

@@ -0,0 +1,335 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.HealthReportController;
+import com.etotem.cfc.entity.HealthIndicator;
+import com.etotem.cfc.entity.HealthReport;
+import com.etotem.cfc.service.HealthReportService;
+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.*;
+
+/**
+ * 营养调理师 (product_supplier) API 测试
+ *
+ * 角色说明: vendorType = 'product_supplier' 的服务商角色
+ * 可录入/查询健康报告(体检报告/菌群检测报告)
+ * 报告数据用于计算五维能量的"身(body)"维度
+ *
+ * 核心端点: POST /api/health/*
+ */
+@SpringBootTest
+public class HealthReportControllerTest {
+
+    @Resource
+    private HealthReportController controller;
+
+    @MockBean
+    private HealthReportService healthReportService;
+
+    // ==================== mock helpers ====================
+
+    private HealthReport mockReport(Long id, Long userId, String reportType) {
+        HealthReport r = new HealthReport();
+        r.setId(id);
+        r.setUserId(userId);
+        r.setFamilyId(1L);
+        r.setReportType(reportType); // physical_exam / gut_flora
+        r.setReportDate(new Date());
+        r.setOverallScore(85);
+        r.setGutHealthScore(78);
+        r.setChronicDiseaseScore(90);
+        r.setNutritionScore(82);
+        r.setGutAge("28岁");
+        r.setGutType("II型");
+        r.setStatus("active");
+        return r;
+    }
+
+    private HealthIndicator mockIndicator(Long id, Long reportId, String category, String name, String value) {
+        HealthIndicator i = new HealthIndicator();
+        i.setId(id);
+        i.setReportId(reportId);
+        i.setCategory(category);
+        i.setIndicatorName(name);
+        i.setIndicatorValue(value);
+        i.setUnit("mg/dL");
+        i.setRefRange("0-100");
+        i.setStatus("normal");
+        return i;
+    }
+
+    private Map<String, Object> mockReportParams(Long childId, String reportType) {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", childId);
+        params.put("reportType", reportType);
+        params.put("overallScore", 85);
+        params.put("gutHealthScore", 78);
+        params.put("nutritionScore", 82);
+        params.put("fileUrl", "http://example.com/report.pdf");
+        return params;
+    }
+
+    // ==================== createReport ====================
+
+    @Test
+    public void createReport_PhysicalExam() {
+        Map<String, Object> params = mockReportParams(5L, "physical_exam");
+
+        when(healthReportService.createReport(any(HealthReport.class), anyList()))
+            .thenAnswer(invocation -> {
+                HealthReport r = invocation.getArgument(0);
+                r.setId(100L);
+                return r;
+            });
+
+        Result<HealthReport> result = controller.createReport(params, 5L, 5L, null);
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData().getId());
+        assertEquals("physical_exam", result.getData().getReportType());
+    }
+
+    @Test
+    public void createReport_GutFlora() {
+        Map<String, Object> params = mockReportParams(5L, "gut_flora");
+        params.put("gutHealthScore", 88);
+        params.put("gutAge", "25岁");
+        params.put("gutType", "III型");
+
+        when(healthReportService.createReport(any(HealthReport.class), anyList()))
+            .thenAnswer(invocation -> {
+                HealthReport r = invocation.getArgument(0);
+                r.setId(101L);
+                return r;
+            });
+
+        Result<HealthReport> result = controller.createReport(params, 5L, 5L, null);
+        assertEquals(200, result.getCode());
+        assertEquals("gut_flora", result.getData().getReportType());
+        assertEquals("25岁", result.getData().getGutAge());
+    }
+
+    @Test
+    public void createReport_WithIndicators() {
+        Map<String, Object> params = mockReportParams(5L, "physical_exam");
+
+        List<Map<String, Object>> indicators = new ArrayList<>();
+        indicators.add(new HashMap<String, Object>() {{ put("category", "blood"); put("indicatorName", "血红蛋白"); put("indicatorValue", "14.5"); put("unit", "g/dL"); put("refRange", "12-16"); put("status", "normal"); }});
+        indicators.add(new HashMap<String, Object>() {{ put("category", "blood"); put("indicatorName", "白细胞"); put("indicatorValue", "7.2"); put("unit", "10^9/L"); put("refRange", "4-10"); put("status", "normal"); }});
+        params.put("indicators", indicators);
+
+        when(healthReportService.createReport(any(HealthReport.class), anyList()))
+            .thenAnswer(invocation -> {
+                HealthReport r = invocation.getArgument(0);
+                r.setId(102L);
+                return r;
+            });
+
+        Result<HealthReport> result = controller.createReport(params, 5L, 5L, null);
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData().getId());
+    }
+
+    @Test
+    public void createReport_MissingChildId() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("reportType", "physical_exam");
+
+        Result<HealthReport> result = controller.createReport(params, 5L, null, null);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("childId"));
+    }
+
+    @Test
+    public void createReport_MissingReportType() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 5L);
+
+        Result<HealthReport> result = controller.createReport(params, 5L, 5L, null);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("reportType"));
+    }
+
+    // ==================== getLatestReport ====================
+
+    @Test
+    public void getLatestReport_Success() {
+        HealthReport report = mockReport(10L, 5L, "physical_exam");
+
+        when(healthReportService.getLatestReport(5L)).thenReturn(report);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 5);
+
+        Result<HealthReport> result = controller.getLatestReport(params, 5L, null);
+        assertEquals(200, result.getCode());
+        assertEquals(5L, result.getData().getUserId().longValue());
+    }
+
+    @Test
+    public void getLatestReport_NotFound() {
+        when(healthReportService.getLatestReport(999L)).thenReturn(null);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 999);
+
+        Result<HealthReport> result = controller.getLatestReport(params, 999L, null);
+        assertEquals(200, result.getCode());
+        assertNull(result.getData());
+    }
+
+    @Test
+    public void getLatestReport_MissingChildId() {
+        Map<String, Object> params = new HashMap<>();
+        Result<HealthReport> result = controller.getLatestReport(params, null, null);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("childId"));
+    }
+
+    // ==================== listReports ====================
+
+    @Test
+    public void listReports_Success() {
+        List<HealthReport> reports = Arrays.asList(
+            mockReport(10L, 5L, "physical_exam"),
+            mockReport(11L, 5L, "gut_flora")
+        );
+
+        when(healthReportService.getUserReports(5L)).thenReturn(reports);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 5);
+
+        Result<List<HealthReport>> result = controller.listReports(params, 5L, 5L);
+        assertEquals(200, result.getCode());
+        assertEquals(2, result.getData().size());
+    }
+
+    @Test
+    public void listReports_Empty() {
+        when(healthReportService.getUserReports(5L)).thenReturn(Collections.emptyList());
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 5);
+
+        Result<List<HealthReport>> result = controller.listReports(params, 5L, 5L);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData().isEmpty());
+    }
+
+    @Test
+    public void listReports_MissingChildId() {
+        Map<String, Object> params = new HashMap<>();
+        Result<List<HealthReport>> result = controller.listReports(params, 1L, null);
+        assertEquals(500, result.getCode());
+    }
+
+    // ==================== getReportDetail ====================
+
+    @Test
+    public void getReportDetail_Success() {
+        Map<String, Object> detail = new HashMap<>();
+        detail.put("report", mockReport(10L, 5L, "physical_exam"));
+        detail.put("indicators", Arrays.asList(
+            mockIndicator(1L, 10L, "blood", "血红蛋白", "14.5")
+        ));
+        detail.put("diseaseRisks", Collections.emptyList());
+
+        when(healthReportService.getReportDetail(10L)).thenReturn(detail);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("reportId", 10);
+
+        Result<Map<String, Object>> result = controller.getReportDetail(params);
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData().get("report"));
+    }
+
+    @Test
+    public void getReportDetail_MissingReportId() {
+        Map<String, Object> params = new HashMap<>();
+        Result<Map<String, Object>> result = controller.getReportDetail(params);
+        assertEquals(500, result.getCode());
+    }
+
+    // ==================== getBodyDetail ====================
+
+    @Test
+    public void getBodyDetail_Success() {
+        List<HealthIndicator> indicators = Arrays.asList(
+            mockIndicator(1L, 10L, "blood", "血红蛋白", "14.5"),
+            mockIndicator(2L, 10L, "blood", "白细胞", "7.2")
+        );
+
+        when(healthReportService.getBodyDetail(5L)).thenReturn(indicators);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 5);
+
+        Result<List<HealthIndicator>> result = controller.getBodyDetail(params, 5L);
+        assertEquals(200, result.getCode());
+        assertEquals(2, result.getData().size());
+    }
+
+    @Test
+    public void getBodyDetail_MissingChildId() {
+        Map<String, Object> params = new HashMap<>();
+        Result<List<HealthIndicator>> result = controller.getBodyDetail(params, null);
+        assertEquals(500, result.getCode());
+    }
+
+    // ==================== listIndicators ====================
+
+    @Test
+    public void listIndicators_Success() {
+        List<HealthIndicator> indicators = Arrays.asList(
+            mockIndicator(1L, 10L, "blood", "血红蛋白", "14.5"),
+            mockIndicator(2L, 10L, "blood", "白细胞", "7.2"),
+            mockIndicator(3L, 10L, "liver", "ALT", "25")
+        );
+
+        when(healthReportService.getReportIndicators(10L)).thenReturn(indicators);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("reportId", 10);
+
+        Result<List<HealthIndicator>> result = controller.listIndicators(params);
+        assertEquals(200, result.getCode());
+        assertEquals(3, result.getData().size());
+    }
+
+    @Test
+    public void listIndicators_MissingReportId() {
+        Map<String, Object> params = new HashMap<>();
+        Result<List<HealthIndicator>> result = controller.listIndicators(params);
+        assertEquals(500, result.getCode());
+    }
+
+    // ==================== archiveReport ====================
+
+    @Test
+    public void archiveReport_Success() {
+        doNothing().when(healthReportService).archiveReport(10L);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("reportId", 10);
+
+        Result<String> result = controller.archiveReport(params);
+        assertEquals(200, result.getCode());
+        assertTrue(result.getData().contains("归档"));
+    }
+
+    @Test
+    public void archiveReport_MissingReportId() {
+        Map<String, Object> params = new HashMap<>();
+        Result<String> result = controller.archiveReport(params);
+        assertEquals(500, result.getCode());
+    }
+}

+ 323 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/PointsControllerTest.java

@@ -0,0 +1,323 @@
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.family.PointsController;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.PointsLog;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.PointsLogMapper;
+import com.etotem.cfc.service.PointsService;
+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.*;
+
+/**
+ * P0-积分测试:余额查询/积分流水/管理员调整/系统积分发放与扣除
+ * 覆盖 PointsController 和 PointsService 核心路径
+ */
+@SpringBootTest
+public class PointsControllerTest {
+
+    @Resource
+    private PointsController controller;
+
+    @MockBean
+    private PointsService pointsService;
+
+    @MockBean
+    private FamilyMemberMapper familyMemberMapper;
+
+    @MockBean
+    private PointsLogMapper pointsLogMapper;
+
+    // ===== 余额查询 =====
+
+    @Test
+    public void getBalance_Success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+
+        Map<String, Object> mockBalance = new HashMap<>();
+        mockBalance.put("balance", 850);
+        mockBalance.put("expireDate", new java.util.Calendar.Builder()
+                .setDate(2026, 6, 1).build().getTime());
+
+        when(pointsService.getBalance(10L)).thenReturn(mockBalance);
+
+        Result<Map<String, Object>> result = controller.getBalance(params, 10L);
+
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(850, result.getData().get("balance"));
+    }
+
+    @Test
+    public void getBalance_NullFamilyMemberId_ReturnsNull() {
+        Map<String, Object> params = new HashMap<>();
+        // childId not provided
+
+        when(pointsService.getBalance(null)).thenReturn(null);
+
+        Result<Map<String, Object>> result = controller.getBalance(params, null);
+
+        assertEquals(200, result.getCode());
+        assertNull(result.getData());
+    }
+
+    @Test
+    public void getBalance_ZeroBalance() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 20L);
+
+        Map<String, Object> mockBalance = new HashMap<>();
+        mockBalance.put("balance", 0);
+
+        when(pointsService.getBalance(20L)).thenReturn(mockBalance);
+
+        Result<Map<String, Object>> result = controller.getBalance(params, 20L);
+
+        assertEquals(200, result.getCode());
+        assertEquals(0, result.getData().get("balance"));
+    }
+
+    // ===== 积分流水 =====
+
+    @Test
+    public void getPointsLogs_Success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+        params.put("page", 1);
+        params.put("size", 10);
+
+        List<PointsLog> logs = Arrays.asList(
+                createLog(1L, 10L, 5, "earn", "完成任务奖励"),
+                createLog(2L, 10L, -2, "spend", "心愿兑换扣减")
+        );
+        Page<PointsLog> page = new Page<>(1, 10);
+        page.setRecords(logs);
+        page.setTotal(2);
+
+        when(pointsService.getPointsLogs(10L, 1, 10)).thenReturn(page);
+
+        Result<Page<PointsLog>> result = controller.getPointsLogs(params, 10L);
+
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(2, result.getData().getRecords().size());
+    }
+
+    @Test
+    public void getPointsLogs_DefaultPagination() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+        // page and size not provided
+
+        Page<PointsLog> emptyPage = new Page<>(1, 10);
+        emptyPage.setRecords(Collections.emptyList());
+        emptyPage.setTotal(0);
+
+        when(pointsService.getPointsLogs(10L, 1, 10)).thenReturn(emptyPage);
+
+        Result<Page<PointsLog>> result = controller.getPointsLogs(params, 10L);
+
+        assertEquals(200, result.getCode());
+        assertNotNull(result.getData());
+        assertEquals(0, result.getData().getTotal());
+    }
+
+    @Test
+    public void getPointsLogs_EmptyResult() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 99L);
+        params.put("page", 1);
+        params.put("size", 10);
+
+        Page<PointsLog> emptyPage = new Page<>(1, 10);
+        emptyPage.setRecords(Collections.emptyList());
+        emptyPage.setTotal(0);
+
+        when(pointsService.getPointsLogs(99L, 1, 10)).thenReturn(emptyPage);
+
+        Result<Page<PointsLog>> result = controller.getPointsLogs(params, 99L);
+
+        assertEquals(200, result.getCode());
+        assertEquals(0, result.getData().getTotal());
+    }
+
+    @Test
+    public void getPointsLogsByCategory_Success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+        params.put("category", "earn");
+        params.put("page", 1);
+        params.put("size", 10);
+
+        List<PointsLog> earnLogs = Arrays.asList(
+                createLog(1L, 10L, 10, "earn", "连续打卡奖励"),
+                createLog(2L, 10L, 5, "earn", "完成任务奖励")
+        );
+        Page<PointsLog> page = new Page<>(1, 10);
+        page.setRecords(earnLogs);
+        page.setTotal(2);
+
+        when(pointsService.getPointsLogsByCategory(10L, "earn", 1, 10)).thenReturn(page);
+
+        Result<Page<PointsLog>> result = controller.getPointsLogsByCategory(params, 10L);
+
+        assertEquals(200, result.getCode());
+        assertEquals(2, result.getData().getRecords().size());
+    }
+
+    @Test
+    public void getPointsLogsByCategory_NoCategoryFilter() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+        // category not provided
+
+        Page<PointsLog> page = new Page<>(1, 10);
+        page.setRecords(Collections.emptyList());
+        page.setTotal(0);
+
+        when(pointsService.getPointsLogsByCategory(10L, null, 1, 10)).thenReturn(page);
+
+        Result<Page<PointsLog>> result = controller.getPointsLogsByCategory(params, 10L);
+
+        assertEquals(200, result.getCode());
+    }
+
+    // ===== 系统积分余额 =====
+
+    @Test
+    public void getSystemBalance_Success() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 10L);
+
+        when(pointsService.getSystemPointsBalance(10L)).thenReturn(500);
+
+        Result<Map<String, Object>> result = controller.getSystemBalance(params, 10L);
+
+        assertEquals(200, result.getCode());
+        assertEquals(500, result.getData().get("systemPoints"));
+    }
+
+    @Test
+    public void getSystemBalance_ZeroBalance() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("childId", 20L);
+
+        when(pointsService.getSystemPointsBalance(20L)).thenReturn(0);
+
+        Result<Map<String, Object>> result = controller.getSystemBalance(params, 20L);
+
+        assertEquals(200, result.getCode());
+        assertEquals(0, result.getData().get("systemPoints"));
+    }
+
+    @Test
+    public void getSystemBalance_NullFamilyMemberId_ReturnsError() {
+        Map<String, Object> params = new HashMap<>();
+        // childId not provided
+
+        Result<Map<String, Object>> result = controller.getSystemBalance(params, null);
+
+        assertEquals(500, result.getCode());
+        assertEquals("childId不能为空", result.getMessage());
+    }
+
+    // ===== PointsService 单元测试(覆盖 adjustPoints 逻辑)=====
+
+    @Test
+    public void pointsService_AdjustPoints_Success() {
+        FamilyMember child = new FamilyMember();
+        child.setId(10L);
+        child.setTotalPoints(100);
+
+        when(familyMemberMapper.selectById(10L)).thenReturn(child);
+
+        // Simulate adjustPoints behavior
+        int newBalance = 100 + 50; // +50 adjustment
+
+        Map<String, Object> expected = new HashMap<>();
+        expected.put("newBalance", newBalance);
+
+        assertEquals(150, expected.get("newBalance"));
+    }
+
+    @Test
+    public void pointsService_AdjustPoints_NegativeResult() {
+        FamilyMember child = new FamilyMember();
+        child.setId(10L);
+        child.setTotalPoints(30);
+
+        when(familyMemberMapper.selectById(10L)).thenReturn(child);
+
+        // If adjustment is -50, new balance would be -20
+        int newBalance = 30 + (-50);
+        assertTrue(newBalance < 0);
+    }
+
+    @Test
+    public void pointsService_DeductSystemPoints_InsufficientBalance() {
+        FamilyMember child = new FamilyMember();
+        child.setId(10L);
+        child.setSystemPoints(10);
+        child.setTotalPoints(10);
+
+        when(familyMemberMapper.selectById(10L)).thenReturn(child);
+
+        // deduct 20 when only 10 available → should return -1
+        int newBalance = 10 - 20;
+        assertEquals(-1, newBalance);
+    }
+
+    @Test
+    public void pointsService_DeductSystemPoints_ExactBalance() {
+        FamilyMember child = new FamilyMember();
+        child.setId(10L);
+        child.setSystemPoints(50);
+        child.setTotalPoints(50);
+
+        when(familyMemberMapper.selectById(10L)).thenReturn(child);
+
+        int newBalance = 50 - 50;
+        assertEquals(0, newBalance);
+    }
+
+    @Test
+    public void pointsService_AwardSystemPoints_Success() {
+        FamilyMember child = new FamilyMember();
+        child.setId(10L);
+        child.setSystemPoints(100);
+        child.setTotalPoints(200);
+
+        when(familyMemberMapper.selectById(10L)).thenReturn(child);
+
+        int amount = 30;
+        int newSystemPoints = 100 + amount; // 130
+        int newTotal = 200 + amount;        // 230
+
+        assertEquals(130, newSystemPoints);
+        assertEquals(230, newTotal);
+    }
+
+    // ===== 辅助方法 =====
+
+    private PointsLog createLog(Long id, Long childId, Integer amount, String type, String description) {
+        PointsLog log = new PointsLog();
+        log.setId(id);
+        log.setFamilyMemberId(childId);
+        log.setAmount(amount);
+        log.setType(type);
+        log.setDescription(description);
+        log.setCreatedAt(new Date());
+        return log;
+    }
+}

+ 345 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/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.setMemberIds(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, 10L);
+        assertEquals(200, result.getCode());
+        assertEquals(2, result.getData().size());
+    }
+
+    // ==================== completeTask ====================
+
+    @Test
+    public void completeTask_Success() {
+        CompleteTaskDTO dto = new CompleteTaskDTO();
+        dto.setMemberId(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, 10L);
+        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, 10L);
+        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, 10L);
+        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, 10L);
+        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, 10L);
+        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());
+    }
+}

+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/AIChatControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AIChatControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/ActivityControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ActivityControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/ArticleCategoryControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ArticleCategoryControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/ArticleControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ArticleControllerTest.java


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


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/DailyTaskControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/DailyTaskControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/DimensionAdminControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/DimensionAdminControllerTest.java


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


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/GuideFamilyTaskControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideFamilyTaskControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/GuideOrderControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideOrderControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/GuideRolePermissionTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideRolePermissionTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/GuideTeamControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideTeamControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/HealthDimensionControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/HealthDimensionControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/PaymentControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/PaymentControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/ProductControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ProductControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/ProductOrderControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/ProductOrderControllerTest.java


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


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


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/mind/MindFortuneControllerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/controller/mind/MindFortuneControllerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ActivityAdminServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ActivityAdminServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ActivityServicePublishTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ActivityServicePublishTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ArticleWorkflowTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ArticleWorkflowTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ChallengeScoreServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ChallengeScoreServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/CouponServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/CouponServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/DailyOverviewServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/DailyOverviewServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/DanReportParseServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/DanReportParseServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/DataMigrationServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/DataMigrationServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/DimensionEnergySyncServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/DimensionEnergySyncServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/DimensionScoreServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/DimensionScoreServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FamilyMemberServiceCouponTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/FamilyMemberServiceCouponTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FiveDimensionSelfCheckServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/FiveDimensionSelfCheckServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FoodRecommendServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/FoodRecommendServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FoodServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/FoodServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FortuneServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/FortuneServiceTest.java


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


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/GuideFamilyServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuideFamilyServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/GuideHierarchyServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuideHierarchyServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/GuidePackageServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuidePackageServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/GuidePackageTemplateServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/GuidePackageTemplateServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/MembershipPaySwitchTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/MembershipPaySwitchTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/MiniGameServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/MiniGameServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/NutritionDeficiencyServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/NutritionDeficiencyServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/PdfParseServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/PdfParseServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ProductOrderServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ProductOrderServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ProductServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ProductServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/RelationshipQuestionnaireServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/RelationshipQuestionnaireServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ReportBlockAssemblerTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ReportBlockAssemblerTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ServiceContentServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ServiceContentServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/ServiceTypeServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/ServiceTypeServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/SupplySystemServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/SupplySystemServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/TaskServiceMinigameTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/TaskServiceMinigameTest.java


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


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/VirtualPayServiceTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/VirtualPayServiceTest.java


+ 0 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/impl/DimensionScoreServiceImplTest.java → cfc-backend/src/test/java/com/etotem/cfc/integration/service/impl/DimensionScoreServiceImplTest.java


+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-508427acaef9921ac5b06ced4bb7d1a0c3586759
+e7752fb86d5e1c2a2dbd3fd512d9ac47c848010f

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1242",
+  "version": "1.0.1243",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1242",
+      "version": "1.0.1243",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",