Ver código fonte

docs: add family member relationship conversion spec and plan

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User 2 meses atrás
pai
commit
65c6f55123

+ 364 - 0
docs/superpowers/plans/2026-07-07-家庭成员辈分重构实施计划.md

@@ -0,0 +1,364 @@
+# 家庭成员辈分重构实施计划
+
+> **文档版本**: v2.0
+> **状态**: 待开始
+> **日期**: 2026-07-07
+> **功能**: 用 generation(辈分)模型替代 relationship_types 关系类型定义
+> **依赖**: `docs/superpowers/specs/2026-07-07-家庭成员关系视角转换设计.md`
+
+---
+
+## 一、变更范围
+
+### 1.1 删除
+
+| 项 | 说明 |
+|----|------|
+| `relationship_types` 表 | 整体删除 |
+| `RelationshipType` 实体类 | 删除 |
+| `RelationshipTypeMapper` | 删除 |
+| `RelationshipTypeService` | 删除(如存在) |
+| `family_members.relationship_type` 列 | 迁移后删除 |
+| `family_members.role_override` 列 | 迁移后删除 |
+| 管理端「关系类型管理」页面和路由 | 删除 |
+| `FamilyMemberService` 中管理端关系类型方法 | 删除 |
+| 小程序 `RelationshipPicker.vue`(如存在) | 删除 |
+
+### 1.2 新增
+
+| 项 | 说明 |
+|----|------|
+| `family_members.generation` 列 | 辈分值,INT |
+| `family_members.is_spouse` 列 | 是否配偶(仅同辈有效),TINYINT(1) DEFAULT 0 |
+| `GenerationLevel` 枚举(Java) | 7级辈分:GREAT_GRANDPARENT / GRANDPARENT / PARENT / PEER / CHILD / GRANDCHILD / GREAT_GRANDCHILD |
+| `generationLevel.js`(前端常量) | 前端辈分选项定义 |
+| `GenerationPicker.vue`(小程序组件) | 辈分选择器 |
+
+### 1.3 修改
+
+| 项 | 说明 |
+|----|------|
+| `FamilyMember` 实体 | 删除 relationshipType/roleOverride;新增 generation/isSpouse |
+| `AddFamilyMemberDTO` | 替换 relationshipType → relativeMemberId + generationLevel + peerType |
+| `FamilyMemberVO` | 新增 generation;删除 relationshipType/relationshipTypeName |
+| `FamilyMemberService` | 重写 addMember()、computeEffectiveRole()、computeRelativeLabel()、listMembers()、toFamilyMemberVO() |
+| `FamilyMembersController` | 更新请求/响应 DTO |
+| `schema.sql` | 同步表结构 |
+| `DatabaseInitializer` | 新增迁移 |
+
+---
+
+## 二、数据库迁移(DatabaseInitializer)
+
+### 迁移 1:添加新列
+
+```java
+ensureColumn("family_members", "generation", "INT COMMENT '辈分值(0=家庭创建者,+n=向上,-n=向下)'");
+ensureColumn("family_members", "is_spouse", "TINYINT(1) DEFAULT 0 COMMENT '是否配偶(同辈)'");
+```
+
+### 迁移 2:旧数据 generation 赋值
+
+现有数据只有 4 种 relationship_type(spouse/parent/child/sibling),直接映射:
+
+```sql
+UPDATE family_members SET generation = 0, is_spouse = 1 WHERE relationship_type = 'spouse';
+UPDATE family_members SET generation = 1  WHERE relationship_type = 'parent';
+UPDATE family_members SET generation = -1 WHERE relationship_type IN ('child');
+UPDATE family_members SET generation = 0  WHERE relationship_type = 'sibling';
+UPDATE family_members SET generation = 0  WHERE generation IS NULL;
+```
+
+### 迁移 3:删除废弃列(确认无误后)
+
+```java
+try {
+    jdbcTemplate.execute("ALTER TABLE family_members DROP COLUMN relationship_type");
+    jdbcTemplate.execute("ALTER TABLE family_members DROP COLUMN role_override");
+} catch (Exception e) { /* 忽略 */ }
+```
+
+### 迁移 4:删除 relationship_types 表(确认无误后)
+
+```java
+try {
+    jdbcTemplate.execute("DROP TABLE IF EXISTS relationship_types");
+} catch (Exception e) { /* 忽略 */ }
+```
+
+---
+
+## 三、新增文件
+
+### 3.1 GenerationLevel.java
+
+路径:`cfc-backend/src/main/java/com/etotem/cfc/enums/GenerationLevel.java`
+
+```java
+package com.etotem.cfc.enums;
+
+public enum GenerationLevel {
+    GREAT_GRANDPARENT("great_grandparent", 3),
+    GRANDPARENT("grandparent", 2),
+    PARENT("parent", 1),
+    PEER("peer", 0),
+    CHILD("child", -1),
+    GRANDCHILD("grandchild", -2),
+    GREAT_GRANDCHILD("great_grandchild", -3);
+
+    private final String value;
+    private final int offset;
+
+    GenerationLevel(String value, int offset) {
+        this.value = value;
+        this.offset = offset;
+    }
+
+    public String getValue() { return value; }
+    public int getOffset() { return offset; }
+
+    public static GenerationLevel fromValue(String value) {
+        for (GenerationLevel gl : values()) {
+            if (gl.value.equals(value)) return gl;
+        }
+        throw new IllegalArgumentException("未知辈分等级: " + value);
+    }
+}
+```
+
+### 3.2 generationLevel.js
+
+路径:`cfc-frontend/utils/generationLevel.js`
+
+```javascript
+export const GENERATION_LEVELS = [
+  { value: 'great_grandparent', label: '曾祖辈', desc: '高3辈' },
+  { value: 'grandparent',       label: '祖辈',    desc: '高2辈' },
+  { value: 'parent',            label: '父母辈',  desc: '高1辈' },
+  { value: 'peer',              label: '同辈',    desc: '同辈' },
+  { value: 'child',             label: '子侄辈',  desc: '低1辈' },
+  { value: 'grandchild',        label: '孙辈',    desc: '低2辈' },
+  { value: 'great_grandchild',  label: '曾孙辈',  desc: '低3辈' }
+];
+
+export const PEER_TYPES = [
+  { value: 'sibling', label: '亲兄弟姐妹' },
+  { value: 'spouse',  label: '配偶' }
+];
+```
+
+---
+
+## 四、实体变更
+
+### 4.1 FamilyMember.java
+
+```java
+// 删除
+// private String relationshipType;
+// private String roleOverride;
+
+// 新增
+private Integer generation;   // 辈分值(0=家庭创建者)
+private Boolean isSpouse;     // 是否配偶
+
+// 便捷方法
+public boolean isMale() { return "male".equals(gender); }
+public boolean isFemale() { return "female".equals(gender); }
+```
+
+### 4.2 FamilyMemberVO.java
+
+```java
+// 新增
+private Integer generation;   // 辈分级
+
+// 删除
+// private String relationshipType;
+// private String relationshipTypeName;
+```
+
+### 4.3 AddFamilyMemberDTO.java(新建)
+
+```json
+{
+    "relativeMemberId": 2,
+    "generationLevel": "child",
+    "peerType": "sibling",
+    "nickname": "小明",
+    "gender": "male",
+    "birthday": "2013-05-01"
+}
+```
+
+---
+
+## 五、核心逻辑重写
+
+### 5.1 computeEffectiveRole()
+
+```java
+private String computeEffectiveRole(FamilyMember member) {
+    if (member.getGeneration() == null) {
+        // 兼容旧数据:按年龄 fallback
+        int age = calculateAge(member.getBirthday());
+        int threshold = systemConfigService.getChildAgeThreshold();
+        return age < threshold ? "child" : "parent";
+    }
+    return member.getGeneration() >= 0 ? "parent" : "child";
+}
+```
+
+### 5.2 computeRelativeLabel()
+
+纯 generation diff 计算,无树遍历,无直系/旁系区分:
+
+```java
+private String computeRelativeLabel(FamilyMember viewer, FamilyMember target, List<FamilyMember> allMembers) {
+    if (viewer.getId().equals(target.getId())) return "我";
+
+    int genDiff = target.getGeneration() - viewer.getGeneration();
+    boolean isSpouse = Boolean.TRUE.equals(target.getIsSpouse());
+
+    switch (genDiff) {
+        case 0:  return computePeerLabel(viewer, target, isSpouse);
+        case 1:  return target.isMale() ? "爸爸" : "妈妈";
+        case -1: return computeParentChildLabel(viewer, target, allMembers);
+        case 2:  return target.isMale() ? "爷爷" : "奶奶";
+        case -2: return target.isMale() ? "孙子" : "孙女";
+        case 3:  return target.isMale() ? "曾祖父" : "曾祖母";
+        case -3: return target.isMale() ? "曾孙" : "曾孙女";
+        default: return genDiff > 0 ? "长辈" : "晚辈";
+    }
+}
+```
+
+### 5.3 addMember()
+
+```java
+@Transactional
+public FamilyMemberVO addMember(Long userId, AddFamilyMemberDTO dto) {
+    User user = userMapper.selectById(userId);
+    if (user == null || user.getFamilyId() == null) {
+        throw new RuntimeException("用户未加入家庭");
+    }
+
+    FamilyMember relative = familyMemberMapper.selectById(dto.getRelativeMemberId());
+    if (relative == null || !relative.getFamilyId().equals(user.getFamilyId())) {
+        throw new RuntimeException("关联成员不存在");
+    }
+
+    int offset = GenerationLevel.fromValue(dto.getGenerationLevel()).getOffset();
+    int generation = relative.getGeneration() + offset;
+    boolean isSpouse = "peer".equals(dto.getGenerationLevel())
+                       && "spouse".equals(dto.getPeerType());
+
+    FamilyMember member = new FamilyMember();
+    member.setFamilyId(relative.getFamilyId());
+    member.setNickname(dto.getNickname().trim());
+    member.setGender(dto.getGender());
+    member.setBirthday(parseDate(dto.getBirthday()));
+    member.setGeneration(generation);
+    member.setIsSpouse(isSpouse);
+    member.setShowToFamily(1);
+    member.setTotalPoints(0);
+    member.setCreatedAt(new Date());
+    member.setUpdatedAt(new Date());
+
+    familyMemberMapper.insert(member);
+    return toFamilyMemberVO(member);
+}
+```
+
+---
+
+## 六、API 变更
+
+| 端点 | 操作 |
+|------|:----:|
+| `POST /api/family/member/add` | 请求体改为 relativeMemberId + generationLevel + peerType |
+| `POST /api/family/member/list` | 响应移除 relationshipType/relationshipTypeName,新增 generation |
+| `POST /api/family/member/update` | 同上 |
+| `GET /api/family/member/types` | 删除 |
+| `GET/POST /api/admin/relationship-type/*` | 删除 |
+
+---
+
+## 七、执行顺序
+
+### Phase 1:基础设施
+
+1. 新建 `GenerationLevel.java` 枚举
+2. 修改 `FamilyMember.java` 实体(删旧字段 + 增新字段)
+3. 新增 `AddFamilyMemberDTO.java`
+4. 修改 `FamilyMemberVO.java`
+5. DatabaseInitializer 迁移1:添加 generation + is_spouse 列
+6. DatabaseInitializer 迁移2:填充旧数据 generation
+
+### Phase 2:后端逻辑重写
+
+7. 重写 `FamilyMemberService.computeEffectiveRole()`
+8. 重写 `FamilyMemberService.computeRelativeLabel()`
+9. 重写 `FamilyMemberService.addMember()`
+10. 修改 `FamilyMemberService.listMembers()` 和 `toFamilyMemberVO()`
+11. 删除管理端关系类型管理方法
+12. 修改 `FamilyMembersController`
+13. `mvn compile` 验证
+
+### Phase 3:清理
+
+14. DatabaseInitializer 迁移3:删除废弃列
+15. DatabaseInitializer 迁移4:删除 relationship_types 表
+16. 同步 schema.sql
+17. 删除 RelationshipType.java、RelationshipTypeMapper.java 等文件
+
+### Phase 4:管理端(cfc-web)
+
+18. 删除关系类型管理页面
+19. 从 Layout.vue 移除菜单项
+20. 从 router 移除对应路由
+21. 成员添加页面替换为辈分选择器
+
+### Phase 5:小程序前端
+
+22. 新建 `generationLevel.js` 常量
+23. 新建 `GenerationPicker.vue` 组件
+24. 修改成员添加页面(替换关系类型选择 → 辈分选择)
+
+---
+
+## 八、验证步骤
+
+### 编译
+
+```bash
+cd cfc-backend && mvn clean compile
+```
+
+### API 验证
+
+```bash
+# 添加子侄辈
+curl -X POST http://localhost:9082/api/family/member/add \
+  -H "Content-Type: application/json" \
+  -d '{"relativeMemberId":2,"generationLevel":"child","nickname":"小明","gender":"male","birthday":"2013-05-01"}'
+
+# 添加配偶
+curl -X POST ... -d '{"relativeMemberId":1,"generationLevel":"peer","peerType":"spouse","nickname":"李女士","gender":"female"}'
+```
+
+### 迁移验证
+
+```sql
+SELECT id, nickname, generation, is_spouse FROM family_members WHERE family_id = ?;
+```
+
+---
+
+## 九、风险
+
+| 风险 | 缓解 |
+|------|------|
+| 旧代码引用 relationshipType | mvn compile + grep 全局搜索 |
+| 管理端关系类型管理被用户使用中 | 提前通知 |
+| 小程序缓存了 relationshipType 旧响应 | 后端过渡期兼容返回空值或清理缓存 |

