Forráskód Böngészése

feat(family): TASK-P0-001 辈分重构 — 订阅式 generation + is_spouse,删除关系类型表与字段

Xiaogang Liao 1 hónapja
szülő
commit
b61cd80f27
25 módosított fájl, 265 hozzáadás és 783 törlés
  1. 0 43
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/RelationshipTypeAdminController.java
  2. 0 8
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyMembersController.java
  3. 0 5
      cfc-backend/src/main/java/com/etotem/cfc/dto/AddFamilyMemberDTO.java
  4. 1 3
      cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberInfoDTO.java
  5. 0 9
      cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java
  6. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/dto/HealthAlertVO.java
  7. 2 1
      cfc-backend/src/main/java/com/etotem/cfc/dto/RelationshipScoreVO.java
  8. 0 6
      cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMember.java
  9. 0 43
      cfc-backend/src/main/java/com/etotem/cfc/entity/RelationshipType.java
  10. 0 9
      cfc-backend/src/main/java/com/etotem/cfc/mapper/RelationshipTypeMapper.java
  11. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/service/CompatibilityService.java
  12. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyHealthScoreService.java
  13. 119 204
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java
  14. 0 1
      cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java
  15. 10 3
      cfc-backend/src/main/java/com/etotem/cfc/service/RelationshipQualityService.java
  16. 1 1
      cfc-backend/src/main/java/com/etotem/cfc/service/RelationshipQuestionnaireService.java
  17. 1 4
      cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
  18. 3 20
      cfc-backend/src/main/resources/schema.sql
  19. 13 28
      cfc-backend/src/test/java/com/etotem/cfc/controller/family/FamilyMembersControllerTest.java
  20. 9 49
      cfc-backend/src/test/java/com/etotem/cfc/integration/FamilyMemberManagementFlowTest.java
  21. 48 80
      cfc-frontend/components/GenerationPicker.vue
  22. 54 0
      cfc-frontend/utils/generationLevel.js
  23. 0 24
      cfc-web/src/api/admin.js
  24. 0 1
      cfc-web/src/views/Layout.vue
  25. 0 238
      cfc-web/src/views/admin/RelationshipTypes.vue

+ 0 - 43
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/RelationshipTypeAdminController.java

@@ -1,43 +0,0 @@
-package com.etotem.cfc.controller.admin;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.RelationshipType;
-import com.etotem.cfc.service.FamilyMemberService;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.web.bind.annotation.*;
-
-import javax.annotation.Resource;
-import java.util.List;
-
-@Tag(name = "关系类型管理(管理员)", description = "关系类型CRUD")
-@RestController
-@RequestMapping("/api/admin/relationship-type")
-public class RelationshipTypeAdminController {
-
-    @Resource
-    private FamilyMemberService familyMemberService;
-
-    @Operation(summary = "获取所有关系类型")
-    @PostMapping("/list")
-    public Result<List<RelationshipType>> list() {
-        return Result.success(familyMemberService.listAllRelationshipTypes());
-    }
-
-    @Operation(summary = "保存关系类型(新增或更新)")
-    @PostMapping("/save")
-    public Result<RelationshipType> save(@RequestBody RelationshipType type) {
-        return Result.success(familyMemberService.saveRelationshipType(type));
-    }
-
-    @Operation(summary = "删除关系类型")
-    @PostMapping("/delete")
-    public Result<String> delete(@RequestBody java.util.Map<String, Long> body) {
-        Long id = body.get("id");
-        if (id == null) {
-            return Result.error("缺少ID");
-        }
-        familyMemberService.deleteRelationshipType(id);
-        return Result.success("删除成功");
-    }
-}

+ 0 - 8
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyMembersController.java

@@ -4,7 +4,6 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.AddFamilyMemberDTO;
 import com.etotem.cfc.dto.FamilyMemberVO;
 import com.etotem.cfc.dto.SwitchMemberVO;
-import com.etotem.cfc.entity.RelationshipType;
 import com.etotem.cfc.entity.FamilyMemberLog;
 import com.etotem.cfc.service.FamilyMemberService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -134,13 +133,6 @@ public class FamilyMembersController {
         }
     }
 
