# FamilyMemberStrip 关系展示增强 实现计划 > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 在 FamilyMemberStrip/FamilyMemberCard 上直接呈现关系类型与关系现状(信任度/亲密度/沟通质量),长按菜单升级为带关系现状区块的 Action Sheet,支持未创建家庭引导 + 当前用户添加/邀请成员入口,并新增 pairwise 接口支撑成员详情页以任意成员为中心的视图。 **架构:** 后端在 `family_relationships` 表增加评分三列并新增 pairwise 接口(问卷提交时同步写入);前端在 FamilyMemberCard 徽章下增加三个迷你色点,FamilyMemberStrip 增加空状态引导与 `centerMemberId` prop,长按菜单升级为分区 Action Sheet。 **技术栈:** Spring Boot 2.7.18 + MyBatis-Plus(后端)/ uni-app Vue 2 Options API(前端) **关联设计文档:** `docs/superpowers/specs/2026-08-05-family-relationship-strip-design.md` --- ## 文件结构 **后端(cfc-backend/src/main/java/com/etotem/cfc/):** - 修改 `entity/FamilyRelationship.java` — 增加 trustScore/intimacyScore/communicationScore 三字段 - 修改 `config/DatabaseInitializer.java` — 迁移154:family_relationships 表加三列 - 修改 `resources/schema.sql` — family_relationships CREATE TABLE 补三列 - 修改 `service/RelationshipQuestionnaireService.java` — submitQuestionnaire 增加 pairwise 落库 - 修改 `dto/FamilyMemberVO.java` — 补齐 communicationScore 字段 - 修改 `service/FamilyMemberService.java` — listMembers 补齐 communicationScore - 修改 `service/RelationshipQualityService.java` — 新增 getPairwiseRelationships() - 修改 `controller/family/RelationshipQualityController.java` — 新增 POST /pairwise **前端(cfc-frontend/):** - 修改 `components/FamilyMemberCard.vue` — 徽章下加三色点 - 修改 `components/FamilyMemberStrip.vue` — 空状态引导 + centerMemberId prop + Action Sheet 升级 - 修改 `utils/api.js` — 新增 getPairwiseRelationships() - 修改 `pages/home-pages/member-home-detail.vue` — 接入 centerMemberId 模式 **测试:** - 无现成后端测试框架(mvn test 只有 1 个测试类),以 `mvn clean compile` 验证 - 前端以 `npm run build:mp-weixin` 验证 --- ### 任务 1:后端实体 + 数据库迁移 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyRelationship.java` - 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` - 修改:`cfc-backend/src/main/resources/schema.sql` - [ ] **步骤 1:实体加字段** 在 `FamilyRelationship.java` 的 `relationshipKey` 字段后追加: ```java /** 信任度(答卷人视角) 0-100 */ private BigDecimal trustScore; /** 亲密度(答卷人视角) 0-100 */ private BigDecimal intimacyScore; /** 沟通质量(答卷人视角) 0-100 */ private BigDecimal communicationScore; ``` 并补充 import(若未引入 `java.math.BigDecimal`,加在 `import java.io.Serializable;` 之后)。 - [ ] **步骤 2:DatabaseInitializer 加迁移** 找到 `// 迁移153:` 注释块,在其后追加(用 `ensureColumn` 三次调用): ```java // 迁移154: family_relationships表添加关系质量评分三列(关系条展示增强) ensureColumn("family_relationships", "trust_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '信任度(答卷人视角)'"); ensureColumn("family_relationships", "intimacy_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '亲密度(答卷人视角)'"); ensureColumn("family_relationships", "communication_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '沟通质量(答卷人视角)'"); ``` - [ ] **步骤 3:同步 schema.sql** `schema.sql` 的 `family_relationships` CREATE TABLE(约 1818 行)在 `relationship_key` 行后加三列: ```sql trust_score DECIMAL(5,2) DEFAULT 0 COMMENT '信任度(答卷人视角)', intimacy_score DECIMAL(5,2) DEFAULT 0 COMMENT '亲密度(答卷人视角)', communication_score DECIMAL(5,2) DEFAULT 0 COMMENT '沟通质量(答卷人视角)', ``` - [ ] **步骤 4:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 5:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyRelationship.java cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql git commit -m "feat(backend): family_relationships表增加关系质量评分三列" ``` --- ### 任务 2:问卷提交同步写入 pairwise 关系评分 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/RelationshipQuestionnaireService.java` - [ ] **步骤 1:注入 FamilyRelationshipMapper 并补 import** 该文件已有 `@Resource private FamilyMemberMapper familyMemberMapper;`(line 38)与全部所需 import(LambdaQueryWrapper/FamilyMember/BigDecimal/Date/ArrayList)。只需新增 mapper 注入: ```java @Resource private com.etotem.cfc.mapper.FamilyRelationshipMapper familyRelationshipMapper; ``` 并在 import 区(`import com.etotem.cfc.entity.RelationshipQuestionnaireSnapshot;` 之后)补: ```java import com.etotem.cfc.entity.FamilyRelationship; import com.etotem.cfc.mapper.FamilyRelationshipMapper; ``` - [ ] **步骤 2:修改 submitQuestionnaire,提交后调用 pairwise 落库** 在 `submitQuestionnaire` 中 `updateMemberScores(snapshot.getFamilyMemberId(), scores);` 之后追加: ```java // 同步写入 pairwise 关系评分(答卷人视角 → 被评成员) syncPairwiseScores(snapshot.getRespondentId(), snapshot.getFamilyMemberId(), scores); ``` - [ ] **步骤 3:新增 syncPairwiseScores 方法** 在 `updateMemberScores` 方法后新增: ```java /** * 将问卷评分写入 family_relationships 表(答卷人视角) * 答卷人无 family_member 记录或关系行不存在时,静默跳过(不阻塞问卷保存) */ private void syncPairwiseScores(Long respondentUserId, Long targetMemberId, BigDecimal[] scores) { try { if (respondentUserId == null || targetMemberId == null || scores == null) return; // 找到答卷人对应的 family_member 记录 LambdaQueryWrapper respWrapper = new LambdaQueryWrapper<>(); respWrapper.eq(FamilyMember::getUserId, respondentUserId); FamilyMember respondent = familyMemberMapper.selectOne(respWrapper); if (respondent == null) { log.warn("答卷人用户{}无family_member记录,跳过pairwise评分同步", respondentUserId); return; } // 查 from→to 关系行(有则更新,无则插入) LambdaQueryWrapper relWrapper = new LambdaQueryWrapper<>(); relWrapper.eq(FamilyRelationship::getFromMemberId, respondent.getId()) .eq(FamilyRelationship::getToMemberId, targetMemberId); FamilyRelationship rel = familyRelationshipMapper.selectOne(relWrapper); Date now = new Date(); if (rel != null) { rel.setTrustScore(scores[0]); rel.setIntimacyScore(scores[1]); rel.setCommunicationScore(scores[2]); rel.setUpdatedAt(now); familyRelationshipMapper.updateById(rel); } else { FamilyMember target = familyMemberMapper.selectById(targetMemberId); if (target == null) return; FamilyRelationship newRel = new FamilyRelationship(); newRel.setFamilyId(target.getFamilyId()); newRel.setFromMemberId(respondent.getId()); newRel.setToMemberId(targetMemberId); int respGen = respondent.getGeneration() != null ? respondent.getGeneration() : 0; int targetGen = target.getGeneration() != null ? target.getGeneration() : 0; newRel.setGenerationValue(respGen - targetGen); newRel.setTrustScore(scores[0]); newRel.setIntimacyScore(scores[1]); newRel.setCommunicationScore(scores[2]); newRel.setCreatedAt(now); newRel.setUpdatedAt(now); familyRelationshipMapper.insert(newRel); } } catch (Exception e) { log.warn("pairwise评分同步失败: {}", e.getMessage()); } } ``` 需要新增 import:`com.etotem.cfc.entity.FamilyRelationship`(文件顶部 `import com.etotem.cfc.entity.RelationshipQuestionnaireSnapshot;` 之后)。 - [ ] **步骤 4:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 5:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/RelationshipQuestionnaireService.java git commit -m "feat(backend): 问卷提交时同步pairwise关系评分到family_relationships" ``` --- ### 任务 3:后端 VO 补齐 communicationScore + pairwise 接口 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java` - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java` - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/RelationshipQualityService.java` - 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/family/RelationshipQualityController.java` - [ ] **步骤 1:FamilyMemberVO 增加 communicationScore 字段** `FamilyMemberVO.java` 现有 `intimacyScore`(line 65)与 `lastInteractionAt`(line 68)之间补: ```java /** 沟通质量分数 */ private java.math.BigDecimal communicationScore; ``` - [ ] **步骤 2:FamilyMemberService.listMembers 补齐 communicationScore 与 isSelf** `FamilyMemberService.java` 现有 `vo.setTrustScore(fm.getTrustScore());`(line 226)与 `vo.setIntimacyScore(fm.getIntimacyScore());`(line 227),在其后补 communicationScore;同时该方法**从未设置 `isSelf`**(已核实,全文件无 `setIsSelf`),需一并补齐——否则首页 8 处现有 FamilyMemberStrip 调用中"添加成员/邀请成员"入口永不显示,且现有 `canRemove` 的 `!actionMember.isSelf` 判断恒为 true(对自己也显示移除按钮): ```java vo.setCommunicationScore(fm.getCommunicationScore()); vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId())); ``` > 说明:`viewerMember` 在该方法 line 175-181 已计算(当前查看者在 family_members 中的记录),直接复用。`isSelf` 语义 = 该成员就是当前登录用户在家庭中的记录。 - [ ] **步骤 3:RelationshipQualityService 新增 getPairwiseRelationships 方法** 该类已 import `LambdaQueryWrapper / FamilyMember / FamilyMemberMapper / BigDecimal / ArrayList / Date`,且已有 private `computeEffectiveRole(FamilyMember)`(line 211)可复用。新增注入: ```java @Resource private com.etotem.cfc.service.FamilyRelationshipService familyRelationshipService; ``` 在 `getScores` 方法后新增: ```java /** * 获取以指定成员为中心的所有成员关系(关系类型 + 三项评分) */ public List getPairwiseRelationships(Long centerMemberId, Long familyId) { List result = new ArrayList<>(); FamilyMember center = familyMemberMapper.selectById(centerMemberId); if (center == null || (familyId != null && !familyId.equals(center.getFamilyId()))) { throw new RuntimeException("成员不存在或不属于该家庭"); } if (familyId == null) { familyId = center.getFamilyId(); } List members = familyMemberMapper.selectList( new LambdaQueryWrapper().eq(FamilyMember::getFamilyId, familyId)); // 中心成员 → 各成员的关系行(评分与关系类型来源) List rels = familyRelationshipService.getRelationsForMember(centerMemberId); Map relMap = new java.util.HashMap<>(); for (FamilyRelationship rel : rels) { relMap.put(rel.getToMemberId(), rel); } for (FamilyMember fm : members) { FamilyMemberVO vo = new FamilyMemberVO(); vo.setId(fm.getId()); vo.setSource("family_member"); vo.setUserId(fm.getUserId()); vo.setNickname(fm.getNickname()); vo.setPhone(fm.getPhone()); vo.setAvatar(fm.getAvatar()); vo.setGender(fm.getGender()); vo.setGeneration(fm.getGeneration()); vo.setEffectiveRole(computeEffectiveRole(fm)); vo.setRoleLabel(computeEffectiveRole(fm)); vo.setIsSelf(centerMemberId.equals(fm.getId())); vo.setAge(fm.getAge()); FamilyRelationship rel = relMap.get(fm.getId()); if (rel != null) { vo.setGenerationValue(rel.getGenerationValue()); String label = familyRelationshipService.computeRelationshipLabel(rel.getGenerationValue(), fm.getGender()); vo.setRelativeLabel(label); vo.setRelationship(label); vo.setTrustScore(rel.getTrustScore()); vo.setIntimacyScore(rel.getIntimacyScore()); vo.setCommunicationScore(rel.getCommunicationScore()); } else if (centerMemberId.equals(fm.getId())) { vo.setRelationship("我"); vo.setRelativeLabel("我"); } else { vo.setTrustScore(BigDecimal.ZERO); vo.setIntimacyScore(BigDecimal.ZERO); vo.setCommunicationScore(BigDecimal.ZERO); } result.add(vo); } return result; } ``` 需要新增 import:`com.etotem.cfc.entity.FamilyRelationship`、`com.etotem.cfc.dto.FamilyMemberVO`、`java.util.Map`。 > **注意(已核实):** `FamilyMember` 实体自带 `age` 字段(line 67),直接 `vo.setAge(fm.getAge())`,无需调 `FamilyMemberService.calculateAge`(那是 private 方法)。`computeEffectiveRole` 是本类 private 方法,可直接调用。 - [ ] **步骤 4:Controller 新增 pairwise 端点** `RelationshipQualityController` 已有 `getFamilyIdFromRequest(userId, body)`(line 30)与 `@PostMapping("/scores")` 模式。在 `getScores` 方法后新增: ```java @Operation(summary = "获取以指定成员为中心的所有成员关系(pairwise)") @PostMapping("/pairwise") public Result> getPairwise(@RequestBody Map body, @RequestAttribute("userId") Long userId) { if (userId == null) { return Result.error("请先登录"); } Long centerMemberId = body.get("centerMemberId") != null ? Long.valueOf(body.get("centerMemberId").toString()) : null; if (centerMemberId == null) { return Result.error("centerMemberId不能为空"); } Long familyId = getFamilyIdFromRequest(userId, body); try { List members = relationshipQualityService.getPairwiseRelationships(centerMemberId, familyId); return Result.success(members); } catch (RuntimeException e) { return Result.error(e.getMessage()); } } ``` 需要新增 import:`com.etotem.cfc.dto.FamilyMemberVO`。 - [ ] **步骤 5:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 6:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java cfc-backend/src/main/java/com/etotem/cfc/service/RelationshipQualityService.java cfc-backend/src/main/java/com/etotem/cfc/controller/family/RelationshipQualityController.java git commit -m "feat(backend): 关系pairwise接口(以成员为中心) + VO补齐沟通评分" ``` --- ### 任务 4:前端 FamilyMemberCard 三色点 **文件:** - 修改:`cfc-frontend/components/FamilyMemberCard.vue` - [ ] **步骤 1:模板加三色点(关系徽章下方)** `card-relation-badge` 的 `` 之后、`card-spouse-mark` 之前插入: ```html ``` > **注意:** 模板中禁止带参数方法调用(`dotColor(scoreKey('trust'))` 嵌套调用在微信小程序模板编译器可能报错)。**改为计算属性返回三个色值数组:** ```html ``` - [ ] **步骤 2:script 增加计算属性** 在 `computed` 中 `relationColor` 后新增: ```js hasRelationScores: function() { var m = this.member if (!m) return false var t = Number(m.trustScore) || 0 var i = Number(m.intimacyScore) || 0 var c = Number(m.communicationScore) || 0 return t > 0 || i > 0 || c > 0 }, relationDotColors: function() { var m = this.member var empty = '#E5E7EB' if (!m) return [empty, empty, empty] var self = this var keys = ['trustScore', 'intimacyScore', 'communicationScore'] return keys.map(function(k) { var v = Number(m[k]) || 0 if (v <= 0) return empty if (v >= 70) return '#10B981' if (v >= 40) return '#F59E0B' return '#EF4444' }) } ``` - [ ] **步骤 3:样式** `