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

docs: 家庭信息查看+家庭命名 实现计划

Xiaogang Liao пре 1 недеља
родитељ
комит
dde3b6c8f6
1 измењених фајлова са 855 додато и 0 уклоњено
  1. 855 0
      docs/superpowers/plans/2026-10-30-family-overview.md

+ 855 - 0
docs/superpowers/plans/2026-10-30-family-overview.md

@@ -0,0 +1,855 @@
+# 管理端家庭信息查看 + 家庭命名「XX家庭」实现计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 在管理端家庭列表增加「家庭相关信息查看」(报告/方案/任务,计数+可下钻明细),并实现家庭主账号填真实姓名后家庭名自动改为「XX家庭」。
+
+**Architecture:** 后端新增 `AdminFamilyOverviewController`(`/api/admin/families/{id}/overview` + `/tasks`)+ `FamilyAdminOverviewService`,计数用 `selectMaps+groupBy`、任务明细走分页;前端在 `Families.vue`「更多」下拉加「查看信息」→ 新建 `FamilyOverviewDialog.vue`(计数卡片+三 tab 明细)。家庭命名:`FamilyNamingSupport.buildFamilyName` 静态方法,创建家庭与更新资料两处调用,存量一次性迁移回填。
+
+**Tech Stack:** Java 17 / Spring Boot 2.7.18 / MyBatis-Plus 3.5.3.1 / Lombok / JUnit5+Mockito(`@SpringBootTest`+`@MockBean`);前端 Vue2 + Element UI 2.15.13。
+
+**Spec:** `docs/superpowers/specs/2026-10-30-family-overview-naming-design.md`
+
+## Global Constraints
+
+- 后端鉴权沿用 `@RequestAttribute("role") String role` + `if (!"admin".equals(role)) return Result.error("无权限")`(`JwtInterceptor` 已设置该属性)。
+- MyBatis-Plus 3.5.3.1:`selectCount` 返回 `Long`;**分组计数必须用 `selectMaps` + `groupBy`**(禁用 `selectCount` 做分组)。
+- 计数字段类型用 `long`(非 `int`)。
+- 报告/方案明细全量返回(上限 8/4),任务明细走独立分页接口(默认 20/页、上限 100)。
+- 方案**不返回** `plan_content` / `plan_json` / `member_ids` / `dimensions` / `review_comment`(响应体裁剪)。
+- 命名规则:`real_name` 非空 → `realName.trim() + "家庭"`;否则回退 `nickname + "的家庭"`;nickname 空 → `"我的家庭"`。
+- 命名迁移放 `DatabaseInitializer.runMigrations()` 新迁移(幂等),执行前 dry-run 报行数给主理人确认。
+- `creator_id` 为 NULL → overview 主账号字段返回空串;迁移 INNER JOIN 跳过。
+- 前端复用 `StatusTag`(`value+options` 映射式)、`StatCard`(`clickable`+`@click`)、`SectionTitle`。
+
+## Review Focus
+
+1. **非 admin 角色访问 overview/tasks** → 应返回 `Result.error("无权限")` 而非抛异常(Task 5 测试覆盖)。
+2. **不存在的 familyId** → overview 应返回 `Result.error("家庭不存在")`,tasks 返回空页而非报错(Task 5 测试覆盖)。
+3. **报告 status 全量展示**(active/deleted/archived)→ 管理端需能看到已归档报告(Task 4 服务测试断言 count 含 archived)。
+4. **任务数超单页** → 分页正确返回 `total` 与当前页(Task 4/5 测试覆盖)。
+5. **real_name 为空/全空格、nickname 为空** → 命名回退链正确(Task 1 测试覆盖)。
+6. **真实姓名变更时家庭名联动** → 仅当 realName 非空且与原值不同时改家庭名(Task 2 测试覆盖)。
+
+---
+
+### Task 1: FamilyNamingSupport 工具类(命名规则,TDD)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/util/FamilyNamingSupport.java`
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyNamingSupportTest.java`
+
+**Interfaces:**
+- Produces: `static String buildFamilyName(User user)` — 输入 `User`(有 `realName`、`nickname`),返回家庭名。
+
+- [ ] **Step 1: 写失败测试** `FamilyNamingSupportTest.java`
+
+```java
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.util.FamilyNamingSupport;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class FamilyNamingSupportTest {
+    private User user(String realName, String nickname) {
+        User u = new User();
+        u.setRealName(realName);
+        u.setNickname(nickname);
+        return u;
+    }
+    @Test void realName_优先() { assertEquals("张三家庭", FamilyNamingSupport.buildFamilyName(user("张三", "昵称"))); }
+    @Test void realName空_回退昵称() { assertEquals("昵称的家庭", FamilyNamingSupport.buildFamilyName(user("", "昵称"))); }
+    @Test void realName全空格_回退昵称() { assertEquals("昵称的家庭", FamilyNamingSupport.buildFamilyName(user("   ", "昵称"))); }
+    @Test void 都空_我的家庭() { assertEquals("我的家庭", FamilyNamingSupport.buildFamilyName(user(null, null))); }
+}
+```
+
+- [ ] **Step 2: 运行测试确认失败**
+
+Run: `cd cfc-backend && mvn -q test -Dtest=FamilyNamingSupportTest -DfailIfNoTests=false`
+Expected: FAIL(`FamilyNamingSupport` 不存在,编译失败)
+
+- [ ] **Step 3: 实现** `FamilyNamingSupport.java`
+
+```java
+package com.etotem.cfc.util;
+
+import com.etotem.cfc.entity.User;
+
+/** 家庭命名规则:真实姓名优先 → 昵称 → 兜底 */
+public final class FamilyNamingSupport {
+    private FamilyNamingSupport() {}
+    public static String buildFamilyName(User user) {
+        if (user.getRealName() != null && !user.getRealName().trim().isEmpty()) {
+            return user.getRealName().trim() + "家庭";
+        }
+        String nickname = user.getNickname();
+        return (nickname != null && !nickname.isEmpty()) ? nickname + "的家庭" : "我的家庭";
+    }
+}
+```
+
+- [ ] **Step 4: 运行测试确认通过**
+
+Run: `cd cfc-backend && mvn -q test -Dtest=FamilyNamingSupportTest -DfailIfNoTests=false`
+Expected: PASS(4/4)
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/util/FamilyNamingSupport.java cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyNamingSupportTest.java
+git commit -m "feat(family): 家庭命名规则工具 buildFamilyName"
+```
+
+---
+
+### Task 2: 创建家庭与更新资料两处调用新命名规则
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java:912-921`(`createFamilyForUser`)
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java:1159-1273`(`updateUserInfo`)
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/service/UserServiceFamilyNameLinkTest.java`
+
+**Interfaces:**
+- Consumes: `FamilyNamingSupport.buildFamilyName(User)`(Task 1)
+- Produces: `updateUserInfo` 行为变更:真实姓名变化时同步家庭名。
+
+- [ ] **Step 1: 改 `createFamilyForUser` 命名**
+
+将 `FamilyMemberService.createFamilyForUser` 中:
+```java
+String familyName = (user.getNickname() != null && !user.getNickname().isEmpty())
+        ? user.getNickname() + "的家庭" : "我的家庭";
+family.setName(familyName);
+```
+改为:
+```java
+family.setName(FamilyNamingSupport.buildFamilyName(user));
+```
+(顶部 import `com.etotem.cfc.util.FamilyNamingSupport`)
+
+- [ ] **Step 2: 改 `updateUserInfo` 联动**
+
+在 `updateUserInfo` 方法开头 `User user = userMapper.selectById(userId);` 之后记录:
+```java
+String oldRealName = user.getRealName();
+```
+在 `userMapper.updateById(user);`(约 1273 行)之后、`syncUserConsignee` 之前插入:
+```java
+// 家庭名联动:主账号真实姓名变化时同步为「XX家庭」(非关键字段,失败仅告警)
+if (dto.getRealName() != null && !dto.getRealName().trim().isEmpty()
+        && !dto.getRealName().trim().equals(oldRealName == null ? "" : oldRealName.trim())) {
+    try {
+        LambdaQueryWrapper<Family> fw = new LambdaQueryWrapper<>();
+        fw.eq(Family::getCreatorId, userId);
+        Family ownedFamily = familyMapper.selectOne(fw);
+        if (ownedFamily != null) {
+            ownedFamily.setName(dto.getRealName().trim() + "家庭");
+            ownedFamily.setUpdatedAt(new Date());
+            familyMapper.updateById(ownedFamily);
+        }
+    } catch (Exception e) {
+        log.warn("同步家庭名为真实姓名失败: userId={}, realName={}", userId, dto.getRealName(), e);
+    }
+}
+```
+(`UserService` 已 import `FamilyMapper`/`Family`;确认 `log` 已存在,否则加 `@Slf4j`)
+
+- [ ] **Step 3: 写测试** `UserServiceFamilyNameLinkTest.java`(`@SpringBootTest`+`@MockBean`)
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+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 static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@SpringBootTest
+public class UserServiceFamilyNameLinkTest {
+    @Resource private UserService userService;
+    @MockBean private UserMapper userMapper;
+    @MockBean private FamilyMapper familyMapper;
+
+    @Test void realName变化_联动家庭名() {
+        User u = new User(); u.setId(10L); u.setRole("parent"); u.setRealName("旧名"); u.setFamilyId(7L);
+        when(userMapper.selectById(10L)).thenReturn(u);
+        Family f = new Family(); f.setId(7L); f.setName("旧名家庭"); f.setCreatorId(10L);
+        when(familyMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(f);
+        when(userMapper.updateById(any())).thenReturn(1);
+
+        com.etotem.cfc.dto.UpdateUserDTO dto = new com.etotem.cfc.dto.UpdateUserDTO();
+        dto.setRealName("王五");
+        userService.updateUserInfo(10L, dto);
+
+        verify(familyMapper).updateById(argThat(fam -> "王五家庭".equals(fam.getName())));
+    }
+
+    @Test void realName未变_不改家庭名() {
+        User u = new User(); u.setId(10L); u.setRole("parent"); u.setRealName("王五");
+        when(userMapper.selectById(10L)).thenReturn(u);
+        when(userMapper.updateById(any())).thenReturn(1);
+
+        com.etotem.cfc.dto.UpdateUserDTO dto = new com.etotem.cfc.dto.UpdateUserDTO();
+        dto.setRealName("王五");
+        userService.updateUserInfo(10L, dto);
+
+        verify(familyMapper, never()).updateById(any());
+    }
+}
+```
+
+> 注:`updateUserInfo` 可能调用 `syncUserConsignee`(需 `@MockBean` UserAddressMapper 等)——若测试环境缺 bean,用 `@MockBean` 补齐报错提示的依赖,或对 `syncUserConsignee` 打桩。测试以"能验证家庭名联动断言"为最小目标。
+
+- [ ] **Step 4: 运行测试确认通过**
+
+Run: `cd cfc-backend && mvn -q test -Dtest=UserServiceFamilyNameLinkTest -DfailIfNoTests=false`
+Expected: PASS
+
+- [ ] **Step 5: 运行既有 UserService 相关测试确认无回归**
+
+Run: `cd cfc-backend && mvn -q test -Dtest=UserServiceEffectiveFamilyIdsTest -DfailIfNoTests=false`
+Expected: PASS
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java cfc-backend/src/test/java/com/etotem/cfc/service/UserServiceFamilyNameLinkTest.java
+git commit -m "feat(family): 创建/更新资料时按真实姓名联动家庭名"
+```
+
+---
+
+### Task 3: 后端 DTO / VO 类(家庭信息查看)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyOverviewDTO.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyOverviewCounts.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyReportItemVO.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyPlanItemVO.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/TaskItemVO.java`
+
+**Interfaces:**
+- Produces: `FamilyOverviewDTO`(`family`、`creatorName`、`creatorRealName`、`counts:FamilyOverviewCounts`、`reports:List<FamilyReportItemVO>`、`plans:List<FamilyPlanItemVO>`);`FamilyOverviewCounts`(`long reports,plans,tasks` + `Map<String,Integer> reportTypeCounts,taskStatusCounts`);三个 ItemVO 字段见下。
+
+- [ ] **Step 1: 创建 5 个类(Lombok `@Data`,字段与设计文档 5.1 一致)**
+
+```java
+// FamilyReportItemVO.java
+@Data public class FamilyReportItemVO {
+    private Long id; private String reportType; private String reportNumber;
+    private java.util.Date reportDate; private String personName; private Long subjectId;
+    private String status; private Integer overallScore; private Integer gutHealthScore;
+    private java.util.Date createdAt;
+}
+// FamilyPlanItemVO.java
+@Data public class FamilyPlanItemVO {
+    private Long id; private String planType; private String memberName; private String goal;
+    private String status; private Integer totalDays; private Long teacherId; private java.util.Date createdAt;
+}
+// TaskItemVO.java
+@Data public class TaskItemVO {
+    private Long id; private String title; private String status; private String category;
+    private Integer points; private java.util.Date deadline; private Long childId;
+    private String executorType; private String taskType; private String sourceType;
+    private java.util.Date createdAt; private java.util.Date completedAt;
+}
+// FamilyOverviewCounts.java
+@Data public class FamilyOverviewCounts {
+    private long reports; private long plans; private long tasks;
+    private java.util.Map<String, Integer> reportTypeCounts = new java.util.HashMap<>();
+    private java.util.Map<String, Integer> taskStatusCounts = new java.util.HashMap<>();
+}
+// FamilyOverviewDTO.java
+@Data public class FamilyOverviewDTO {
+    private java.util.Map<String, Object> family;
+    private String creatorName; private String creatorRealName;
+    private FamilyOverviewCounts counts;
+    private java.util.List<FamilyReportItemVO> reports;
+    private java.util.List<FamilyPlanItemVO> plans;
+}
+```
+
+- [ ] **Step 2: 编译确认**
+
+Run: `cd cfc-backend && mvn -q compile -DskipTests`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyOverviewDTO.java cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyOverviewCounts.java cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyReportItemVO.java cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyPlanItemVO.java cfc-backend/src/main/java/com/etotem/cfc/dto/TaskItemVO.java
+git commit -m "feat(family): 家庭信息查看 DTO/VO"
+```
+
+---
+
+### Task 4: FamilyAdminOverviewService(计数+明细,TDD)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/FamilyAdminOverviewService.java`
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/service/FamilyAdminOverviewServiceTest.java`
+
+**Interfaces:**
+- Consumes: DTO/VO(Task 3)
+- Produces: `FamilyOverviewDTO buildOverview(Long familyId)`;`com.baomidou.mybatisplus.extension.plugins.pagination.Page<TaskItemVO> pageTasks(Long familyId, long page, long size)`。
+
+- [ ] **Step 1: 写失败测试** `FamilyAdminOverviewServiceTest.java`(`@SpringBootTest`+`@MockBean` 五个 mapper)
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.dto.FamilyOverviewDTO;
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.mapper.*;
+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.any;
+import static org.mockito.Mockito.when;
+
+@SpringBootTest
+public class FamilyAdminOverviewServiceTest {
+    @Resource private FamilyAdminOverviewService svc;
+    @MockBean private FamilyMapper familyMapper;
+    @MockBean private HealthReportMapper reportMapper;
+    @MockBean private HealthPlanMapper planMapper;
+    @MockBean private TaskMapper taskMapper;
+    @MockBean private UserMapper userMapper;
+
+    @Test void overview_计数与明细() {
+        Family f = new Family(); f.setId(7L); f.setName("王五家庭"); f.setCreatorId(10L);
+        when(familyMapper.selectById(7L)).thenReturn(f);
+        when(reportMapper.selectCount(any())).thenReturn(8L);
+        when(planMapper.selectCount(any())).thenReturn(4L);
+        when(taskMapper.selectCount(any())).thenReturn(346L);
+        // 分组计数用 selectMaps(返回 [{k,v}])
+        when(reportMapper.selectMaps(any())).thenReturn(Arrays.asList(map("gut_flora","5"), map("brain_status","3")));
+        when(taskMapper.selectMaps(any())).thenReturn(Arrays.asList(map("pending","340"), map("completed","6")));
+        when(reportMapper.selectList(any())).thenReturn(new ArrayList<>());
+        when(planMapper.selectList(any())).thenReturn(new ArrayList<>());
+
+        FamilyOverviewDTO dto = svc.buildOverview(7L);
+        assertEquals(8L, dto.getCounts().getReports());
+        assertEquals(4L, dto.getCounts().getPlans());
+        assertEquals(346L, dto.getCounts().getTasks());
+        assertEquals(Integer.valueOf(5), dto.getCounts().getReportTypeCounts().get("gut_flora"));
+        assertEquals(Integer.valueOf(340), dto.getCounts().getTaskStatusCounts().get("pending"));
+        assertNotNull(dto.getReports());
+        assertNotNull(dto.getPlans());
+    }
+    @Test void overview_家庭不存在() {
+        when(familyMapper.selectById(999L)).thenReturn(null);
+        assertThrows(RuntimeException.class, () -> svc.buildOverview(999L));
+    }
+    @Test void pageTasks_分页() {
+        when(taskMapper.selectPage(any(), any())).thenReturn(new Page<TaskItemVO>() {{
+            setTotal(346); setRecords(new ArrayList<>());
+        }});
+        Page<TaskItemVO> page = svc.pageTasks(7L, 1, 20);
+        assertEquals(346L, page.getTotal());
+        assertEquals(20L, page.getSize());
+    }
+    private Map<String,Object> map(String k, String v) { Map<String,Object> m = new HashMap<>(); m.put("k", k); m.put("v", Long.parseLong(v)); return m; }
+}
+```
+
+> 注:若 `selectCount`/`selectList`/`selectPage` 的 mock 与 LambdaQueryWrapper 参数不匹配,改用 `any(LambdaQueryWrapper.class)` 或 `any(Wrapper.class)`。`selectMaps` 分组查询返回的 `v` 为 `Long`(COUNT)。
+
+- [ ] **Step 2: 运行确认失败**
+
+Run: `cd cfc-backend && mvn -q test -Dtest=FamilyAdminOverviewServiceTest -DfailIfNoTests=false`
+Expected: FAIL(service 不存在,编译失败)
+
+- [ ] **Step 3: 实现** `FamilyAdminOverviewService.java`
+
+```java
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.dto.*;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import java.util.*;
+
+@Service
+public class FamilyAdminOverviewService {
+    @Resource private FamilyMapper familyMapper;
+    @Resource private HealthReportMapper reportMapper;
+    @Resource private HealthPlanMapper planMapper;
+    @Resource private TaskMapper taskMapper;
+    @Resource private UserMapper userMapper;
+
+    public FamilyOverviewDTO buildOverview(Long familyId) {
+        Family family = familyMapper.selectById(familyId);
+        if (family == null) throw new RuntimeException("家庭不存在");
+
+        FamilyOverviewDTO dto = new FamilyOverviewDTO();
+        Map<String,Object> f = new HashMap<>();
+        f.put("id", family.getId()); f.put("name", family.getName());
+        f.put("inviteCode", family.getInviteCode()); f.put("creatorId", family.getCreatorId());
+        f.put("createdAt", family.getCreatedAt());
+        f.put("province", family.getProvince()); f.put("city", family.getCity());
+        f.put("district", family.getDistrict()); f.put("street", family.getStreet());
+        dto.setFamily(f);
+
+        User creator = family.getCreatorId() != null ? userMapper.selectById(family.getCreatorId()) : null;
+        dto.setCreatorName(creator != null && creator.getNickname() != null ? creator.getNickname() : "");
+        dto.setCreatorRealName(creator != null && creator.getRealName() != null ? creator.getRealName() : "");
+
+        FamilyOverviewCounts counts = new FamilyOverviewCounts();
+        counts.setReports(reportMapper.selectCount(new LambdaQueryWrapper<HealthReport>().eq(HealthReport::getFamilyId, familyId)));
+        counts.setPlans(planMapper.selectCount(new LambdaQueryWrapper<HealthPlan>().eq(HealthPlan::getFamilyId, familyId)));
+        counts.setTasks(taskMapper.selectCount(new LambdaQueryWrapper<Task>().eq(Task::getFamilyId, familyId)));
+        counts.setReportTypeCounts(countBy(reportMapper, "report_type", "family_id", familyId));
+        counts.setTaskStatusCounts(countBy(taskMapper, "status", "family_id", familyId));
+        dto.setCounts(counts);
+
+        dto.setReports(reportMapper.selectList(new LambdaQueryWrapper<HealthReport>()
+                .eq(HealthReport::getFamilyId, familyId)
+                .orderByDesc(HealthReport::getReportDate).orderByDesc(HealthReport::getId)));
+        dto.setPlans(planMapper.selectList(new LambdaQueryWrapper<HealthPlan>()
+                .eq(HealthPlan::getFamilyId, familyId)
+                .orderByDesc(HealthPlan::getCreatedAt).orderByDesc(HealthPlan::getId)));
+        return dto;
+    }
+
+    private <T> Map<String,Integer> countBy(com.baomidou.mybatisplus.core.mapper.BaseMapper<T> mapper,
+                                            String groupCol, String familyCol, Long familyId) {
+        QueryWrapper<T> qw = new QueryWrapper<>();
+        qw.select(groupCol + " as k", "count(*) as v").eq(familyCol, familyId).groupBy(groupCol);
+        Map<String,Integer> out = new HashMap<>();
+        for (Map<String,Object> row : mapper.selectMaps(qw)) {
+            Object k = row.get("k"); Object v = row.get("v");
+            if (k != null && v != null) out.put(String.valueOf(k), ((Number) v).intValue());
+        }
+        return out;
+    }
+
+    public Page<TaskItemVO> pageTasks(Long familyId, long page, long size) {
+        long safeSize = Math.min(Math.max(size, 1), 100);
+        Page<Task> raw = taskMapper.selectPage(new Page<>(page, safeSize),
+                new LambdaQueryWrapper<Task>().eq(Task::getFamilyId, familyId)
+                        .orderByDesc(Task::getCreatedAt).orderByDesc(Task::getId));
+        Page<TaskItemVO> out = new Page<>(raw.getCurrent(), raw.getSize(), raw.getTotal());
+        out.setRecords(new ArrayList<>());
+        for (Task t : raw.getRecords()) {
+            TaskItemVO vo = new TaskItemVO();
+            vo.setId(t.getId()); vo.setTitle(t.getTitle()); vo.setStatus(t.getStatus());
+            vo.setCategory(t.getCategory()); vo.setPoints(t.getPoints()); vo.setDeadline(t.getDeadline());
+            vo.setChildId(t.getChildId()); vo.setExecutorType(t.getExecutorType());
+            vo.setTaskType(t.getTaskType()); vo.setSourceType(t.getSourceType());
+            vo.setCreatedAt(t.getCreatedAt()); vo.setCompletedAt(t.getCompletedAt());
+            out.getRecords().add(vo);
+        }
+        return out;
+    }
+}
+```
+
+- [ ] **Step 4: 运行测试确认通过**
+
+Run: `cd cfc-backend && mvn -q test -Dtest=FamilyAdminOverviewServiceTest -DfailIfNoTests=false`
+Expected: PASS(3/3)
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyAdminOverviewService.java cfc-backend/src/test/java/com/etotem/cfc/service/FamilyAdminOverviewServiceTest.java
+git commit -m "feat(family): 家庭信息查看服务(计数+报告方案全量+任务分页)"
+```
+
+---
+
+### Task 5: AdminFamilyOverviewController(接口,TDD)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminFamilyOverviewController.java`
+- Test: `cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AdminFamilyOverviewControllerTest.java`
+
+**Interfaces:**
+- Consumes: `FamilyAdminOverviewService`(Task 4)
+- Produces: `POST /api/admin/families/{id}/overview`、`POST /api/admin/families/{id}/tasks`。
+
+- [ ] **Step 1: 写失败测试**(`@SpringBootTest`+`@MockBean` service)
+
+```java
+package com.etotem.cfc.integration.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.admin.AdminFamilyOverviewController;
+import com.etotem.cfc.dto.FamilyOverviewDTO;
+import com.etotem.cfc.service.FamilyAdminOverviewService;
+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 static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.when;
+
+@SpringBootTest
+public class AdminFamilyOverviewControllerTest {
+    @Resource private AdminFamilyOverviewController ctrl;
+    @MockBean private FamilyAdminOverviewService svc;
+
+    @Test void overview_非admin拒绝() {
+        Result<FamilyOverviewDTO> r = ctrl.getOverview(7L, "parent");
+        assertTrue(r.getCode() != 200);
+    }
+    @Test void overview_成功() {
+        when(svc.buildOverview(7L)).thenReturn(new FamilyOverviewDTO());
+        Result<FamilyOverviewDTO> r = ctrl.getOverview(7L, "admin");
+        assertEquals(200, r.getCode());
+        assertNotNull(r.getData());
+    }
+    @Test void tasks_分页() {
+        when(svc.pageTasks(eq(7L), anyLong(), anyLong())).thenReturn(new Page<>());
+        Result<Page<com.etotem.cfc.dto.TaskItemVO>> r = ctrl.getTasks(7L, 1L, 20L, "admin");
+        assertEquals(200, r.getCode());
+    }
+}
+```
+
+- [ ] **Step 2: 运行确认失败**
+
+Run: `cd cfc-backend && mvn -q test -Dtest=AdminFamilyOverviewControllerTest -DfailIfNoTests=false`
+Expected: FAIL(controller 不存在)
+
+- [ ] **Step 3: 实现** `AdminFamilyOverviewController.java`
+
+```java
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.FamilyOverviewDTO;
+import com.etotem.cfc.dto.TaskItemVO;
+import com.etotem.cfc.service.FamilyAdminOverviewService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+
+@Tag(name = "管理端-家庭信息查看", description = "家庭报告/方案/任务概览")
+@RestController
+@RequestMapping("/api/admin")
+public class AdminFamilyOverviewController {
+    @Resource private FamilyAdminOverviewService service;
+
+    @Operation(summary = "家庭信息概览")
+    @PostMapping("/families/{id}/overview")
+    public Result<FamilyOverviewDTO> getOverview(@PathVariable Long id,
+                                                 @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) return Result.error("无权限");
+        try {
+            return Result.success(service.buildOverview(id));
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @Operation(summary = "家庭任务分页")
+    @PostMapping("/families/{id}/tasks")
+    public Result<Page<TaskItemVO>> getTasks(@PathVariable Long id,
+                                             @RequestParam(defaultValue = "1") Long page,
+                                             @RequestParam(defaultValue = "20") Long size,
+                                             @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) return Result.error("无权限");
+        return Result.success(service.pageTasks(id, page, size));
+    }
+}
+```
+
+> 注:`Result` 需有 `getCode()`/`getData()`(现有 `Result` 类已有 success/error;若 `getCode()` 不存在用 `r.getCode()` 或对应 getter——以实际 `Result` 类为准)。
+
+- [ ] **Step 4: 运行测试确认通过**
+
+Run: `cd cfc-backend && mvn -q test -Dtest=AdminFamilyOverviewControllerTest -DfailIfNoTests=false`
+Expected: PASS(3/3)
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminFamilyOverviewController.java cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AdminFamilyOverviewControllerTest.java
+git commit -m "feat(family): 管理端家庭信息查看接口 overview/tasks"
+```
+
+---
+
+### Task 6: DatabaseInitializer 存量回填迁移
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(`runMigrations()` 追加新迁移)
+- Modify: `cfc-backend/src/main/resources/db/schema.sql`(若项目要求同步 schema,见设计文档——本迁移为 DML 回填,无 schema 变更,通常不涉 schema.sql)
+
+**Interfaces:**
+- Produces: 幂等迁移 `migrateFamilyNameBackfill()`(DML,`UPDATE families JOIN users`)。
+
+- [ ] **Step 1: 加迁移方法**(在 `runMigrations()` 末尾追加 `migrateFamilyNameBackfill();` 并实现)
+
+```java
+/** 迁移:家庭主账号已填真实姓名但家庭名仍为旧规则 → 回填为「XX家庭」(幂等,重复执行无副作用) */
+private void migrateFamilyNameBackfill() {
+    String sql = "UPDATE families f JOIN users u ON u.id = f.creator_id " +
+            "SET f.name = CONCAT(TRIM(u.real_name), '家庭'), f.updated_at = NOW() " +
+            "WHERE u.real_name IS NOT NULL AND TRIM(u.real_name) <> '' " +
+            "AND f.name <> CONCAT(TRIM(u.real_name), '家庭')";
+    try {
+        int n = jdbcTemplate.update(sql);
+        log.info("迁移[家庭名回填XX家庭] 影响行数={}", n);
+    } catch (Exception e) {
+        log.warn("迁移[家庭名回填XX家庭] 执行异常: {}", e.getMessage());
+    }
+}
+```
+(`DatabaseInitializer` 已注入 `JdbcTemplate jdbcTemplate`(第 43-44 行),`jdbcTemplate.update(sql)` 返回受影响行数——沿用现有风格。)
+
+- [ ] **Step 2: 编译 + 确认无回归**
+
+Run: `cd cfc-backend && mvn -q compile -DskipTests`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
+git commit -m "feat(family): 存量家庭名回填迁移(幂等)"
+```
+
+> ⚠️ **部署后人工确认点**(非代码步骤):部署重启后,先 dry-run 查影响行数:
+> ```sql
+> SELECT COUNT(*) FROM families f JOIN users u ON u.id=f.creator_id
+> WHERE u.real_name IS NOT NULL AND TRIM(u.real_name)<>'' AND f.name<>CONCAT(TRIM(u.real_name),'家庭');
+> ```
+> 将结果报主理人,确认后再由迁移(启动时自动执行)完成实际回填。**若主理人不批准,跳过**(迁移幂等,可在后续版本再启用)。
+
+---
+
+### Task 7: 前端 API 方法
+
+**Files:**
+- Modify: `cfc-web/src/api/admin.js`(追加两方法)
+
+**Interfaces:**
+- Consumes: `request`(`@/utils/request`)
+- Produces: `getFamilyOverview(id)`、`getFamilyTasks(id, params)`。
+
+- [ ] **Step 1: 追加方法**
+
+```js
+export function getFamilyOverview(id) {
+  return request({ url: `/api/admin/families/${id}/overview`, method: 'post' })
+}
+export function getFamilyTasks(id, params) {
+  return request({ url: `/api/admin/families/${id}/tasks`, method: 'post', data: params })
+}
+```
+
+- [ ] **Step 2: 提交**
+
+```bash
+git add cfc-web/src/api/admin.js
+git commit -m "feat(family): 前端API 家庭信息查看"
+```
+
+---
+
+### Task 8: FamilyOverviewDialog.vue 组件
+
+**Files:**
+- Create: `cfc-web/src/views/admin/components/FamilyOverviewDialog.vue`
+
+**Interfaces:**
+- Consumes: `getFamilyOverview`、`getFamilyTasks`(Task 7);`StatusTag`、`StatCard`、`SectionTitle`。
+- Produces: `<FamilyOverviewDialog :visible.sync="..." :family-id="..." />`,`familyId` 变化时拉取 overview。
+
+- [ ] **Step 1: 实现组件**(结构:基础信息 `el-descriptions` + 3 计数 `StatCard`(`clickable`,点击切 tab)+ `el-tabs` 三个明细表;任务 tab 用 `el-pagination` 服务端分页;枚举映射用 `StatusTag :options`)
+
+模板要点(关键代码,非逐行):
+```html
+<el-dialog :visible.sync="visible" title="家庭信息" width="900px" @open="load">
+  <el-descriptions :column="2" border v-if="overview.family">
+    <el-descriptions-item label="家庭名称">{{ overview.family.name }}</el-descriptions-item>
+    <el-descriptions-item label="邀请码">{{ overview.family.inviteCode }}</el-descriptions-item>
+    <el-descriptions-item label="主账号">{{ overview.creatorRealName || overview.creatorName || '-' }}</el-descriptions-item>
+    <el-descriptions-item label="创建时间">{{ formatDate(overview.family.createdAt) }}</el-descriptions-item>
+  </el-descriptions>
+  <div class="count-cards">
+    <StatCard label="报告" :value="counts.reports" color="primary" clickable @click="activeTab='reports'" />
+    <StatCard label="方案" :value="counts.plans" color="success" clickable @click="activeTab='plans'" />
+    <StatCard label="任务" :value="counts.tasks" color="warning" clickable @click="activeTab='tasks'" />
+  </div>
+  <el-tabs v-model="activeTab">
+    <el-tab-pane label="报告" name="reports">
+      <el-table :data="overview.reports" border max-height="300">
+        <el-table-column label="类型"><template slot-scope="{row}"><StatusTag :options="reportTypeOptions" :value="row.reportType" /></template></el-table-column>
+        <el-table-column prop="reportNumber" label="报告编号" width="140" />
+        <el-table-column label="日期"><template slot-scope="{row}">{{ formatDate(row.reportDate) }}</template></el-table-column>
+        <el-table-column prop="personName" label="姓名" />
+        <el-table-column label="状态"><template slot-scope="{row}"><StatusTag :options="reportStatusOptions" :value="row.status" /></template></el-table-column>
+        <el-table-column prop="overallScore" label="总分" width="70" />
+        <el-table-column prop="gutHealthScore" label="肠道分" width="80" />
+      </el-table>
+    </el-tab-pane>
+    <el-tab-pane label="方案" name="plans">
+      <el-table :data="overview.plans" border max-height="300">
+        <el-table-column label="类型"><template slot-scope="{row}"><StatusTag :options="planTypeOptions" :value="row.planType" /></template></el-table-column>
+        <el-table-column prop="memberName" label="成员" />
+        <el-table-column prop="goal" label="目标" show-overflow-tooltip />
+        <el-table-column label="状态"><template slot-scope="{row}"><StatusTag :options="planStatusOptions" :value="row.status" /></template></el-table-column>
+        <el-table-column prop="totalDays" label="天数" width="70" />
+        <el-table-column label="创建时间"><template slot-scope="{row}">{{ formatDate(row.createdAt) }}</template></el-table-column>
+      </el-table>
+    </el-tab-pane>
+    <el-tab-pane label="任务" name="tasks">
+      <el-table :data="tasks" border max-height="300">
+        <el-table-column prop="title" label="标题" show-overflow-tooltip />
+        <el-table-column label="状态"><template slot-scope="{row}"><StatusTag :options="taskStatusOptions" :value="row.status" /></template></el-table-column>
+        <el-table-column prop="category" label="分类" width="110" />
+        <el-table-column prop="points" label="积分" width="70" />
+        <el-table-column label="截止"><template slot-scope="{row}">{{ formatDate(row.deadline) }}</template></el-table-column>
+      </el-table>
+      <el-pagination class="pagination-right" @current-change="loadTasks" :current-page="taskPage"
+        :page-size="taskSize" :total="taskTotal" layout="total, prev, pager, next" />
+    </el-tab-pane>
+  </el-tabs>
+</el-dialog>
+```
+
+枚举选项(script data):
+```js
+reportTypeOptions: [
+  { value: 'gut_flora', label: '菌群检测', type: 'success' },
+  { value: 'physical_exam', label: '体检报告', type: 'primary' },
+  { value: 'dan', label: 'DAN测评', type: 'warning' },
+  { value: 'brain_status', label: '脑状态', type: 'primary' },
+  { value: 'aptitude', label: '先天潜能', type: 'primary' },
+  { value: 'insurance', label: '保单', type: 'info' },
+  { value: 'fortune', label: '家庭周报', type: 'danger' }
+],
+reportStatusOptions: [
+  { value: 'active', label: '生效', type: 'success' },
+  { value: 'archived', label: '已归档', type: 'warning' },
+  { value: 'deleted', label: '已删除', type: 'info' }
+],
+planTypeOptions: [{ value: 'health_plan', label: '健康方案', type: 'primary' }, { value: 'family_attribution_plan', label: '家庭归因方案', type: 'warning' }],
+planStatusOptions: [
+  { value: 'draft', label: '草稿', type: 'info' }, { value: 'pending_review', label: '待审核', type: 'warning' },
+  { value: 'published', label: '已发布', type: 'success' }, { value: 'rejected', label: '已驳回', type: 'danger' }
+],
+taskStatusOptions: [
+  { value: 'pending', label: '进行中', type: 'warning' }, { value: 'pending_review', label: '待审核', type: 'warning' },
+  { value: 'completed', label: '已完成', type: 'success' }, { value: 'locked', label: '前置锁定', type: 'info' },
+  { value: 'assigned', label: '待接受', type: 'info' }, { value: 'in_progress', label: '进行中', type: 'warning' },
+  { value: 'rejected', label: '已拒绝', type: 'danger' }, { value: 'overdue', label: '已过期', type: 'danger' },
+  { value: 'cancelled', label: '已取消', type: 'info' }
+]
+```
+
+load / loadTasks:
+```js
+async load() { this.overview = null; const res = await getFamilyOverview(this.familyId); this.overview = res.data; this.loadTasks(1) }
+async loadTasks(page) { this.taskPage = page; const res = await getFamilyTasks(this.familyId, { page, size: this.taskSize }); this.tasks = res.data.records; this.taskTotal = res.data.total }
+```
+
+- [ ] **Step 2: 提交**
+
+```bash
+git add cfc-web/src/views/admin/components/FamilyOverviewDialog.vue
+git commit -m "feat(family): 家庭信息查看弹窗组件"
+```
+
+---
+
+### Task 9: Families.vue 接入
+
+**Files:**
+- Modify: `cfc-web/src/views/Families.vue`
+
+**Interfaces:**
+- Consumes: `FamilyOverviewDialog`(Task 8)
+
+- [ ] **Step 1: 下拉菜单加「查看信息」**(`handleActionCmd` 的 dropdown 内、`viewMembers` 前)
+
+```html
+<el-dropdown-item command="viewOverview" icon="el-icon-view">查看信息</el-dropdown-item>
+```
+
+- [ ] **Step 2: script 引入组件与状态**(`import FamilyOverviewDialog from '@/views/admin/components/FamilyOverviewDialog.vue'`;`components:{ FamilyOverviewDialog }`;data 加 `overviewDialogVisible:false, overviewFamilyId:null`)
+
+- [ ] **Step 3: handleActionCmd 加分支 + 方法**
+
+```js
+case 'viewOverview': this.handleViewOverview(row); break;
+// ...
+handleViewOverview(row) { this.overviewFamilyId = row.id; this.overviewDialogVisible = true }
+```
+
+- [ ] **Step 4: 模板末尾挂组件**
+
+```html
+<FamilyOverviewDialog :visible.sync="overviewDialogVisible" :family-id="overviewFamilyId" />
+```
+
+- [ ] **Step 5: 前端构建验证**
+
+Run: `cd cfc-web && npm run build`
+Expected: BUILD SUCCESS(无编译错误)
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add cfc-web/src/views/Families.vue
+git commit -m "feat(family): 家庭列表接入查看信息弹窗"
+```
+
+---
+
+### Task 10: 后端全量测试 + 前端构建(合入前验证)
+
+**Files:** 无新增(验证任务)
+
+- [ ] **Step 1: 后端全量测试(重点相关测试)**
+
+Run: `cd cfc-backend && mvn -q test -Dtest=FamilyNamingSupportTest,UserServiceFamilyNameLinkTest,FamilyAdminOverviewServiceTest,AdminFamilyOverviewControllerTest -DfailIfNoTests=false`
+Expected: ALL PASS
+
+- [ ] **Step 2: 后端整体编译打包**
+
+Run: `cd cfc-backend && mvn clean package -Dmaven.test.skip=true -Dmaven.compile.fork=true`
+Expected: BUILD SUCCESS,产物 `target/cfc-backend-1.0.0.jar`
+
+- [ ] **Step 3: 前端构建**
+
+Run: `cd cfc-web && npm run build`
+Expected: BUILD SUCCESS
+
+---
+
+## 部署(按 AGENTS.md 流程,人工确认后执行,不属于代码任务)
+
+1. 备份:`tar -czf /app/pro/cfc_backup_$(date +%Y%m%d_%H%M%S)_familyinfo.tar.gz -C /app pro/cfc` + mysqldump `families`/`users` 受影响行。
+2. 后端:`mvn clean package` → `cp target/cfc-backend-1.0.0.jar /app/pro/cfc/backend/` → `systemctl restart cfc-backend`。
+3. 前端:`npm run build` → `rsync -a --delete dist/ /app/pro/cfc/frontend/`。
+4. 验证:`systemctl status cfc-backend`(active)+ `curl POST /api/cart/list` 200 + 新 PID 日志无 ERROR + `/admin/` 200。
+5. **存量回填 dry-run**:执行 Task 6 后的人工确认点 SQL → 报主理人 → 确认后由迁移执行。
+6. 冒烟:管理端家庭列表 → 查看信息弹窗 → 三 tab + 任务翻页;家庭名含真实姓名者显示「XX家庭」。