+ 455 - 237
docs/superpowers/specs/2026-07-07-家庭成员关系视角转换设计.md

@@ -1,334 +1,552 @@
-# 家庭成员关系转换需求设计
+# 家庭成员关系视角转换设计
 
-> **文档版本**: v1.0
-> **状态**: 已完成(代码已实现)
+> **文档版本**: v3.0
+> **状态**: 设计定稿
 > **日期**: 2026-07-07
 > **功能名称**: 家庭成员关系视角转换 / 相对关系标签
 
 ---
 
-## 一、需求概述
+## 一、概述
 
-### 1.1 业务背景
+### 1.1 核心变更
 
-同一个家庭中,不同身份的用户看到的"我"和"其他成员的关系标签"不同。例如:
+移除 `relationship_types` 关系类型定义表,改为 **generation(辈分级)模型**。家庭成员的关系完全通过 **辈分差(generation diff)+ 性别 + 年龄** 动态计算,不需要预定义的关系类型字典,也不需要树结构(parentId)。
 
-| 登录身份 | 家庭成员关系显示 |
-|---------|----------------|
-| 儿子(child)登录 | **我** - 爸爸 - 妈妈 |
-| 爸爸(parent)登录 | **我** - 儿子 - 老婆 |
-| 女儿登录,排行老大 | **我** - 哥哥 - 弟弟 - 爸爸 - 妈妈 |
-| 妈妈登录 | **我** - 儿子 - 女儿 - 爸爸 |
+### 1.2 设计原则
 
