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

Merge branch 'feature/family-overview' into cfclub

Xiaogang Liao 1 неделя назад
Родитель
Сommit
c19b579edf
20 измененных файлов с 1952 добавлено и 4 удалено
  1. 16 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 42 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminFamilyOverviewController.java
  3. 23 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyOverviewCounts.java
  4. 25 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyOverviewDTO.java
  5. 28 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyPlanItemVO.java
  6. 32 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyReportItemVO.java
  7. 36 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/TaskItemVO.java
  8. 114 0
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyAdminOverviewService.java
  9. 2 3
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java
  10. 18 0
      cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
  11. 18 0
      cfc-backend/src/main/java/com/etotem/cfc/util/FamilyNamingSupport.java
  12. 45 0
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AdminFamilyOverviewControllerTest.java
  13. 70 0
      cfc-backend/src/test/java/com/etotem/cfc/service/FamilyAdminOverviewServiceTest.java
  14. 57 0
      cfc-backend/src/test/java/com/etotem/cfc/service/UserServiceFamilyNameLinkTest.java
  15. 20 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyNamingSupportTest.java
  16. 7 0
      cfc-web/src/api/admin.js
  17. 15 1
      cfc-web/src/views/Families.vue
  18. 230 0
      cfc-web/src/views/admin/components/FamilyOverviewDialog.vue
  19. 858 0
      docs/superpowers/plans/2026-10-30-family-overview.md
  20. 296 0
      docs/superpowers/specs/2026-10-30-family-overview-naming-design.md

+ 16 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3387,6 +3387,8 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         migrateProductsPurchasePriceToCents();
         // 迁移431: 销售奖档位门槛 ×100(元→分)+ 档位名改为 新秀/小成/中富/大成/至富
         migrateSalesAwardTierThresholds();
+        // 迁移429(家庭): 存量家庭名回填(家庭主账号已填真实姓名但家庭名仍为旧规则 → 按新规则回填「XX家庭」;DML,无 schema 变更,幂等)
+        migrateFamilyNameBackfill();
     }
 
     /**
@@ -3437,6 +3439,20 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         }
     }
 
+    /** 迁移:家庭主账号已填真实姓名但家庭名仍为旧规则 → 回填为「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());
+        }
+    }
+
     /**
      * 迁移421: activities 表新增 audience_scope / audience_type 两列(A2)
      * - audience_scope: all(默认,全部用户可参加) / member_only(仅会员可参加,后端硬门禁)

+ 42 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminFamilyOverviewController.java

@@ -0,0 +1,42 @@
+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;
+import java.util.Map;
+
+@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,
+                                             @RequestBody(required = false) Map<String, Object> params,
+                                             @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) return Result.error("无权限");
+        long page = params != null && params.get("page") != null ? ((Number) params.get("page")).longValue() : 1L;
+        long size = params != null && params.get("size") != null ? ((Number) params.get("size")).longValue() : 20L;
+        return Result.success(service.pageTasks(id, page, size));
+    }
+}

+ 23 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyOverviewCounts.java

@@ -0,0 +1,23 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 家庭信息查看-计数统计
+ */
+@Data
+public class FamilyOverviewCounts {
+
+    private long reports;
+
+    private long plans;
+
+    private long tasks;
+
+    private Map<String, Integer> reportTypeCounts = new HashMap<>();
+
+    private Map<String, Integer> taskStatusCounts = new HashMap<>();
+}

+ 25 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyOverviewDTO.java

@@ -0,0 +1,25 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 家庭信息查看DTO
+ */
+@Data
+public class FamilyOverviewDTO {
+
+    private Map<String, Object> family;
+
+    private String creatorName;
+
+    private String creatorRealName;
+
+    private FamilyOverviewCounts counts;
+
+    private List<FamilyReportItemVO> reports;
+
+    private List<FamilyPlanItemVO> plans;
+}

+ 28 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyPlanItemVO.java

@@ -0,0 +1,28 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 家庭信息查看-计划项VO
+ */
+@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 Date createdAt;
+}

+ 32 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyReportItemVO.java

@@ -0,0 +1,32 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 家庭信息查看-报告项VO
+ */
+@Data
+public class FamilyReportItemVO {
+
+    private Long id;
+
+    private String reportType;
+
+    private String reportNumber;
+
+    private Date reportDate;
+
+    private String personName;
+
+    private Long subjectId;
+
+    private String status;
+
+    private Integer overallScore;
+
+    private Integer gutHealthScore;
+
+    private Date createdAt;
+}

+ 36 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/TaskItemVO.java

@@ -0,0 +1,36 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 家庭信息查看-任务项VO
+ */
+@Data
+public class TaskItemVO {
+
+    private Long id;
+
+    private String title;
+
+    private String status;
+
+    private String category;
+
+    private Integer points;
+
+    private Date deadline;
+
+    private Long childId;
+
+    private String executorType;
+
+    private String taskType;
+
+    private String sourceType;
+
+    private Date createdAt;
+
+    private Date completedAt;
+}

+ 114 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyAdminOverviewService.java

