|
|
@@ -0,0 +1,178 @@
|
|
|
+# 家庭加入流程:身份由邀请人指定 · 视角动态称谓
|
|
|
+
|
|
|
+- 状态:已裁决(2026-11-07,主理人确认口径 A + 一并修配偶称谓)
|
|
|
+- 溯源场景:`tests/user-scenarios/应用场景总览.md` → SC-P-02(邀请成员 + 关系编辑)、SC-X-04(家庭邀请闭环)、SC-G-04(邀请码注册加入家庭)
|
|
|
+- 影响模块:`FamilyInviteController` / `FamilyJoinRequestService` / `FamilyRelationshipService` / `FamilyMemberService` / `cfc-frontend/pages/invite/join-form.vue` / `cfc-frontend/pages/profile-extra/family-members.vue`
|
|
|
+
|
|
|
+## 1. 问题
|
|
|
+
|
|
|
+家庭身份是**相对关系**,不是账户自述属性。当前实现把它当成申请人的自填字段,导致三处断裂:
|
|
|
+
|
|
|
+| # | 问题 | 证据 |
|
|
|
+|---|---|---|
|
|
|
+| P1 | 申请加入时要求申请人自填「家庭身份」(爸爸/妈妈/…)。男主人邀请妻子,妻子只能自述「妈妈」,但男主人视角应看到「妻子」 | `join-form.vue:16-21`、`family_join_requests.family_role` |
|
|
|
+| P2 | 审批只弹确认框,无身份选择;通过后新建 `FamilyMember` 不写 `generation`/`isSpouse`,也不调用 `createRelationsForNewMember` → 该成员在关系图与关系标签中无辈分 | `FamilyJoinRequestService.java:195-225` |
|
|
|
+| P3 | 展示层同辈无配偶分支:`generationValue=0` 一律返回「兄弟/姐妹」,夫妻互称错误 | `FamilyRelationshipService.java:118-145`(`case 0`) |
|
|
|
+| P4 | 审批卡片只显示昵称,邀请人无法用真实姓名确认申请人 | `FamilyJoinRequestDTO` 无 `realName` |
|
|
|
+
|
|
|
+## 2. 目标与验收(R1–R5)
|
|
|
+
|
|
|
+- **R1**:加入申请表单移除「家庭身份」;保留「真实姓名」必填、昵称/头像选填。
|
|
|
+- **R2**:审批通过时,邀请人在弹窗内用 **GenerationPicker(与 `pages/family/add-member.vue` 同一组件、同一选项)** 指定加入者与自己的关系:辈分(曾祖辈/祖辈/父辈/同辈/子辈/孙辈/曾孙辈)+ 同辈时选「兄弟姐妹/配偶」。辈分必填;同辈时类型必填。
|
|
|
+- **R3**:审批通过后,加入者获得**绝对辈分**(`FamilyMember.generation` = 审批人成员辈分 + 选项 offset)与 `isSpouse`,并建立与全体现有成员的双向关系。
|
|
|
+- **R4**:展示层按查看者视角输出称谓:同辈且关系为配偶 → 对方 male=「丈夫」、female=「妻子」;同辈非配偶 →「兄弟/姐妹」;其余沿用辈分映射。
|
|
|
+- **R5**:加入申请卡片显示真实姓名(缺失回退昵称),不再显示申请人自述的身份。
|
|
|
+
|
|
|
+## 3. 领域模型裁决
|
|
|
+
|
|
|
+**身份 = 有向关系,不是用户属性。** `family_relationships` 已是有向结构(`from_member_id → to_member_id + generation_value`),本次不新建表,只补一个维度列:
|
|
|
+
|
|
|
+- `family_relationships.relation_type`:`lineal`(直系,默认)/ `sibling`(同辈非配偶)/ `spouse`(同辈配偶)。**双向各写一行,取值相同。**
|
|
|
+
|
|
|
+`User.familyRole`(爸爸/妈妈/…)保留,但改为**由绝对辈分 + 性别自动推导**写入,保证 `MemberCoverageDTO` / `MemberEnergyDTO` / `InsuranceGapDTO` 等下游不为空。推导表:
|
|
|
+
|
|
|
+| 绝对 generation | male | female |
|
|
|
+|---|---|---|
|
|
|
+| ≥2 | 爷爷 | 奶奶 |
|
|
|
+| 1 | 爸爸 | 妈妈 |
|
|
|
+| 0 或负 | 其他 | 其他 |
|
|
|
+
|
|
|
+> 「姥爷/姥姥」无法由辈分推导(母系信息缺失),统一落「爷爷/奶奶」,用户可在「我的资料」自行修正。
|
|
|
+
|
|
|
+## 4. 接口契约
|
|
|
+
|
|
|
+统一 `@PostMapping`、`Result<T>` 响应。
|
|
|
+
|
|
|
+### 4.1 `POST /api/family/invite/request-join-by-code`
|
|
|
+- 请求体:`{ inviteCode?, token? }`(`familyRole` 字段**保留接收但忽略**,不写库,避免老客户端 400)
|
|
|
+- 响应不变。
|
|
|
+
|
|
|
+### 4.2 `POST /api/user/join-family`
|
|
|
+- 同上:`familyRole` 保留接收但忽略。
|
|
|
+
|
|
|
+### 4.3 `POST /api/family/invite/approve-request`
|
|
|
+- 请求体(新 DTO `JoinApproveDTO`):`{ requestId: Long, comment?: String, generationLevel: String, peerType?: String }`
|
|
|
+- `generationLevel` ∈ `great_grandparent|grandparent|parent|peer|child|grandchild|great_grandchild`,必填
|
|
|
+- `peerType` ∈ `sibling|spouse`,**仅当 `generationLevel=peer` 时必填**
|
|
|
+- 校验失败返回 `Result.error("请选择辈分关系")` / `Result.error("请选择同辈关系类型")`
|
|
|
+- 成功:`Result.success()`
|
|
|
+
|
|
|
+### 4.4 `POST /api/family/invite/pending-requests`
|
|
|
+- 响应 `FamilyJoinRequestDTO` 新增字段:`requesterRealName`、`assignedGeneration`、`assignedPeerType`、`assignedPeerLabel`
|
|
|
+
|
|
|
+### 4.5 `POST /api/family/invite/accept`(直加通道)
|
|
|
+- 保留兼容,`familyRole` 忽略。**前端当前无调用点(`acceptFamilyInvite` 已无引用),不扩展。**
|
|
|
+
|
|
|
+## 5. 数据迁移(`DatabaseInitializer.runMigrations()`,编号续 454)
|
|
|
+
|
|
|
+```
|
|
|
+// 迁移454: family_relationships 增加关系类型列(支持夫妻/兄弟姐妹/直系区分)
|
|
|
+ensureColumn("family_relationships", "relation_type", "VARCHAR(20) DEFAULT 'lineal' COMMENT '关系类型: lineal/sibling/spouse'");
|
|
|
+
|
|
|
+// 迁移455: family_join_requests 记录审批时指定的身份(可追溯)
|
|
|
+ensureColumn("family_join_requests", "assigned_generation", "INT NULL COMMENT '审批时指定的绝对辈分'");
|
|
|
+ensureColumn("family_join_requests", "assigned_peer_type", "VARCHAR(10) NULL COMMENT '审批时指定的同辈类型: sibling/spouse'");
|
|
|
+```
|
|
|
+
|
|
|
+同时同步 `cfc-backend/src/main/resources/schema.sql` 中 `family_relationships` 与 `family_join_requests` 的 `CREATE TABLE` 定义。历史行 `relation_type` 落默认 `lineal`,不强制回填(`generation=0` 且非 spouse 时仍输出「兄弟/姐妹」,与旧行为一致)。
|
|
|
+
|
|
|
+## 6. 后端实现清单
|
|
|
+
|
|
|
+### 6.1 `FamilyRelationshipService`
|
|
|
+1. 新增 4 参:`createRelationsForNewMember(Long familyId, Long newMemberId, Integer newMemberGeneration, String peerType)`;保留 3 参重载委托(`peerType=null`)。
|
|
|
+2. 写入 `relation_type`:
|
|
|
+ - `exGen == newGen && "spouse".equals(peerType)` → 双向 `spouse`
|
|
|
+ - `exGen == newGen && "sibling".equals(peerType)` → 双向 `sibling`
|
|
|
+ - 其他 → `lineal`
|
|
|
+3. 同步写 `relationship_key`(冗余展示列):`spouse` 时按对方性别写 丈夫/妻子,其他沿用 `computeRelationshipLabel`。
|
|
|
+4. 新增 3 参 `computeRelationshipLabel(Integer generationValue, String gender, String relationType)`:
|
|
|
+ - `spouse` → `male ? "丈夫" : "妻子"`(**不再看 generationValue**,配偶必然同辈)
|
|
|
+ - `sibling` → `male ? "兄弟" : "姐妹"`
|
|
|
+ - `lineal`/null → 原逻辑;保留 2 参重载委托。
|
|
|
+5. `getGenerationMapFrom` 保持不变。
|
|
|
+
|
|
|
+### 6.2 `FamilyJoinRequestService.approveJoinRequest`
|
|
|
+签名扩展为 `(Long requestId, Long approverUserId, String comment, String generationLevel, String peerType)`,保留 3 参重载(`generationLevel=null` → 旧逻辑,仅写 familyRole):
|
|
|
+1. 校验 `GenerationLevel.fromValue(generationLevel) != null`;`PEER` 时 `peerType` ∈ {sibling, spouse}。
|
|
|
+2. 计算锚点:按 `(familyId, approverUserId)` 查审批人的 `FamilyMember`,取其 `generation`(null 视为 0)。**绝对辈分 = anchor.generation + level.offset**。
|
|
|
+3. **新建家庭分支**(`user.familyId` 为空/0/同家庭):
|
|
|
+ - 已有 `FamilyMember` → 更新 `generation`、`isSpouse`;
|
|
|
+ - 无 → 新建并设置 `generation`、`isSpouse`(沿用现有头像/昵称兜底逻辑);
|
|
|
+ - 调 `createRelationsForNewMember(familyId, memberId, 绝对辈分, peerType)`。
|
|
|
+4. **合并家庭分支**(`mergeIntoFamily`):合并后同样定位/创建该用户在新家庭的 `FamilyMember`,写入 `generation`/`isSpouse` 并重建关系(旧关系已在 `mergeIntoFamily` 中删除)。
|
|
|
+5. 写 `user.familyRole = deriveFamilyRole(绝对辈分, gender)`(推导表见 §3)。
|
|
|
+6. 回写 `request.familyRole`(推导值)、`request.assignedGeneration`、`request.assignedPeerType`。
|
|
|
+
|
|
|
+### 6.3 其他后端
|
|
|
+- `FamilyJoinRequest` 实体新增 `assignedGeneration`、`assignedPeerType`。
|
|
|
+- `FamilyJoinRequestDTO` 新增 `requesterRealName`、`assignedGeneration`、`assignedPeerType`、`assignedPeerLabel`。
|
|
|
+- `getPendingRequests` / `getMyPendingRequest` 填充 `requesterRealName`(`userMapper.selectById(...).getRealName()`)。
|
|
|
+- `FamilyMemberService` 成员列表处(约 355-362 行)改调用 3 参 `computeRelationshipLabel`,从 `FamilyRelationship` 行读 `relationType` 传入。
|
|
|
+- `InviteAcceptDTO` 保留 `familyRole`;`acceptInvitation` 内 `familyRole` 为 null 时不再报错(现有代码已 `if (familyRole != null)`,无需改)。
|
|
|
+
|
|
|
+## 7. 前端实现清单
|
|
|
+
|
|
|
+### 7.1 `pages/invite/join-form.vue`
|
|
|
+- 删除「家庭身份」`<view class="form-item">` 块、`familyRoleOptions`、`familyRoleIndex`、`onFamilyRoleChange`。
|
|
|
+- 副标题改为:`请填写您的真实姓名,提交后等待邀请人确认`。
|
|
|
+- `submit()` 删除 familyRole 校验与传参,改调 `requestJoinByCode(this.familyInviteCode)`。
|
|
|
+
|
|
|
+### 7.2 `pages/profile-extra/family-members.vue`
|
|
|
+- 申请卡片(约 23-33 行):
|
|
|
+ - 姓名:`{{ r.requesterRealName || r.requesterNickname || '微信用户' }}`
|
|
|
+ - 副行:`申请加入你的家庭`(删除「希望以「{{ r.familyRole }}」身份加入」)
|
|
|
+- 点「同意」→ 打开新弹窗 `approveModal`(不再用 `uni.showModal`):
|
|
|
+ - 顶部:申请人头像 + `真实姓名(昵称)`
|
|
|
+ - 说明:`请选择 TA 与你的关系`
|
|
|
+ - `<GenerationPicker ref="approvePicker" :generationLevel="approveForm.generationLevel" :peerType="approveForm.peerType" />`
|
|
|
+ - 底部:取消 / 确认同意(`loading` 态)
|
|
|
+- `doHandleJoinRequest(r, true)` 前拦截:未选辈分 → `请选择辈分关系`;同辈未选类型 → `请选择同辈关系类型`。
|
|
|
+- 提交:`approveJoinRequest(r.id, '', selectedGen, selectedPeer)`;成功后关闭弹窗、刷新申请列表与成员列表。
|
|
|
+- 「拒绝」保持 `uni.showModal`。
|
|
|
+
|
|
|
+### 7.3 `utils/api.js`
|
|
|
+```js
|
|
|
+export const requestJoinByCode = (inviteCode) => {
|
|
|
+ return request('/api/family/invite/request-join-by-code', 'POST', { inviteCode: inviteCode })
|
|
|
+}
|
|
|
+export const approveJoinRequest = (requestId, comment, generationLevel, peerType) => {
|
|
|
+ return request('/api/family/invite/approve-request', 'POST', {
|
|
|
+ requestId: requestId, comment: comment || '',
|
|
|
+ generationLevel: generationLevel || '', peerType: peerType || ''
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 7.4 小程序约束(硬性)
|
|
|
+- 禁止可选链 `?.`、禁止 CSS Grid、禁止 `:key` 表达式、禁止 `new Date(string)`。
|
|
|
+- `GenerationPicker` 通过 `$refs.approvePicker.selectedGen / selectedPeer` 读取值(与 `add-member.vue:278` 用法一致)。
|
|
|
+
|
|
|
+## 8. 不改动项(边界)
|
|
|
+
|
|
|
+- 不新增表;不删除 `family_join_requests.family_role` 列(历史数据保留)。
|
|
|
+- 不改 `user-edit.vue` 的「家庭身份」下拉(老用户修正称呼仍可用)。
|
|
|
+- 不改 `pages/invite/join.vue` 落地页结构(仍跳 `join-form`)。
|
|
|
+- 不改 `GenerationPicker` 组件本身与 `utils/generationLevel.js`。
|
|
|
+- 不改「切换身份」(`isSwitchable` / `onSwitchMember`)与「编辑资料」(`add-member.vue`)既有逻辑。
|
|
|
+- 不动 `mergeIntoFamily` 的成员迁移逻辑本身,只在其后补身份落地。
|
|
|
+
|
|
|
+## 9. 验收用例(必须实际执行并留证据)
|
|
|
+
|
|
|
+| # | 用例 | 预期 |
|
|
|
+|---|---|---|
|
|
|
+| A1 | 创建者(男)+ 邀请女用户 → 审批选「同辈 / 配偶」 | 创建者家庭页见该成员称谓「妻子」;以女用户身份查看同一页见创建者「丈夫」 |
|
|
|
+| A2 | 邀请男性长辈 → 审批选「父辈」 | 创建者见「父亲」;长辈视角见「儿子」 |
|
|
|
+| A3 | 邀请同辈 → 审批选「同辈 / 兄弟姐妹」 | 双向见「兄弟」或「姐妹」(按各自性别) |
|
|
|
+| A4 | 打开 join-form | 页面无「家庭身份」字段;真实姓名留空提交被拦截并提示「请输入真实姓名」 |
|
|
|
+| A5 | 审批弹窗不选辈分点确认 | 拦截并提示「请选择辈分关系」 |
|
|
|
+| A6 | 选「同辈」不选类型点确认 | 拦截并提示「请选择同辈关系类型」 |
|
|
|
+| A7 | 审批通过后查 `family_relationships` | 加入者与既有成员的双向行均存在,配偶行为 `relation_type='spouse'` |
|
|
|
+| A8 | 加入者在成员详情「编辑资料」重新选辈分 | 关系标签随之更新(不因此报错) |
|
|
|
+
|
|
|
+证据要求:`mvn -q compile` 通过日志;A1–A3 的接口返回或页面截图(H5 构建);A7 的 SQL 查询结果。
|
|
|
+
|
|
|
+## 10. 已知风险
|
|
|
+
|
|
|
+- **R-1 辈分锚点漂移**:现有 `FamilyMemberService.addMember` 传的是 `genLevel.getOffset()`(相对偏移)给 `createRelationsForNewMember` 的「绝对辈分」参数,当关联成员辈分非 0 时结果错误。本次新增的审批链路按绝对辈分正确传参;`addMember` 的同类问题不在本次范围,需另立任务。
|
|
|
+- **R-2 多配偶**:`relation_type='spouse'` 不限制唯一性,同一人可与多个同辈写 spouse。产品侧暂不校验。
|
|
|
+- **R-3 历史成员**:既有家庭成员的 `relation_type` 为默认 `lineal`,配偶仍显示「兄弟/姐妹」,需用户重新「编辑资料」选配偶后修正。
|