|
|
@@ -0,0 +1,334 @@
|
|
|
+# 家庭成员关系转换需求设计
|
|
|
+
|
|
|
+> **文档版本**: v1.0
|
|
|
+> **状态**: 已完成(代码已实现)
|
|
|
+> **日期**: 2026-07-07
|
|
|
+> **功能名称**: 家庭成员关系视角转换 / 相对关系标签
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 一、需求概述
|
|
|
+
|
|
|
+### 1.1 业务背景
|
|
|
+
|
|
|
+同一个家庭中,不同身份的用户看到的"我"和"其他成员的关系标签"不同。例如:
|
|
|
+
|
|
|
+| 登录身份 | 家庭成员关系显示 |
|
|
|
+|---------|----------------|
|
|
|
+| 儿子(child)登录 | **我** - 爸爸 - 妈妈 |
|
|
|
+| 爸爸(parent)登录 | **我** - 儿子 - 老婆 |
|
|
|
+| 女儿登录,排行老大 | **我** - 哥哥 - 弟弟 - 爸爸 - 妈妈 |
|
|
|
+| 妈妈登录 | **我** - 儿子 - 女儿 - 爸爸 |
|
|
|
+
|
|
|
+系统中所有展示家庭成员关系的地方(家庭关系图谱、成员列表、家庭能量条等),关系标签都应该基于**当前登录用户的视角**动态计算。
|
|
|
+
|
|
|
+### 1.2 目标
|
|
|
+
|
|
|
+- 不同身份登录时,同一成员显示不同的关系标签
|
|
|
+- 标签符合中文亲属称谓习惯
|
|
|
+- 支持多子女家庭的长幼排序(长子、次子、三子...)
|
|
|
+- 支持兄弟姐妹关系(按年龄区分哥哥/姐姐/弟弟/妹妹)
|
|
|
+- 支持配偶关系(丈夫/妻子)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 二、业务规则
|
|
|
+
|
|
|
+### 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 兄弟姐妹年龄判断规则
|
|
|
+
|
|
|
+基于双方生日判断:
|
|
|
+- 查看者生日 > 被查看者生日 → 被查看者是**弟弟/妹妹**
|
|
|
+- 查看者生日 < 被查看者生日 → 被查看者是**哥哥/姐姐**
|
|
|
+- 生日相同 → 默认**哥哥/姐姐**
|
|
|
+- 无生日数据 → 默认**哥哥/姐姐**
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 三、技术实现
|
|
|
+
|
|
|
+### 3.1 核心方法
|
|
|
+
|
|
|
+**位置**: `cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java`
|
|
|
+
|
|
|
+```java
|
|
|
+private String computeRelativeLabel(FamilyMember viewer, FamilyMember target, List<FamilyMember> allMembers)
|
|
|
+```
|
|
|
+
|
|
|
+**参数说明**:
|
|
|
+- `viewer`: 当前登录用户对应的家庭成员记录
|
|
|
+- `target`: 被查看的家庭成员记录
|
|
|
+- `allMembers`: 该家庭全部成员列表(用于多子女排序)
|
|
|
+
|
|
|
+**返回值**: 关系标签字符串("我"/"儿子"/"父亲"/"哥哥" 等)
|
|
|
+
|
|
|
+### 3.2 调用时机
|
|
|
+
|
|
|
+在 `listMembers()` 方法中,为每个 `FamilyMemberVO` 计算 `relativeLabel`:
|
|
|
+
|
|
|
+```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);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.3 数据模型
|
|
|
+
|
|
|
+**FamilyMemberVO.java** 相关字段:
|
|
|
+
|
|
|
+```java
|
|
|
+public class FamilyMemberVO {
|
|
|
+ // ...其他字段...
|
|
|
+
|
|
|
+ /** 视角相关的关系标签(如 "我"/"儿子"/"女儿"/"父亲"/"母亲"/"哥哥"/"姐姐") */
|
|
|
+ private String relativeLabel;
|
|
|
+
|
|
|
+ /** 计算后的实际角色(parent/child) */
|
|
|
+ private String effectiveRole;
|
|
|
+
|
|
|
+ /** 角色标签显示文本(家长/孩子/配偶/父母等) */
|
|
|
+ private String roleLabel;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.4 角色计算逻辑
|
|
|
+
|
|
|
+`computeEffectiveRole()` 决定用户是 parent 还是 child:
|
|
|
+
|
|
|
+```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();
|
|
|
+ }
|
|
|
+ // 3. 按年龄计算
|
|
|
+ if (member.getBirthday() != null) {
|
|
|
+ int age = calculateAge(member.getBirthday());
|
|
|
+ int threshold = systemConfigService.getChildAgeThreshold(); // 默认18岁
|
|
|
+ if (age < threshold) return "child";
|
|
|
+ }
|
|
|
+ return "parent";
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.5 API 响应
|
|
|
+
|
|
|
+`POST /api/family/member/list` 返回:
|
|
|
+
|
|
|
+```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
|
|
|
+ }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 四、字段说明
|
|
|
+
|
|
|
+| 字段 | 来源 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `relativeLabel` | `computeRelativeLabel()` 计算 | **视角相关**关系标签,不同查看者看到不同值 |
|
|
|
+| `relationshipTypeName` | `relationship_types` 表 | **绝对**关系名称,不随查看者变化 |
|
|
|
+| `effectiveRole` | `computeEffectiveRole()` 计算 | 角色(parent/child),用于业务判断 |
|
|
|
+| `roleLabel` | 根据 effectiveRole 推断 | 角色展示文本(家长/孩子) |
|
|
|
+
|
|
|
+### 4.1 字段区别
|
|
|
+
|
|
|
+```
|
|
|
+场景:爸爸(id=2)登录,查看儿子(id=1)
|
|
|
+
|
|
|
+字段对比:
|
|
|
+- relativeLabel = "儿子" ← 爸爸看儿子 → 儿子标签
|
|
|
+- relationshipTypeName = "儿子" ← 固定关系名(儿子)
|
|
|
+- effectiveRole = "child" ← 儿子的角色
|
|
|
+- roleLabel = "孩子" ← 角色文本
|
|
|
+```
|
|
|
+
|
|
|
+```
|
|
|
+场景:儿子(id=1)登录,查看爸爸(id=2)
|
|
|
+
|
|
|
+字段对比:
|
|
|
+- relativeLabel = "爸爸" ← 儿子看爸爸 → 父亲标签(口语化)
|
|
|
+- relationshipTypeName = "父亲" ← 固定关系名
|
|
|
+- effectiveRole = "parent" ← 爸爸的角色
|
|
|
+- roleLabel = "家长" ← 角色文本
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五、使用场景
|
|
|
+
|
|
|
+### 5.1 家庭关系图谱(FamilyRelationGraph.vue)
|
|
|
+
|
|
|
+Canvas 节点显示关系标签:
|
|
|
+- 当前用户节点:显示"我"
|
|
|
+- 其他成员节点:显示 `relativeLabel`
|
|
|
+
|
|
|
+### 5.2 家庭成员列表(FamilyMembers.vue)
|
|
|
+
|
|
|
+成员卡片展示关系标签:
|
|
|
+- 替换原有的 relationshipTypeName
|
|
|
+- 提升用户归属感("爸爸"比"父亲"更亲切)
|
|
|
+
|
|
|
+### 5.3 家庭能量条(FamilyEnergyBar)
|
|
|
+
|
|
|
+能量沙盘展示各成员时,使用 `relativeLabel` 标注成员身份。
|
|
|
+
|
|
|
+### 5.4 任务相关
|
|
|
+
|
|
|
+- 创建任务时选择执行者,显示 `relativeLabel` 而非 `nickname`
|
|
|
+- 任务通知中提及成员时使用 `relativeLabel`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 六、边界情况处理
|
|
|
+
|
|
|
+| 情况 | 处理方式 |
|
|
|
+|------|---------|
|
|
|
+| 查看者与被查看者是同一人 | 返回 `我` |
|
|
|
+| 被查看者无性别数据 + child 角色 | 默认 `孩子` |
|
|
|
+| 被查看者无性别数据 + parent 角色 | 默认 `父亲`(parent→parent 配偶情况默认 `配偶`) |
|
|
|
+| 无生日数据无法判断长幼(同龄) | 默认显示 `哥哥`/`姐姐` |
|
|
|
+| 子女数量超过10个 | 使用 `第11`、`第12` 等数字前缀 |
|
|
|
+| viewerMember 为 null(未登录) | 不计算 `relativeLabel`,返回 null |
|
|
|
+| 被查看者无角色(effectiveRole 为空) | fallback 返回 `roleLabel` 或 null |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 七、数据依赖
|
|
|
+
|
|
|
+| 依赖项 | 说明 | 状态 |
|
|
|
+|--------|------|------|
|
|
|
+| `family_members` 表 | 成员关系数据 | ✅ |
|
|
|
+| `relationship_types` 表 | 关系类型字典 | ✅ |
|
|
|
+| `system_config` 表 | child_age_threshold(未成年年龄分界线) | ✅ |
|
|
|
+| `FamilyMemberService.computeEffectiveRole()` | 角色计算 | ✅ |
|
|
|
+| `FamilyMemberService.computeRelativeLabel()` | 关系标签计算 | ✅ |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 八、相关文件清单
|
|
|
+
|
|
|
+| 文件路径 | 操作 | 说明 |
|
|
|
+|---------|------|------|
|
|
|
+| `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` | 已存在 | 关系类型实体 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 九、验收标准
|
|
|
+
|
|
|
+### 9.1 功能验收
|
|
|
+
|
|
|
+- [ ] 爸爸登录:看到所有子女的关系标签为"儿子"/"女儿"(或"长子"等)
|
|
|
+- [ ] 妈妈登录:看到所有子女的关系标签为"儿子"/"女儿"
|
|
|
+- [ ] 儿子登录:看到爸爸的关系标签为"爸爸"(而非"父亲")
|
|
|
+- [ ] 儿子登录:看到妈妈的关系标签为"妈妈"(而非"母亲")
|
|
|
+- [ ] 儿子登录:看到兄弟姐妹时,按年龄正确显示"哥哥"/"姐姐"/"弟弟"/"妹妹"
|
|
|
+- [ ] 子女超过3个时,正确显示"长子"、"次子"、"三子"
|
|
|
+- [ ] parent 看到配偶的关系标签为"丈夫"/"妻子"
|
|
|
+- [ ] 当前用户看到自己的标签为"我"
|
|
|
+
|
|
|
+### 9.2 数据一致性
|
|
|
+
|
|
|
+- [ ] `relativeLabel` 与 `effectiveRole`、`gender` 组合逻辑一致
|
|
|
+- [ ] 多子女排序按生日,不按 ID 或添加顺序
|
|
|
+- [ ] 无性别数据时有合理的 fallback 值
|
|
|
+
|
|
|
+### 9.3 性能
|
|
|
+
|
|
|
+- [ ] `computeRelativeLabel` 为纯内存计算,无额外数据库查询
|
|
|
+- [ ] listMembers 批量返回时无 N+1 查询问题
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十、后续优化方向
|
|
|
+
|
|
|
+1. **关系反向映射**:目前 parent→child 显示"儿子"/"女儿",未来可配置为显示"我"(如"爸爸的宝贝")
|
|
|
+2. **多语言支持**:export 模式时 relocations 可翻译为 "son"/"daughter"/"father"/"mother"
|
|
|
+3. **自定义称谓**:允许用户设置自己喜欢的称谓(如"老爸"vs"爸爸")
|
|
|
+4. **干父母/继父母等特殊关系**:当前规则未覆盖,可扩展 `RelationshipType` 表
|