فهرست منبع

feat(cfc-backend): add FamilyRelationshipService, refactor FamilyMemberService for bidirectional relations

Add FamilyRelationshipService for N*2 bidirectional relationship creation, label computation (father/mother/son/daughter etc), and cleanup. Refactor FamilyMemberService: addMember() auto-generates relationships, listMembers() prioritizes relationship table for labels, kickMember() cleans up relations, add isEditable() for edit permission control. Make AddFamilyMemberDTO.relationshipType optional. Add generationValue to FamilyMemberVO.

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User 2 ماه پیش
والد
کامیت
42948943b5

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/dto/AddFamilyMemberDTO.java

@@ -32,7 +32,7 @@ public class AddFamilyMemberDTO {
     /** 出生日期 yyyy-MM-dd */
     private String birthday;
 
-    /** 关系类型标识(必填):spouse/parent/child/sibling 等 */
+    /** 关系类型标识(选填,不再作为主要关系依据,改为基于generationLevel+peerType自动推断) */
     private String relationshipType;
 
     /** 角色覆盖: auto/parent/child/elderly(选填,默认auto) */

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java

@@ -57,4 +57,7 @@ public class FamilyMemberVO {
 
     /** 是否对家庭成员可见 */
     private Integer showToFamily;
+
+    /** 辈分值(从关系表获取,当前视角) */
+    private Integer generationValue;
 }

+ 49 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java

@@ -6,6 +6,7 @@ import com.etotem.cfc.dto.FamilyMemberVO;
 import com.etotem.cfc.dto.SwitchMemberVO;
 import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.FamilyRelationship;
 import com.etotem.cfc.entity.FamilyMemberAttributes;
 import com.etotem.cfc.entity.RelationshipType;
 import com.etotem.cfc.entity.User;
@@ -53,6 +54,9 @@ public class FamilyMemberService {
     @Resource
     private SystemConfigService systemConfigService;
 
+    @Resource
+    private FamilyRelationshipService familyRelationshipService;
+
     /**
      * 添加家庭成员
      */
@@ -154,6 +158,9 @@ public class FamilyMemberService {
         // children 表已废除,无需同步。角色信息通过 computeEffectiveRole() 自动计算。
         // 若需要 child 专属初始化(如游戏数据),后续在 computeEffectiveRole() 返回 child 时处理。
 
+        // 为新成员创建与所有现有成员的双向关系
+        familyRelationshipService.createRelationsForNewMember(familyId, member.getId(), genLevel.getOffset());
+
         return toFamilyMemberVO(member, relType);
     }
 
@@ -206,9 +213,26 @@ public class FamilyMemberService {
             if (fm.getBirthday() != null) {
                 vo.setAge(calculateAge(fm.getBirthday()));
             }
-            // 计算视角相关的关系标签
+            // 计算视角相关的关系标签(优先使用关系表)
             if (viewerMember != null) {
-                vo.setRelativeLabel(computeRelativeLabel(viewerMember, fm, familyMembers));
+                Integer genValue = null;
+                if (familyRelationshipService != null && viewerMember.getId() != null) {
+                    List<FamilyRelationship> rels = familyRelationshipService.getRelationsForMember(viewerMember.getId());
+                    if (rels != null) {
+                        for (FamilyRelationship rel : rels) {
+                            if (rel.getToMemberId().equals(fm.getId())) {
+                                genValue = rel.getGenerationValue();
+                                break;
+                            }
+                        }
+                    }
+                }
+                if (genValue != null) {
+                    vo.setGenerationValue(genValue);
+                    vo.setRelativeLabel(familyRelationshipService.computeRelationshipLabel(genValue, fm.getGender()));
+                } else {
+                    vo.setRelativeLabel(computeRelativeLabel(viewerMember, fm, familyMembers));
+                }
             }
             result.add(vo);
         }
@@ -260,6 +284,8 @@ public class FamilyMemberService {
         // 从 family_members 踢出
         FamilyMember member = familyMemberMapper.selectById(memberId);
         if (member != null && member.getFamilyId().equals(operator.getFamilyId())) {
+            // 先清理关系记录
+            familyRelationshipService.deleteRelationsForMember(memberId);
             familyMemberMapper.deleteById(memberId);
             // 如果关联了 User,清除其 familyId
             if (member.getUserId() != null) {
@@ -281,6 +307,22 @@ public class FamilyMemberService {
         throw new RuntimeException("成员不存在");
     }
 
+    /**
+     * 判断成员是否可编辑
+     * 规则:有关联 userId 或有 phone 且 phone 能匹配到 User 的成员不可编辑
+     */
+    public boolean isEditable(Long memberId) {
+        FamilyMember member = familyMemberMapper.selectById(memberId);
+        if (member == null) return false;
+        if (member.getUserId() != null) return false;
+        if (member.getPhone() != null && !member.getPhone().isEmpty()) {
+            User linked = userMapper.selectOne(
+                new LambdaQueryWrapper<User>().eq(User::getPhone, member.getPhone()));
+            if (linked != null) return false;
+        }
+        return true;
+    }
+
     /**
      * 更新成员信息
      */
@@ -296,6 +338,11 @@ public class FamilyMemberService {
             throw new RuntimeException("成员不存在或不属于您的家庭");
         }
 
+        // 检查编辑权限
+        if (!isEditable(memberId)) {
+            throw new RuntimeException("该成员关联了登录账号,不可编辑");
+        }
+
         if (dto.getNickname() != null && !dto.getNickname().trim().isEmpty()) {
             member.setNickname(dto.getNickname().trim());
         }

+ 146 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyRelationshipService.java

@@ -0,0 +1,146 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.FamilyRelationship;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.FamilyRelationshipMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Service
+public class FamilyRelationshipService {
+
+    private static final Logger log = LoggerFactory.getLogger(FamilyRelationshipService.class);
+
+    @Resource
+    private FamilyRelationshipMapper familyRelationshipMapper;
+
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
+    /**
+     * 为新成员创建与所有现有成员的双向关系
+     * 算法:
+     * - 新成员与现有成员 Mi 之间的辈分关系:
+     *   existingMi → newMember: generation = Mi.generation - newMemberGeneration
+     *   newMember → existingMi: generation = newMemberGeneration - Mi.generation
+     * - 如果 existingMi.generation == null, 默认 0
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void createRelationsForNewMember(Long familyId, Long newMemberId, Integer newMemberGeneration) {
+        // 获取除新成员外的所有现有成员
+        List<FamilyMember> existing = familyMemberMapper.selectList(
+            new LambdaQueryWrapper<FamilyMember>()
+                .eq(FamilyMember::getFamilyId, familyId)
+                .ne(FamilyMember::getId, newMemberId));
+
+        if (existing.isEmpty()) {
+            return; // 第一个成员,无需创建关系
+        }
+
+        Date now = new Date();
+        for (FamilyMember existingMember : existing) {
+            int exGen = existingMember.getGeneration() != null ? existingMember.getGeneration() : 0;
+            int newGen = newMemberGeneration != null ? newMemberGeneration : 0;
+
+            // A (existing) → B (new): generation = exGen - newGen
+            FamilyRelationship rel1 = new FamilyRelationship();
+            rel1.setFamilyId(familyId);
+            rel1.setFromMemberId(existingMember.getId());
+            rel1.setToMemberId(newMemberId);
+            rel1.setGenerationValue(exGen - newGen);
+            rel1.setCreatedAt(now);
+            rel1.setUpdatedAt(now);
+            try {
+                familyRelationshipMapper.insert(rel1);
+            } catch (Exception e) {
+                log.warn("插入关系记录(from existing→new)可能重复: {}", e.getMessage());
+            }
+
+            // B (new) → A (existing): generation = newGen - exGen
+            FamilyRelationship rel2 = new FamilyRelationship();
+            rel2.setFamilyId(familyId);
+            rel2.setFromMemberId(newMemberId);
+            rel2.setToMemberId(existingMember.getId());
+            rel2.setGenerationValue(newGen - exGen);
+            rel2.setCreatedAt(now);
+            rel2.setUpdatedAt(now);
+            try {
+                familyRelationshipMapper.insert(rel2);
+            } catch (Exception e) {
+                log.warn("插入关系记录(from new→existing)可能重复: {}", e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * 获取某个成员的所有关系(以该成员为起点的)
+     */
+    public List<FamilyRelationship> getRelationsForMember(Long memberId) {
+        return familyRelationshipMapper.selectList(
+            new LambdaQueryWrapper<FamilyRelationship>()
+                .eq(FamilyRelationship::getFromMemberId, memberId));
+    }
+
+    /**
+     * 删除某个成员的所有关系(双向:from 和 to 都删)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteRelationsForMember(Long memberId) {
+        familyRelationshipMapper.delete(
+            new LambdaQueryWrapper<FamilyRelationship>()
+                .eq(FamilyRelationship::getFromMemberId, memberId)
+                .or()
+                .eq(FamilyRelationship::getToMemberId, memberId));
+    }
+
+    /**
+     * 获取某个成员的所有关系中,目标成员ID→辈分值的映射
+     */
+    public Map<Long, Integer> getGenerationMapFrom(Long fromMemberId) {
+        List<FamilyRelationship> rels = getRelationsForMember(fromMemberId);
+        return rels.stream()
+            .collect(Collectors.toMap(FamilyRelationship::getToMemberId, FamilyRelationship::getGenerationValue));
+    }
+
+    /**
+     * 根据辈分值计算关系标签
+     */
+    public String computeRelationshipLabel(Integer generationValue, String gender) {
+        if (generationValue == null) return "未知";
+        boolean isMale = "male".equals(gender);
+
+        switch (generationValue) {
+            // 向上(对方是长辈)
+            case 3:
+                return "曾祖" + (isMale ? "父" : "母");
+            case 2:
+                return isMale ? "祖父" : "祖母";
+            case 1:
+                return isMale ? "父亲" : "母亲";
+            // 同辈
+            case 0:
+                return isMale ? "兄弟" : "姐妹";
+            // 向下(对方是晚辈)
+            case -1:
+                return isMale ? "儿子" : "女儿";
+            case -2:
+                return isMale ? "孙子" : "孙女";
+            case -3:
+                return "曾" + (isMale ? "孙" : "孙女");
+            default:
+                return generationValue > 0
+                    ? "上" + generationValue + "代"
+                    : "下" + Math.abs(generationValue) + "代";
+        }
+    }
+}