-系统中所有展示家庭成员关系的地方(家庭关系图谱、成员列表、家庭能量条等),关系标签都应该基于**当前登录用户的视角**动态计算。
+| 原则 | 说明 |
+|------|------|
+| **无旁系亲属** | 姑姑/舅舅/叔叔/侄子/侄女/堂表亲等不在本项目家庭管理范围,即使生活在一起也不在本系统一个家庭中管理 |
+| **无 parentId** | 孩子不需要知道具体是哪个 parent,只需要知道他是 child 即可(用于登录限制) |
+| **多家庭共享老人** | 老人的信息可以在多个子女的家庭里分别出现(各自添加即可,无需跨家庭数据同步) |
+| **纯辈分计算** | relativeLabel 完全通过 `generation diff + gender + 年龄排序` 确定 |
 
-### 1.2 目标
+### 1.3 解决的关键问题
 
-- 不同身份登录时,同一成员显示不同的关系标签
-- 标签符合中文亲属称谓习惯
-- 支持多子女家庭的长幼排序(长子、次子、三子...)
-- 支持兄弟姐妹关系(按年龄区分哥哥/姐姐/弟弟/妹妹)
-- 支持配偶关系(丈夫/妻子)
+| 问题 | 原方案 | 新方案 |
+|------|--------|--------|
+| 祖孙关系无法准确计算 | parent/child 二值不够 | generation diff = ±2 区分 |
+| 需要维护关系类型字典 | 新增一种关系就要加一条 typeKey | 不需要,辈分是固定7级 |
+| 标签显示受限于类型定义 | typeKey 定死"长子"/"次子" | 按生日动态排序"长子"/"次子" |
+| 权限与标签混用同一字段 | defaultRole 既管权限又管标签 | 权限用 generation,标签用 generation diff |
 
 ---
 
-## 二、业务规则
-
-### 2.1 关系标签计算规则
-
-| 查看者角色 | 被查看者角色 | 性别 | 关系标签 |
-|-----------|------------|------|---------|
-| **自己** | — | — | `我` |
-| parent | child | male | `儿子`(独子女)或 `长子`/`次子`/`三子`...(多子女) |
-| parent | child | female | `女儿`(独子女)或 `长女`/`次女`/`三女`...(多子女) |
-| parent | child | 无 | `孩子` |
-| child | parent | male | `父亲` |
-| child | parent | female | `母亲` |
-| child | parent | 无 | `父亲` |
-| child | child(弟妹) | male | `弟弟` |
-| child | child(弟妹) | female | `妹妹` |
-| child | child(兄姐) | male | `哥哥` |
-| child | child(兄姐) | female | `姐姐` |
-| child | child(同龄) | male | `哥哥`(默认) |
-| child | child(同龄) | female | `姐姐`(默认) |
-| child | child(无性别) | — | `孩子` |
-| parent | parent(配偶) | male | `丈夫` |
-| parent | parent(配偶) | female | `妻子` |
-| parent | parent(配偶) | 无 | `配偶` |
-
-### 2.2 多子女排序规则
-
-1. 所有子女按**生日从小到大**排序(年长在前)
-2. 排序后按序号赋予前缀:`长`、`次`、`三`、`四`、`五`、`六`、`七`、`八`、`九`、`十`
-3. 超过十子女时使用数字:`第11`、`第12`...
-4. 例:三个儿子(生日依次为 2015/2018/2020年)→ `长子`、`次子`、`三子`
-
-### 2.3 兄弟姐妹年龄判断规则
-
-基于双方生日判断:
-- 查看者生日 > 被查看者生日 → 被查看者是**弟弟/妹妹**
-- 查看者生日 < 被查看者生日 → 被查看者是**哥哥/姐姐**
-- 生日相同 → 默认**哥哥/姐姐**
-- 无生日数据 → 默认**哥哥/姐姐**
+## 二、辈分体系(上下各三级)
 