-    @Operation(summary = "获取关系类型列表")
-    @PostMapping("/relationship-types")
-    public Result<List<RelationshipType>> getRelationshipTypes() {
-        List<RelationshipType> types = familyMemberService.getRelationshipTypes();
-        return Result.success(types);
-    }
-
     @Operation(summary = "检查成员是否可编辑")
     @PostMapping("/editable")
     public Result<Boolean> checkEditable(HttpServletRequest request,

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

@@ -44,9 +44,4 @@ public class AddFamilyMemberDTO {
     /** 是否剖腹产: 0=顺产, 1=剖腹产 (选填) */
     private Integer isCesarean;
 
-    /** 关系类型标识(选填,不再作为主要关系依据,改为基于generationLevel+peerType自动推断) */
-    private String relationshipType;
-
-    /** 角色覆盖: auto/parent/child/elderly(选填,默认auto) */
-    private String roleOverride;
 }

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

@@ -13,9 +13,7 @@ public class FamilyMemberInfoDTO {
     private String gender;
     private String phone;
     private String avatar;
-    private String relationshipType;
-    private String relationshipTypeName;
-    private String roleOverride;
+    private Integer generation;
     private String effectiveRole;
     private Integer totalPoints;
     private Integer showToFamily;

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

@@ -28,18 +28,9 @@ public class FamilyMemberVO {
 
     private String gender;
 
-    /** 关系类型key(仅family_member) */
-    private String relationshipType;
-
-    /** 关系类型显示名 */
-    private String relationshipTypeName;
-
     /** 计算后的实际角色(前端展示用) */
     private String effectiveRole;
 
-    /** 管理员设置的角色覆盖值 */
-    private String roleOverride;
-
     /** 角色标签显示文本(家长/孩子/配偶/父母等) */
     private String roleLabel;
 

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

@@ -9,7 +9,8 @@ public class HealthAlertVO {
 
     private String nickname;
 
-    private String relationshipType;
+    /** 有效角色 parent/child */
+    private String effectiveRole;
 
     /** 无互动天数 */
     private Integer daysSinceInteraction;

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

@@ -11,7 +11,8 @@ public class RelationshipScoreVO {
 
     private String nickname;
 
-    private String relationshipType;
+    /** 有效角色 parent/child */
+    private String effectiveRole;
 
     /** 信任度 0-100 */
     private BigDecimal trustScore;

+ 0 - 6
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMember.java

@@ -50,12 +50,6 @@ public class FamilyMember implements Serializable {
     /** 是否剖腹产: 0=顺产, 1=剖腹产 */
     private Integer isCesarean;
 
-    /** 关系类型key(关联relationship_types.type_key) */
-    private String relationshipType;
-
-    /** 角色覆盖: auto/parent/child/elderly,空=auto */
-    private String roleOverride;
-
     /** 辈分值(0=家庭创建者,+n=向上n代,-n=向下n代) */
     private Integer generation;
 

+ 0 - 43
cfc-backend/src/main/java/com/etotem/cfc/entity/RelationshipType.java

@@ -1,43 +0,0 @@
-package com.etotem.cfc.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-
-import java.io.Serializable;
-import java.util.Date;
-
-@Data
-@TableName("relationship_types")
-public class RelationshipType implements Serializable {
-
-    @TableId(type = IdType.AUTO)
-    private Long id;
-
-    /** 类型标识: spouse/parent/child/sibling */
-    private String typeKey;
-
-    /** 类型显示名: 配偶/父母/子女/兄弟姐妹 */
-    private String typeName;
-
-    /** 默认角色模板: parent/child */
-    private String defaultRole;
-
-    /** 排序序号 */
-    private Integer sortOrder;
-
-    /** 是否启用: 1=启用, 0=禁用 */
-    private Integer enabled;
-
-    /** 关系分类: blood(血亲)/marriage(姻亲)/social(社会关系) */
-    private String category;
-
-    /** 是否血亲关系: 1=是, 0=否 */
-    private Integer isBloodRelation;
-
-    /** 是否直系亲属: 1=是, 0=否 */
-    private Integer isImmediateFamily;
-
-    private Date createdAt;
-}

+ 0 - 9
cfc-backend/src/main/java/com/etotem/cfc/mapper/RelationshipTypeMapper.java

@@ -1,9 +0,0 @@
-package com.etotem.cfc.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.etotem.cfc.entity.RelationshipType;
-import org.apache.ibatis.annotations.Mapper;
-
-@Mapper
-public interface RelationshipTypeMapper extends BaseMapper<RelationshipType> {
-}

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

@@ -561,7 +561,7 @@ public class CompatibilityService {
         FamilyMemberVO vo = new FamilyMemberVO();
         vo.setId(member.getId());
         vo.setNickname(member.getNickname());
-        vo.setMemberType(member.getRelationshipType() != null ? "parent" : "child");
+        vo.setMemberType(member.getGeneration() != null && member.getGeneration() < 0 ? "child" : "parent");
         return vo;
     }
 

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

@@ -38,7 +38,7 @@ public class FamilyHealthScoreService {
 
         List<FamilyMember> children = new ArrayList<>();
         for (FamilyMember m : allMembers) {
-            if ("child".equals(m.getRoleOverride())) {
+            if (m.getGeneration() != null && m.getGeneration() < 0) {
                 children.add(m);
             }
         }

+ 119 - 204
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java

@@ -10,14 +10,12 @@ import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.FamilyRelationship;
 import com.etotem.cfc.entity.FamilyMemberAttributes;
 import com.etotem.cfc.entity.FamilyMemberLog;
-import com.etotem.cfc.entity.RelationshipType;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.enums.GenerationLevel;
 import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.FamilyMemberAttributesMapper;
 import com.etotem.cfc.mapper.FamilyMemberLogMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
-import com.etotem.cfc.mapper.RelationshipTypeMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -39,9 +37,6 @@ public class FamilyMemberService {
     @Resource
     private FamilyMemberMapper familyMemberMapper;
 
-    @Resource
-    private RelationshipTypeMapper relationshipTypeMapper;
-
     @Resource
     private UserMapper userMapper;
 
@@ -88,37 +83,18 @@ public class FamilyMemberService {
         if (genLevel == null) {
             throw new IllegalArgumentException("无效的辈分等级: " + dto.getGenerationLevel());
         }
-        String relationshipTypeKey;
-        boolean isSpouse = false;
-        if (genLevel == GenerationLevel.PEER) {
-            if (dto.getPeerType() == null || dto.getPeerType().trim().isEmpty()) {
-                throw new IllegalArgumentException("同辈关系必须指定 peerType (sibling/spouse)");
-            }
-            switch (dto.getPeerType()) {
-                case "spouse":
-                    relationshipTypeKey = "spouse";
-                    isSpouse = true;
-                    break;
-                case "sibling":
-                    relationshipTypeKey = "sibling";
-                    break;
-                default:
-                    throw new IllegalArgumentException("无效的同辈类型: " + dto.getPeerType());
-            }
-        } else if (genLevel.getOffset() > 0) {
-            relationshipTypeKey = "parent";
-        } else {
-            relationshipTypeKey = "child";
-        }
 
-        // 验证关系类型是否存在
-        RelationshipType relType = relationshipTypeMapper.selectOne(
-                new LambdaQueryWrapper<RelationshipType>()
-                        .eq(RelationshipType::getTypeKey, relationshipTypeKey)
-                        .eq(RelationshipType::getEnabled, 1));
-        if (relType == null) {
-            throw new IllegalArgumentException("无效的关系类型: " + relationshipTypeKey);
+        // 计算辈分值(相对关联成员)
+        FamilyMember relative = null;
+        if (dto.getRelativeMemberId() != null) {
+            relative = familyMemberMapper.selectById(dto.getRelativeMemberId());
+            if (relative == null || !relative.getFamilyId().equals(familyId)) {
+                throw new IllegalArgumentException("关联成员不存在");
+            }
         }
+        int generation = (relative != null ? relative.getGeneration() : 0) + genLevel.getOffset();
+        boolean isSpouse = genLevel == GenerationLevel.PEER
+                && "spouse".equals(dto.getPeerType());
 
         // 创建家庭成员记录
         FamilyMember member = new FamilyMember();
@@ -139,10 +115,8 @@ public class FamilyMemberService {
         member.setBirthWeight(dto.getBirthWeight());
         member.setBirthPlace(dto.getBirthPlace());
         member.setIsCesarean(dto.getIsCesarean());
-        member.setRelationshipType(relationshipTypeKey);
-        member.setGeneration(genLevel.getOffset());
+        member.setGeneration(generation);
         member.setIsSpouse(isSpouse);
-        member.setRoleOverride("auto");
         member.setShowToFamily(1);
         member.setTotalPoints(0);
         member.setCreatedAt(new Date());
@@ -174,7 +148,7 @@ public class FamilyMemberService {
         // 为新成员创建与所有现有成员的双向关系
         familyRelationshipService.createRelationsForNewMember(familyId, member.getId(), genLevel.getOffset());
 
-        return toFamilyMemberVO(member, relType);
+        return toFamilyMemberVO(member);
     }
 
     /**
@@ -188,17 +162,11 @@ public class FamilyMemberService {
         Long familyId = user.getFamilyId();
         List<FamilyMemberVO> result = new ArrayList<>();
 
-        // 1. 查询 relationship_types 用于映射显示名
-        List<RelationshipType> relTypes = relationshipTypeMapper.selectList(
-                new LambdaQueryWrapper<RelationshipType>().eq(RelationshipType::getEnabled, 1));
-        java.util.Map<String, String> relTypeNameMap = relTypes.stream()
-                .collect(Collectors.toMap(RelationshipType::getTypeKey, RelationshipType::getTypeName));
-
-        // 2. 查询 family_members 表
+        // 查询 family_members 表
         List<FamilyMember> familyMembers = familyMemberMapper.selectList(
                 new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId));
 
-        // 2b. 查找当前查看者在 family_members 中的记录(用于计算视角标签)
+        // 查找当前查看者在 family_members 中的记录(用于计算视角标签)
         FamilyMember viewerMember = null;
         for (FamilyMember fm : familyMembers) {
             if (userId.equals(fm.getUserId())) {
@@ -216,11 +184,9 @@ public class FamilyMemberService {
             vo.setPhone(fm.getPhone());
             vo.setAvatar(fm.getAvatar());
             vo.setGender(fm.getGender());
-            vo.setRelationshipType(fm.getRelationshipType());
-            vo.setRelationshipTypeName(relTypeNameMap.getOrDefault(fm.getRelationshipType(), fm.getRelationshipType()));
+            vo.setGeneration(fm.getGeneration());
             vo.setEffectiveRole(computeEffectiveRole(fm));
-            vo.setRoleOverride(fm.getRoleOverride());
-            vo.setRoleLabel(relTypeNameMap.getOrDefault(fm.getRelationshipType(), computeEffectiveRole(fm)));
+            vo.setRoleLabel(computeEffectiveRole(fm));
             vo.setTotalPoints(fm.getTotalPoints());
             vo.setShowToFamily(fm.getShowToFamily());
             if (fm.getBirthday() != null) {
@@ -379,80 +345,17 @@ public class FamilyMemberService {
                 throw new IllegalArgumentException("出生日期格式错误,应为yyyy-MM-dd");
             }
         }
-        if (dto.getRoleOverride() != null) {
-            member.setRoleOverride(dto.getRoleOverride());
-        }
 
         member.setUpdatedAt(new Date());
         familyMemberMapper.updateById(member);
         logMemberAction(member.getFamilyId(), member.getId(), "update", oldNickname, member.getNickname(), userId);
 
-        // 同步更新 family_members 中的 child 专属字段
-        if ("child".equals(computeEffectiveRole(member))) {
-            // family_members 本身已包含 child 专属字段,直接 updateById(member) 已足够
-        }
-
-        // 查询关系类型名称
-        RelationshipType relType = relationshipTypeMapper.selectOne(
-                new LambdaQueryWrapper<RelationshipType>()
-                        .eq(RelationshipType::getTypeKey, member.getRelationshipType()));
-        return toFamilyMemberVO(member, relType);
-    }
-
-    /**
-     * 获取所有启用的关系类型列表
-     */
-    public List<RelationshipType> getRelationshipTypes() {
-        return relationshipTypeMapper.selectList(
-                new LambdaQueryWrapper<RelationshipType>()
-                        .eq(RelationshipType::getEnabled, 1)
-                        .orderByAsc(RelationshipType::getSortOrder));
-    }
-
-    /**
-     * 根据关系类型key获取关系类型
-     */
-    public RelationshipType getRelationshipTypeByKey(String typeKey) {
-        return relationshipTypeMapper.selectOne(
-                new LambdaQueryWrapper<RelationshipType>()
-                        .eq(RelationshipType::getTypeKey, typeKey));
-    }
-
-    // ===== 管理端方法 =====
-
-    public List<RelationshipType> listAllRelationshipTypes() {
-        return relationshipTypeMapper.selectList(
-                new LambdaQueryWrapper<RelationshipType>()
-                        .orderByAsc(RelationshipType::getSortOrder));
-    }
-
-    @Transactional(rollbackFor = Exception.class)
-    public RelationshipType saveRelationshipType(RelationshipType type) {
-        if (type.getId() != null) {
-            RelationshipType existing = relationshipTypeMapper.selectById(type.getId());
-            if (existing != null) {
-                existing.setTypeKey(type.getTypeKey());
-                existing.setTypeName(type.getTypeName());
-                existing.setDefaultRole(type.getDefaultRole());
-                existing.setSortOrder(type.getSortOrder());
-                existing.setEnabled(type.getEnabled());
-                relationshipTypeMapper.updateById(existing);
-                return existing;
-            }
-        }
-        type.setCreatedAt(new Date());
-        relationshipTypeMapper.insert(type);
-        return type;
-    }
-
-    @Transactional(rollbackFor = Exception.class)
-    public void deleteRelationshipType(Long id) {
-        relationshipTypeMapper.deleteById(id);
+        return toFamilyMemberVO(member);
     }
 
     // ===== 内部方法 =====
 
-    private FamilyMemberVO toFamilyMemberVO(FamilyMember member, RelationshipType relType) {
+    private FamilyMemberVO toFamilyMemberVO(FamilyMember member) {
         FamilyMemberVO vo = new FamilyMemberVO();
         vo.setId(member.getId());
         vo.setSource("family_member");
@@ -461,16 +364,9 @@ public class FamilyMemberService {
         vo.setPhone(member.getPhone());
         vo.setAvatar(member.getAvatar());
         vo.setGender(member.getGender());
-        vo.setRelationshipType(member.getRelationshipType());
-        if (relType != null) {
-            vo.setRelationshipTypeName(relType.getTypeName());
-            vo.setRoleLabel(relType.getTypeName());
-        } else {
-            vo.setRelationshipTypeName(member.getRelationshipType());
-            vo.setRoleLabel(computeEffectiveRole(member));
-        }
+        vo.setGeneration(member.getGeneration());
         vo.setEffectiveRole(computeEffectiveRole(member));
-        vo.setRoleOverride(member.getRoleOverride());
+        vo.setRoleLabel(computeEffectiveRole(member));
         vo.setTotalPoints(member.getTotalPoints());
         vo.setShowToFamily(member.getShowToFamily());
         if (member.getBirthday() != null) {
@@ -495,21 +391,14 @@ public class FamilyMemberService {
 
     /**
      * 计算成员的实际角色(effective role)
-     * 优先级: roleOverride > defaultRole(关系类型模板) > 按年龄计算
+     * 基于 generation:>=0 → parent, <0 → child;无 generation 则按年龄 fallback
      */
     private String computeEffectiveRole(FamilyMember member) {
-        // 1. roleOverride 非 auto/null → 直接返回
-        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. 按年龄计算
+        // 基于 generation 计算:generation >= 0 = parent, generation < 0 = child
+        if (member.getGeneration() != null) {
+            return member.getGeneration() >= 0 ? "parent" : "child";
+        }
+        // 兼容旧数据(无 generation):按年龄 fallback
         if (member.getBirthday() != null) {
             int age = calculateAge(member.getBirthday());
             int childThreshold = systemConfigService.getChildAgeThreshold();
@@ -519,103 +408,129 @@ public class FamilyMemberService {
     }
 
     /**
-     * 计算视角相关的关系标签
-     * 规则:
-     * - 查看者自己 → "我"
-     * - parent→child:独生→"儿子"/"女儿",多子女→"长"/"次"/"三"...+"子"/"女"
-     * - child→parent → "父亲"/"母亲"
-     * - child→child → 按年龄比较→"哥哥"/"姐姐"/"弟弟"/"妹妹"
-     * - parent→parent → "丈夫"/"妻子"
-     * - 性别缺失且为孩子角色 → "孩子"
+     * 计算视角相关的关系标签(基于 generation 差值)
+     * - generation diff > 0: 长辈
+     * - generation diff < 0: 晚辈
+     * - generation diff = 0: 同辈(配偶/兄弟姐妹)
      */
     private String computeRelativeLabel(FamilyMember viewer, FamilyMember target, List<FamilyMember> allMembers) {
-        // 自己 → "我"
         if (viewer.getId().equals(target.getId())) {
             return "我";
         }
 
+        // 优先使用 generation diff
+        if (viewer.getGeneration() != null && target.getGeneration() != null) {
+            int genDiff = target.getGeneration() - viewer.getGeneration();
+            boolean isSpouse = Boolean.TRUE.equals(target.getIsSpouse());
+            String targetGender = target.getGender();
+
+            switch (genDiff) {
+                case 0:
+                    if (isSpouse) {
+                        return "male".equals(targetGender) ? "丈夫" : "妻子";
+                    }
+                    return computeSiblingLabel(viewer, target);
+                case 1:
+                    return "male".equals(targetGender) ? "爸爸" : "妈妈";
+                case -1:
+                    return computeParentChildLabel(viewer, target, allMembers);
+                case 2:
+                    return "male".equals(targetGender) ? "爷爷" : "奶奶";
+                case -2:
+                    return "male".equals(targetGender) ? "孙子" : "孙女";
+                case 3:
+                    return "male".equals(targetGender) ? "曾祖父" : "曾祖母";
+                case -3:
+                    return "male".equals(targetGender) ? "曾孙" : "曾孙女";
+                default:
+                    return genDiff > 0 ? "长辈" : "晚辈";
+            }
+        }
+
+        // 无 generation 时的 fallback(旧数据兼容)
         String viewerRole = computeEffectiveRole(viewer);
         String targetRole = computeEffectiveRole(target);
         String targetGender = target.getGender();
 
         // parent → child
         if ("parent".equals(viewerRole) && "child".equals(targetRole)) {
-            // 收集所有孩子,按生日从小到大排序(年长的排前面)
-            List<FamilyMember> children = allMembers.stream()
-                    .filter(m -> "child".equals(computeEffectiveRole(m)))
-                    .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 (children.size() == 1) {
-                // 独生子女,无序号前缀
-                if ("male".equals(targetGender)) return "儿子";
-                if ("female".equals(targetGender)) return "女儿";
-                return "孩子";
-            }
-
-            // 多子女,按年龄排序加序号前缀
-            String[] ordinals = {"长", "次", "三", "四", "五", "六", "七", "八", "九", "十"};
-            String prefix = (index >= 0 && index < ordinals.length) ? ordinals[index] : ("第" + (index + 1));
-            if ("male".equals(targetGender)) return prefix + "子";
-            if ("female".equals(targetGender)) return prefix + "女";
-            return prefix + "孩子";
+            return computeParentChildLabel(viewer, target, allMembers);
         }
 
         // child → parent
         if ("child".equals(viewerRole) && "parent".equals(targetRole)) {
-            if ("male".equals(targetGender)) return "父亲";
-            if ("female".equals(targetGender)) return "母亲";
-            return "父亲";
+            return "male".equals(targetGender) ? "父亲" : "母亲";
         }
 
         // child → child (sibling)
         if ("child".equals(viewerRole) && "child".equals(targetRole)) {
-            Date viewerBday = viewer.getBirthday();
-            Date targetBday = target.getBirthday();
-
-            if (viewerBday != null && targetBday != null) {
-                int cmp = viewerBday.compareTo(targetBday);
-                if (cmp > 0) {
-                    // viewer older → target younger
-                    if ("male".equals(targetGender)) return "弟弟";
-                    if ("female".equals(targetGender)) return "妹妹";
-                    return "孩子";
-                } else if (cmp < 0) {
-                    // viewer younger → target older
-                    if ("male".equals(targetGender)) return "哥哥";
-                    if ("female".equals(targetGender)) return "姐姐";
-                    return "孩子";
-                }
-            }
-            // 无法比较生日或同龄 → 默认用哥哥/姐姐
-            if ("male".equals(targetGender)) return "哥哥";
-            if ("female".equals(targetGender)) return "姐姐";
-            return "孩子";
+            return computeSiblingLabel(viewer, target);
         }
 
         // parent → parent (partner)
         if ("parent".equals(viewerRole) && "parent".equals(targetRole)) {
-            if ("male".equals(targetGender)) return "丈夫";
-            if ("female".equals(targetGender)) return "妻子";
-            return "配偶";
+            return "male".equals(targetGender) ? "丈夫" : "妻子";
         }
 
-        // 默认fallback:孩子角色无性别 → "孩子"
-        if ("child".equals(targetRole)) {
+        return null;
+    }
+
+    /**
+     * 计算父母视角下的子女标签(含序数)
+     */
+    private String computeParentChildLabel(FamilyMember viewer, FamilyMember target, List<FamilyMember> allMembers) {
+        String targetGender = target.getGender();
+        // 收集所有孩子,按 generation 降序(辈分越高,年龄越大)或生日排序
+        List<FamilyMember> children = allMembers.stream()
+                .filter(m -> !m.getId().equals(viewer.getId()))
+                .filter(m -> {
+                    if (viewer.getGeneration() != null && m.getGeneration() != null) {
+                        return m.getGeneration() < viewer.getGeneration();
+                    }
+                    return "child".equals(computeEffectiveRole(m));
+                })
+                .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 (children.size() <= 1 || index < 0) {
             if ("male".equals(targetGender)) return "儿子";
             if ("female".equals(targetGender)) return "女儿";
             return "孩子";
         }
-        return null;
+
+        String[] ordinals = {"长", "次", "三", "四", "五", "六", "七", "八", "九", "十"};
+        String prefix = (index < ordinals.length) ? ordinals[index] : ("第" + (index + 1));
+        if ("male".equals(targetGender)) return prefix + "子";
+        if ("female".equals(targetGender)) return prefix + "女";
+        return prefix + "孩子";
+    }
+
+    /**
+     * 计算兄弟姐妹标签(按生日比较)
+     */
+    private String computeSiblingLabel(FamilyMember viewer, FamilyMember target) {
+        String targetGender = target.getGender();
+        Date viewerBday = viewer.getBirthday();
+        Date targetBday = target.getBirthday();
+
+        if (viewerBday != null && targetBday != null) {
+            int cmp = viewerBday.compareTo(targetBday);
+            if (cmp > 0) {
+                return "male".equals(targetGender) ? "弟弟" : "妹妹";
+            } else if (cmp < 0) {
+                return "male".equals(targetGender) ? "哥哥" : "姐姐";
+            }
+        }
+        // 同龄或无法比较
+        return "male".equals(targetGender) ? "哥哥" : "姐姐";
     }
 
     private void logMemberAction(Long familyId, Long memberId, String action, String oldValue, String newValue, Long operatorId) {

+ 0 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java

@@ -857,7 +857,6 @@ public class HealthReportService {
         newMember.setFamilyId(familyId);
         newMember.setNickname(cleanName);
         newMember.setGender(gender);
-        newMember.setRoleOverride("auto");
         newMember.setShowToFamily(1);
         newMember.setTotalPoints(0);
         Date now = new Date();

+ 10 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/RelationshipQualityService.java

@@ -46,7 +46,7 @@ public class RelationshipQualityService {
             RelationshipScoreVO vo = new RelationshipScoreVO();
             vo.setMemberId(member.getId());
             vo.setNickname(member.getNickname());
-            vo.setRelationshipType(member.getRelationshipType());
+            vo.setEffectiveRole(computeEffectiveRole(member));
             vo.setTrustScore(member.getTrustScore() != null ? member.getTrustScore() : BigDecimal.ZERO);
             vo.setIntimacyScore(member.getIntimacyScore() != null ? member.getIntimacyScore() : BigDecimal.ZERO);
             vo.setCommunicationScore(member.getCommunicationScore() != null ? member.getCommunicationScore() : BigDecimal.ZERO);
@@ -80,7 +80,7 @@ public class RelationshipQualityService {
         RelationshipScoreVO vo = new RelationshipScoreVO();
         vo.setMemberId(member.getId());
         vo.setNickname(member.getNickname());
-        vo.setRelationshipType(member.getRelationshipType());
+        vo.setEffectiveRole(computeEffectiveRole(member));
         vo.setTrustScore(member.getTrustScore() != null ? member.getTrustScore() : BigDecimal.ZERO);
         vo.setIntimacyScore(member.getIntimacyScore() != null ? member.getIntimacyScore() : BigDecimal.ZERO);
         vo.setCommunicationScore(member.getCommunicationScore() != null ? member.getCommunicationScore() : BigDecimal.ZERO);
@@ -115,7 +115,7 @@ public class RelationshipQualityService {
                 HealthAlertVO alert = new HealthAlertVO();
                 alert.setMemberId(member.getId());
                 alert.setNickname(member.getNickname());
-                alert.setRelationshipType(member.getRelationshipType());
+                alert.setEffectiveRole(computeEffectiveRole(member));
 
                 if (lastInteraction == null) {
                     alert.setDaysSinceInteraction(-1);
@@ -207,4 +207,11 @@ public class RelationshipQualityService {
         }
         return "healthy";
     }
+
+    private String computeEffectiveRole(FamilyMember member) {
+        if (member.getGeneration() != null) {
+            return member.getGeneration() >= 0 ? "parent" : "child";
+        }
+        return "parent";
+    }
 }

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

@@ -68,7 +68,7 @@ public class RelationshipQuestionnaireService {
         snapshot.setFamilyMemberId(dto.getMemberId());
         snapshot.setRespondentId(dto.getRespondentId());
         snapshot.setMemberName(member.getNickname());
-        snapshot.setRelationshipType(member.getRelationshipType());
+        snapshot.setRelationshipType(member.getGeneration() != null && member.getGeneration() < 0 ? "child" : "parent");
         snapshot.setAiGeneratedJson(aiJson);
         snapshot.setVersion(version);
         snapshot.setCreatedAt(new Date());

+ 1 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -1359,8 +1359,6 @@ private FamilyInvitationService familyInvitationService;
         if (dto.getAge() != null) {
             member.setAge(dto.getAge());
         }
-        member.setRelationshipType(dto.getRelationshipType());
-        member.setRoleOverride(dto.getRoleOverride());
         member.setTotalPoints(0);
         member.setCreatedAt(new Date());
         member.setUpdatedAt(new Date());
@@ -1439,8 +1437,7 @@ private FamilyInvitationService familyInvitationService;
         dto.setGender(member.getGender());
         dto.setPhone(member.getPhone());
         dto.setAvatar(member.getAvatar());
-        dto.setRelationshipType(member.getRelationshipType());
-        dto.setRoleOverride(member.getRoleOverride());
+        dto.setGeneration(member.getGeneration());
         dto.setShowToFamily(member.getShowToFamily());
         dto.setTotalPoints(member.getTotalPoints());
         return dto;

+ 3 - 20
cfc-backend/src/main/resources/schema.sql

@@ -1728,8 +1728,8 @@ CREATE TABLE IF NOT EXISTS family_members (
     avatar           VARCHAR(500) COMMENT '头像',
     gender           VARCHAR(10)  COMMENT '性别: male/female',
     birthday         DATE         COMMENT '出生日期',
-    relationship_type VARCHAR(50) NOT NULL COMMENT '关系类型key',
-    role_override VARCHAR(20) DEFAULT 'auto' COMMENT '角色覆盖: auto/parent/child/elderly,空=auto',
+    generation INT COMMENT '辈分值(0=家庭创建者,+n=向上,-n=向下)',
+    is_spouse TINYINT(1) DEFAULT 0 COMMENT '是否配偶(同辈中)',
     show_to_family INT DEFAULT 1 COMMENT '对家庭成员可见: 1=可见, 0=不可见',
     total_points INT DEFAULT 0 COMMENT '总积分',
     -- 孩子专属字段(原children表合并)
@@ -1764,24 +1764,7 @@ CREATE TABLE IF NOT EXISTS family_relationships (
     INDEX idx_to (to_member_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭成员双向关系表';
 
--- 关系类型配置表(管理端可配置)
-CREATE TABLE IF NOT EXISTS relationship_types (
-    id               BIGINT AUTO_INCREMENT PRIMARY KEY,
-    type_key         VARCHAR(50)  NOT NULL UNIQUE COMMENT '类型标识: spouse/parent/child/sibling',
-    type_name        VARCHAR(100) NOT NULL COMMENT '类型显示名: 配偶/父母/子女/兄弟姐妹',
-    capability_role  VARCHAR(20)  NOT NULL DEFAULT 'parent' COMMENT '能力角色: parent/child',
-    default_role     VARCHAR(20)  DEFAULT NULL COMMENT '默认角色模板: parent/child(已废弃, 兼容保留)',
-    sort_order       INT DEFAULT 0 COMMENT '排序序号',
-    enabled          INT DEFAULT 1 COMMENT '是否启用: 1=启用, 0=禁用',
-    created_at       DATETIME DEFAULT CURRENT_TIMESTAMP
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关系类型配置表';
-
--- 默认关系类型数据(default_role: parent/child 为该关系类型的默认角色模板)
-INSERT IGNORE INTO relationship_types (type_key, type_name, default_role, sort_order, enabled) VALUES
-('spouse', '配偶', 'parent', 1, 1),
-('parent', '父母', 'parent', 2, 1),
-('child', '子女', 'child', 3, 1),
-('sibling', '兄弟姐妹', 'child', 4, 1);
+-- 【已废弃】relationship_types 表已删除(generation 辈分模型替代)
 
 -- 用户收货地址表
 CREATE TABLE IF NOT EXISTS user_address (

+ 13 - 28
cfc-backend/src/test/java/com/etotem/cfc/controller/family/FamilyMembersControllerTest.java

@@ -4,7 +4,6 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.AddFamilyMemberDTO;
 import com.etotem.cfc.dto.FamilyMemberVO;
 import com.etotem.cfc.dto.SwitchMemberVO;
-import com.etotem.cfc.entity.RelationshipType;
 import com.etotem.cfc.service.FamilyMemberService;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.context.SpringBootTest;
@@ -40,18 +39,19 @@ public class FamilyMembersControllerTest {
         return request;
     }
 
-    private FamilyMemberVO mockMemberVO(Long id, String nickname, String relationshipType) {
+    private FamilyMemberVO mockMemberVO(Long id, String nickname, String effectiveRole) {
         FamilyMemberVO vo = new FamilyMemberVO();
         vo.setId(id);
         vo.setNickname(nickname);
-        vo.setRelationshipType(relationshipType);
+        vo.setEffectiveRole(effectiveRole);
         return vo;
     }
 
-    private AddFamilyMemberDTO mockAddDTO(String nickname, String relationshipType) {
+    private AddFamilyMemberDTO mockAddDTO(String nickname, String generationLevel) {
         AddFamilyMemberDTO dto = new AddFamilyMemberDTO();
         dto.setNickname(nickname);
-        dto.setRelationshipType(relationshipType);
+        dto.setGenerationLevel(generationLevel);
+        dto.setRelativeMemberId(1L);
         return dto;
     }
 
@@ -60,10 +60,10 @@ public class FamilyMembersControllerTest {
     @Test
     public void addMember_Success() {
         MockHttpServletRequest request = mockRequest(1L);
-        AddFamilyMemberDTO dto = mockAddDTO("小明", "son");
+        AddFamilyMemberDTO dto = mockAddDTO("小明", "child");
 
         when(familyMemberService.addMember(eq(1L), any(AddFamilyMemberDTO.class)))
-            .thenReturn(mockMemberVO(100L, "小明", "son"));
+            .thenReturn(mockMemberVO(100L, "小明", "child"));
 
         Result<FamilyMemberVO> result = controller.addMember(request, dto);
         assertEquals(200, result.getCode());
@@ -73,7 +73,7 @@ public class FamilyMembersControllerTest {
     @Test
     public void addMember_NotLoggedIn() {
         MockHttpServletRequest request = new MockHttpServletRequest();
-        AddFamilyMemberDTO dto = mockAddDTO("小明", "son");
+        AddFamilyMemberDTO dto = mockAddDTO("小明", "child");
 
         Result<FamilyMemberVO> result = controller.addMember(request, dto);
         assertEquals(500, result.getCode());
@@ -86,11 +86,11 @@ public class FamilyMembersControllerTest {
         AddFamilyMemberDTO dto = mockAddDTO("", "");
 
         when(familyMemberService.addMember(eq(1L), any(AddFamilyMemberDTO.class)))
-            .thenThrow(new IllegalArgumentException("关系类型不能为空"));
+            .thenThrow(new IllegalArgumentException("辈分等级不能为空"));
 
         Result<FamilyMemberVO> result = controller.addMember(request, dto);
         assertEquals(500, result.getCode());
-        assertTrue(result.getMessage().contains("关系类型不能为空"));
+        assertTrue(result.getMessage().contains("辈分等级不能为空"));
     }
 
     // ==================== listMembers ====================
@@ -99,8 +99,8 @@ public class FamilyMembersControllerTest {
     public void listMembers_Success() {
         MockHttpServletRequest request = mockRequest(1L);
         List<FamilyMemberVO> members = Arrays.asList(
-            mockMemberVO(1L, "爸爸", "father"),
-            mockMemberVO(2L, "妈妈", "mother")
+            mockMemberVO(1L, "爸爸", "parent"),
+            mockMemberVO(2L, "妈妈", "parent")
         );
 
         when(familyMemberService.listMembers(1L)).thenReturn(members);
@@ -180,7 +180,7 @@ public class FamilyMembersControllerTest {
         body.put("nickname", "新名字");
 
         when(familyMemberService.updateMember(eq(1L), eq(5L), any(AddFamilyMemberDTO.class)))
-            .thenReturn(mockMemberVO(5L, "新名字", "son"));
+            .thenReturn(mockMemberVO(5L, "新名字", "parent"));
 
         Result<FamilyMemberVO> result = controller.updateMember(request, body);
         assertEquals(200, result.getCode());
@@ -197,19 +197,4 @@ public class FamilyMembersControllerTest {
         assertTrue(result.getMessage().contains("缺少成员ID"));
     }
 
-    // ==================== getRelationshipTypes ====================
-
-    @Test
-    public void getRelationshipTypes_Success() {
-        List<RelationshipType> types = Arrays.asList(
-            new RelationshipType() {{ setTypeKey("father"); setTypeName("爸爸"); }},
-            new RelationshipType() {{ setTypeKey("mother"); setTypeName("妈妈"); }}
-        );
-
-        when(familyMemberService.getRelationshipTypes()).thenReturn(types);
-
-        Result<List<RelationshipType>> result = controller.getRelationshipTypes();
-        assertEquals(200, result.getCode());
-        assertEquals(2, result.getData().size());
-    }
 }

+ 9 - 49
cfc-backend/src/test/java/com/etotem/cfc/integration/FamilyMemberManagementFlowTest.java

@@ -5,7 +5,6 @@ import com.etotem.cfc.controller.family.FamilyMembersController;
 import com.etotem.cfc.dto.AddFamilyMemberDTO;
 import com.etotem.cfc.dto.FamilyMemberVO;
 import com.etotem.cfc.dto.SwitchMemberVO;
-import com.etotem.cfc.entity.RelationshipType;
 import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.service.FamilyMemberService;
@@ -67,38 +66,7 @@ public class FamilyMemberManagementFlowTest {
     private static final Long FAMILY_ID = 3001L;
     private static final String SWITCH_TOKEN = "switch_token_abc123";
 
-    // ========== 场景1:获取关系类型列表 ==========
-
-    /**
-     * 场景:管理员进入添加成员页面,需要选择关系类型
-     * 角色:家长(管理员)
-     * 触发条件:打开添加家庭成员页面
-     */
-    @Test
-    @DisplayName("[场景1] 获取关系类型列表 - 期望返回可用的关系类型")
-    void parentGetsRelationshipTypes_Success() {
-        // === Given:系统存在预置的关系类型 ===
-        List<RelationshipType> mockTypes = Arrays.asList(
-                createRelationshipType(1L, "父亲", "parent", 1),
-                createRelationshipType(2L, "母亲", "parent", 2),
-                createRelationshipType(3L, "儿子", "child", 3),
-                createRelationshipType(4L, "女儿", "child", 4),
-                createRelationshipType(5L, "爷爷", "elder", 5),
-                createRelationshipType(6L, "奶奶", "elder", 6)
-        );
-        when(familyMemberService.getRelationshipTypes()).thenReturn(mockTypes);
-
-        // === When:请求获取关系类型列表 ===
-        Result<List<RelationshipType>> result = familyMembersController.getRelationshipTypes();
-
-        // === Then:Milestone-1 - 返回完整的关系类型 ===
-        assertEquals(200, result.getCode());
-        assertNotNull(result.getData());
-        assertEquals(6, result.getData().size());
-        verify(familyMemberService).getRelationshipTypes();
-    }
-
-    // ========== 场景2:添加新家庭成员 ==========
+    // ========== 场景1:添加新家庭成员 ==========
 
     /**
      * 场景:家长添加一个新的孩子成员到家庭
@@ -113,7 +81,8 @@ public class FamilyMemberManagementFlowTest {
         addDTO.setNickname("王小明");
         addDTO.setGender("男");
         addDTO.setBirthday("2015-06-15");
-        addDTO.setRelationshipType("child");
+        addDTO.setGenerationLevel("child");
+        addDTO.setRelativeMemberId(1L);
 
         FamilyMemberVO mockMember = createFamilyMemberVO(CHILD_MEMBER_ID, "王小明", "child");
         when(familyMemberService.addMember(eq(PARENT_USER_ID), any(AddFamilyMemberDTO.class)))
@@ -128,7 +97,7 @@ public class FamilyMemberManagementFlowTest {
         assertNotNull(result.getData());
         assertNotNull(result.getData().getId());
         assertEquals("王小明", result.getData().getNickname());
-        assertEquals("child", result.getData().getRelationshipType());
+        assertEquals("child", result.getData().getEffectiveRole());
         verify(familyMemberService).addMember(eq(PARENT_USER_ID), any(AddFamilyMemberDTO.class));
     }
 
@@ -143,7 +112,8 @@ public class FamilyMemberManagementFlowTest {
         // === Given:家长未填写成员姓名 ===
         AddFamilyMemberDTO addDTO = new AddFamilyMemberDTO();
         addDTO.setNickname(""); // 空姓名
-        addDTO.setRelationshipType("child");
+        addDTO.setGenerationLevel("child");
+        addDTO.setRelativeMemberId(1L);
 
         when(familyMemberService.addMember(eq(PARENT_USER_ID), any(AddFamilyMemberDTO.class)))
                 .thenThrow(new IllegalArgumentException("姓名不能为空"));
@@ -173,7 +143,7 @@ public class FamilyMemberManagementFlowTest {
                 createFamilyMemberVO(1L, "张爸爸", "parent"),
                 createFamilyMemberVO(2L, "李妈妈", "parent"),
                 createFamilyMemberVO(CHILD_MEMBER_ID, "王小明", "child"),
-                createFamilyMemberVO(4L, "王爷爷", "elder")
+                createFamilyMemberVO(4L, "王爷爷", "parent")
         );
         when(familyMemberService.listMembers(PARENT_USER_ID)).thenReturn(mockMembers);
 
@@ -354,21 +324,11 @@ public class FamilyMemberManagementFlowTest {
 
     // ========== Helper Methods ==========
 
-    private RelationshipType createRelationshipType(Long id, String typeName, String typeKey, Integer sortOrder) {
-        RelationshipType type = new RelationshipType();
-        type.setId(id);
-        type.setTypeName(typeName);
-        type.setTypeKey(typeKey);
-        type.setSortOrder(sortOrder);
-        type.setEnabled(1);
-        return type;
-    }
-
-    private FamilyMemberVO createFamilyMemberVO(Long id, String nickname, String relationshipType) {
+    private FamilyMemberVO createFamilyMemberVO(Long id, String nickname, String effectiveRole) {
         FamilyMemberVO vo = new FamilyMemberVO();
         vo.setId(id);
         vo.setNickname(nickname);
-        vo.setRelationshipType(relationshipType);
+        vo.setEffectiveRole(effectiveRole);
         vo.setGender("男");
         return vo;
     }

+ 48 - 80
cfc-frontend/components/GenerationPicker.vue

@@ -3,24 +3,27 @@
     <view class="form-item required" v-if="showLabel">
       <text class="label"><text class="required-mark">*</text>辈分关系</text>
     </view>
-    <picker :range="generationNames" :value="selectedIndex" @change="onGenerationChange" :disabled="disabled">
+    <picker :range="genLabels" @change="onGenerationChange" :disabled="disabled">
       <view class="picker" :class="{ 'picker-disabled': disabled }">
-        <text>{{ selectedGenerationName || '请选择辈分关系' }}</text>
+        <text>{{ selectedGenLabel || '请选择辈分' }}</text>
       </view>
     </picker>
-    
-    <!-- 同辈配偶选择器(仅当选择"同辈"时显示) -->
-    <view class="peer-type-selector" v-if="selectedGeneration === 'PEER' && showPeerType">
+
+    <!-- 同辈关系类型(仅当选中的是同辈时显示) -->
+    <view class="peer-type-wrap" v-if="selectedGen === 'peer'">
+      <view class="form-item">
+        <text class="label"><text class="required-mark">*</text>同辈关系类型</text>
+      </view>
       <view class="radio-group">
         <label class="radio-item" @tap="selectPeerType('sibling')">
-          <view class="radio-icon" :class="{ active: peerType === 'sibling' }">
-            <view class="radio-inner" v-if="peerType === 'sibling'"></view>
+          <view class="radio-icon" :class="{ active: selectedPeer === 'sibling' }">
+            <view class="radio-inner" v-if="selectedPeer === 'sibling'"></view>
           </view>
           <text class="radio-label">兄弟姐妹</text>
         </label>
         <label class="radio-item" @tap="selectPeerType('spouse')">
-          <view class="radio-icon" :class="{ active: peerType === 'spouse' }">
-            <view class="radio-inner" v-if="peerType === 'spouse'"></view>
+          <view class="radio-icon" :class="{ active: selectedPeer === 'spouse' }">
+            <view class="radio-inner" v-if="selectedPeer === 'spouse'"></view>
           </view>
           <text class="radio-label">配偶</text>
         </label>
@@ -30,7 +33,7 @@
 </template>
 
 <script>
-import { GenerationLevel } from '../utils/constants.js'
+var { GENERATION_LEVELS, PEER_TYPES, getGenerationLabel, getPeerTypeLabel } = require('../utils/generationLevel.js')
 
 export default {
   name: 'GenerationPicker',
@@ -38,85 +41,50 @@ export default {
     generationLevel: { type: String, default: '' },
     peerType: { type: String, default: '' },
     disabled: { type: Boolean, default: false },
-    showLabel: { type: Boolean, default: true },
-    showPeerType: { type: Boolean, default: true }
+    showLabel: { type: Boolean, default: true }
   },
   data() {
     return {
-      generations: [
-        { value: 'ELDER_2', name: '祖父母辈(+2)' },
-        { value: 'ELDER_1', name: '父母辈(+1)' },
-        { value: 'PEER', name: '同辈(0)' },
-        { value: 'YOUNGER_1', name: '子女辈(-1)' },
-        { value: 'YOUNGER_2', name: '孙辈(-2)' }
-      ],
-      selectedGeneration: '',
-      selectedPeerType: ''
+      GEN_LEVELS: GENERATION_LEVELS,
+      selectedGen: this.generationLevel || '',
+      selectedPeer: this.peerType || ''
     }
   },
   computed: {
-    generationNames() {
-      var names = []
-      for (var i = 0; i < this.generations.length; i++) {
-        names.push(this.generations[i].name)
-      }
-      return names
-    },
-    selectedIndex() {
-      if (!this.selectedGeneration) return 0
-      for (var i = 0; i < this.generations.length; i++) {
-        if (this.generations[i].value === this.selectedGeneration) {
-          return i
-        }
+    genLabels() {
+      var labels = []
+      for (var i = 0; i < this.GEN_LEVELS.length; i++) {
+        labels.push(this.GEN_LEVELS[i].label)
       }
-      return 0
+      return labels
     },
-    selectedGenerationName() {
-      if (!this.selectedGeneration) return ''
-      for (var i = 0; i < this.generations.length; i++) {
-        if (this.generations[i].value === this.selectedGeneration) {
-          return this.generations[i].name
-        }
-      }
-      return ''
-    }
-  },
-  mounted() {
-    if (this.generationLevel) {
-      this.selectedGeneration = this.generationLevel
-    }
-    if (this.peerType) {
-      this.selectedPeerType = this.peerType
+    selectedGenLabel() {
+      return getGenerationLabel(this.selectedGen)
     }
   },
   watch: {
     generationLevel(val) {
-      if (val) {
-        this.selectedGeneration = val
-      }
+      this.selectedGen = val || ''
     },
     peerType(val) {
-      if (val) {
-        this.selectedPeerType = val
-      }
+      this.selectedPeer = val || ''
     }
   },
   methods: {
     onGenerationChange(e) {
       var idx = e.detail.value
-      if (idx >= 0 && idx < this.generations.length) {
-        this.selectedGeneration = this.generations[idx].value
-        this.$emit('update:generationLevel', this.selectedGeneration)
-        
-        // 如果不是同辈,清空 peerType
-        if (this.selectedGeneration !== 'PEER') {
-          this.selectedPeerType = ''
+      if (idx >= 0 && idx < this.GEN_LEVELS.length) {
+        var gen = this.GEN_LEVELS[idx]
+        this.selectedGen = gen.value
+        if (gen.value !== 'peer') {
+          this.selectedPeer = ''
           this.$emit('update:peerType', '')
         }
+        this.$emit('update:generationLevel', this.selectedGen)
       }
     },
     selectPeerType(type) {
-      this.selectedPeerType = type
+      this.selectedPeer = type
       this.$emit('update:peerType', type)
     }
   }
@@ -125,7 +93,19 @@ export default {
 
 <style scoped>
 .generation-picker-wrap {
-  margin-bottom: 30rpx;
+  width: 100%;
+}
+.picker {
+  border: 1rpx solid #ddd;
+  border-radius: 10rpx;
+  padding: 20rpx;
+  font-size: 28rpx;
+  color: #666;
+  background: #fff;
+}
+.picker-disabled {
+  background: #f5f5f5;
+  color: #999;
 }
 .form-item {
   margin-bottom: 10rpx;
@@ -141,19 +121,7 @@ export default {
   color: #F97316;
   margin-right: 4rpx;
 }
-.picker {
-  border: 1rpx solid #ddd;
-  border-radius: 10rpx;
-  padding: 20rpx;
-  font-size: 28rpx;
-  color: #666;
-  background: #fff;
-}
-.picker-disabled {
-  background: #f5f5f5;
-  color: #999;
-}
-.peer-type-selector {
+.peer-type-wrap {
   margin-top: 20rpx;
   padding: 20rpx;
   background: #f9f9f9;
@@ -192,4 +160,4 @@ export default {
   font-size: 28rpx;
   color: #333;
 }
-</style>
+</style>

+ 54 - 0
cfc-frontend/utils/generationLevel.js

@@ -0,0 +1,54 @@
+/**
+ * 辈分等级常量(与后端 GenerationLevel 枚举对应)
+ * 家庭创建者 generation = 0
+ * +n = 向上一 n 代(父母辈=+1,祖辈=+2,曾祖辈=+3)
+ * -n = 向下一 n 代(子侄辈=-1,孙辈=-2,曾孙辈=-3)
+ */
+var GENERATION_LEVELS = [
+  { value: 'great_grandparent', label: '曾祖辈', offset: 3, icon: '👴' },
+  { value: 'grandparent', label: '祖辈', offset: 2, icon: '👴' },
+  { value: 'parent', label: '父辈', offset: 1, icon: '👨' },
+  { value: 'peer', label: '同辈', offset: 0, icon: '👤' },
+  { value: 'child', label: '子辈', offset: -1, icon: '👶' },
+  { value: 'grandchild', label: '孙辈', offset: -2, icon: '👶' },
+  { value: 'great_grandchild', label: '曾孙辈', offset: -3, icon: '👶' }
+]
+
+/**
+ * 平辈关系类型(仅 generation=peer 时使用)
+ */
+var PEER_TYPES = [
+  { value: 'spouse', label: '配偶' },
+  { value: 'sibling', label: '兄弟姐妹' }
+]
+
+/**
+ * 根据 generation value 获取显示标签
+ */
+function getGenerationLabel(value) {
+  for (var i = 0; i < GENERATION_LEVELS.length; i++) {
+    if (GENERATION_LEVELS[i].value === value) {
+      return GENERATION_LEVELS[i].label
+    }
+  }
+  return ''
+}
+
+/**
+ * 根据 peerType value 获取显示标签
+ */
+function getPeerTypeLabel(value) {
+  for (var i = 0; i < PEER_TYPES.length; i++) {
+    if (PEER_TYPES[i].value === value) {
+      return PEER_TYPES[i].label
+    }
+  }
+  return ''
+}
+
+module.exports = {
+  GENERATION_LEVELS: GENERATION_LEVELS,
+  PEER_TYPES: PEER_TYPES,
+  getGenerationLabel: getGenerationLabel,
+  getPeerTypeLabel: getPeerTypeLabel
+}

+ 0 - 24
cfc-web/src/api/admin.js

@@ -852,30 +852,6 @@ export function getSystemHealth() {
   })
 }
 
-// 关系类型管理
-export function listRelationshipTypes() {
-  return request({
-    url: '/api/admin/relationship-type/list',
-    method: 'post'
-  })
-}
-
-export function saveRelationshipType(data) {
-  return request({
-    url: '/api/admin/relationship-type/save',
-    method: 'post',
-    data
-  })
-}
-
-export function deleteRelationshipType(id) {
-  return request({
-    url: '/api/admin/relationship-type/delete',
-    method: 'post',
-    data: { id }
-  })
-}
-
 // ========== 虚拟服务商团队管理 API ==========
 
 export function listVirtualTeams(params) {

+ 0 - 1
cfc-web/src/views/Layout.vue

@@ -264,7 +264,6 @@ export default {
             { title: '配置中心', icon: 'el-icon-setting', perm: 'system:config',
               children: [
                 { path: '/dimension-config', label: '维度配置', icon: 'el-icon-data-line', perm: 'system:config' },
-                { path: '/relationship-types', label: '关系类型', icon: 'el-icon-share', perm: 'system:config' },
               ]},
             // --- 供应商体系 ---
             { title: '供应商体系', icon: 'el-icon-s-management', perm: 'system:supply',

+ 0 - 238
cfc-web/src/views/admin/RelationshipTypes.vue

@@ -1,238 +0,0 @@
-<template>
-  <div class="relationship-types admin-page">
-    <el-card>
-      <div slot="header" class="admin-page-header">
-        <span class="admin-page-title">关系类型配置</span>
-        <div class="flex items-center gap-sm">
-          <el-input v-model="keyword" placeholder="搜索关系类型..." prefix-icon="el-icon-search" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
-          <el-button type="primary" size="small" @click="handleCreate">+ 添加关系类型</el-button>
-        </div>
-      </div>
-      <div class="table-scroll-wrap">
-      <el-table :data="filteredData" stripe :max-height="tableHeight">
-        <el-table-column prop="id" label="ID" width="80"></el-table-column>
-        <el-table-column prop="typeKey" label="编码" width="120"></el-table-column>
-        <el-table-column prop="typeName" label="显示名称" width="120"></el-table-column>
-        <el-table-column prop="defaultRole" label="默认角色" width="120">
-          <template slot-scope="scope">
-            <el-tag :type="scope.row.defaultRole === 'parent' ? 'primary' : 'success'" size="mini">
-              {{ scope.row.defaultRole === 'parent' ? '家长' : '孩子' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="sortOrder" label="排序" width="80"></el-table-column>
-        <el-table-column prop="enabled" label="状态" width="80">
-          <template slot-scope="scope">
-            <el-tag :type="scope.row.enabled === 1 ? 'success' : 'info'" size="mini">
-              {{ scope.row.enabled === 1 ? '启用' : '禁用' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="category" label="关系分类" width="120">
-          <template slot-scope="scope">
-            <el-tag :type="getCategoryType(scope.row.category)" size="mini">
-              {{ getCategoryLabel(scope.row.category) }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="isBloodRelation" label="血亲" width="80">
-          <template slot-scope="scope">
-            <el-tag :type="scope.row.isBloodRelation === 1 ? 'danger' : 'info'" size="mini">
-              {{ scope.row.isBloodRelation === 1 ? '是' : '否' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="isImmediateFamily" label="直系亲属" width="100">
-          <template slot-scope="scope">
-            <el-tag :type="scope.row.isImmediateFamily === 1 ? 'warning' : 'info'" size="mini">
-              {{ scope.row.isImmediateFamily === 1 ? '是' : '否' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="200" fixed="right">
-          <template slot-scope="{ row }">
-            <el-button size="mini" type="primary" @click="handleEdit(row)">编辑</el-button>
-            <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
-              <el-button size="mini">
-                更多<i class="el-icon-arrow-down el-icon--right"></i>
-              </el-button>
-              <el-dropdown-menu slot="dropdown">
-                <el-dropdown-item command="delete" icon="el-icon-delete">删除</el-dropdown-item>
-              </el-dropdown-menu>
-            </el-dropdown>
-          </template>
-        </el-table-column>
-      </el-table></div>
-    </el-card>
-
-    <el-dialog :visible.sync="dialogVisible" :title="isEdit ? '编辑关系类型' : '添加关系类型'" width="500px">
-      <el-form :model="form" label-width="100px">
-        <el-form-item label="编码">
-          <el-input v-model="form.typeKey" placeholder="如: child, spouse, parent, sibling"></el-input>
-        </el-form-item>
-        <el-form-item label="显示名称">
-          <el-input v-model="form.typeName" placeholder="如: 子女, 配偶, 父母, 兄弟姐妹"></el-input>
-        </el-form-item>
-        <el-form-item label="默认角色">
-          <el-select v-model="form.defaultRole" placeholder="选择默认角色">
-            <el-option label="家长(配偶/父母等)" value="parent"></el-option>
-            <el-option label="孩子(子女/兄弟姐妹等)" value="child"></el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="排序">
-          <el-input-number v-model="form.sortOrder" :min="0" :max="999" style="width:150px"></el-input-number>
-        </el-form-item>
-        <el-form-item label="状态">
-          <el-switch v-model="form.enabled" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁用"></el-switch>
-        </el-form-item>
-        <el-form-item label="关系分类">
-          <el-select v-model="form.category" placeholder="选择关系分类">
-            <el-option label="血亲" value="blood"></el-option>
-            <el-option label="姻亲" value="marriage"></el-option>
-            <el-option label="社会关系" value="social"></el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="血亲关系">
-          <el-switch v-model="form.isBloodRelation" :active-value="1" :inactive-value="0" active-text="是" inactive-text="否"></el-switch>
-        </el-form-item>
-        <el-form-item label="直系亲属">
-          <el-switch v-model="form.isImmediateFamily" :active-value="1" :inactive-value="0" active-text="是" inactive-text="否"></el-switch>
-        </el-form-item>
-      </el-form>
-      <div slot="footer">
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleSubmit">保存</el-button>
-      </div>
-    </el-dialog>
-
-    <el-dialog title="操作成功" :visible.sync="successDialogVisible" width="400px" :close-on-click-modal="false">
-      <div style="text-align:center;padding:10px 0">
-        <i class="el-icon-success" style="color:#67C23A;font-size:32px"></i>
-        <p style="margin:12px 0 0;font-size:16px;color:#303133">保存成功</p>
-      </div>
-      <div slot="footer">
-        <el-button type="primary" @click="handleContinue">继续添加</el-button>
-        <el-button @click="handleClose">关闭</el-button>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import { listRelationshipTypes, saveRelationshipType, deleteRelationshipType } from '@/api/admin'
-
-export default {
-  name: 'RelationshipTypes',
-  data() {
-    return {
-      types: [],
-      dialogVisible: false,
-      successDialogVisible: false,
-      isEdit: false,
-      form: this.getEmptyForm(),
-      keyword: ''
-    }
-  },
-  computed: {
-    tableHeight() {
-      return window.innerHeight - 300
-    },
-    filteredData() {
-      if (!this.keyword) return this.types
-      const kw = this.keyword.toLowerCase()
-      return this.types.filter(item =>
-        [item.typeKey, item.typeName, item.category].some(v => (v||'').toLowerCase().includes(kw))
-      )
-    }
-  },
-  created() {
-    this.loadData()
-  },
-  methods: {
-    handleSearch() {},
-    getEmptyForm() {
-      return {
-        id: null,
-        typeKey: '',
-        typeName: '',
-        defaultRole: 'child',
-        sortOrder: 0,
-        enabled: 1,
-        category: 'social',
-        isBloodRelation: 0,
-        isImmediateFamily: 0
-      }
-    },
-    async loadData() {
-      try {
-        const res = await listRelationshipTypes()
-        if (res.data) this.types = res.data
-      } catch (e) {
-        console.error(e)
-      }
-    },
-    handleCreate() {
-      this.isEdit = false
-      this.form = this.getEmptyForm()
-      this.dialogVisible = true
-    },
-    handleEdit(row) {
-      this.isEdit = true
-      this.form = { ...row }
-      this.dialogVisible = true
-    },
-    async handleDelete(row) {
-      try {
-        await this.$confirm('确定删除该关系类型吗?删除后已使用该类型的成员不受影响。', '提示', { type: 'warning' })
-        await deleteRelationshipType(row.id)
-        this.$message.success('删除成功')
-        this.loadData()
-      } catch (e) {
-        if (e !== 'cancel') this.$message.error('删除失败')
-      }
-    },
-    async handleSubmit() {
-      if (!this.form.typeKey || !this.form.typeName) {
-        this.$message.warning('编码和显示名称不能为空')
-        return
-      }
-      try {
-        await saveRelationshipType(this.form)
-        this.$message.success('保存成功')
-        this.successDialogVisible = true
-      } catch (e) {
-        this.$message.error('保存失败')
-      }
-    },
-    handleActionCmd(row, cmd) {
-      switch (cmd) {
-        case 'delete':
-          this.handleDelete(row)
-          break
-      }
-    },
-    handleContinue() {
-      this.successDialogVisible = false
-      this.isEdit = false
-      this.form = this.getEmptyForm()
-      this.dialogVisible = true
-    },
-    handleClose() {
-      this.successDialogVisible = false
-      this.dialogVisible = false
-      this.loadData()
-    },
-    getCategoryType(category) {
-      const map = { blood: 'danger', marriage: 'warning', social: 'info' }
-      return map[category] || 'info'
-    },
-    getCategoryLabel(category) {
-      const map = { blood: '血亲', marriage: '姻亲', social: '社会关系' }
-      return map[category] || category
-    }
-  }
-}
-</script>
-
-<style scoped>
-</style>