@@ -0,0 +1,114 @@
+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);
+
+        // 报告明细:全量、按报告日期倒序(同日期按 id 倒序)
+        List<HealthReport> reports = reportMapper.selectList(new LambdaQueryWrapper<HealthReport>()
+                .eq(HealthReport::getFamilyId, familyId)
+                .orderByDesc(HealthReport::getReportDate).orderByDesc(HealthReport::getId));
+        List<FamilyReportItemVO> reportVOs = new ArrayList<>();
+        for (HealthReport r : reports) {
+            FamilyReportItemVO vo = new FamilyReportItemVO();
+            vo.setId(r.getId()); vo.setReportType(r.getReportType()); vo.setReportNumber(r.getReportNumber());
+            vo.setReportDate(r.getReportDate()); vo.setPersonName(r.getPersonName()); vo.setSubjectId(r.getSubjectId());
+            vo.setStatus(r.getStatus()); vo.setOverallScore(r.getOverallScore());
+            vo.setGutHealthScore(r.getGutHealthScore()); vo.setCreatedAt(r.getCreatedAt());
+            reportVOs.add(vo);
+        }
+        dto.setReports(reportVOs);
+
+        // 方案明细:全量、按创建时间倒序(同时间按 id 倒序)
+        List<HealthPlan> plans = planMapper.selectList(new LambdaQueryWrapper<HealthPlan>()
+                .eq(HealthPlan::getFamilyId, familyId)
+                .orderByDesc(HealthPlan::getCreatedAt).orderByDesc(HealthPlan::getId));
+        List<FamilyPlanItemVO> planVOs = new ArrayList<>();
+        for (HealthPlan p : plans) {
+            FamilyPlanItemVO vo = new FamilyPlanItemVO();
+            vo.setId(p.getId()); vo.setPlanType(p.getPlanType()); vo.setMemberName(p.getMemberName());
+            vo.setGoal(p.getGoal()); vo.setStatus(p.getStatus()); vo.setTotalDays(p.getTotalDays());
+            vo.setTeacherId(p.getTeacherId()); vo.setCreatedAt(p.getCreatedAt());
+            planVOs.add(vo);
+        }
+        dto.setPlans(planVOs);
+        return dto;
+    }
+
+    /**
+     * 分组计数:selectMaps + groupBy(MyBatis-Plus 3.5.3.1 的 selectCount 不支持分组,
+     * 分组必须走 selectMaps;COUNT 返回 Long,映射为 Integer)。
+     */
+    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.getMemberId()); 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;
+    }
+}

+ 2 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java

@@ -34,6 +34,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.util.FamilyNamingSupport;
 
 @Service
 public class FamilyMemberService {
@@ -911,9 +912,7 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
      */
     private Long createFamilyForUser(User user) {
         Family family = new Family();
-        String familyName = (user.getNickname() != null && !user.getNickname().isEmpty())
-                ? user.getNickname() + "的家庭" : "我的家庭";
-        family.setName(familyName);
+        family.setName(FamilyNamingSupport.buildFamilyName(user));
         family.setInviteCode(generateInviteCode());
         family.setCreatorId(user.getId());
         family.setCreatedAt(new Date());

+ 18 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -1162,6 +1162,7 @@ FamilyMember child = familyMemberMapper.selectById(memberId);
         if (user == null) {
             return false;
         }
+        String oldRealName = user.getRealName();
         // 管理员角色不允许修改
         if ("admin".equals(user.getRole())) {
             throw new RuntimeException("管理员角色不允许修改");
@@ -1278,6 +1279,23 @@ FamilyMember child = familyMemberMapper.selectById(memberId);
 
         userMapper.updateById(user);
 
+        // 家庭名联动:主账号真实姓名变化时同步为「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);
+            }
+        }
+
         // 用户填写地区后,自动创建/更新一条联系人(Consignee),与订单联系人共享数据
         syncUserConsignee(userId, user);
 

+ 18 - 0
cfc-backend/src/main/java/com/etotem/cfc/util/FamilyNamingSupport.java

@@ -0,0 +1,18 @@
+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();
+        if (nickname != null && !nickname.trim().isEmpty()) {
+            return nickname.trim() + "的家庭";
+        }
+        return "我的家庭";
+    }
+}

+ 45 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/controller/AdminFamilyOverviewControllerTest.java

@@ -0,0 +1,45 @@
+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 需显式指定 datasource URL(生产库 192.168.16.251 不可达),
+ * 使用本机 MariaDB(127.0.0.1:3306/zxyj)——与 Task 2/4 预批准覆盖一致。
+ */
+@SpringBootTest(properties = {
+    "spring.datasource.url=jdbc:mysql://127.0.0.1:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true",
+    "spring.datasource.username=zxyj",
+    "spring.datasource.password=zxyj@123"
+})
+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, null, "admin");
+        assertEquals(200, r.getCode());
+    }
+}

+ 70 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FamilyAdminOverviewServiceTest.java

@@ -0,0 +1,70 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.dto.FamilyOverviewDTO;
+import com.etotem.cfc.dto.TaskItemVO;
+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 需显式指定 datasource URL,因为 application.yml 的默认值
+ * 指向 192.168.16.251(不可达),而本机 MariaDB(127.0.0.1:3306/zxyj)schema 完整。
+ */
+@SpringBootTest(properties = {
+    "spring.datasource.url=jdbc:mysql://127.0.0.1:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true",
+    "spring.datasource.username=zxyj",
+    "spring.datasource.password=zxyj@123"
+})
+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_分页() {
+        // mock 模拟 mapper 收到 page=1,size=20 请求后返回的分页结果(含 size 回显)
+        when(taskMapper.selectPage(any(), any())).thenReturn(new Page<TaskItemVO>() {{
+            setTotal(346); setSize(20); 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; }
+}

+ 57 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/UserServiceFamilyNameLinkTest.java

@@ -0,0 +1,57 @@
+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.*;
+
+/**
+ * 验证 updateUserInfo 在 realName 变化时联动更新家庭名。
+ *
+ * 注意:@SpringBootTest 需显式指定 datasource URL,因为 application.yml 的默认值
+ * 指向 192.168.16.251(不可达),而本地开发机 MariaDB(127.0.0.1:3306/zxyj)
+ * 有完整的 zxyj schema 可引导上下文加载。
+ */
+@SpringBootTest(properties = {
+    "spring.datasource.url=jdbc:mysql://127.0.0.1:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true",
+    "spring.datasource.username=zxyj",
+    "spring.datasource.password=zxyj@123"
+})
+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());
+    }
+}