----
+### 2.1 七级辈分定义
 
-## 三、技术实现
+| 辈分级 | Generation Offset | 名称 | 示例称谓 |
+|:------:|:-----------------:|:----:|:---------|
+| +3 | 上三级 | **曾祖辈** | 曾祖父、曾祖母 |
+| +2 | 上二级 | **祖辈** | 爷爷、奶奶 |
+| +1 | 上一级 | **父母辈** | 爸爸、妈妈 |
+| 0 | 同级 | **同辈** | 哥哥、姐姐、弟弟、妹妹、丈夫、妻子 |
+| -1 | 下一级 | **子侄辈** | 儿子、女儿 |
+| -2 | 下二级 | **孙辈** | 孙子、孙女 |
+| -3 | 下三级 | **曾孙辈** | 曾孙、曾孙女 |
 
-### 3.1 核心方法
+> 注意:不区分直系/旁系,不区分父系/母系,不区分"叔叔/姑姑/侄子/侄女/外公/外婆/堂表亲"等旁系称谓——这些不在本项目家庭管理范围内。
 
-**位置**: `cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java`
+### 2.2 Generation 值的定义
 
-```java
-private String computeRelativeLabel(FamilyMember viewer, FamilyMember target, List<FamilyMember> allMembers)
+```
+家庭创建者 generation = 0
+
+其他成员的 generation = 关联成员.generation + 辈分偏移
+  - 曾祖辈: offset = +3
+  - 祖辈:   offset = +2
+  - 父母辈: offset = +1
+  - 同辈:   offset = 0
+  - 子侄辈: offset = -1
+  - 孙辈:   offset = -2
+  - 曾孙辈: offset = -3
 ```
 
-**参数说明**:
-- `viewer`: 当前登录用户对应的家庭成员记录
-- `target`: 被查看的家庭成员记录
-- `allMembers`: 该家庭全部成员列表(用于多子女排序)
-
-**返回值**: 关系标签字符串("我"/"儿子"/"父亲"/"哥哥" 等)
-
-### 3.2 调用时机
+### 2.3 权限推导规则
 
-在 `listMembers()` 方法中,为每个 `FamilyMemberVO` 计算 `relativeLabel`
+`effectiveRole`(parent/child)直接从 generation 推导:
 
 ```java
