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

fix(family): 配偶关系改为仅对锚点成员生效,修正同辈关系类型判定

- createRelationsForNewMember 增加 anchorMemberId 参数:同辈 spouse 只写给锚点成员(审批人/关联成员),
  其他同辈一律 sibling。原实现会把「妻子/丈夫」套到家庭内所有同辈成员上
- 审批链路传入 anchor=审批人 FamilyMember.id
- FamilyMemberService.addMember 修正传入绝对辈分(原误传 genLevel.getOffset()),并补 peerType 与锚点
- 规格文档同步 §6.1 锚点规则、§10 R-1(已修)/R-2/R-4(编辑资料不重建关系,另立任务)
Agnes Agent 5 дней назад
Родитель
Сommit
6567acc339

+ 6 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyJoinRequestService.java

@@ -190,7 +190,9 @@ public class FamilyJoinRequestService {
         }
 
         // 绝对辈分 = 审批人成员辈分(null 视为 0)+ 选项 offset
+        // anchorMemberId = 审批人在本家庭的成员记录(关系锚点:配偶关系只对锚点生效)
         Integer absoluteGeneration = null;
+        Long anchorMemberId = null;
         boolean isSpouse = false;
         if (withIdentity) {
             FamilyMember anchor = familyMemberMapper.selectOne(
@@ -200,6 +202,7 @@ public class FamilyJoinRequestService {
                             .last("LIMIT 1"));
             int anchorGen = anchor != null && anchor.getGeneration() != null ? anchor.getGeneration() : 0;
             absoluteGeneration = anchorGen + level.getOffset();
+            anchorMemberId = anchor != null ? anchor.getId() : null;
             isSpouse = "peer".equals(generationLevel) && "spouse".equals(peerType);
         }
 
@@ -289,7 +292,9 @@ public class FamilyJoinRequestService {
             familyMemberMapper.updateById(member);
 
             // 与新家庭全体现有成员建立双向关系(含 relation_type: lineal/sibling/spouse)
-            familyRelationshipService.createRelationsForNewMember(family.getId(), member.getId(), absoluteGeneration, peerType);
+            // 锚点=审批人成员:spouse 只写在「加入者 ↔ 审批人」之间,其他同辈一律 sibling
+            familyRelationshipService.createRelationsForNewMember(
+                    family.getId(), member.getId(), absoluteGeneration, peerType, anchorMemberId);
 
             // User.familyRole 由绝对辈分 + 性别推导,保证下游 DTO 不为空
             user.setFamilyRole(deriveFamilyRole(absoluteGeneration, user.getGender()));

+ 6 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java

@@ -216,7 +216,12 @@ public class FamilyMemberService {
         // 若需要 child 专属初始化(如游戏数据),后续在 computeEffectiveRole() 返回 child 时处理。
 
         // 为新成员创建与所有现有成员的双向关系
-        familyRelationshipService.createRelationsForNewMember(familyId, member.getId(), genLevel.getOffset());
+        // 注意:第三个参数是「绝对辈分」而非相对偏移;锚点=关联成员(本次添加所参照的人),
+        // spouse 只写在「新成员 ↔ 关联成员」之间,家庭内其他同辈一律 sibling。
+        familyRelationshipService.createRelationsForNewMember(
+                familyId, member.getId(), generation,
+                genLevel == GenerationLevel.PEER ? dto.getPeerType() : null,
+                relative != null ? relative.getId() : null);
 
         // 发放家庭人口券(POPULATION):发到家庭维度
         try {

+ 20 - 7
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyRelationshipService.java

@@ -35,6 +35,14 @@ public class FamilyRelationshipService {
         createRelationsForNewMember(familyId, newMemberId, newMemberGeneration, null);
     }
 
+    /**
+     * 为新成员创建与所有现有成员的双向关系(兼容旧调用:无同辈类型、无锚点)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void createRelationsForNewMember(Long familyId, Long newMemberId, Integer newMemberGeneration, String peerType) {
+        createRelationsForNewMember(familyId, newMemberId, newMemberGeneration, peerType, null);
+    }
+
     /**
      * 为新成员创建与所有现有成员的双向关系
      * 算法:
@@ -43,12 +51,15 @@ public class FamilyRelationshipService {
      *   newMember → existingMi: generation = newMemberGeneration - Mi.generation
      * - 如果 existingMi.generation == null, 默认 0
      * - relationType 判定(双向各写一行,取值相同):
-     *   同辈且 peerType=spouse  → spouse
-     *   同辈且 peerType=sibling → sibling
-     *   其他                     → lineal(默认)
+     *   同辈 且 peerType=spouse 且 existingMember 是锚点成员 → spouse
+     *   同辈 且 peerType 非空 且 非锚点成员              → sibling
+     *   其他(peerType 为空 / 跨辈分)                   → lineal(默认)
+     * <p>锚点成员(anchorMemberId)= 指定「辈分/同辈类型」时参照的那个人(审批人 / 关联成员)。
+     * 配偶关系只存在于「新成员 ↔ 锚点成员」一对;家庭里其他同辈成员与新成员一律为 sibling,
+     * 否则「妻子/丈夫」称谓会被错误套到所有同辈成员上。</p>
      */
     @Transactional(rollbackFor = Exception.class)
-    public void createRelationsForNewMember(Long familyId, Long newMemberId, Integer newMemberGeneration, String peerType) {
+    public void createRelationsForNewMember(Long familyId, Long newMemberId, Integer newMemberGeneration, String peerType, Long anchorMemberId) {
         // 获取除新成员外的所有现有成员
         List<FamilyMember> existing = familyMemberMapper.selectList(
             new LambdaQueryWrapper<FamilyMember>()
@@ -67,11 +78,13 @@ public class FamilyRelationshipService {
             int exGen = existingMember.getGeneration() != null ? existingMember.getGeneration() : 0;
             int newGen = newMemberGeneration != null ? newMemberGeneration : 0;
 
-            // 关系类型:双向取值相同
+            // 关系类型:双向取值相同;配偶仅对锚点成员生效
             String relationType = "lineal";
-            if (exGen == newGen && "spouse".equals(peerType)) {
+            boolean sameGen = exGen == newGen;
+            boolean isAnchor = anchorMemberId != null && anchorMemberId.equals(existingMember.getId());
+            if (sameGen && "spouse".equals(peerType) && isAnchor) {
                 relationType = "spouse";
-            } else if (exGen == newGen && "sibling".equals(peerType)) {
+            } else if (sameGen && peerType != null && !peerType.trim().isEmpty()) {
                 relationType = "sibling";
             }
 

+ 11 - 5
docs/superpowers/specs/2026-11-07-family-join-dynamic-identity.md

@@ -79,7 +79,12 @@ ensureColumn("family_join_requests", "assigned_peer_type", "VARCHAR(10) NULL COM
 ## 6. 后端实现清单
 
 ### 6.1 `FamilyRelationshipService`
-1. 新增 4 参:`createRelationsForNewMember(Long familyId, Long newMemberId, Integer newMemberGeneration, String peerType)`;保留 3 参重载委托(`peerType=null`)。
+1. 新增 5 参:`createRelationsForNewMember(Long familyId, Long newMemberId, Integer newMemberGeneration, String peerType, Long anchorMemberId)`;保留 4 参、3 参重载委托(`anchorMemberId=null`)。
+   **锚点规则(关键)**:`anchorMemberId` = 指定辈分时参照的那个人(审批人 / 关联成员)。关系类型判定:
+   - 同辈 && `peerType=spouse` && `existingMember.id == anchorMemberId` → `spouse`
+   - 同辈 && `peerType` 非空 && 非锚点 → `sibling`
+   - 其余(`peerType` 为空 / 跨辈分)→ `lineal`
+   配偶关系只存在于「新成员 ↔ 锚点」一对,否则「妻子/丈夫」会被错误套到家庭内所有同辈成员上。
 2. 写入 `relation_type`:
    - `exGen == newGen && "spouse".equals(peerType)` → 双向 `spouse`
    - `exGen == newGen && "sibling".equals(peerType)` → 双向 `sibling`
@@ -98,7 +103,7 @@ ensureColumn("family_join_requests", "assigned_peer_type", "VARCHAR(10) NULL COM
 3. **新建家庭分支**(`user.familyId` 为空/0/同家庭):
    - 已有 `FamilyMember` → 更新 `generation`、`isSpouse`;
    - 无 → 新建并设置 `generation`、`isSpouse`(沿用现有头像/昵称兜底逻辑);
-   - 调 `createRelationsForNewMember(familyId, memberId, 绝对辈分, peerType)`。
+   - 调 `createRelationsForNewMember(familyId, memberId, 绝对辈分, peerType, anchorMemberId)`,其中 `anchorMemberId` = 审批人在该家庭的 `FamilyMember.id`(取不到时传 null,此时同辈一律 `sibling`,不写 `spouse`)。
 4. **合并家庭分支**(`mergeIntoFamily`):合并后同样定位/创建该用户在新家庭的 `FamilyMember`,写入 `generation`/`isSpouse` 并重建关系(旧关系已在 `mergeIntoFamily` 中删除)。
 5. 写 `user.familyRole = deriveFamilyRole(绝对辈分, gender)`(推导表见 §3)。
 6. 回写 `request.familyRole`(推导值)、`request.assignedGeneration`、`request.assignedPeerType`。
@@ -173,6 +178,7 @@ export const approveJoinRequest = (requestId, comment, generationLevel, peerType
 
 ## 10. 已知风险
 
-- **R-1 辈分锚点漂移**:现有 `FamilyMemberService.addMember` 传的是 `genLevel.getOffset()`(相对偏移)给 `createRelationsForNewMember` 的「绝对辈分」参数,当关联成员辈分非 0 时结果错误。本次新增的审批链路按绝对辈分正确传参;`addMember` 的同类问题不在本次范围,需另立任务。
-- **R-2 多配偶**:`relation_type='spouse'` 不限制唯一性,同一人可与多个同辈写 spouse。产品侧暂不校验。
-- **R-3 历史成员**:既有家庭成员的 `relation_type` 为默认 `lineal`,配偶仍显示「兄弟/姐妹」,需用户重新「编辑资料」选配偶后修正。
+- **R-1 辈分锚点漂移**:`FamilyMemberService.addMember` 原先把 `genLevel.getOffset()`(相对偏移)传给「绝对辈分」参数,关联成员辈分非 0 时结果错误。**已在本次修复**:改传已算好的绝对辈分 `generation`,并补传 `peerType` 与锚点 `relative.getId()`。
+- **R-2 配偶唯一性**:`relation_type='spouse'` 不限制唯一性,同一人仍可与多个同辈写 spouse(例如分两次加入两位同辈配偶)。**配偶仅对锚点成员生效**,锚点外同辈一律 `sibling`;产品侧不额外校验一夫一妻。
+- **R-3 历史成员**:既有家庭成员的 `relation_type` 为默认 `lineal`,配偶仍显示「兄弟/姐妹」,需用户重新「编辑资料」选配偶后修正(且受 R-4 制约,见下)。
+- **R-4 编辑资料不重建关系(既有缺陷,本次不修)**:`FamilyMemberService.updateMember` 改辈分时只更新 `member.generation`/`isSpouse`,既不重建 `family_relationships`、也不写 `relation_type`;而成员列表标签优先读关系表,故「编辑资料」改身份后标签不刷新。修复方案(重建关系 + 锚点取操作者成员)另立任务。