|
|
@@ -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 | — | 长辈 / 晚辈 |
|