-private List<FamilyMemberVO> toFamilyMemberVOList(List<FamilyMember> members, FamilyMember viewerMember) {
-    // ...
-    for (FamilyMember fm : members) {
-        FamilyMemberVO vo = toFamilyMemberVO(fm);
-        // 设置视角相关的关系标签
-        if (viewerMember != null) {
-            vo.setRelativeLabel(computeRelativeLabel(viewerMember, fm, members));
-        }
-        result.add(vo);
+public String computeEffectiveRole(FamilyMember member) {
+    if (member.getGeneration() == null) {
+        // 兼容旧数据,按年龄 fallback
+        int age = calculateAge(member.getBirthday());
+        int threshold = systemConfigService.getChildAgeThreshold(); // 默认18
+        return age < threshold ? "child" : "parent";
     }
-    return result;
+    return member.getGeneration() >= 0 ? "parent" : "child";
 }
 ```
 
-### 3.3 数据模型
+不再需要 `roleOverride`、`defaultRole`、年龄阈值等复杂逻辑。
 
-**FamilyMemberVO.java** 相关字段:
+### 2.4 多家庭老人场景
 
-```java
-public class FamilyMemberVO {
-    // ...其他字段...
+同一个老人可以在多个子女家庭中分别添加(例如在 A 家为 奶奶,在 B 家也为 奶奶)。每个家庭各自管理老人的数据(健康报告、任务等),之间不共享。老人数据在各家独立。
 
-    /** 视角相关的关系标签(如 "我"/"儿子"/"女儿"/"父亲"/"母亲"/"哥哥"/"姐姐") */
-    private String relativeLabel;
+---
+
+## 三、关系标签计算(relativeLabel)
 
-    /** 计算后的实际角色(parent/child) */
-    private String effectiveRole;
+### 3.1 核心算法
 
-    /** 角色标签显示文本(家长/孩子/配偶/父母等) */
-    private String roleLabel;
+```java
+private String computeRelativeLabel(FamilyMember viewer, FamilyMember target) {
+    if (viewer.getId().equals(target.getId())) return "我";
+    
+    int genDiff = target.getGeneration() - viewer.getGeneration();
+    boolean isSpouse = Boolean.TRUE.equals(target.getIsSpouse());
+    
+    switch (genDiff) {
+        case 0:  return computePeerLabel(viewer, target, isSpouse);
+        case 1:  return target.isMale() ? "爸爸" : "妈妈";
+        case -1: return target.isMale() ? "儿子" : "女儿";
+        case 2:  return target.isMale() ? "爷爷" : "奶奶";
+        case -2: return target.isMale() ? "孙子" : "孙女";
+        case 3:  return target.isMale() ? "曾祖父" : "曾祖母";
+        case -3: return target.isMale() ? "曾孙" : "曾孙女";
+        default: return genDiff > 0 ? "长辈" : "晚辈";
+    }
 }
 ```
 
-### 3.4 角色计算逻辑
-
-`computeEffectiveRole()` 决定用户是 parent 还是 child:
+### 3.2 同辈标签规则
 
 ```java
-public String computeEffectiveRole(FamilyMember member) {
-    // 1. roleOverride 非 auto → 直接返回
-    if (member.getRoleOverride() != null && !"auto".equals(member.getRoleOverride())) {
-        return member.getRoleOverride();
-    }
-    // 2. 取关系类型模板值
-    RelationshipType relType = relationshipTypeMapper.selectOne(
-        new LambdaQueryWrapper<RelationshipType>()
-            .eq(RelationshipType::getTypeKey, member.getRelationshipType()));
-    if (relType != null && relType.getDefaultRole() != null) {
-        return relType.getDefaultRole();
+private String computePeerLabel(FamilyMember viewer, FamilyMember target, boolean isSpouse) {
+    if (isSpouse) {
+        return target.isMale() ? "丈夫" : "妻子";
     }
-    // 3. 按年龄计算
-    if (member.getBirthday() != null) {
-        int age = calculateAge(member.getBirthday());
-        int threshold = systemConfigService.getChildAgeThreshold(); // 默认18岁
-        if (age < threshold) return "child";
+    // 兄弟姐妹:按年龄判断
+    if (viewer.getBirthday() != null && target.getBirthday() != null) {
+        if (viewer.getBirthday().before(target.getBirthday())) {
+            // viewer 更年长 → target 是弟/妹
+            return target.isMale() ? "弟弟" : "妹妹";
+        } else if (viewer.getBirthday().after(target.getBirthday())) {
+            // viewer 更年幼 → target 是哥/姐
+            return target.isMale() ? "哥哥" : "姐姐";
+        }
     }
-    return "parent";
+    // 同龄或无生日 → 默认哥/姐
+    return target.isMale() ? "哥哥" : "姐姐";
 }
 ```
 
-### 3.5 API 响应
+### 3.3 多子女排序(parent→child 视角)
 
-`POST /api/family/member/list` 返回
+当家长(parent)看所有同辈的孩子时,按生日从小到大排序加上长幼前缀
 
-```json
-{
-  "code": 200,
-  "data": [
-    {
-      "id": 1,
-      "nickname": "小明",
-      "relationshipType": "son",
-      "relationshipTypeName": "儿子",
-      "effectiveRole": "child",
-      "roleLabel": "孩子",
-      "relativeLabel": "我",
-      "gender": "male",
-      "age": 12,
-      "birthday": "2013-05-01"
-    },
-    {
-      "id": 2,
-      "nickname": "张先生",
-      "relationshipType": "father",
-      "relationshipTypeName": "父亲",
-      "effectiveRole": "parent",
-      "roleLabel": "家长",
-      "relativeLabel": "爸爸",
-      "gender": "male",
-      "age": 40
-    },
-    {
-      "id": 3,
-      "nickname": "李女士",
-      "relationshipType": "mother",
-      "relationshipTypeName": "母亲",
-      "effectiveRole": "parent",
-      "roleLabel": "家长",
-      "relativeLabel": "妈妈",
-      "gender": "female",
-      "age": 38
+```java
+private String computeParentChildLabel(FamilyMember viewer, FamilyMember target, List<FamilyMember> allMembers) {
+    // 收集所有同家庭的 child 成员(genDiff == -1)
+    List<FamilyMember> children = allMembers.stream()
+        .filter(m -> !m.getId().equals(viewer.getId())
+                  && m.getGeneration() != null
+                  && m.getGeneration() == viewer.getGeneration() - 1)
+        .sorted(Comparator.comparing(FamilyMember::getBirthday, 
+                Comparator.nullsLast(Comparator.naturalOrder())))
+        .collect(Collectors.toList());
+    
+    int index = -1;
+    for (int i = 0; i < children.size(); i++) {
+        if (children.get(i).getId().equals(target.getId())) {
+            index = i;
+            break;
+        }
     }
-  ]
+    if (index < 0) {
+        return target.isMale() ? "儿子" : "女儿";
+    }
+    
+    String[] ordinals = {"长", "次", "三", "四", "五", "六", "七", "八", "九", "十"};
+    String prefix = index < ordinals.length ? ordinals[index] : "第" + (index + 1);
+    if (target.isMale()) return prefix + "子";
+    if (target.isFemale()) return prefix + "女";
+    return prefix + "孩子";
 }
 ```
 
 ---
 
-## 四、字段说明
+## 四、数据模型
+
+### 4.1 `family_members` 表变更
+
+| 变更 | 字段 | 类型 | 说明 |
+|:----:|------|:----:|------|
+| ❌ 删除 | `relationship_type` | varchar | 不再需要 |
+| ❌ 删除 | `role_override` | varchar | 不再需要 |
+| ✅ 新增 | `generation` | int | 辈分级,家庭创建者=0,向上为正向下为负 |
+| ✅ 新增 | `is_spouse` | tinyint(1) DEFAULT 0 | 同辈中标记是否为配偶关系 |
+| 🔵 保留 | `gender` | varchar | male / female |
+| 🔵 保留 | `birthday` | date | 用于年龄排序 |
 
-| 字段 | 来源 | 说明 |
-|------|------|------|
-| `relativeLabel` | `computeRelativeLabel()` 计算 | **视角相关**关系标签,不同查看者看到不同值 |
-| `relationshipTypeName` | `relationship_types` 表 | **绝对**关系名称,不随查看者变化 |
-| `effectiveRole` | `computeEffectiveRole()` 计算 | 角色(parent/child),用于业务判断 |
-| `roleLabel` | 根据 effectiveRole 推断 | 角色展示文本(家长/孩子) |
+### 4.2 删除 `relationship_types` 表
 
-### 4.1 字段区别
+整体删除 `relationship_types` 实体、Mapper、Service 引用。
 
+### 4.3 实体类变更
+
+```java
+public class FamilyMember {
+    // ...其他字段...
+    
+    // private String relationshipType;  // ❌ 删除
+    // private String roleOverride;       // ❌ 删除
+    
+    private Integer generation;       // ✅ 新增:辈分级
+    private Boolean isSpouse;         // ✅ 新增:是否配偶
+    
+    private String gender;          // 保留
+    private Date birthday;          // 保留
+    
+    // 便捷方法
+    public boolean isMale() { return "male".equals(gender); }
+    public boolean isFemale() { return "female".equals(gender); }
+}
 ```
-场景:爸爸(id=2)登录,查看儿子(id=1)
 
-字段对比:
-- relativeLabel = "儿子"     ← 爸爸看儿子 → 儿子标签
-- relationshipTypeName = "儿子"  ← 固定关系名(儿子)
-- effectiveRole = "child"     ← 儿子的角色
-- roleLabel = "孩子"           ← 角色文本
+### 4.4 FamilyMemberVO 变更
+
+```java
+public class FamilyMemberVO {
+    // ...其他字段...
+    
+    private String relativeLabel;     // 保留:视角相关关系标签
+    private String effectiveRole;     // 保留:parent/child(从 generation 推导)
+    private String roleLabel;         // 保留:角色展示文本
+    private Integer generation;       // ✅ 新增:辈分级
+    
+    // private String relationshipType;     // ❌ 删除
+    // private String relationshipTypeName; // ❌ 删除
+}
 ```
 
+---
+
+## 五、添加成员流程
+
+### 5.1 前端交互
+
+**步骤1** — 选择关联成员(参照点):
+> 与谁的关系:[家庭成员下拉列表 ★]
+> *显示昵称 + 关系标签,如"张先生(爸爸)"*
+
+**步骤2** — 选择辈分关系:
+> 辈分:[曾祖辈 / 祖辈 / 父母辈 / 同辈 / 子侄辈 / 孙辈 / 曾孙辈 ★]
+> *选中后预览效果,如"选择「子侄辈」→ 新成员将是张先生的子侄辈"*
+
+**步骤3** — 填写资料:
+> 昵称:[文本输入]
+> 性别:[男 / 女]
+> 生日:[日期选择]
+
+**步骤4** — 同辈附加确认(仅当选"同辈"时出现):
+> 关系类型:[亲兄弟姐妹 / 配偶]
+
+### 5.2 后端处理
+
+```java
+@Transactional
+public FamilyMemberVO addMember(Long userId, AddFamilyMemberDTO dto) {
+    // 1. 验证关联成员存在并同家庭
+    FamilyMember relative = familyMemberMapper.selectById(dto.getRelativeMemberId());
+    if (relative == null || !relative.getFamilyId().equals(user.getFamilyId())) {
+        throw new BusinessException("关联成员不存在");
+    }
+    
+    // 2. 计算 generation = relative.generation + offset
+    int offset = GenerationLevel.fromValue(dto.getGenerationLevel()).getOffset();
+    int generation = relative.getGeneration() + offset;
+    
+    // 3. 设置 isSpouse
+    boolean isSpouse = "peer".equals(dto.getGenerationLevel()) 
+                       && "spouse".equals(dto.getPeerType());
+    
+    // 4. 构建成员
+    FamilyMember member = new FamilyMember();
+    member.setFamilyId(relative.getFamilyId());
+    member.setNickname(dto.getNickname().trim());
+    member.setGender(dto.getGender());
+    member.setBirthday(parseBirthday(dto.getBirthday()));
+    member.setGeneration(generation);
+    member.setIsSpouse(isSpouse);
+    member.setShowToFamily(1);
+    member.setTotalPoints(0);
+    member.setCreatedAt(new Date());
+    member.setUpdatedAt(new Date());
+    
+    familyMemberMapper.insert(member);
+    
+    return toFamilyMemberVO(member);
+}
 ```
-场景:儿子(id=1)登录,查看爸爸(id=2)
 
-字段对比:
-- relativeLabel = "爸爸"     ← 儿子看爸爸 → 父亲标签(口语化)
-- relationshipTypeName = "父亲" ← 固定关系名
-- effectiveRole = "parent"    ← 爸爸的角色
-- roleLabel = "家长"           ← 角色文本
+### 5.3 辈分选择 UI 组件
+
+底部弹出式滚轮选择器(Picker):
+
+```
+┌─────────────────────────────────────┐
+│        选择辈分关系                    │
+├─────────────────────────────────────┤
+│  ○ 曾祖辈(高3辈)                     │
+│  ○ 祖辈(高2辈)                       │
+│  ○ 父母辈(高1辈)                     │
+│  ● 同辈                               │
+│  ○ 子侄辈(低1辈)                     │
+│  ○ 孙辈(低2辈)                       │
+│  ○ 曾孙辈(低3辈)                     │
+├─────────────────────────────────────┤
+│  "与张先生的同辈"                      │
+├─────────────────────────────────────┤
+│         [取消]    [确定]               │
+└─────────────────────────────────────┘
 ```
 
 ---
 
-## 五、使用场景
+## 六、API 变更
 
-### 5.1 家庭关系图谱(FamilyRelationGraph.vue)
+### 6.1 接口变更表
 
-Canvas 节点显示关系标签:
-- 当前用户节点:显示"我"
-- 其他成员节点:显示 `relativeLabel`
+| 端点 | 变更 | 说明 |
+|------|:----:|------|
+| `POST /api/family/member/add` | ✅ 请求体变更 | 接收 `relativeMemberId` + `generationLevel` + `peerType` |
+| `POST /api/family/member/list` | ✅ 响应变更 | 返回 `generation` 字段 |
+| `POST /api/family/member/update` | ✅ 请求体变更 | 同 add |
+| `POST /api/family/member/switch` | 🔵 不变 | 不受影响 |
+| `GET /api/family/member/types` | ❌ 删除 | 不再需要 |
+| `GET/POST /api/admin/relationship-type/*` | ❌ 删除 | 不再需要 |
 
-### 5.2 家庭成员列表(FamilyMembers.vue)
+### 6.2 添加成员请求体
 
-成员卡片展示关系标签:
-- 替换原有的 relationshipTypeName
-- 提升用户归属感("爸爸"比"父亲"更亲切)
-
-### 5.3 家庭能量条(FamilyEnergyBar)
+```json
+{
+    "relativeMemberId": 2,
+    "generationLevel": "child",
+    "peerType": "sibling",
+    "nickname": "小明",
+    "gender": "male",
+    "birthday": "2013-05-01"
+}
+```
 
-能量沙盘展示各成员时,使用 `relativeLabel` 标注成员身份。
+| 字段 | 必填 | 说明 |
+|------|:----:|------|
+| `relativeMemberId` | ✅ | 关联成员 ID(作为辈分参照点) |
+| `generationLevel` | ✅ | `great_grandparent` / `grandparent` / `parent` / `peer` / `child` / `grandchild` / `great_grandchild` |
+| `peerType` | 仅 peer | `sibling`:亲兄弟姐妹;`spouse`:配偶 |
+| `nickname` | ✅ | 昵称 |
+| `gender` | ✅ | male / female |
+| `birthday` | ❌ | yyyy-MM-dd 格式 |
 
-### 5.4 任务相关
+### 6.3 成员列表响应
 
-- 创建任务时选择执行者,显示 `relativeLabel` 而非 `nickname`
-- 任务通知中提及成员时使用 `relativeLabel`
+```json
+{
+    "code": 200,
+    "data": [
+        {
+            "id": 1,
+            "nickname": "小明",
+            "generation": -1,
+            "effectiveRole": "child",
+            "roleLabel": "孩子",
+            "relativeLabel": "我",
+            "gender": "male",
+            "age": 13
+        },
+        {
+            "id": 2,
+            "nickname": "张先生",
+            "generation": 0,
+            "effectiveRole": "parent",
+            "roleLabel": "家长",
+            "relativeLabel": "爸爸",
+            "gender": "male",
+            "age": 40
+        }
+    ]
+}
+```
 
 ---
 
-## 六、边界情况处理
+## 七、涉及文件变更清单
 
-| 情况 | 处理方式 |
-|------|---------|
-| 查看者与被查看者是同一人 | 返回 `我` |
-| 被查看者无性别数据 + child 角色 | 默认 `孩子` |
-| 被查看者无性别数据 + parent 角色 | 默认 `父亲`(parent→parent 配偶情况默认 `配偶`) |
-| 无生日数据无法判断长幼(同龄) | 默认显示 `哥哥`/`姐姐` |
-| 子女数量超过10个 | 使用 `第11`、`第12` 等数字前缀 |
-| viewerMember 为 null(未登录) | 不计算 `relativeLabel`,返回 null |
-| 被查看者无角色(effectiveRole 为空) | fallback 返回 `roleLabel` 或 null |
+### 7.1 删除
+
+| 文件 | 说明 |
+|------|------|
+| `cfc-backend/.../entity/RelationshipType.java` | 实体类,整体删除 |
+| `cfc-backend/.../mapper/RelationshipTypeMapper.java` | Mapper,整体删除 |
+| `cfc-backend/.../service/RelationshipTypeService.java` | Service,整体删除 |
+| `cfc-frontend/components/RelationshipPicker.vue`(如存在) | 关系类型选择组件,替换为辈分选择器 |
+| 管理端关系类型管理页面 | 整个页面删除 |
+
+### 7.2 修改
+
+| 文件 | 说明 |
+|------|------|
+| `cfc-backend/.../entity/FamilyMember.java` | 删除 relationshipType、roleOverride;新增 generation、isSpouse |
+| `cfc-backend/.../dto/FamilyMemberVO.java` | 删除 relationshipType/relationshipTypeName;新增 generation |
+| `cfc-backend/.../service/FamilyMemberService.java` | 重写 computeRelativeLabel()(无树遍历纯diff)、computeEffectiveRole()、addMember()、listMembers()、toFamilyMemberVO() |
+| `cfc-backend/.../controller/FamilyMembersController.java` | 更新 addMember/updateMember 接口 DTO 类型 |
+| `cfc-backend/.../config/DatabaseInitializer.java` | 新增迁移:添加 generation/is_spouse 列、迁移旧数据、删除废弃列 |
+| `cfc-backend/src/main/resources/schema.sql` | 同步表结构 |
+| `cfc-frontend/pages/family/member-add.vue`(或类似) | 替换关系类型选择为辈分选择器 |
+
+### 7.3 新增
+
+| 文件 | 说明 |
+|------|------|
+| `cfc-backend/.../enums/GenerationLevel.java` | 辈分等级枚举(7级 + offset 方法) |
+| `cfc-frontend/utils/generationLevel.js` | 前端辈分选项常量 |
+| `cfc-frontend/components/GenerationPicker.vue` | 辈分选择器组件(7级列表 + 预览) |
+| `cfc-backend/.../dto/AddFamilyMemberDTO.java` | 新增 DTO:relativeMemberId + generationLevel + peerType |
 
 ---
 
-## 七、数据依赖
+## 八、迁移方案
+
+### 8.1 数据迁移(DatabaseInitializer)
+
+迁移 1 — 新增列:
+```java
+ensureColumn("family_members", "generation", "INT COMMENT '辈分值'");
+ensureColumn("family_members", "is_spouse", "TINYINT(1) DEFAULT 0 COMMENT '是否配偶'");
+```
+
+迁移 2 — 填充 generation(基于 relationshipType):
+```sql
+-- 配偶与创建者同辈
+UPDATE family_members SET generation = 0, is_spouse = 1
+WHERE relationship_type = 'spouse';
+
+-- 父母辈
+UPDATE family_members SET generation = 1
+WHERE relationship_type = 'parent';
 
-| 依赖项 | 说明 | 状态 |
-|--------|------|------|
-| `family_members` 表 | 成员关系数据 | ✅ |
-| `relationship_types` 表 | 关系类型字典 | ✅ |
-| `system_config` 表 | child_age_threshold(未成年年龄分界线) | ✅ |
-| `FamilyMemberService.computeEffectiveRole()` | 角色计算 | ✅ |
-| `FamilyMemberService.computeRelativeLabel()` | 关系标签计算 | ✅ |
+-- 子女辈
+UPDATE family_members SET generation = -1
+WHERE relationship_type = 'child' OR relationship_type IN ('son', 'daughter');
+
+-- 兄弟姐妹 → 同辈
+UPDATE family_members SET generation = 0
+WHERE relationship_type = 'sibling';
+
+-- 其他或未匹配
+UPDATE family_members SET generation = 0
+WHERE generation IS NULL;
+```
+
+迁移 3 — 删除废弃列和表(确认无误后):
+```sql
+ALTER TABLE family_members DROP COLUMN relationship_type;
+ALTER TABLE family_members DROP COLUMN role_override;
+DROP TABLE IF EXISTS relationship_types;
+```
+
+### 8.2 部署顺序
+
+1. 数据库:添加 `generation` + `is_spouse` 列(可空)
+2. 运行数据迁移填充已有记录
+3. 部署新后端代码
+4. 更新管理端(删除关系类型管理页面)
+5. 确认无误后删除废弃列和表
+6. 更新小程序前端
 
 ---
 
-## 八、相关文件清单
+## 九、边界情况
 
-| 文件路径 | 操作 | 说明 |
-|---------|------|------|
-| `cfc-backend/.../service/FamilyMemberService.java` | 已实现 | `computeRelativeLabel()` 方法(419-517行) |
-| `cfc-backend/.../dto/FamilyMemberVO.java` | 已实现 | `relativeLabel` 字段(46-47行) |
-| `cfc-backend/.../service/FamilyMemberService.java` | 已实现 | `toFamilyMemberVOList()` 调用处(171-175行) |
-| `cfc-backend/.../service/FamilyMemberService.java` | 已实现 | `computeEffectiveRole()` 方法(384-417行) |
-| `cfc-backend/.../entity/FamilyMember.java` | 已存在 | 成员实体 |
-| `cfc-backend/.../entity/RelationshipType.java` | 已存在 | 关系类型实体 |
+| 情况 | 处理方式 |
+|:-----|:---------|
+| 查看者与被查看者是同一人 | 返回 `我` |
+| 无性别数据 | 使用 `孩子` / `家长` 等中性 fallback |
+| 无生日数据无法判断长幼 | 默认显示 `哥哥` / `姐姐`(年长优先级) |
+| generation diff > 3 或 < -3 | fallback 到 `长辈` / `晚辈` |
+| 家庭成员数超过 10 个子女 | 按生日排序使用 `长子`~`十子`,超过用 `第11子` |
+| viewerMember 为 null(未登录) | 不计算 relativeLabel,返回 null |
+| generation 为 null(旧数据未迁移) | computeEffectiveRole 按年龄 fallback |
 
 ---
 
-## 九、验收标准
+## 、验收标准
 
-### 9.1 功能验收
+### 10.1 功能验收
 
-- [ ] 爸爸登录:看到所有子女的关系标签为"儿子"/"女儿"(或"长子"等)
-- [ ] 妈妈登录:看到所有子女的关系标签为"儿子"/"女儿"
-- [ ] 儿子登录:看到爸爸的关系标签为"爸爸"(而非"父亲")
-- [ ] 儿子登录:看到妈妈的关系标签为"妈妈"(而非"母亲")
-- [ ] 儿子登录:看到兄弟姐妹时,按年龄正确显示"哥哥"/"姐姐"/"弟弟"/"妹妹"
-- [ ] 子女超过3个时,正确显示"长子"、"次子"、"三子"
-- [ ] parent 看到配偶的关系标签为"丈夫"/"妻子"
+- [ ] 爸爸(generation=0)登录 → 看子侄辈成员显示"儿子"或"女儿"
+- [ ] 妈妈(generation=0,is_spouse=true)登录 → 同上
+- [ ] 儿子(generation=-1)登录 → 看父母辈成员显示"爸爸"/"妈妈"
+- [ ] 儿子登录 → 看同辈按年龄显示"哥哥"/"弟弟"/"姐姐"/"妹妹"
+- [ ] 爸爸登录 → 看配偶显示"妻子"
+- [ ] 儿子登录 → 看祖辈成员显示"爷爷"/"奶奶"
+- [ ] 家长登录 → 看所有子女按年龄排序"长子"/"次子"/"三子"
 - [ ] 当前用户看到自己的标签为"我"
+- [ ] 添加子侄辈 → generation = -1
+- [ ] 添加祖辈 → generation = +2
+- [ ] 添加配偶 → is_spouse = true, generation = relative.generation
 
-### 9.2 数据一致性
+### 10.2 数据模型验收
 
-- [ ] `relativeLabel` 与 `effectiveRole`、`gender` 组合逻辑一致
-- [ ] 多子女排序按生日,不按 ID 或添加顺序
-- [ ] 无性别数据时有合理的 fallback 值
+- [ ] `family_members` 表不再有 `relationship_type` 和 `role_override` 字段
+- [ ] `relationship_types` 表和关联代码已删除
+- [ ] `generation` + `is_spouse` 字段已正确填充
+- [ ] 所有查询/更新接口不再引用已删除的字段
 
-### 9.3 性能
+### 10.3 性能
 
-- [ ] `computeRelativeLabel` 为纯内存计算,无额外数据库查询
-- [ ] listMembers 批量返回时无 N+1 查询问题
+- [ ] `computeRelativeLabel` 为纯内存计算,无额外查询
+- [ ] listMembers 批量返回时无 N+1 问题
+- [ ] 无 parentId 树的遍历开销
 
 ---
 
-## 十、后续优化方向
-
-1. **关系反向映射**:目前 parent→child 显示"儿子"/"女儿",未来可配置为显示"我"(如"爸爸的宝贝")
-2. **多语言支持**:export 模式时 relocations 可翻译为 "son"/"daughter"/"father"/"mother"
-3. **自定义称谓**:允许用户设置自己喜欢的称谓(如"老爸"vs"爸爸")
-4. **干父母/继父母等特殊关系**:当前规则未覆盖,可扩展 `RelationshipType` 表
+## 十一、命名对照表
+
+### 11.1 辈分等级标识
+
+| 后端枚举值 | offset | 前端显示 |
+|:----------:|:------:|:---------|
+| `great_grandparent` | +3 | 曾祖辈 |
+| `grandparent` | +2 | 祖辈 |
+| `parent` | +1 | 父母辈 |
+| `peer` | 0 | 同辈 |
+| `child` | -1 | 子侄辈 |
+| `grandchild` | -2 | 孙辈 |
+| `great_grandchild` | -3 | 曾孙辈 |
+
+### 11.2 同辈子类型
+
+| 值 | 显示 | 说明 |
+|:--:|:-----|:-----|
+| `sibling` | 亲兄弟姐妹 | 同辈非配偶 |
+| `spouse` | 配偶 | is_spouse = true |
+
+### 11.3 完整的 relativeLabel 映射表
+
+| genDiff | 条件 | 标签 |
+|:-------:|:----:|:----:|
+| 0 | isSpouse=true, male | 丈夫 |
+| 0 | isSpouse=true, female | 妻子 |
+| 0 | viewer 更年长, male | 弟弟 |
+| 0 | viewer 更年长, female | 妹妹 |
+| 0 | viewer 更年幼, male | 哥哥 |
+| 0 | viewer 更年幼, female | 姐姐 |
+| +1 | male | 爸爸 |
+| +1 | female | 妈妈 |
+| -1 | male | 儿子 |
+| -1 | female | 女儿 |
+| +2 | male | 爷爷 |
+| +2 | female | 奶奶 |
+| -2 | male | 孙子 |
+| -2 | female | 孙女 |
+| +3 | male | 曾祖父 |
+| +3 | female | 曾祖母 |
+| -3 | male | 曾孙 |
+| -3 | female | 曾孙女 |
+| >3 或 < -3 | — | 长辈 / 晚辈 |