+ 20 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyNamingSupportTest.java

@@ -0,0 +1,20 @@
+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))); }
+    @Test void nickname全空格_我的家庭() { assertEquals("我的家庭", FamilyNamingSupport.buildFamilyName(user(null, "   "))); }
+}

+ 7 - 0
cfc-web/src/api/admin.js

@@ -121,6 +121,13 @@ export function getFamilyMembers(id) {
   })
 }
 
+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 })
+}
+
 export function setMemberAdminRole(familyId, memberId, isAdmin) {
   return request({
     url: `/api/admin/families/${familyId}/members/set-admin`,

+ 15 - 1
cfc-web/src/views/Families.vue

@@ -61,6 +61,7 @@
                 更多<i class="el-icon-arrow-down el-icon--right"></i>
               </el-button>
               <el-dropdown-menu slot="dropdown">
+                <el-dropdown-item command="viewOverview" icon="el-icon-view">查看信息</el-dropdown-item>
                 <el-dropdown-item command="viewMembers" icon="el-icon-user">查看成员</el-dropdown-item>
                 <el-dropdown-item command="viewCoupons" icon="el-icon-ticket">优惠券列表</el-dropdown-item>
                 <el-dropdown-item command="viewUsage" icon="el-icon-document">使用记录</el-dropdown-item>
@@ -293,6 +294,8 @@
   </el-table>
   
   </el-table>
+    <!-- 家庭信息总览弹窗 (Task 9) -->
+    <FamilyOverviewDialog :visible.sync="overviewDialogVisible" :family-id="overviewFamilyId" />
   </div>
 </template>
 
@@ -301,10 +304,14 @@ import SortMixin from '@/mixins/SortMixin'
 import ListStateMixin from '@/mixins/ListStateMixin'
 import { getFamilyList, getFamilyMembers, updateFamily, deleteFamily, setMemberAdminRole } from '@/api/admin'
 import { getCouponList, issueFamilyCoupon, getFamilyCoupons, getFamilyCouponUsage } from '@/api/coupon'
+import FamilyOverviewDialog from '@/views/admin/components/FamilyOverviewDialog.vue'
 
 export default {
   name: 'Families',
   mixins: [SortMixin, ListStateMixin],
+  components: {
+    FamilyOverviewDialog
+  },
   computed: {
     tableHeight() {
       return window.innerHeight - 340
@@ -356,7 +363,9 @@ export default {
       usageListData: [],
       usageLoading: false,
       usagePagination: { current: 1, size: 20, total: 0 },
-      usageDialogVisible: false
+      usageDialogVisible: false,
+      overviewDialogVisible: false,
+      overviewFamilyId: null
     }
   },
   mounted() {
@@ -373,6 +382,7 @@ export default {
     },
     handleActionCmd(row, cmd) {
       switch (cmd) {
+        case 'viewOverview': this.handleViewOverview(row); break;
         case 'viewMembers': this.handleViewMembers(row); break;
         case 'viewCoupons': this.handleViewCoupons(row); break;
         case 'viewUsage': this.handleViewUsage(row); break;
@@ -460,6 +470,10 @@ export default {
         }
       }).catch(() => {})
     },
+    handleViewOverview(row) {
+      this.overviewFamilyId = row.id
+      this.overviewDialogVisible = true
+    },
     async handleViewMembers(row) {
       await this.handleViewMembersById(row.id)
     },

+ 230 - 0
cfc-web/src/views/admin/components/FamilyOverviewDialog.vue

@@ -0,0 +1,230 @@
+<template>
+  <el-dialog
+    :visible.sync="dialogVisible"
+    title="家庭信息"
+    width="900px"
+    top="6vh"
+    @open="load"
+    :close-on-click-modal="false"
+  >
+    <div v-loading="loading">
+      <template v-if="overview && overview.family">
+        <el-descriptions :column="2" border>
+          <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>
+      </template>
+      <el-empty v-else :description="loading ? '加载中...' : '暂无数据'" />
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { getFamilyOverview, getFamilyTasks } from '@/api/admin'
+import { formatDate } from '@/utils/format'
+import StatCard from '@/components/StatCard.vue'
+import StatusTag from '@/components/StatusTag.vue'
+
+export default {
+  name: 'FamilyOverviewDialog',
+  props: {
+    /** 弹窗可见性,父组件通过 :visible.sync 控制 */
+    visible: { type: Boolean, default: false },
+    /** 家庭 ID,变化或弹窗打开时触发加载 */
+    familyId: { type: [Number, String], default: null }
+  },
+  data() {
+    return {
+      loading: false,
+      overview: null,
+      activeTab: 'reports',
+      tasks: [],
+      taskPage: 1,
+      taskSize: 20,
+      taskTotal: 0,
+      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' }
+      ]
+    }
+  },
+  computed: {
+    /**
+     * 受控 dialog 可见性:写时派发 update:visible,保证父组件的 :visible.sync 同步。
+     * (el-dialog 关闭时 emit update:visible(false),若直接绑 visible prop 会导致
+     * 直接修改 prop 且无法回传父组件,故用计算属性桥接。)
+     */
+    dialogVisible: {
+      get() {
+        return this.visible
+      },
+      set(val) {
+        this.$emit('update:visible', val)
+      }
+    },
+    /** 计数卡片数据源(overview 未加载时兜底为空对象,避免 counts.xxx 报错) */
+    counts() {
+      return (this.overview && this.overview.counts) || {}
+    }
+  },
+  watch: {
+    /** 家庭 ID 变化时(弹窗已打开)重新加载 */
+    familyId() {
+      if (this.visible) this.load()
+    }
+  },
+  methods: {
+    formatDate,
+    async load() {
+      if (!this.familyId) return
+      this.loading = true
+      this.overview = null
+      this.tasks = []
+      this.taskTotal = 0
+      this.taskPage = 1
+      try {
+        const res = await getFamilyOverview(this.familyId)
+        this.overview = res.data
+        this.loadTasks(1)
+      } catch (e) {
+        this.$message.error('加载家庭信息失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    async loadTasks(page) {
+      this.taskPage = page
+      try {
+        const res = await getFamilyTasks(this.familyId, { page, size: this.taskSize })
+        this.tasks = res.data.records || []
+        this.taskTotal = res.data.total || 0
+      } catch (e) {
+        this.$message.error('加载任务列表失败')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.count-cards {
+  display: flex;
+  gap: 16px;
+  margin: 16px 0;
+}
+.count-cards >>> .adm-stat-card {
+  flex: 1;
+}
+.pagination-right {
+  margin-top: 12px;
+  text-align: right;
+}
+</style>

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

@@ -0,0 +1,858 @@
+# 管理端家庭信息查看 + 家庭命名「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 -DskipTests=false -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 -DskipTests=false -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 -DskipTests=false -Dtest=UserServiceFamilyNameLinkTest -DfailIfNoTests=false`
+Expected: PASS
+
+- [ ] **Step 5: 运行既有 UserService 相关测试确认无回归**
+
+Run: `cd cfc-backend && mvn -q test -DskipTests=false -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 -DskipTests=false -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 -DskipTests=false -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, null, "admin");
+        assertEquals(200, r.getCode());
+    }
+}
+```
+
+- [ ] **Step 2: 运行确认失败**
+
+Run: `cd cfc-backend && mvn -q test -DskipTests=false -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,
+                                             @RequestBody(required = false) Map<String, Object> params,
+                                             @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) return Result.error("无权限");
+        long page = params != null && params.get("page") != null ? ((Number) params.get("page")).longValue() : 1L;
+        long size = params != null && params.get("size") != null ? ((Number) params.get("size")).longValue() : 20L;
+        return Result.success(service.pageTasks(id, page, size));
+    }
+}
+```
+(import `java.util.Map`;分页参数从 `@RequestBody` body 读取——与现有 `getFamilies` 风格及前端 `data: params` 一致,非 `@RequestParam`)
+
+> 注:`Result` 需有 `getCode()`/`getData()`(现有 `Result` 类已有 success/error;若 `getCode()` 不存在用 `r.getCode()` 或对应 getter——以实际 `Result` 类为准)。
+
+- [ ] **Step 4: 运行测试确认通过**
+
+Run: `cd cfc-backend && mvn -q test -DskipTests=false -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 }
+```
+script 需 `import { formatDate } from '@/utils/format'`(`formatDate` 来自 `cfc-web/src/utils/format.js`,Families.vue 已同源使用)。
+
+- [ ] **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 -DskipTests=false -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家庭」。

+ 296 - 0
docs/superpowers/specs/2026-10-30-family-overview-naming-design.md

@@ -0,0 +1,296 @@
+# 设计:管理端家庭信息查看 + 家庭名称「XX家庭」
+
+日期:2026-10-30
+分支:`feature/family-overview`(worktree `/app/cfc-family-overview`)
+范围:cfc-backend(管理端接口 + 家庭命名规则)、cfc-web(管理端家庭列表页)
+
+---
+
+## 1. 意图与背景
+
+管理端「家庭列表」页(`cfc-web/src/views/Families.vue`)目前只能看到家庭名称、邀请码、成员、优惠券。
+运营/客服排查一个家庭时,需要跨多个模块(报告管理、健康方案管理、任务管理)分别用家庭名过滤,
+无法在一个地方看到该家庭实际沉淀了什么。用户要求:
+
+1. **家庭管理增加「家庭相关信息查看」功能**,包含报告、方案、任务三类。
+2. **家庭主账号填写真实姓名后,家庭名称自动改为「真实姓名家庭」**。
+
+## 2. 目标 / 非目标
+
+### 目标
+- G1:管理端家庭列表每行可一键打开该家庭的报告 / 方案 / 任务概览,含计数卡片 + 可下钻明细。
+- G2:家庭主账号(`families.creator_id` 对应用户)的 `users.real_name` 非空时,`families.name` 变为「真实姓名 + 家庭」。
+- G3:新命名规则对**存量数据**同样生效(一次性回填迁移)。
+- G4:零回归——不破坏现有家庭列表 / 成员 / 优惠券功能;不改变用户端既有接口行为(除家庭名展示值)。
+
+### 非目标
+- 不做家庭维度的报告详情页跳转(只读展示,不提供编辑/下载)。
+- 不做方案、任务的新增/修改(管理端只读)。
+- 不改报告/方案/任务本身的业务规则与表结构。
+- 不引入新表、不引入新依赖。
+
+## 3. 现状(已勘察核实)
+
+### 后端
+| 位置 | 现状 |
+|---|---|
+| `AdminController`(`/api/admin`) | 已有 `POST /families`(列表)、`POST /families/{id}`(详情)、`POST /families/{id}/members`、`POST /families/{id}/update`、`POST /families/{id}/delete`;均校验 `role == "admin"` |
+| `Family` 实体 | 字段:`id, name, inviteCode, streetId, province, city, district, street, teacherId, butlerId, creatorId, createdAt, updatedAt` |
+| `User` 实体 | 有 `realName`(真实姓名)、`familyId`、`role` |
+| `FamilyMemberService.createFamilyForUser` | 建家庭时命名:`nickname + "的家庭"`,无昵称则 `"我的家庭"` |
+| `UserService.updateUserInfo` | 用户改资料时写 `realName`,**不联动家庭名** |
+
+### 前端
+| 位置 | 现状 |
+|---|---|
+| `cfc-web/src/views/Families.vue` | 家庭列表页;已有「编辑」(仅家庭名称)/「查看成员」/「查看优惠券」/「发放优惠券」弹窗;操作列 140px 宽 |
+| `cfc-web/src/api/admin.js` | 已有 `getFamilyList / getFamilyDetail / updateFamily / deleteFamily / getFamilyMembers / setMemberAdminRole` |
+| `cfc-web/src/components/` | 已有 `StatusTag.vue`、`SectionTitle.vue`、`StatCard.vue`、`PageHeader.vue`(可复用) |
+| `cfc-web/src/views/admin/components/` | 已有 `ProductPicker.vue`、`ProductSkuEditor.vue`(子组件放置惯例) |
+
+### 数据规模(生产实测)
+| 数据 | 单家庭最大条数 | 说明 |
+|---|---|---|
+| `health_reports` | 8 | 类型:`gut_flora`(41) / `brain_status`(6) / `aptitude`(2) / `physical_exam`(1) |
+| `health_plans` | 4 | 状态:`published`(8) / `draft`(7) / `pending_review`(2);`plan_type` 均为 `health_plan` |
+| `tasks` | **346** | 状态:`pending`(3629) / `completed`(140) / `locked`(82) / `pending_review`(3);枚举含 assigned/in_progress/rejected/overdue/cancelled |
+
+> **关键约束**:任务量远大于报告与方案。若 overview 接口一次性返回全部任务明细,
+> 346 条 × N 字段会撑大响应体。设计上采用「计数 + 分页明细」。
+
+### 三张表的关联字段(已核实)
+- `health_reports`: `family_id`, `subject_id`, `report_type`, `report_number`, `report_date`, `person_name`, `status`(active/deleted/archived), `overall_score`, `gut_health_score`, `created_at`
+- `health_plans`: `family_id`, `member_ids`, `member_name`, `dimensions`, `goal`, `plan_content`, `plan_json`, `plan_type`, `status`, `teacher_id`, `total_days`, `created_at`
+- `tasks`: `family_id`, `family_member_id`, `child_id`, `title`, `description`, `status`, `category`, `points`, `deadline`, `executor_type`, `executor_id`, `task_type`, `source_type`, `created_at`, `completed_at`
+
+### 3.1 实现核验结论(源码 + 生产库实测,非假设)
+
+| 项 | 核验结论 |
+|---|---|
+| 管理端鉴权 | `JwtInterceptor` 第 153 行 `request.setAttribute("role", role)`;`"admin".equals(role)` 校验约定**成立**。新控制器沿用 `@RequestAttribute("role") String role` |
+| MyBatis-Plus 版本 | `3.5.3.1`(`selectCount` 返回 `Long`)。**分组计数不能用 `selectCount`**(会把 GROUP BY 包进 COUNT 子查询),须用 `selectMaps + groupBy`——项目已有先例(`FileRecordService` 第 301-306 行 `selectMaps` + `QueryWrapper.groupBy`) |
+| `UserService` 依赖 | 已注入 `FamilyMapper`(第 48 行),**无循环依赖**;`updateUserInfo` **无 `@Transactional`**(家庭名联动失败不回滚用户资料——可接受,家庭名为非关键展示值) |
+| Task 逻辑删除 | `Task` 实体**无 `@TableLogic`**、表无 `deleted` 列 → 计数/分页直接 `WHERE family_id=?`,无需处理软删 |
+| `health_plans.member_name` | 单值显示名(`varchar(50)`),直接展示;`member_ids` 为逗号分隔列表,**不需要** |
+| `families.creator_id` | 有 **5 行为 NULL**(历史/系统创建)→ overview 的 `creatorName/creatorRealName` 需 **null-guard**;命名迁移 `INNER JOIN` 自然跳过 NULL(安全) |
+| 前端 StatCard | `StatCard.vue` **已支持** `clickable` prop + `@click`(点击切换 tab 直接用) |
+| 前端 StatusTag | props 含 `value` + `options:[{value,label,type}]`(**自带枚举映射**,传 `options` 即可,无需外部 if-else) |
+| Element UI 版本 | `2.15.13` → `el-descriptions`、`el-tabs`、`el-pagination` 均可用 |
+| 代码坐标 | 已核对:`createFamilyForUser` 命名逻辑在 913-915 行;`updateUserInfo` 在 1159 行起;`getFamilyMembers` 在 826 行起 |
+| 现有接口冲突 | `AdminController` **无** 现成 reports/plans 查询端点;新路径 `/families/{id}/overview`、`/families/{id}/tasks` 与现有 `/families/{id}`、`/families/{id}/members` 段不同,**无路由歧义**(Spring 允许多控制器共享 `/api/admin` 前缀,路径唯一即可) |
+
+## 4. 决策(主理人已确认)
+
+| # | 决策点 | 结论 |
+|---|---|---|
+| D1 | 查看功能粒度 | **计数 + 可下钻明细**(计数卡片 → 展开明细列表) |
+| D2 | 「管理员」语义 | **家庭主账号**(`families.creator_id` 对应用户)填写真实姓名触发改名 |
+| D3 | 明细数据量 | 任务多(最大 346),overview 只回计数 + 报告/方案全量(≤8/≤4),任务明细走独立分页接口 |
+| D4 | 报告是否过滤 status | 不过滤,**全量返回并展示 status 列**(管理端需看到 archived/deleted 以便排查) |
+| D5 | 方案是否展示内容 | 展示 `goal` + `member_name` + `status` + `total_days`,**不返回 `plan_content`/`plan_json` 大字段**(避免响应体膨胀) |
+| D6 | 命名规则来源 | 真实姓名 `users.real_name` 非空 → `name = real_name + "家庭"`;否则保持现有 `nickname + "的家庭"` / `"我的家庭"` |
+| D7 | 改名触发时机 | ① 创建家庭时按新规则命名;② 用户更新资料(`realName` 变化)时联动;③ 存量一次性回填迁移 |
+| D8 | 改名冲突 | 用户手工改过的家庭名会被覆盖——**接受**(主理人要求「家庭名称改成真实姓名的XX家庭」) |
+| D9 | realName 清空 | 用户把真实姓名**清空**时不自动回退家庭名(需求只规定「填写后→改名」,清空不触发;家庭名维持「XX家庭」)——**实现者自行决定,无需上报** |
+| D10 | 更新资料事务 | `updateUserInfo` 不加 `@Transactional`;家庭名联动失败仅记日志、不阻断用户资料保存(家庭名为非关键展示值)——**实现者自行决定** |
+
+## 5. 设计
+
+### 5.1 后端:家庭信息查看
+
+**新增 DTO**(`com.etotem.cfc.dto`):
+
+```
+FamilyOverviewDTO
+  family: Map<String,Object>        // id, name, inviteCode, creatorId, createdAt, province/city/district/street
+  creatorName: String                // 主账号昵称(展示用)
+  creatorRealName: String            // 主账号真实姓名(展示用,可能为空)
+  counts: FamilyOverviewCounts
+    reports: int
+    plans: int
+    tasks: int
+    reportTypeCounts: Map<String,Integer>   // gut_flora:N, brain_status:N ...
+    taskStatusCounts: Map<String,Integer>   // pending:N, completed:N ...
+  reports: List<FamilyReportItemVO>
+  plans: List<FamilyPlanItemVO>
+```
+
+**新增 Service**:`FamilyAdminOverviewService`(`com.etotem.cfc.service`)
+- `FamilyOverviewDTO buildOverview(Long familyId)`:
+  - 校验 family 存在,否则抛 `RuntimeException("家庭不存在")`
+  - 计数:`selectCount` 返回 `Long`,字段类型用 `long`;分组计数用 **`selectMaps` + `groupBy`**(`QueryWrapper.select("report_type as k","count(*) as v").eq("family_id",fid).groupBy("report_type")`,结果映射到 `Map<String,Integer>`)
+  - 报告明细:`LambdaQueryWrapper.eq(familyId).orderByDesc(report_date).orderByDesc(id)`,**全量返回**(最大 8 条)
+  - 方案明细:`eq(familyId).orderByDesc(created_at).orderByDesc(id)`,**全量返回**(最大 4 条)
+  - 任务明细:**不在此返回**,仅计数
+- `Page<Task> pageTasks(Long familyId, long page, long size)`:任务分页,按 `created_at desc, id desc`
+
+**新增 Controller**(新增独立 `AdminFamilyOverviewController`,路径 `/api/admin`):
+为避免把 AdminController 继续撑大,单独建控制器:
+
+| 方法 | 路径 | 入参 | 返回 |
+|---|---|---|---|
+| POST | `/api/admin/families/{id}/overview` | — | `Result<FamilyOverviewDTO>` |
+| POST | `/api/admin/families/{id}/tasks` | `{page, size}` | `Result<Page<TaskItemVO>>` |
+
+均校验 `role == "admin"`(与现有 `getFamilyMembers` 同款校验:`@RequestAttribute("role") String role`)。
+任务分页默认 `size=20`,上限 100。
+
+**VO 字段裁剪**(避免响应体膨胀):
+- `FamilyReportItemVO`: `id, reportType, reportNumber, reportDate, personName, subjectId, status, overallScore, gutHealthScore, createdAt`
+- `FamilyPlanItemVO`: `id, planType, memberName, goal, status, totalDays, teacherId, createdAt`
+- `TaskItemVO`: `id, title, status, category, points, deadline, childId, executorType, taskType, sourceType, createdAt, completedAt`
+
+**边界处理**:
+- `creatorId` 为 NULL → `creatorName/creatorRealName` 返回空串(null-guard)
+- `personName` / `memberName` / `overallScore` 可空 → 前端展示 `-`(`StatusTag` 自带 `fallback="-"`)
+- 报告 `status`(active/deleted/archived)→ 前端用 `StatusTag` 展示:active=成功绿、archived=警告、deleted=信息灰
+
+### 5.2 后端:家庭命名「XX家庭」
+
+**新增工具方法**(放 `FamilyMemberService` 私有方法,或抽到 `FamilyNamingSupport`):
+
+```java
+static String buildFamilyName(User user) {
+    String realName = user.getRealName();
+    if (realName != null && !realName.trim().isEmpty()) {
+        return realName.trim() + "家庭";
+    }
+    String nickname = user.getNickname();
+    return (nickname != null && !nickname.isEmpty()) ? nickname + "的家庭" : "我的家庭";
+}
+```
+
+**触发点 1 — 创建家庭**:`FamilyMemberService.createFamilyForUser(user)` 第 913-915 行
+把 `family.setName(familyName)` 改为 `family.setName(buildFamilyName(user))`。
+
+**触发点 2 — 更新资料联动**:`UserService.updateUserInfo(userId, dto)`
+在 `userMapper.updateById(user)` 之后,若 `dto.getRealName()` 非空且与原值不同:
+- 查该用户作为 `creator_id` 的家庭(`families.creator_id = userId`)
+- 存在则 `family.name = realName.trim() + "家庭"` 并更新
+- **不加 `@Transactional`**;家庭名联动失败仅 `log.warn`,不阻断用户资料保存(D10)
+
+**触发点 3 — 存量回填**(一次性迁移,放 `DatabaseInitializer.runMigrations()` 新迁移号,幂等):
+
+```sql
+UPDATE families f
+JOIN users u ON u.id = f.creator_id
+SET f.name = CONCAT(TRIM(u.real_name), '家庭')
+WHERE u.real_name IS NOT NULL AND TRIM(u.real_name) <> ''
+  AND f.name <> CONCAT(TRIM(u.real_name), '家庭');
+```
+
+> **先只读 dry-run 查影响行数**,把结果报给主理人确认后再决定是否执行实际 UPDATE。
+> 迁移放在 `DatabaseInitializer`(幂等、可重复启动执行),与项目既有迁移约定一致。
+
+### 5.3 前端:管理端家庭列表页
+
+**新增 API**(`cfc-web/src/api/admin.js`):
+```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 }) }
+```
+
+**新增组件**:`cfc-web/src/views/admin/components/FamilyOverviewDialog.vue`
+- props: `visible`(sync)、`familyId`
+- 结构:
+  1. `el-descriptions` 家庭基础信息(名称 / 邀请码 / 创建时间 / 主账号)
+  2. **计数卡片区**:3 个 `StatCard`(报告 / 方案 / 任务),点击切换下方 tab;报告卡片下附类型分布小标签
+  3. **明细区**:`el-tabs`
+     - 「报告」tab:`el-table` 全量(列:类型 / 报告编号 / 报告日期 / 姓名 / 状态 / 总分 / 肠道分),状态用 `StatusTag`
+     - 「方案」tab:`el-table` 全量(列:类型 / 成员 / 目标 / 状态 / 天数 / 创建时间)
+     - 「任务」tab:`el-table` + `el-pagination`(服务端分页,默认 20/页)
+- 报告类型中文映射复用 `ReportManagement.vue` 的 `gut_flora=菌群检测 / physical_exam=体检报告 / dan=DAN测评 / insurance=保单 / fortune=家庭周报`,补充 `brain_status=脑状态 / aptitude=先天潜能`
+- 方案状态映射复用 `HealthPlanManage.vue`:`draft=草稿 / pending_review=待审核 / published=已发布 / rejected=已驳回`;`plan_type` 映射 `health_plan=健康方案 / family_attribution_plan=家庭归因方案`
+- 任务状态映射复用 `Tasks.vue`:`pending=进行中 / pending_review=待审核 / completed=已完成`,补充 `locked=前置锁定 / assigned=待接受 / in_progress=进行中 / rejected=已拒绝 / overdue=已过期 / cancelled=已取消`(用 `StatusTag` 的 `options` 数组,`value`=任务 status)
+- **任务 tab 列**:标题 / 状态(StatusTag)/ 分类 / 积分 / 截止时间 / 来源类型 / 创建时间。`taskType`(onetime=一次性/recurring=循环)、`sourceType`(self/assigned/plan/health_plan/...)用 `StatusTag` `options` 映射,无映射值则原样显示(`fallback`)
+- **方案 tab 列**:类型(plan_type 中文)/ 成员 / 目标(goal,超长省略)/ 状态(StatusTag)/ 天数 / 创建时间
+- **报告 tab 列**:类型(reportType 中文)/ 报告编号 / 报告日期 / 姓名 / 状态(StatusTag)/ 总分 / 肠道分
+
+**接入 Families.vue**:
+- 「更多」下拉菜单新增一项「查看信息」,`command="viewOverview"`,`@click` 走 `handleActionCmd(row,'viewOverview')`(操作列是 **250px fixed + 更多下拉**,无需改列宽)
+- `handleActionCmd` 的 switch 加 `case 'viewOverview': this.handleViewOverview(row); break;`
+- `handleViewOverview(row)` → `this.overviewDialogVisible = true; this.overviewFamilyId = row.id`
+- 引入 `FamilyOverviewDialog` 并挂到模板末尾(data 增加 `overviewDialogVisible`、`overviewFamilyId`)
+
+**注意**:现有编辑弹窗可手工改家庭名 → 与 G2 自动命名可能互相覆盖(见 D8,已接受)。
+
+## 6. 测试与验证
+
+### 单元测试(后端,JUnit + Mockito,随项目既有测试约定)
+| 测试 | 断言 |
+|---|---|
+| `FamilyAdminOverviewServiceTest` | 计数与明细条数正确;报告/方案全量返回;任务不在 overview 内 |
+| `FamilyNamingTest` | `buildFamilyName`:有 realName → 「张三家庭」;realName 空 → 回退 nickname 规则;realName 全空格 → 回退 |
+| `AdminFamilyOverviewControllerTest` | 非 admin 角色 → 拒绝;家庭不存在 → 错误提示 |
+
+### 前端验证(手工,vue-cli dev)
+- 家庭列表页点「查看信息」→ 弹窗显示计数卡片 → 切换三个 tab → 任务 tab 翻页正常
+- 真实姓名已填写的家庭(如 creator real_name 非空)→ 列表「家庭名称」列显示「XX家庭」
+
+### 部署验证(按 AGENTS.md 部署流程)
+1. `git status` 干净 → `git fetch && git pull --rebase origin cfclub` → 提交推送
+2. 备份 `/app/pro/cfc`(tar)**+ 备份 `families` 表受影响行**(mysqldump)
+3. 后端 `mvn clean package -Dmaven.test.skip=true` → 复制 jar → `systemctl restart cfc-backend`
+4. 前端 `npm run build` → `rsync -a --delete dist/ /app/pro/cfc/frontend/`
+5. 后端健康:`systemctl status cfc-backend` + `curl POST /api/cart/list` 预期 200
+6. 日志检查:新 PID 无 ERROR
+7. 存量回填:**dry-run 先报行数给主理人**,确认后再执行
+8. 管理端页面冒烟:`/admin/` 返回 200,家庭列表正常加载
+
+## 7. 风险
+
+| 风险 | 缓解 |
+|---|---|
+| 存量回填覆盖管理员手工命名的家庭 | 主理人已接受(D8);dry-run 先报数量 |
+| overview 响应体过大 | 任务明细走分页接口(D3);报告/方案已确认上限 8/4 |
+| 任务明细接口被高频调用 | 默认 20/页、上限 100;管理端内部使用 |
+| `FamilyMemberService` 单文件已很大(1400+ 行),继续加方法 | 命名方法抽到独立 `FamilyNamingSupport` 静态类,便于单测 |
+| 前端操作列变宽挤压 | 操作列已是 250px fixed + 更多下拉,「查看信息」加入下拉菜单,**不改列宽** |
+| 用户更新资料触发家庭名变更,可能与管理员改名冲突 | 接受(D8),行为在文档中明确 |
+| 分组计数用 `selectCount` 会静默出错 | 强制用 `selectMaps + groupBy`(已在 3.1 核验表标注,代码评审检查项) |
+| `creator_id` 为 NULL 的 5 个家庭拿不到主账号信息 | null-guard 返回空串;命名迁移 INNER JOIN 跳过(不误改) |
+| `updateUserInfo` 无事务,家庭名联动可能部分失败 | 家庭名为非关键展示值,失败仅告警日志(D10) |
+
+## 7.1 明确排除的边界情况
+
+- 真实姓名为空/全空格 → 家庭名回退现有 nickname 规则(D6)
+- 用户清空真实姓名 → 家庭名**不自动回退**,维持「XX家庭」(D9)
+- `creator_id` 为 NULL → 跳过改名迁移,overview 主账号信息显示空
+- 报告 `person_name` 为空 → 前端显示 `-`
+- 任务 `deadline` 为 NULL(进度型任务常见)→ 前端显示 `-`
+
+## 8. 交付物
+
+- 后端:`FamilyAdminOverviewService`、`AdminFamilyOverviewController`、`FamilyOverviewDTO` + 3 个 VO、`FamilyNamingSupport`、`UserService`/`FamilyMemberService` 联动改动、`DatabaseInitializer` 新迁移
+- 后端测试:3 个测试类
+- 前端:`api/admin.js` 2 个新方法、`FamilyOverviewDialog.vue`、`Families.vue` 接入
+- 文档:本设计文档(`docs/superpowers/specs/2026-10-30-family-overview-naming-design.md`)
+
+## 9. 留给实现者的问题(已全部解答)
+
+- 命名规则优先级 → D6(real_name 优先,回退 nickname)
+- 触发时机 → D7(三处:创建 / 更新资料联动 / 存量迁移)
+- 明细是否分页 → D3(任务分页,报告方案全量)
+- 报告是否过滤 status → D4(全量返回,展示 status 列)
+- 方案是否返回大字段 → D5(不返回 plan_content/plan_json)
+- 存量回填如何执行 → 第 6 节 dry-run → 确认 → 执行
+- 分组计数用什么 API → `selectMaps + groupBy`(3.1 核验表)
+- 计数类型 → `long`(MP 3.5.3.1 `selectCount` 返回 `Long`)
+- 更新资料是否加事务 → 不加(D10)
+- realName 清空是否回退 → 不回退(D9)
+- creatorId 为 NULL 怎么办 → null-guard + 迁移跳过(3.1 / 7.1)
+- 任务/方案 tab 显示哪些列、枚举如何中文化 → 5.3 末四条
+
+## 10. Builder 复审记录
+
+设计文档初稿经 builder 子代理复审,提出 8 项待核实:
+1. 管理端鉴权属性/角色值 → **已核实成立**(`JwtInterceptor:153`)
+2. MP 分组计数与 Long/int → **已修正设计**(`selectMaps + groupBy`,字段 `long`)
+3. `updateUserInfo` 联动可行性 → **已核实**(DTO 有 `realName`、已注入 `FamilyMapper`、无事务→D10)
+4. `creatorId` NULL / realName 清空 → **已补设计**(null-guard + D9)
+5. 前端组件 props 契约 → **已核实**(StatCard 支持 click、StatusTag 支持 options、Element UI 2.15.13)
+6. 任务/方案枚举标签缺口 → **已补 5.3 列定义与映射**
+7. 软删除与列名 → **已核实**(Task 无 `@TableLogic`、无 deleted 列;member_name 单值)
+8. 代码坐标漂移 → **已核对准确**(实现时按语义定位,不硬编码行号)
+
+结论:8 项均已闭环,无需再向上追问。