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

Merge remote-tracking branch 'origin/cfclub' into cfclub

E2E Test Bot 5 дней назад
Родитель
Сommit
fd91d5618a
27 измененных файлов с 1054 добавлено и 88 удалено
  1. 8 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java
  3. 14 5
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyInviteController.java
  4. 4 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyJoinRequestDTO.java
  5. 18 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/JoinApproveDTO.java
  6. 6 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyJoinRequest.java
  7. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyRelationship.java
  8. 137 4
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyJoinRequestService.java
  9. 46 4
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java
  10. 122 2
      cfc-backend/src/main/java/com/etotem/cfc/service/FamilyRelationshipService.java
  11. 4 0
      cfc-backend/src/main/resources/schema.sql
  12. 4 31
      cfc-frontend/pages/invite/join-form.vue
  13. 168 12
      cfc-frontend/pages/profile-extra/family-members.vue
  14. 11 5
      cfc-frontend/utils/api.js
  15. 1 1
      cfc-web/.last_build_commit
  16. 2 2
      cfc-web/package-lock.json
  17. 1 1
      cfc-web/package.json
  18. 44 0
      cfc-web/public/CHANGELOG-v1.0.md
  19. 45 1
      cfc-web/public/CHANGELOG.md
  20. 13 5
      docs/superpowers/specs/2026-11-07-family-join-dynamic-identity.md
  21. 23 9
      docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md
  22. 55 0
      docs/tdd/orchestration/dx-health-kickoff-20261107.md
  23. 163 0
      docs/tdd/orchestration/report-dev-2026-11-07-1913.md
  24. 156 0
      docs/tdd/orchestration/report-dev-2026-11-07-1955.md
  25. 4 4
      docs/tdd/test/automation-run-2026-11-07-2010.md
  26. 0 1
      docs/tdd/test/blockers.md
  27. 1 1
      docs/tdd/test/notice-2026-11-07-2010-73round.md

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3463,6 +3463,14 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
 
         // 迁移454: 旧体系接口调用打点表(P2-T3 退役证据,按 api_path+period 每日聚合)
         createLegacyDimensionCallLogTable();
+
+        // 迁移455: family_relationships 增加关系类型列(支持夫妻/兄弟姐妹/直系区分,家庭加入身份由邀请人指定)
+        // 规格: docs/superpowers/specs/2026-11-07-family-join-dynamic-identity.md(注:编号 455/456 因 454 已被打点表占用)
+        ensureColumn("family_relationships", "relation_type", "VARCHAR(20) DEFAULT 'lineal' COMMENT '关系类型: lineal/sibling/spouse'");
+
+        // 迁移456: family_join_requests 记录审批时指定的身份(可追溯)
+        ensureColumn("family_join_requests", "assigned_generation", "INT NULL COMMENT '审批时指定的绝对辈分'");
+        ensureColumn("family_join_requests", "assigned_peer_type", "VARCHAR(10) NULL COMMENT '审批时指定的同辈类型: sibling/spouse'");
     }
 
     /**

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java

@@ -113,6 +113,7 @@ private static final String[] PUBLIC_PATHS = {
         "/api/health/report/",              // 健康报告:家庭私有健康数据(上传/解析/列表/详情/指标/删除)
         "/api/health/checkin/",             // 健康打卡:成员私有数据
         "/api/xiaomi-health/",              // 小米运动健康:OAuth 授权绑定/同步/撤销,成员私有凭证与数据
+        "/api/huawei-health/",              // 华为运动健康:OAuth 授权绑定/同步/撤销,成员私有凭证与数据,必须登录(与 xiaomi-health 对齐)
         "/api/tasks/",                      // 任务:家庭私有数据(今日/历史/审核/完成)
         "/api/energy/",                     // 五维能量:用户私有数据(sandbox/logs/grant/score-history...)
         "/api/wishes/",                     // 心愿:家庭私有数据

+ 14 - 5
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyInviteController.java

@@ -6,6 +6,7 @@ import com.etotem.cfc.dto.InviteAcceptDTO;
 import com.etotem.cfc.dto.InviteGenerateDTO;
 import com.etotem.cfc.dto.InviteRevokeDTO;
 import com.etotem.cfc.dto.InviteValidateDTO;
+import com.etotem.cfc.dto.JoinApproveDTO;
 import com.etotem.cfc.dto.TransferAdminDTO;
 import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.User;
@@ -145,21 +146,29 @@ public class FamilyInviteController {
 
     /**
      * 邀请人审批加入请求(谁分享的卡谁确认;旧数据无邀请人时由家庭管理员兜底)
+     * 身份由邀请人在审批时指定:generationLevel(辈分,必填)+ peerType(同辈类型,仅 peer 时必填)
      */
     @Operation(summary = "审批加入请求")
     @PostMapping("/approve-request")
-    public Result<Void> approveJoinRequest(@RequestBody Map<String, Object> body,
+    public Result<Void> approveJoinRequest(@RequestBody JoinApproveDTO dto,
         @RequestAttribute("userId") Long approverUserId) {
         if (approverUserId == null) {
             return Result.error("请先登录");
         }
-        Long requestId = ParamUtils.getLong(body.get("requestId"));
-        String comment = body.get("comment") != null ? body.get("comment").toString() : "";
-        if (requestId == null) {
+        if (dto == null || dto.getRequestId() == null) {
             return Result.error("请求ID不能为空");
         }
+        String generationLevel = dto.getGenerationLevel();
+        if (generationLevel == null || generationLevel.trim().isEmpty()) {
+            return Result.error("请选择辈分关系");
+        }
+        if ("peer".equals(generationLevel)
+                && (dto.getPeerType() == null || dto.getPeerType().trim().isEmpty())) {
+            return Result.error("请选择同辈关系类型");
+        }
         try {
-            familyJoinRequestService.approveJoinRequest(requestId, approverUserId, comment);
+            familyJoinRequestService.approveJoinRequest(dto.getRequestId(), approverUserId,
+                    dto.getComment() != null ? dto.getComment() : "", generationLevel, dto.getPeerType());
             return Result.success();
         } catch (Exception e) {
             return Result.error(e.getMessage());

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyJoinRequestDTO.java

@@ -11,6 +11,7 @@ public class FamilyJoinRequestDTO implements Serializable {
     private Long id;
     private Long requesterUserId;
     private String requesterNickname;
+    private String requesterRealName; // 申请人真实姓名(缺失时前端回退昵称)
     private String requesterAvatar;
     private String inviteCode;
     private Long inviterUserId;
@@ -19,6 +20,9 @@ public class FamilyJoinRequestDTO implements Serializable {
     private String status; // pending / approved / rejected
     private String adminComment;
     private String familyRole; // 申请人在家庭中的角色
+    private Integer assignedGeneration; // 审批时指定的绝对辈分
+    private String assignedPeerType; // 审批时指定的同辈类型: sibling/spouse
+    private String assignedPeerLabel; // 同辈类型中文标签(配偶/兄弟姐妹)
     private String createdAt;
     private String updatedAt;
 }

+ 18 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/JoinApproveDTO.java

@@ -0,0 +1,18 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 审批家庭加入请求 DTO(身份由邀请人在审批时指定:辈分关系 + 同辈类型)
+ */
+@Data
+public class JoinApproveDTO implements Serializable {
+    private Long requestId;
+    private String comment;
+    /** 辈分: great_grandparent|grandparent|parent|peer|child|grandchild|great_grandchild,必填 */
+    private String generationLevel;
+    /** 同辈类型: sibling|spouse,仅当 generationLevel=peer 时必填 */
+    private String peerType;
+}

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

@@ -44,6 +44,12 @@ public class FamilyJoinRequest implements Serializable {
     /** 申请人在家庭中的角色:爸爸/妈妈/爷爷/奶奶等 */
     private String familyRole;
 
+    /** 审批时指定的绝对辈分(邀请人在审批弹窗中选定,可追溯) */
+    private Integer assignedGeneration;
+
+    /** 审批时指定的同辈类型: sibling/spouse(仅同辈审批时写入) */
+    private String assignedPeerType;
+
     /** 创建时间 */
     private Date createdAt;
 

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyRelationship.java

@@ -27,6 +27,9 @@ public class FamilyRelationship implements Serializable {
     /** 辈分值: -1=子女, 0=同辈, 1=父母, 2=祖父母, 3=曾祖父母 */
     private Integer generationValue;
 
+    /** 关系类型: lineal(直系默认)/sibling(同辈非配偶)/spouse(同辈配偶);双向各写一行,取值相同 */
+    private String relationType;
+
     /** 关系标签key(冗余用于显示) */
     private String relationshipKey;
 

+ 137 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyJoinRequestService.java

@@ -7,6 +7,7 @@ import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.FamilyJoinRequest;
 import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.User;
+import com.etotem.cfc.enums.GenerationLevel;
 import com.etotem.cfc.mapper.FamilyInvitationMapper;
 import com.etotem.cfc.mapper.FamilyJoinRequestMapper;
 import com.etotem.cfc.mapper.FamilyMapper;
@@ -43,6 +44,9 @@ public class FamilyJoinRequestService {
     @Resource
     private FamilyInvitationService familyInvitationService;
 
+    @Resource
+    private FamilyRelationshipService familyRelationshipService;
+
     @Resource
     private UnlockGateService unlockGateService;
 
@@ -128,7 +132,8 @@ public class FamilyJoinRequestService {
         request.setInviterUserId(inviterUserId);
         request.setJoinMethod(joinMethod);
         request.setInviteRef(inviteCode.trim());
-        request.setFamilyRole(familyRole);
+        // 身份改由邀请人审批时指定(规格 2026-11-07-family-join-dynamic-identity):
+        // familyRole 保留接收但忽略,不在此写入申请表;审批通过时按「绝对辈分+性别」回写推导值
         request.setStatus("pending");
         request.setCreatedAt(new Date());
         request.setUpdatedAt(new Date());
@@ -140,9 +145,17 @@ public class FamilyJoinRequestService {
     }
 
     /**
-     * 审批通过加入请求(审批人=邀请人,谁分享的卡谁确认;旧数据无邀请人时回退为家庭管理员)
+     * 审批通过加入请求(兼容旧调用:不指定辈分 → 仅写 familyRole,不落地辈分/关系)
      */
     public void approveJoinRequest(Long requestId, Long approverUserId, String comment) {
+        approveJoinRequest(requestId, approverUserId, comment, null, null);
+    }
+
+    /**
+     * 审批通过加入请求(审批人=邀请人,谁分享的卡谁确认;旧数据无邀请人时回退为家庭管理员)
+     * 身份由邀请人在审批时指定:generationLevel(辈分)+ peerType(同辈类型)
+     */
+    public void approveJoinRequest(Long requestId, Long approverUserId, String comment, String generationLevel, String peerType) {
         FamilyJoinRequest request = familyJoinRequestMapper.selectById(requestId);
         if (request == null) {
             throw new RuntimeException("加入请求不存在");
@@ -164,6 +177,35 @@ public class FamilyJoinRequestService {
             throw new RuntimeException("请求用户不存在");
         }
 
+        // 身份解析:generationLevel 为空 → 旧逻辑(仅写 familyRole);否则校验并计算绝对辈分
+        GenerationLevel level = GenerationLevel.fromValue(generationLevel);
+        boolean withIdentity = generationLevel != null && !generationLevel.trim().isEmpty();
+        if (withIdentity) {
+            if (level == null) {
+                throw new RuntimeException("请选择辈分关系");
+            }
+            if (level == GenerationLevel.PEER && !"sibling".equals(peerType) && !"spouse".equals(peerType)) {
+                throw new RuntimeException("请选择同辈关系类型");
+            }
+        }
+
+        // 绝对辈分 = 审批人成员辈分(null 视为 0)+ 选项 offset
+        // anchorMemberId = 审批人在本家庭的成员记录(关系锚点:配偶关系只对锚点生效)
+        Integer absoluteGeneration = null;
+        Long anchorMemberId = null;
+        boolean isSpouse = false;
+        if (withIdentity) {
+            FamilyMember anchor = familyMemberMapper.selectOne(
+                    new LambdaQueryWrapper<FamilyMember>()
+                            .eq(FamilyMember::getFamilyId, family.getId())
+                            .eq(FamilyMember::getUserId, approverUserId)
+                            .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);
+        }
+
         if (user.getFamilyId() != null && user.getFamilyId() != 0L && !user.getFamilyId().equals(family.getId())) {
             // 申请人已有其他家庭:切换会被拒绝的情形前置拦截,给出明确主体与解决指引。
             // 关键:错误主体是【申请人】而不是审批人——申请人若为原家庭创建者,切换家庭
@@ -189,7 +231,9 @@ public class FamilyJoinRequestService {
         } else {
             if (user.getFamilyId() == null || user.getFamilyId() == 0L || !user.getFamilyId().equals(family.getId())) {
                 user.setFamilyId(family.getId());
-                user.setFamilyRole(request.getFamilyRole());
+                if (!withIdentity) {
+                    user.setFamilyRole(request.getFamilyRole());
+                }
                 user.setUpdatedAt(new Date());
                 userMapper.updateById(user);
 
@@ -218,6 +262,46 @@ public class FamilyJoinRequestService {
             }
         }
 
+        // 身份与关系落地(新建家庭分支与合并家庭分支统一处理):
+        // 合并分支 mergeIntoFamily 已迁移成员记录并对旧关系做清理,此处补写辈分并重建与新家庭的关系。
+        if (withIdentity) {
+            FamilyMember member = familyMemberMapper.selectOne(
+                    new LambdaQueryWrapper<FamilyMember>()
+                            .eq(FamilyMember::getFamilyId, family.getId())
+                            .eq(FamilyMember::getUserId, user.getId())
+                            .last("LIMIT 1"));
+            if (member == null) {
+                // 兜底创建(merge 路径已保证成员记录存在,仅历史异常数据走此分支)
+                String memberAvatar = (user.getAvatar() != null && !user.getAvatar().isEmpty())
+                        ? user.getAvatar() : "/static/avatar.jpg";
+                member = new FamilyMember();
+                member.setFamilyId(family.getId());
+                member.setUserId(user.getId());
+                member.setNickname(user.getNickname());
+                member.setAvatar(memberAvatar);
+                member.setGender(user.getGender());
+                member.setShowToFamily(1);
+                member.setTotalPoints(0);
+                member.setCreatedAt(new Date());
+                member.setUpdatedAt(new Date());
+                familyMemberMapper.insert(member);
+            }
+            member.setGeneration(absoluteGeneration);
+            member.setIsSpouse(isSpouse);
+            member.setUpdatedAt(new Date());
+            familyMemberMapper.updateById(member);
+
+            // 与新家庭全体现有成员建立双向关系(含 relation_type: lineal/sibling/spouse)
+            // 锚点=审批人成员:spouse 只写在「加入者 ↔ 审批人」之间,其他同辈一律 sibling
+            familyRelationshipService.createRelationsForNewMember(
+                    family.getId(), member.getId(), absoluteGeneration, peerType, anchorMemberId);
+
+            // User.familyRole 由绝对辈分 + 性别推导,保证下游 DTO 不为空
+            user.setFamilyRole(deriveFamilyRole(absoluteGeneration, user.getGender()));
+            user.setUpdatedAt(new Date());
+            userMapper.updateById(user);
+        }
+
         // 平台推荐人绑定(需求:未注册/已注册但无推荐人 → 绑定为邀请人的上级推荐人;已有推荐人 → 不动)
         bindReferrerFromInviter(request, user);
 
@@ -232,6 +316,12 @@ public class FamilyJoinRequestService {
         request.setStatus("approved");
         request.setApproverUserId(approverUserId);
         request.setComment(comment);
+        if (withIdentity) {
+            // 回写申请表:推导的 familyRole + 审批时指定的绝对辈分/同辈类型(可追溯)
+            request.setFamilyRole(deriveFamilyRole(absoluteGeneration, user.getGender()));
+            request.setAssignedGeneration(absoluteGeneration);
+            request.setAssignedPeerType(peerType);
+        }
         request.setUpdatedAt(new Date());
         familyJoinRequestMapper.updateById(request);
 
@@ -242,7 +332,8 @@ public class FamilyJoinRequestService {
             log.warn("刷新家庭通关解锁失败(不阻断审批): requestId={}, error={}", requestId, e.getMessage());
         }
 
-        log.info("已审批通过加入请求: requestId={}, userId={}, approverId={}", requestId, user.getId(), approverUserId);
+        log.info("已审批通过加入请求: requestId={}, userId={}, approverId={}, generationLevel={}, peerType={}",
+                requestId, user.getId(), approverUserId, generationLevel, peerType);
     }
 
     /**
@@ -299,6 +390,7 @@ public class FamilyJoinRequestService {
             dto.setId(req.getId());
             dto.setRequesterUserId(req.getRequesterUserId());
             dto.setRequesterNickname(user != null ? user.getNickname() : "未知用户");
+            dto.setRequesterRealName(user != null ? user.getRealName() : null);
             dto.setRequesterAvatar(user != null ? user.getAvatar() : "");
             dto.setInviterUserId(req.getInviterUserId());
             dto.setInviteCode(req.getInviteRef());
@@ -307,6 +399,9 @@ public class FamilyJoinRequestService {
             dto.setTargetFamilyName(family != null ? family.getName() : "");
             dto.setStatus(req.getStatus());
             dto.setFamilyRole(req.getFamilyRole());
+            dto.setAssignedGeneration(req.getAssignedGeneration());
+            dto.setAssignedPeerType(req.getAssignedPeerType());
+            dto.setAssignedPeerLabel(assignedPeerLabel(req.getAssignedPeerType()));
             dto.setAdminComment(req.getComment());
             dto.setCreatedAt(req.getCreatedAt() != null ? req.getCreatedAt().toString() : "");
             return dto;
@@ -327,13 +422,21 @@ public class FamilyJoinRequestService {
             return null;
         }
         Family family = familyMapper.selectById(req.getTargetFamilyId());
+        User requester = req.getRequesterUserId() != null ? userMapper.selectById(req.getRequesterUserId()) : null;
         FamilyJoinRequestDTO dto = new FamilyJoinRequestDTO();
         dto.setId(req.getId());
+        dto.setRequesterUserId(req.getRequesterUserId());
+        dto.setRequesterNickname(requester != null ? requester.getNickname() : "");
+        dto.setRequesterRealName(requester != null ? requester.getRealName() : null);
+        dto.setRequesterAvatar(requester != null ? requester.getAvatar() : "");
         dto.setTargetFamilyId(req.getTargetFamilyId());
         dto.setTargetFamilyName(family != null ? family.getName() : "");
         dto.setStatus(req.getStatus());
         dto.setInviteCode(req.getInviteRef());
         dto.setFamilyRole(req.getFamilyRole());
+        dto.setAssignedGeneration(req.getAssignedGeneration());
+        dto.setAssignedPeerType(req.getAssignedPeerType());
+        dto.setAssignedPeerLabel(assignedPeerLabel(req.getAssignedPeerType()));
         dto.setAdminComment(req.getComment());
         dto.setCreatedAt(req.getCreatedAt() != null ? req.getCreatedAt().toString() : "");
         return dto;
@@ -415,4 +518,34 @@ public class FamilyJoinRequestService {
                     : "只有家庭管理员才能审批");
         }
     }
+
+    /**
+     * User.familyRole 推导表(绝对辈分 + 性别):
+     * ≥2 → 爷爷/奶奶(「姥爷/姥姥」无法由辈分推导,统一落奶奶/爷爷,用户可在「我的资料」修正)
+     * 1  → 爸爸/妈妈
+     * 其余 → 其他(保证 MemberCoverageDTO/MemberEnergyDTO/InsuranceGapDTO 等下游不为空)
+     */
+    private String deriveFamilyRole(int generation, String gender) {
+        boolean male = "male".equals(gender);
+        if (generation >= 2) {
+            return male ? "爷爷" : "奶奶";
+        }
+        if (generation == 1) {
+            return male ? "爸爸" : "妈妈";
+        }
+        return "其他";
+    }
+
+    /**
+     * 同辈类型中文标签(展示层冗余,与 utils/generationLevel.js PEER_TYPES 对应)
+     */
+    private String assignedPeerLabel(String peerType) {
+        if ("spouse".equals(peerType)) {
+            return "配偶";
+        }
+        if ("sibling".equals(peerType)) {
+            return "兄弟姐妹";
+        }
+        return "";
+    }
 }

+ 46 - 4
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 {
@@ -344,12 +349,14 @@ public class FamilyMemberService {
             // 计算视角相关的关系标签
             if (viewerMember != null) {
                 Integer genValue = null;
+                String relType = 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();
+                                relType = rel.getRelationType();
                                 break;
                             }
                         }
@@ -357,8 +364,9 @@ public class FamilyMemberService {
                 }
                 if (genValue != null) {
                     vo.setGenerationValue(genValue);
-                    vo.setRelativeLabel(familyRelationshipService.computeRelationshipLabel(genValue, fm.getGender()));
-                    vo.setRelationship(familyRelationshipService.computeRelationshipLabel(genValue, fm.getGender()));
+                    String label = familyRelationshipService.computeRelationshipLabel(genValue, fm.getGender(), relType);
+                    vo.setRelativeLabel(label);
+                    vo.setRelationship(label);
                 } else {
                     vo.setRelativeLabel(computeRelativeLabel(viewerMember, fm, familyMembers));
                     vo.setRelationship(computeRelativeLabel(viewerMember, fm, familyMembers));
@@ -1037,18 +1045,52 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
             }
         }
         // 管理员可更新辈分关系
+        boolean identityChanged = false;
+        String rebuildPeerType = null;
         if (isAdmin && dto.getGenerationLevel() != null && !dto.getGenerationLevel().trim().isEmpty()) {
             GenerationLevel genLevel = GenerationLevel.fromValue(dto.getGenerationLevel());
             if (genLevel != null) {
+                // 本人自编辑不设「配偶」:关系是相对的,配偶只能由对方(或管理员代设)指定,
+                // 自己给自己标配偶会产生「我与我配偶」的歧义关系行。
+                boolean selfEdit = member.getUserId() != null && member.getUserId().equals(userId);
                 member.setGeneration(genLevel.getOffset());
-                boolean isSpouse = genLevel == GenerationLevel.PEER && "spouse".equals(dto.getPeerType());
+                boolean isSpouse = genLevel == GenerationLevel.PEER
+                        && "spouse".equals(dto.getPeerType()) && !selfEdit;
                 member.setIsSpouse(isSpouse);
+                identityChanged = true;
+                rebuildPeerType = genLevel == GenerationLevel.PEER
+                        ? (isSpouse ? "spouse" : "sibling")
+                        : null;
             }
         }
         // 孩子身份标记(仅家庭管理员可设置;落库到 users 表对应记录)
         applyIsChildFlag(userId, user.getFamilyId(), member.getUserId(), dto.getIsChild());
         member.setUpdatedAt(new Date());
         familyMemberMapper.updateById(member);
+
+        // 辈分/同辈类型变更后重建关系表:成员列表的关系标签优先读 family_relationships,
+        // 不重建会出现「编辑资料改了身份、列表标签不刷新」(规格 §10 R-4)。
+        // 锚点=操作者(编辑人)成员记录:管理员给某成员选「配偶」时,指 TA 与操作者是配偶;
+        // 本人自编辑时锚点为 null(不写 spouse)。
+        if (identityChanged) {
+            Long anchorMemberId = null;
+            boolean selfEdit = member.getUserId() != null && member.getUserId().equals(userId);
+            if (!selfEdit) {
+                FamilyMember operator = familyMemberMapper.selectOne(new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getFamilyId, member.getFamilyId())
+                        .eq(FamilyMember::getUserId, userId)
+                        .last("LIMIT 1"));
+                anchorMemberId = operator != null ? operator.getId() : null;
+            }
+            try {
+                familyRelationshipService.rebuildRelationsForMember(
+                        member.getFamilyId(), member.getId(), member.getGeneration(), rebuildPeerType, anchorMemberId);
+            } catch (RuntimeException e) {
+                // 重建失败必须整体回滚:否则辈分已改而关系未重建,正是 R-4 要消除的不一致状态
+                log.error("重建成员关系失败,回滚本次资料更新: memberId={}, error={}", member.getId(), e.getMessage());
+                throw e;
+            }
+        }
         logMemberAction(member.getFamilyId(), member.getId(), "update", oldNickname, member.getNickname(), userId);
         return toFamilyMemberVO(member);
     }

+ 122 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyRelationshipService.java

@@ -11,7 +11,9 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -27,6 +29,22 @@ public class FamilyRelationshipService {
     @Resource
     private FamilyMemberMapper familyMemberMapper;
 
+    /**
+     * 为新成员创建与所有现有成员的双向关系(兼容旧调用:同辈类型按直系处理)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void createRelationsForNewMember(Long familyId, Long newMemberId, Integer newMemberGeneration) {
+        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);
+    }
+
     /**
      * 为新成员创建与所有现有成员的双向关系
      * 算法:
@@ -34,9 +52,16 @@ public class FamilyRelationshipService {
      *   existingMi → newMember: generation = Mi.generation - newMemberGeneration
      *   newMember → existingMi: generation = newMemberGeneration - Mi.generation
      * - 如果 existingMi.generation == null, 默认 0
+     * - relationType 判定(双向各写一行,取值相同):
+     *   同辈 且 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) {
+    public void createRelationsForNewMember(Long familyId, Long newMemberId, Integer newMemberGeneration, String peerType, Long anchorMemberId) {
         // 获取除新成员外的所有现有成员
         List<FamilyMember> existing = familyMemberMapper.selectList(
             new LambdaQueryWrapper<FamilyMember>()
@@ -47,17 +72,33 @@ public class FamilyRelationshipService {
             return; // 第一个成员,无需创建关系
         }
 
+        // 读取新成员(用于按对方性别生成关系标签)
+        FamilyMember newMember = familyMemberMapper.selectById(newMemberId);
+
         Date now = new Date();
         for (FamilyMember existingMember : existing) {
             int exGen = existingMember.getGeneration() != null ? existingMember.getGeneration() : 0;
             int newGen = newMemberGeneration != null ? newMemberGeneration : 0;
 
+            // 关系类型:双向取值相同;配偶仅对锚点成员生效
+            String relationType = "lineal";
+            boolean sameGen = exGen == newGen;
+            boolean isAnchor = anchorMemberId != null && anchorMemberId.equals(existingMember.getId());
+            if (sameGen && "spouse".equals(peerType) && isAnchor) {
+                relationType = "spouse";
+            } else if (sameGen && peerType != null && !peerType.trim().isEmpty()) {
+                relationType = "sibling";
+            }
+
             // 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.setRelationType(relationType);
+            rel1.setRelationshipKey(computeRelationshipLabel(exGen - newGen,
+                    newMember != null ? newMember.getGender() : null, relationType));
             rel1.setCreatedAt(now);
             rel1.setUpdatedAt(now);
             try {
@@ -72,6 +113,9 @@ public class FamilyRelationshipService {
             rel2.setFromMemberId(newMemberId);
             rel2.setToMemberId(existingMember.getId());
             rel2.setGenerationValue(newGen - exGen);
+            rel2.setRelationType(relationType);
+            rel2.setRelationshipKey(computeRelationshipLabel(newGen - exGen,
+                    existingMember.getGender(), relationType));
             rel2.setCreatedAt(now);
             rel2.setUpdatedAt(now);
             try {
@@ -113,7 +157,7 @@ public class FamilyRelationshipService {
     }
 
     /**
-     * 根据辈分值计算关系标签
+     * 根据辈分值计算关系标签(保留 2 参重载:直系/默认逻辑)
      */
     public String computeRelationshipLabel(Integer generationValue, String gender) {
         if (generationValue == null) return "未知";
@@ -143,4 +187,80 @@ public class FamilyRelationshipService {
                     : "下" + Math.abs(generationValue) + "代";
         }
     }
+
+    /**
+     * 按查看者视角 + 关系类型输出称谓(R4):
+     * - spouse(同辈配偶)→ 男「丈夫」/女「妻子」(不再看辈分,配偶必然同辈)
+     * - sibling(同辈非配偶)→ 兄弟/姐妹
+     * - lineal/null → 原 2 参逻辑
+     */
+    public String computeRelationshipLabel(Integer generationValue, String gender, String relationType) {
+        boolean isMale = "male".equals(gender);
+        if ("spouse".equals(relationType)) {
+            return isMale ? "丈夫" : "妻子";
+        }
+        if ("sibling".equals(relationType)) {
+            return isMale ? "兄弟" : "姐妹";
+        }
+        return computeRelationshipLabel(generationValue, gender);
+    }
+
+    /**
+     * 重建某个成员的全部关系(用于「编辑资料」修改辈分/同辈类型,《规格 §10 R-4》:
+     * 原先只改 family_members.generation 不重建 family_relationships,导致成员列表标签
+     * 仍读旧关系而不刷新)。
+     *
+     * <p>实现要点:先按 (from→to) 快照双向行的关系质量评分(信任/亲密/沟通,来自
+     * relationship-questionnaire 的答卷人视角),再删旧建新,最后按同一方向回填评分,
+     * 避免重建把历史评分清零。</p>
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void rebuildRelationsForMember(Long familyId, Long memberId, Integer newMemberGeneration,
+                                          String peerType, Long anchorMemberId) {
+        if (familyId == null || memberId == null) {
+            return;
+        }
+        List<FamilyRelationship> oldRows = selectRelationsTouching(memberId);
+        Map<String, FamilyRelationship> scoreSnapshot = new HashMap<>();
+        for (FamilyRelationship r : oldRows) {
+            boolean hasScore = r.getTrustScore() != null
+                    || r.getIntimacyScore() != null
+                    || r.getCommunicationScore() != null;
+            if (hasScore) {
+                scoreSnapshot.put(r.getFromMemberId() + "->" + r.getToMemberId(), r);
+            }
+        }
+
+        deleteRelationsForMember(memberId);
+        createRelationsForNewMember(familyId, memberId, newMemberGeneration, peerType, anchorMemberId);
+
+        if (scoreSnapshot.isEmpty()) {
+            return;
+        }
+        List<FamilyRelationship> freshRows = selectRelationsTouching(memberId);
+        for (FamilyRelationship r : freshRows) {
+            FamilyRelationship old = scoreSnapshot.get(r.getFromMemberId() + "->" + r.getToMemberId());
+            if (old == null) {
+                continue;
+            }
+            r.setTrustScore(old.getTrustScore());
+            r.setIntimacyScore(old.getIntimacyScore());
+            r.setCommunicationScore(old.getCommunicationScore());
+            try {
+                familyRelationshipMapper.updateById(r);
+            } catch (Exception e) {
+                log.warn("关系评分回填失败(不阻断): relId={}, error={}", r.getId(), e.getMessage());
+            }
+        }
+    }
+
+    /** 查询所有「以该成员为起点或终点」的关系行(双向) */
+    private List<FamilyRelationship> selectRelationsTouching(Long memberId) {
+        List<FamilyRelationship> rows = familyRelationshipMapper.selectList(
+                new LambdaQueryWrapper<FamilyRelationship>()
+                        .and(w -> w.eq(FamilyRelationship::getFromMemberId, memberId)
+                                .or()
+                                .eq(FamilyRelationship::getToMemberId, memberId)));
+        return rows != null ? new ArrayList<>(rows) : new ArrayList<>();
+    }
 }

+ 4 - 0
cfc-backend/src/main/resources/schema.sql

@@ -2294,6 +2294,7 @@ CREATE TABLE IF NOT EXISTS family_relationships (
     from_member_id   BIGINT       NOT NULL COMMENT '关系起点成员ID',
     to_member_id     BIGINT       NOT NULL COMMENT '关系终点成员ID',
     generation_value INT          NOT NULL COMMENT '辈分值: -1=子女, 0=同辈, 1=父母, 2=祖父母, 3=曾祖父母',
+    relation_type    VARCHAR(20)  DEFAULT 'lineal' COMMENT '关系类型: lineal(直系)/sibling(同辈非配偶)/spouse(同辈配偶)',
     relationship_key VARCHAR(50)  COMMENT '关系标签key(冗余用于显示, 如father/mother/son/daughter/spouse/sibling)',
     trust_score        DECIMAL(5,2) DEFAULT 0 COMMENT '信任度(答卷人视角)',
     intimacy_score     DECIMAL(5,2) DEFAULT 0 COMMENT '亲密度(答卷人视角)',
@@ -4816,6 +4817,9 @@ CREATE TABLE IF NOT EXISTS family_join_requests (
     status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/approved/rejected/cancelled',
     approver_user_id BIGINT COMMENT '审批人ID',
     comment TEXT COMMENT '审批备注',
+    family_role VARCHAR(20) DEFAULT NULL COMMENT '申请人在家庭中的角色:爸爸/妈妈/爷爷/奶奶等(审批通过时按绝对辈分+性别回写推导值)',
+    assigned_generation INT NULL COMMENT '审批时指定的绝对辈分',
+    assigned_peer_type VARCHAR(10) NULL COMMENT '审批时指定的同辈类型: sibling/spouse',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     INDEX idx_requester (requester_user_id),

+ 4 - 31
cfc-frontend/pages/invite/join-form.vue

@@ -2,7 +2,7 @@
 	<view class="join-form-container" :style="{ '--font-scale': fontScale }">
 		<view class="header">
 			<text class="title">加入家庭</text>
-			<text class="subtitle">请填写您的真实姓名和家庭身份,提交后等待管理员审批</text>
+			<text class="subtitle">请填写您的真实姓名,提交后等待邀请人确认</text>
 		</view>
 
 		<view class="form-section">
@@ -12,14 +12,6 @@
 				<input type="text" v-model="form.realName" placeholder="请输入真实姓名" class="input" maxlength="20" />
 			</view>
 
-			<!-- 家庭身份(必填) -->
-			<view class="form-item">
-				<text class="label">家庭身份 <text class="required">*</text></text>
-				<picker mode="selector" :range="familyRoleOptions" :value="familyRoleIndex" @change="onFamilyRoleChange">
-					<view class="picker-input">{{ form.familyRole || '请选择(如:爸爸、妈妈)' }}</view>
-				</picker>
-			</view>
-
 			<!-- 昵称(选填,默认用户+手机尾号) -->
 			<view class="form-item">
 				<text class="label">昵称</text>
@@ -57,26 +49,14 @@ export default {
 	data() {
 		return {
 			familyInviteCode: '',
-			familyRoleOptions: ['爸爸', '妈妈', '爷爷', '奶奶', '姥爷', '姥姥', '其他'],
 			form: {
 				realName: '',
-				familyRole: '',
 				nickname: '',
 				avatar: ''
 			},
 			submitting: false
 		}
 	},
-	computed: {
-		familyRoleIndex: {
-			get() {
-				return this.familyRoleOptions.indexOf(this.form.familyRole)
-			},
-			set(val) {
-				this.form.familyRole = this.familyRoleOptions[val] || ''
-			}
-		}
-	},
 	onLoad(options) {
 		// 接收家庭邀请码:join.vue 传 familyInviteCode;登录页衔接传 pendingFamilyInviteCode 对应值
 		var code = (options && options.familyInviteCode) || uni.getStorageSync('pendingFamilyInviteCode') || ''
@@ -103,19 +83,12 @@ export default {
 				}
 			})
 		},
-		onFamilyRoleChange(e) {
-			this.form.familyRole = this.familyRoleOptions[e.detail.value] || ''
-		},
 		async submit() {
 			if (this.submitting) return
 			if (!this.form.realName || !this.form.realName.trim()) {
 				uni.showToast({ title: '请输入真实姓名', icon: 'none' })
 				return
 			}
-			if (!this.form.familyRole) {
-				uni.showToast({ title: '请选择家庭身份', icon: 'none' })
-				return
-			}
 			if (!this.familyInviteCode) {
 				uni.showToast({ title: '缺少家庭邀请码', icon: 'none' })
 				return
@@ -132,10 +105,10 @@ export default {
 				} catch (e) {
 					console.log('[join-form] 保存基本信息失败,继续提交加入申请', e)
 				}
-				// 提交加入家庭申请
-				await requestJoinByCode(this.familyInviteCode, this.form.familyRole)
+				// 提交加入家庭申请(身份由邀请人审批时指定,不再自填)
+				await requestJoinByCode(this.familyInviteCode)
 				uni.removeStorageSync('pendingFamilyInviteCode')
-				uni.showToast({ title: '加入家庭申请已提交,等待管理员审批', icon: 'success' })
+				uni.showToast({ title: '加入家庭申请已提交,等待邀请人确认', icon: 'success' })
 				setTimeout(function() {
 					uni.reLaunch({ url: '/pages/index-home/index' })
 				}, 1500)

+ 168 - 12
cfc-frontend/pages/profile-extra/family-members.vue

@@ -23,8 +23,8 @@
       <view class="jr-item" v-for="r in joinRequests" :key="r.id">
         <image class="jr-avatar" :src="r.requesterAvatar || '/static/default-avatar.png'" mode="aspectFill"></image>
         <view class="jr-info">
-          <text class="jr-name">{{ r.requesterNickname || '微信用户' }}</text>
-          <text class="jr-meta">希望以「{{ r.familyRole || '成员' }}」身份加入</text>
+          <text class="jr-name">{{ r.requesterRealName || r.requesterNickname || '微信用户' }}</text>
+          <text class="jr-meta">申请加入你的家庭</text>
         </view>
         <view class="jr-actions">
           <button class="jr-btn jr-accept" @click="onHandleJoinRequest(r, true)">同意</button>
@@ -168,6 +168,26 @@
       </view>
     </view>
 
+    <!-- ===== 同意加入申请弹窗(邀请人指定加入者辈分关系;不再 uni.showModal) ===== -->
+    <view class="modal-mask" v-if="showApproveModal" @click="closeApproveModal">
+      <view class="approve-modal-card" @click.stop>
+        <view class="approve-header">
+          <image class="approve-avatar" :src="approveTarget && (approveTarget.requesterAvatar || '/static/default-avatar.png')" mode="aspectFill"></image>
+          <view class="approve-info">
+            <text class="approve-name">{{ approveDisplayName }}</text>
+          </view>
+          <text class="approve-close" @click="closeApproveModal">×</text>
+        </view>
+        <text class="approve-desc">请选择 TA 与你的关系</text>
+        <!-- 与 pages/family/add-member.vue 同一组件、同一选项;提交时通过 $refs.approvePicker.selectedGen/selectedPeer 读取 -->
+        <generation-picker ref="approvePicker" :generationLevel="approveForm.generationLevel" :peerType="approveForm.peerType" />
+        <view class="approve-actions">
+          <button class="approve-btn approve-cancel" @click="closeApproveModal">取消</button>
+          <button class="approve-btn approve-confirm" :loading="approving" :disabled="approving" @click="confirmApprove">确认同意</button>
+        </view>
+      </view>
+    </view>
+
     <!-- ===== 底部操作栏 ===== -->
     <view class="bottom-bar">
       <button class="bottom-btn btn-add" @click="onAddMember">+ 添加成员</button>
@@ -257,6 +277,12 @@ export default {
       renameInput: '',
       renameSaving: false,
 
+      // ===== 同意加入申请弹窗状态(邀请人指定辈分关系) =====
+      showApproveModal: false,
+      approveTarget: null,
+      approveForm: { generationLevel: '', peerType: '' },
+      approving: false
+
       }
   },
   computed: {
@@ -287,6 +313,17 @@ export default {
       var list = this.permMembers || []
       var found = list.filter(function(m) { return String(m.value) === String(self.permGranteeUserId) })
       return found.length > 0 ? found[0].label : ''
+    },
+    // 同意加入弹窗标题:真实姓名(昵称),两者均缺省回退「微信用户」
+    approveDisplayName() {
+      var t = this.approveTarget
+      if (!t) return ''
+      var real = t.requesterRealName || ''
+      var nick = t.requesterNickname || ''
+      if (real && nick && real !== nick) {
+        return real + '(' + nick + ')'
+      }
+      return real || nick || '微信用户'
     }
   },
   onShow() {
@@ -821,28 +858,75 @@ export default {
       if (!r) {
         return
       }
-      var nickname = r.requesterNickname || '该用户'
+      if (approve) {
+        // 同意:弹出辈分关系选择弹窗(身份由邀请人指定,不再用 uni.showModal)
+        self.openApproveModal(r)
+        return
+      }
+      // 拒绝保持 uni.showModal
+      var nickname = r.requesterRealName || r.requesterNickname || '该用户'
       uni.showModal({
-        title: approve ? '同意加入' : '拒绝申请',
-        content: approve ? ('确定同意 ' + nickname + ' 加入家庭吗?') : ('确定拒绝 ' + nickname + ' 的加入申请吗?'),
+        title: '拒绝申请',
+        content: '确定拒绝 ' + nickname + ' 的加入申请吗?',
         success: function(mr) {
           if (mr.confirm) {
-            self.doHandleJoinRequest(r, approve)
+            self.doHandleJoinRequest(r, false)
           }
         }
       })
     },
+    openApproveModal(r) {
+      this.approveTarget = r
+      this.approveForm = { generationLevel: '', peerType: '' }
+      this.showApproveModal = true
+    },
+    closeApproveModal() {
+      if (this.approving) return
+      this.showApproveModal = false
+      this.approveTarget = null
+    },
+    async confirmApprove() {
+      var self = this
+      if (this.approving || !this.approveTarget) {
+        return
+      }
+      var picker = this.$refs.approvePicker
+      var selectedGen = (picker && picker.selectedGen) || this.approveForm.generationLevel
+      var selectedPeer = (picker && picker.selectedPeer) || this.approveForm.peerType
+      if (!selectedGen) {
+        uni.showToast({ title: '请选择辈分关系', icon: 'none' })
+        return
+      }
+      if (selectedGen === 'peer' && !selectedPeer) {
+        uni.showToast({ title: '请选择同辈关系类型', icon: 'none' })
+        return
+      }
+      this.approving = true
+      try {
+        var res = await approveJoinRequest(this.approveTarget.id, '', selectedGen, selectedPeer)
+        if (res && res.code === 200) {
+          uni.showToast({ title: '已同意加入', icon: 'success' })
+          this.showApproveModal = false
+          this.approveTarget = null
+          this.loadJoinRequests()
+          this.loadMembers()
+        } else {
+          uni.showToast({ title: (res && res.message) || '操作失败', icon: 'none' })
+          this.loadJoinRequests()
+        }
+      } catch (e) {
+        uni.showToast({ title: (e && e.message) || '操作失败', icon: 'none' })
+      } finally {
+        this.approving = false
+      }
+    },
     async doHandleJoinRequest(r, approve) {
       var self = this
       try {
-        var action = approve ? approveJoinRequest : rejectJoinRequest
-        var res = await action(r.id, '')
+        var res = await rejectJoinRequest(r.id, '')
         if (res && res.code === 200) {
-          uni.showToast({ title: approve ? '已同意加入' : '已拒绝', icon: 'success' })
+          uni.showToast({ title: '已拒绝', icon: 'success' })
           self.loadJoinRequests()
-          if (approve) {
-            self.loadMembers()
-          }
         } else {
           uni.showToast({ title: (res && res.message) || '操作失败', icon: 'none' })
           self.loadJoinRequests()
@@ -1422,4 +1506,76 @@ $c-elm-danger-text:  $el-danger; // Element UI 危险文字
 .rename-btn { flex: 1; border-radius: 24rpx; font-size: calc(28rpx * var(--font-scale, 1)); }
 .rename-cancel { background: $facade-fill-2; color: $facade-text-body; }
 .rename-save { background: $brand-primary; color: $app-surface; }
+
+/* ===== 同意加入申请弹窗 ===== */
+.approve-modal-card {
+  width: 600rpx;
+  background: $app-surface;
+  border-radius: 24rpx;
+  padding: 36rpx 32rpx 32rpx;
+}
+.approve-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.approve-avatar {
+  width: 88rpx;
+  height: 88rpx;
+  border-radius: 50%;
+  background: $facade-fill;
+  flex-shrink: 0;
+  margin-right: 18rpx;
+}
+.approve-info {
+  flex: 1;
+  min-width: 0;
+}
+.approve-name {
+  font-size: calc(32rpx * var(--font-scale, 1));
+  font-weight: 600;
+  color: $facade-text-strong;
+  display: block;
+}
+.approve-close {
+  font-size: calc(40rpx * var(--font-scale, 1));
+  color: $facade-text-muted;
+  padding: 0 8rpx;
+}
+.approve-desc {
+  font-size: calc(26rpx * var(--font-scale, 1));
+  color: $facade-text-muted;
+  margin-bottom: 24rpx;
+  display: block;
+}
+.approve-actions {
+  display: flex;
+  flex-direction: row;
+  gap: 20rpx;
+  margin-top: 32rpx;
+}
+.approve-btn {
+  flex: 1;
+  border-radius: 24rpx;
+  font-size: calc(28rpx * var(--font-scale, 1));
+  padding: 20rpx 0;
+  line-height: 1.4;
+  border: none;
+}
+.approve-btn::after {
+  border: none;
+}
+.approve-cancel {
+  background: $facade-fill-2;
+  color: $facade-text-body;
+}
+.approve-confirm {
+  background: $brand-primary;
+  color: $app-surface;
+}
+.approve-confirm[disabled] {
+  background: $facade-fill-2;
+  color: $facade-text-muted;
+}
 </style>

+ 11 - 5
cfc-frontend/utils/api.js

@@ -1555,8 +1555,9 @@ export const transferFamilyAdmin = (newAdminId) => {
 }
 
 // 通过邀请码/令牌申请加入家庭(token 路径绑定邀请人,等待邀请人确认;旧码路径管理员兜底)
-export const requestJoinByCode = (inviteCode, familyRole) => {
-  return request('/api/family/invite/request-join-by-code', 'POST', { inviteCode: inviteCode, familyRole: familyRole || '' })
+// 家庭身份不再由申请人自填(familyRole 由后端保留接收但忽略,身份转由邀请人审批时指定)
+export const requestJoinByCode = (inviteCode) => {
+  return request('/api/family/invite/request-join-by-code', 'POST', { inviteCode: inviteCode })
 }
 
 // 查询待我处理的加入请求(邀请人视角;旧数据由家庭管理员兜底)
@@ -1564,9 +1565,14 @@ export const getPendingJoinRequests = () => {
   return request('/api/family/invite/pending-requests', 'POST', {})
 }
 
-// 审批通过加入请求(邀请人确认;旧数据无邀请人时家庭管理员兜底)
-export const approveJoinRequest = (requestId, comment) => {
-  return request('/api/family/invite/approve-request', 'POST', { requestId: requestId, comment: comment || '' })
+// 审批通过加入请求(邀请人确认;身份由邀请人在审批弹窗中指定:辈分 + 同辈类型)
+export const approveJoinRequest = (requestId, comment, generationLevel, peerType) => {
+  return request('/api/family/invite/approve-request', 'POST', {
+    requestId: requestId,
+    comment: comment || '',
+    generationLevel: generationLevel || '',
+    peerType: peerType || ''
+  })
 }
 
 // 拒绝加入请求(邀请人;旧数据无邀请人时家庭管理员兜底)

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-18c91377ee25e32e948cdef56f5bc860fa3bc633
+37b0231ad11d2ed4e438d2a9f4f1503b450ef518

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.2171",
+  "version": "1.0.2175",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.2171",
+      "version": "1.0.2175",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.2172",
+  "version": "1.0.2176",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 44 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,50 @@
 
 ---
 
+## v1.0.2176 (2026-11-07)
+
+### 文档
+- 研发负责人 1955 轮汇报 | B-50 华为鉴权修复 cc7b3e00b 合入 HEAD bd2a42be6 | 状态机 DONE14/GREEN8 全稳态 | 部署收口 7f1c238cd confirmed21 | 241 SC/15 跑通 | 池空稳态零派发
+
+
+## v1.0.2175 (2026-11-07)
+
+### 文档
+- 研发负责人 错峰03 2026-11-07 1913 轮汇报 | B-50 huawei鉴权修复cc7b3e00b合入+fam-join 6567acc33 合入主线+状态机DONE14/GREEN8 | B-39/40/47核验闭环
+
+### Bug 修复
+- 华为运动健康5端点未鉴权200直通——/api/huawei-health/ 加入 LOGIN_REQUIRED_PATHS 强制登录(401),与 xiaomi-health 对齐
+
+
+## v1.0.2174 (2026-11-07)
+
+### Bug 修复
+- 配偶关系改为仅对锚点成员生效,修正同辈关系类型判定
+
+### 其他
+-   其他同辈一律 sibling。原实现会把「妻子/丈夫」套到家庭内所有同辈成员上
+- - 审批链路传入 anchor=审批人 FamilyMember.id
+- - FamilyMemberService.addMember 修正传入绝对辈分(原误传 genLevel.getOffset()),并补 peerType 与锚点
+- - 规格文档同步 §6.1 锚点规则、§10 R-1(已修)/R-2/R-4(编辑资料不重建关系,另立任务)
+- 
+- - 加入申请不再自填家庭身份:join-form.vue 删除身份选择,request-join-by-code 的 familyRole 保留接收但忽略不写库
+- - 审批时指定身份:approve-request 改收 JoinApproveDTO(generationLevel/peerType),按审批人辈分(null视为0)+offset 求绝对辈分,落地 member.generation/isSpouse 并 createRelationsForNewMember 写 relation_type;mergeIntoFamily 分支同样落地
+- - 夫妻互称修复:FamilyRelationshipService.computeRelationshipLabel 增 3 参(spouse→丈夫/妻子,sibling→兄弟/姐妹,lineal→原逻辑),成员列表按 rel.relation_type 传参
+- - User.familyRole 按绝对辈分+性别推导(>=2 爷爷/奶奶,1 爸爸/妈妈,其余其他),回写申请表留痕(assigned_generation/assigned_peer_type)
+- - 审批卡片展示真实姓名+副行「申请加入你的家庭」,同意改弹窗内嵌 GenerationPicker(辈分必填/同辈必选类型)
+- 迁移编号:runMigrations 已有「迁移454: 旧体系接口调用打点表」,故本次新增使用 455/456(偏离规格文字 454/455,详见代码注释)
+- 
+
+### 新功能
+- 家庭加入身份改由邀请人审批时指定(辈分关系)+修复夫妻称谓
+
+
+## v1.0.2173 (2026-11-07)
+
+### 测试
+- 三套本地套件全绿零漂移(frontend313/web90/langgraph51) | 新拓扑执行缺口B-51(本机Chromium缺失admin未跑)+B-52(health契约版本冲突) | ledger3793(+13) | 通知研发B-50/B-47
+
+
 ## v1.0.2172 (2026-11-07)
 
 ### 文档

+ 45 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.2172
+> 当前版本: v1.0.2176
 
 ## 历史版本
 
@@ -8,6 +8,50 @@
 
 ---
 
+## v1.0.2176 (2026-11-07)
+
+### 文档
+- 研发负责人 1955 轮汇报 | B-50 华为鉴权修复 cc7b3e00b 合入 HEAD bd2a42be6 | 状态机 DONE14/GREEN8 全稳态 | 部署收口 7f1c238cd confirmed21 | 241 SC/15 跑通 | 池空稳态零派发
+
+
+## v1.0.2175 (2026-11-07)
+
+### 文档
+- 研发负责人 错峰03 2026-11-07 1913 轮汇报 | B-50 huawei鉴权修复cc7b3e00b合入+fam-join 6567acc33 合入主线+状态机DONE14/GREEN8 | B-39/40/47核验闭环
+
+### Bug 修复
+- 华为运动健康5端点未鉴权200直通——/api/huawei-health/ 加入 LOGIN_REQUIRED_PATHS 强制登录(401),与 xiaomi-health 对齐
+
+
+## v1.0.2174 (2026-11-07)
+
+### Bug 修复
+- 配偶关系改为仅对锚点成员生效,修正同辈关系类型判定
+
+### 其他
+-   其他同辈一律 sibling。原实现会把「妻子/丈夫」套到家庭内所有同辈成员上
+- - 审批链路传入 anchor=审批人 FamilyMember.id
+- - FamilyMemberService.addMember 修正传入绝对辈分(原误传 genLevel.getOffset()),并补 peerType 与锚点
+- - 规格文档同步 §6.1 锚点规则、§10 R-1(已修)/R-2/R-4(编辑资料不重建关系,另立任务)
+- 
+- - 加入申请不再自填家庭身份:join-form.vue 删除身份选择,request-join-by-code 的 familyRole 保留接收但忽略不写库
+- - 审批时指定身份:approve-request 改收 JoinApproveDTO(generationLevel/peerType),按审批人辈分(null视为0)+offset 求绝对辈分,落地 member.generation/isSpouse 并 createRelationsForNewMember 写 relation_type;mergeIntoFamily 分支同样落地
+- - 夫妻互称修复:FamilyRelationshipService.computeRelationshipLabel 增 3 参(spouse→丈夫/妻子,sibling→兄弟/姐妹,lineal→原逻辑),成员列表按 rel.relation_type 传参
+- - User.familyRole 按绝对辈分+性别推导(>=2 爷爷/奶奶,1 爸爸/妈妈,其余其他),回写申请表留痕(assigned_generation/assigned_peer_type)
+- - 审批卡片展示真实姓名+副行「申请加入你的家庭」,同意改弹窗内嵌 GenerationPicker(辈分必填/同辈必选类型)
+- 迁移编号:runMigrations 已有「迁移454: 旧体系接口调用打点表」,故本次新增使用 455/456(偏离规格文字 454/455,详见代码注释)
+- 
+
+### 新功能
+- 家庭加入身份改由邀请人审批时指定(辈分关系)+修复夫妻称谓
+
+
+## v1.0.2173 (2026-11-07)
+
+### 测试
+- 三套本地套件全绿零漂移(frontend313/web90/langgraph51) | 新拓扑执行缺口B-51(本机Chromium缺失admin未跑)+B-52(health契约版本冲突) | ledger3793(+13) | 通知研发B-50/B-47
+
+
 ## v1.0.2172 (2026-11-07)
 
 ### 文档

+ 13 - 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,9 @@ 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`,而成员列表标签优先读关系表,导致「编辑资料」改身份后标签不刷新。现改为变更后调用 `FamilyRelationshipService.rebuildRelationsForMember(...)`:先按 `from→to` 快照关系质量评分(信任/亲密/沟通,来自关系问卷的答卷人视角),再删旧建新,最后按同方向回填评分(避免清零)。
+  - **锚点规则**:锚点 = 操作者(编辑人)在该家庭的成员记录 —— 管理员给某成员选「配偶」表示「TA 与操作者是配偶」;操作者即目标成员本人时锚点置 null 且不写 `spouse`(关系是相对的,自己不能给自己标配偶)。
+  - **失败策略**:重建失败整事务回滚,不出现「辈分已改、关系未变」的中间态。

+ 23 - 9
docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md

@@ -1,10 +1,10 @@
 # 推荐人分润 & 团队分润双体系设计
 
-> **版本**:v0.5(新增「吸引力升级设计」:低利润下的激励放大,借鉴 Enagic 合法精华,待评审)
+> **版本**:v0.6(新增「升级双门槛:每次升级必须直推一个」+ #12/#13/#14 拍板落地,待评审)
 > **日期**:2026-11-07
 > **作者**:研发负责人
 > **状态**:🟡 待评审
-> **修订**:v0.2:① 团队邀请参考家庭邀请(区分已加入/未加入平台);② 推荐人体系**沿用 CF 值**(现有 F03a/F03b),团队体系**新增货币直接分佣**;③ 补星级量化定义 + 团队奖。v0.3:团队奖池从 T 中切 **20%**(8 份池 = T×80%)。v0.4:星级档位按真实价格校准(0/8千/2.5万/7万/14万/25万)+ **星级透明化与成长可视化**(实时看板 / 差距换算 / 月度简报 / 权益可见,见 §4.3.1)。v0.5:**吸引力升级设计**——单产品利润小下的激励放大(§4.7):收入管道化 / 星级倍率权重 / 带教里程碑 / 实时可见,借鉴 Enagic 合法精华(不照搬 8 层与终身等级)。
+> **修订**:v0.2:① 团队邀请参考家庭邀请(区分已加入/未加入平台);② 推荐人体系**沿用 CF 值**(现有 F03a/F03b),团队体系**新增货币直接分佣**;③ 补星级量化定义 + 团队奖。v0.3:团队奖池从 T 中切 **20%**(8 份池 = T×80%)。v0.4:星级档位按真实价格校准 + **星级透明化与成长可视化**(§4.3.1)。v0.5:**吸引力升级设计**(§4.7,借鉴 Enagic 合法精华)。v0.6:**升级双门槛**——每次升级须业绩分达标 **且** 滚动 12 个月直推有效新用户 ≥1(§4.3);#12 档位入后台配置、#13/#14 按推荐落地。
 
 ---
 
@@ -196,7 +196,19 @@ CfCommissionService.settle / settleReferrerOnly
 | 五星 | ≥ 140,000 | 带教团队负责人(带 3-5 人团队) | 5 |
 | 六星 | ≥ 250,000 | 顶流团队长(带教团队 + 强推荐) | **6(最多)** |
 
-> **📐 档位推演依据(2026-11-07,按真实业务价格校准)**:以成长规划师 **1对1 咨询 ¥500/次**(`DatabaseInitializer` 商品 seed,`growth planner consult`)为主业务单元,业绩分 = 直接×50% + 团队×40% + 推荐×10%。五个典型画像实测业绩分:兼职新规划师(年 20 次+团队 5 次+推荐 5 千)≈ **6,500**;活跃规划师(80+20+2 万)≈ **26,000**;资深规划师(200+80+5 万)≈ **71,000**;带教团队负责人(300+300+10 万)≈ **145,000**;顶流团队长(400+600+20 万)≈ **240,000**。各档门槛取「对应画像略上方」,保证**跳一跳够得着**;其中**六星(25 万)单靠个人直接服务几乎不可达**(纯咨询需约 1000 次/年),必须是「带教团队 + 强推荐」的团队长级——这正是「团队服务好拿得多、多劳多得」的体现。**门槛均可在 `team_profit.star_tiers` 配置,以上为推荐默认值**;若业务起步期体量小,可整体下调(见 §11 待拍板 #12)。
+> **📐 档位推演依据(2026-11-07,按真实业务价格校准)**:以成长规划师 **1对1 咨询 ¥500/次**(`DatabaseInitializer` 商品 seed,`growth planner consult`)为主业务单元,业绩分 = 直接×50% + 团队×40% + 推荐×10%。五个典型画像实测业绩分:兼职新规划师(年 20 次+团队 5 次+推荐 5 千)≈ **6,500**;活跃规划师(80+20+2 万)≈ **26,000**;资深规划师(200+80+5 万)≈ **71,000**;带教团队负责人(300+300+10 万)≈ **145,000**;顶流团队长(400+600+20 万)≈ **240,000**。各档门槛取「对应画像略上方」,保证**跳一跳够得着**;其中**六星(25 万)单靠个人直接服务几乎不可达**(纯咨询需约 1000 次/年),必须是「带教团队 + 强推荐」的团队长级——这正是「团队服务好拿得多、多劳多得」的体现。**门槛均可在 `team_profit.star_tiers` 后台配置**;若业务起步期体量小,可整体下调(见 §11 #12 已拍板)。
+
+**升级双门槛(主理人意见⑥:每次升级必须直推一个)**
+
+星级升降不仅看业绩分,还需满足「**滚动 12 个月内直推有效新用户 ≥1**」这一带新门槛——保证团队成员持续为平台引入新人,而非只吃老客户红利。升级条件 = **业绩分 ≥ 目标档门槛 AND 本窗口直推有效新用户 ≥ 1**。两个条件缺一不可。
+
+| 指标 | 口径 | 数据来源 |
+|------|------|---------|
+| 业绩分 | 滚动 12 个月加权总分(§4.3 公式) | `service_settlements` × `cf_balance_log(F03a)` × `ppoint_consumption_log` |
+| 直推有效新用户 | 本人 referral_tree level=1 子节点中,近 12 个月完成**首次实付消费**的注册用户数 | `referral_tree` + `product_orders.buyer_id IN (level1 members) AND first_order_at BETWEEN window_start..now`;阈值按 `team_profit.valid_new_ref_min_order_cent` 控制(防白号刷级,默认建议 ≥¥99) |
+| 判定规则 | `score >= tier_threshold AND direct_valid_new_refs >= 1` 才允许升级;两条件均不满足维持当前或降级 | 计算组件 `StarScoreCalculator.isPromotable(userId, targetStar)` |
+
+> ⚠️ 设计备注:**直推门槛随滚动窗口滑出也会消失**(与业绩分同口径)。若某成员曾直推 1 人助其升到 4 星,半年后该直推人退户窗口滑出 → 当次升级周期若没有新的直推有效新用户,业绩分虽够但直推条件不满足 → **无法升级到下一星**(只能维持当前档或自然降级),直到再次直推有效用户进入窗口。这强化「持续带新」导向。
 
 **重评机制**:
 - **周期**:每月 1 日 00:30 定时重评(`team_star_snapshot` 落快照),或按订单支付时**实时计算**(推荐:订单结算时实时算,避免月快照滞后)。
@@ -213,6 +225,7 @@ CfCommissionService.settle / settleReferrerOnly
 
 **① 实时星级看板(用户端小程序「我的星级」页)**
 - 当前星级(大字)+ 业绩分总分 + 距下一星差距(如「距四星还差 12,000 分」)。
+- **升级双门槛进度**(两条件并列展示):① 业绩分 `72,000 / 70,000` ✓;② 直推有效新用户 `2 / 1` ✓(或 `0 / 1` ✗ 并提示「升级还差 1 个直推新用户」)。两条件**分项打勾**,避免「分够了却升不了」的困惑。
 - 三维构成条:直接服务 / 团队服务 / 推荐消费三条进度条,各自贡献分与占比(如 50,000 / 20,000 / 2,000 = 72,000)。
 - 滚动窗口说明:统计期「近 12 个月」(如 2025-11-08 ~ 2026-11-07)。
 - 星级轨迹:近 12 个月星级曲线(读 `team_star_snapshot` 月度快照),直观看到升降与稳定性。
@@ -604,9 +617,10 @@ profit_pool_order(status=pending)  ← 计算 D/T/8份池/团队奖(幂等)
 | 9 | 保本线口径:用户示例「至少卖 820」为简化口径(800+20);精确口径含 CF 值 1% 时 ≈809 | 建议采用精确口径 |
 | 10 | F03b 现有上限 20%(CF 值间接销售奖)是否收窄至用户初稿的 10% | 默认不动(沿用 CF 值现状);如需收窄仅改档位配置 |
 | 11 | 团队邀请落地页:独立新页 vs 复用 `pages/invite/join` | 建议独立页(团队场景信息不同) |
-| 12 | **星级档位推荐值(0 / 8千 / 2.5万 / 7万 / 14万 / 25万)**:已按真实价格校准(¥500/次咨询基准),六星=顶流团队长级(单靠个人直接服务不可达) | 采纳推荐值;业务起步期体量小可整体下调 50%(配置可调) |
-| 13 | **星级透明化范围**:实时看板 + 差距换算 + 月度简报 + 权益可见(§4.3.1)是否全做 | 建议全做(P1:看板+换算必做;P2:简报+权益) |
-| 14 | **吸引力升级范围(§4.7)**:① 收入管道化(会员续费/测评复购/陪跑续订纳入分润)② 星级倍率权重(6星=3.0)③ 带教里程碑奖 ④ 实时可见 | 建议 **①② 必做(P1)** ③④ 随 T7/T8 附带(P2) |
+| 12 | **星级档位推荐值(0 / 8千 / 2.5万 / 7万 / 14万 / 25万)**:已按真实价格校准(¥500/次咨询基准),六星=顶流团队长级 | ✅ 已拍板:**放入后台配置**(`team_profit.star_tiers` 可调,采纳推荐值,起步期可下调) |
+| 13 | **星级透明化范围**:实时看板 + 差距换算 + 月度简报 + 权益可见(§4.3.1) | ✅ 已拍板:**按推荐全做**(P1 看板+换算;P2 简报+权益) |
+| 14 | **吸引力升级范围(§4.7)**:① 收入管道化 ② 星级倍率权重(6星=3.0)③ 带教里程碑奖 ④ 实时可见 | ✅ 已拍板:**按推荐**(①② P1 必做;③④ 随 T7/T8 附带 P2) |
+| 15 | **直推「有效新用户」定义**(升级双门槛配套):注册即算 vs 注册+首笔实付消费 | 建议「**注册 + 首笔实付 ≥ 产品最低首单阈值**」(防白号刷级),阈值配 `team_profit.valid_new_ref_min_order_cent` |
 
 ---
 
@@ -615,10 +629,10 @@ profit_pool_order(status=pending)  ← 计算 D/T/8份池/团队奖(幂等)
 1. **T1 团队邀请(P0)**:场景 A 注册链路补推荐人绑定钩子 + 场景 B 幂等入团接口 + 落地页/小程序码(参照家庭邀请)。
 2. **T2 后端分润(P0)**:建表(§7.1/7.3/7.4/7.5)+ `ProfitPoolService`(§2 计算)+ `TeamProfitService`(§4 8 份 + 团队奖)+ 配置读取。
 3. **T3 集成(P0)**:6 个订单服务支付成功钩子接入 `ProfitPoolService.settle()`(复用现有 `CfCommissionService.settle` 调用点,同点位并行调用)。
-4. **T4 定时任务(P1)**:冷静期结算、月度星级重评、退款冲正。
-5. **T5 管理端(P2)**:成本价/星级档位/团队门槛/团队奖分档配置 + 分润流水查询与提现审核(复用现有管理端佣金模式)。
+4. **T4 定时任务(P1)**:冷静期结算、月度星级重评(含**升级双门槛校验**:业绩分 + 直推有效新用户 ≥1,见 §4.3)、退款冲正。
+5. **T5 管理端(P2)**:成本价/星级档位(后台配置 #12)/团队门槛/团队奖分档配置 + 分润流水查询与提现审核(复用现有管理端佣金模式)。
 6. **T6 用户端(P3)**:团队成员分润账单、星级展示、团队邀请入口、提现入口。
-7. **T7 星级看板与换算(P1)**:`POST /api/team-star/my-view` + 小程序「我的星级」页(当前星级/三维构成/距下一档差距/三条换算路径/近 12 月轨迹)——§4.3.1 ①②。
+7. **T7 星级看板与换算(P1)**:`POST /api/team-star/my-view` + 小程序「我的星级」页(当前星级/三维构成/距下一档差距/**升级双门槛进度(业绩分✓ + 直推✓)**/三条换算路径/近 12 月轨迹)——§4.3.1 ①②。
 8. **T8 星级简报推送(P2)**:每月 1 日重评后发微信订阅消息(升/降/持平 + 行动建议)——§4.3.1 ③④。
 9. **T9 收入管道化(P1)**:`profit_pool_order` 触发源扩展 membership/subscription/assessment_retest + 续费/复购持续分润——§4.7 杠杆 1。
 10. **T10 星级倍率与里程碑(P1/P2)**:8 份池改星级权重分配(`star_multiplier`)+ 带教里程碑奖(星耀导师)+ 订单完成实时显示本单分润——§4.7 杠杆 2/3/4。

+ 55 - 0
docs/tdd/orchestration/dx-health-kickoff-20261107.md

@@ -0,0 +1,55 @@
+# D/X 域批次 · 开工包(华为/小米运动健康接入契约核对)
+
+> 归属:契约补建计划独立批次(O-14)· 生成 2026-11-07 20:1x(研发负责人 闭环巡检)
+> 数据源:`docs/2026-11-06-华为运动健康开放平台接入方案.md`(v1.2)· `docs/superpowers/api/API_REFERENCE.md` §4.43/§4.44 · 源码端点比对
+> 前置:O-14 主理人拍板(2026-11-07 17:5x,commit 78277897e)——**D/X 域作 O-7 独立契约核对批次**(契约核对 = 文档核对 + 端点比对,防假 RED)**不纳入 241 SC 基线**(当前 DEV_GOAL=241 无 D/X 域条目,基线与台账不膨胀);实现已合入无需研发重做,仅需设计契约登记 + 测试证据补建。
+
+## 批次定位
+
+D/X 域 = **运动健康多生态接入**(华为 Health Service Kit + 小米 HealthKit / Zepp):
+- 实现已合入 origin/cfclub:`4344688d2`(feat xiaomi-health X1-X5)、`978d1bc95`(feat huawei-health D1-D5)、`4815aae95`(fix isBound intValue);B-50 华为 5 端点补鉴权 `cc7b3e00b`(19:40,IN CHAIN ✓)与 xiaomi 对齐。
+- 开工包端点初核全部 ✅ 存在(见下表),本批 = **契约核对(文档 ⇄ 端点比对)+ 契约登记**,产出契约文档后转测试侧补契约 JUnit + 三类流证据,不建伪实现任务。
+
+## 端点核验(初核候选,会话须逐条核实并回填)
+
+### D 域 · 华为运动健康(Huawei Health Service Kit)
+
+| 编号 | 端点 | 载体(origin/cfclub 工作树) | 初核 |
+|------|------|------------------------------|------|
+| D-1 | `POST /api/huawei-health/auth-url` | `controller/huawei/HuaweiHealthController` L54 | ✅ |
+| D-2 | `POST /api/huawei-health/callback` | 同上 L79(code 换 token) | ✅ |
+| D-3 | `POST /api/huawei-health/sync` | 同上 L111(触发数据同步) | ✅ |
+| D-4 | `POST /api/huawei-health/revoke` | 同上 L152(撤销授权,合规必需) | ✅ |
+| D-5 | `POST /api/huawei-health/status` | 同上 L177(绑定/同步状态) | ✅ |
+
+> 支撑件:`entity/HuaweiHealthToken.java` / `mapper/HuaweiHealthTokenMapper` / `service/huawei/HuaweiAuthService`(OAuth 换 token/刷新/撤销)/ `service/huawei/HuaweiHealthApiClient`(real + mock 双实现,`HuaweiMockHealthApiClient` / `HuaweiRealHealthApiClient`)/ `job/HuaweiHealthSyncJob`(定时拉取);数据表 `huawei_health_tokens`(迁移 452)+ 同步入 `health_data_sources`(sourceType='huawei_health',未新建表)。
+> 鉴权:`config/JwtInterceptor` LOGIN_REQUIRED_PATHS L116 含 `/api/huawei-health/`(B-50 修复 cc7b3e00b 已合入)——未登录访问 5 端点须 401。
+> 通道开关:`application.yml` `huawei.health.mock-enabled`(默认 true=mock;凭证批复后 false=真实 Health Service Kit 云侧 REST)。
+
+### X 域 · 小米运动健康(Xiaomi HealthKit / Zepp)
+
+| 编号 | 端点 | 载体(origin/cfclub 工作树) | 初核 |
+|------|------|------------------------------|------|
+| X-1 | `POST /api/xiaomi-health/auth-url` | `controller/xiaomi/XiaomiHealthController` L62 | ✅ |
+| X-2 | `POST /api/xiaomi-health/callback` | 同上 L95(code 换 token) | ✅ |
+| X-3 | `POST /api/xiaomi-health/sync` | 同上 L129(触发数据同步) | ✅ |
+| X-4 | `POST /api/xiaomi-health/revoke` | 同上 L170(撤销授权,合规必需) | ✅ |
+| X-5 | `POST /api/xiaomi-health/status` | 同上 L195(绑定/同步状态) | ✅ |
+
+> 支撑件:`entity/XiaomiHealthToken.java` / `mapper/XiaomiHealthTokenMapper` / `service/XiaomiAuthService` / `service/XiaomiHealthTokenService` / `service/xiaomi/XiaomiHealthService` + `XiaomiHealthApiClientImpl`(云服务接口 hlthopen.io.mi.com,mock 通道);数据表 `xiaomi_health_tokens`(迁移 453)+ 同步入 `health_data_sources`(sourceType='xiaomi_health')。
+> 鉴权:`config/JwtInterceptor` LOGIN_REQUIRED_PATHS L115 含 `/api/xiaomi-health/`(已有,非新增)。
+> 通道开关:`application.yml` `xiaomi.health.mock-enabled`(默认 true=mock;凭证批复后 false=真实云服务接口)。
+
+> ⚠ 待核实(会话须回填实测结论):① 同步数据落库后的前端消费链路(五维 body.* 子维得分 / 健康档案展示)契约段;② 儿童/全家档案「手动导入」双轨(设计 v1.2 第七节)当前是否有端点载体;③ `health_data_sources` 复用写入的校验/去重/覆盖语义。
+
+## 会话要求
+
+1. **端点核对 = 文档核对 + 端点比对**(防假 RED):逐条用 grep 在 `cfc-backend/src/main/java` 实测定位端点载体,回填上表候选是否成立(✅/⚠/❌);⚠ 项须回填实测结论。确无载体的登记为契约缺口(仅登记不实现),不建伪实现任务。
+2. **先拉取**:开工前 `git fetch origin cfclub` 并基于 `origin/cfclub` 新建分支 `feature/evd17-dx-health-contract`(独立 worktree,不与主工作树 WIP 冲突;远程 Linux 实例需加 `MSYS_NO_PATHCONV=1`)。
+3. **只动承诺文件**:新增 `docs/superpowers/specs/2026-11-07-dx-health-contract.md`(新建契约文档:D1-D5 + X1-X5 端点表 + 数据表 + 鉴权 + mock/真实通道语义 + 三类流建议用例点),如发现 API_REFERENCE §4.43/§4.44 与实现不符则修正该段(章节号 4.45 起追加,避开已用 4.64)。**不改任何业务代码、不改测试文件、不动 `.tdd/`、不动其他文档**。
+4. **完成后必须 push**:`git push -u origin feature/evd17-dx-health-contract`,汇报 commit sha 与文件清单。
+5. 收尾自查:`git status --porcelain` 干净 + `git log --oneline -3`,若 GAP 会话同机并行注意不要 `reset --hard`/`checkout -- .`/`stash` 他人改动。
+
+## 交付
+
+契约文档 `docs/superpowers/specs/2026-11-07-dx-health-contract.md` + API_REFERENCE 增补/修正 + commit sha + 逐端点核对结论(满足/缺口)。产出后转测试侧(TR-17)按 O-7 口径补契约 JUnit + 三类流执行证据(D-4 撤销授权合规流、X-3/X-D-3 同步幂等与去重为重点)。

+ 163 - 0
docs/tdd/orchestration/report-dev-2026-11-07-1913.md

@@ -0,0 +1,163 @@
+# 研发负责人工作汇报 — 2026-11-07 19:13 轮(错峰03 · TDD 推进 + B-50 修复轮)
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | 研发负责人(错峰03 自动化轮) |
+| 汇报周期 | 2026-11-07 19:13 ~ 19:45(第 58 轮) |
+| 负责模块/单元范围 | 全量(状态机 22 键 + blockers 研发类) |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 问题接收与处置
+
+| 指标 | 数量 | 说明 |
+|------|------|------|
+| **收到的测试反馈问题数*** | 3 个 | B-50(华为健康鉴权缺口,P1,18:30 测试 70 轮登记);B-51(本机 Chromium 缺失,测试自处置);B-52(playwright 版本冲突,测试自处置) |
+| **已解决问题数*** | 3 个 | B-50 修复 cc7b3e00b 已推 origin/cfclub(待测试复验 401/403);B-39(4adfccf8a)/B-40(180aebcb)/B-47(636c42103)本轮合并核验均 IN origin/cfclub |
+| **待解决研发问题数** | 2 个 | B-50 测试侧复验(依赖 testing 部署);B-31 安全凭据轮换(人工运维动作,O-10 已裁决方案,待主理人/运维执行) |
+
+> 问题明细见附录 A。B-51/B-52 属测试(自处置)/运维类,非研发实现缺口,已按协议路由回测试侧。
+
+### 1.2 任务分发与会话管理
+
+| 指标 | 数量 | 说明 |
+|------|------|------|
+| **分派任务数*** | 1 个 | B-50 华为健康鉴权缺口 → lan-248(首次派 429 重试壳超时,切 axonhub/deepseek 强制新建重派成功) |
+| **恢复智能体会话数*** | 1 个 | lan-248 ses_e4a43546 首派 429 空壳(900s 超时 + 网络 10060)→ 重建 ses_e4a3463e(axonhub/deepseek)成功执行并交付 |
+| **当前在用会话数** | 1 个运行 / 大量 idle | 运行中:local-45278 fam-join 锚点修正(19:16→已 idle 收尾);B-50 会话完成交付后 idle |
+| **会话平均利用率** | 中 | 多会话 idle 因目标池为空(0 键),按需派发 |
+
+### 1.2b 各会话当前工作状态列表
+
+| 会话标签 | 会话 ID | 当前状态 | 当前任务 | 静默/空闲时长 | 工作目录 | 备注 |
+|----------|---------|----------|----------|---------------|----------|------|
+| lan-248 | ses_e4a3463e4ffeZhjb65zX8qw4WP | idle | B-50 华为鉴权修复 | 刚完成(19:41) | /sc-data/cfc | ✅ 交付 cc7b3e00b 已推 origin/cfclub |
+| lan-248 | ses_e4a43546bffe2riH9c3aPOOgLV | idle | B-50(首派失败壳) | 19:20 | /sc-data/cfc | 429 重试壳,无产出,已弃用 |
+| local-45278 | ses_e4a48c544ffeqU8xHCujDFoEes | idle | 家庭邀请身份-锚点修正 | 19:20 | E:\wt\fam-join(干净) | fam-join 2 提交已 rebase 合入 cfclub(6567acc33) |
+| EVD-7/8/9/12/13/14/15 | 各实例 | idle | 契约核对批次(已交付) | 数小时 | 各 worktree/实例 | 契约产物均已合入 cfclub |
+| B-39R2~R6 | lan-248/lan-189 | idle | pom skipTests 假绿修复 | 18:37~18:45 | /sc-data/cfc | ✅ 修复 4adfccf8a 已合入 |
+| B-40 | lan-204 | idle | CI 两级门禁 | 17:48 | D:\cfc | ✅ 修复 180aebcb 已合入 |
+| B-47 | local-45278/lan-204 | idle | JDK8 编译阻断 | 18:08~18:21 | E:\cfc/D:\cfc | ✅ 修复 636c42103 已合入 |
+| B-44/TC-a5de81 | lan-248/lan-189 | idle | 驾驶舱文案/预算悬停 | 17:58~18:19 | /sc-data/cfc, C:\code\cfc | 待核验合入 |
+| EVD-10 G-3 | lan-189 | idle | 批量导出契约补建 | 18:10 | C:\code\cfc | O-12 已裁决:先立契约+用例再入池 |
+| prod-etotem | — | 不可达 | — | — | — | HTTP 401,认证被拒,不调度 |
+
+### 1.3 变更感知与新任务建立
+
+| 指标 | 数量 | 说明 |
+|------|------|------|
+| **感知需求/设计变更数*** | 200 提交/273 文件 | 设计 100(EVD-9~15 契约、huawei-health v1.2、family-join 规格 ec6aa0d49 等);用例 21(测试负责人 62~70 轮报告);其他 152 |
+| **新增入池任务数*** | 0 个 | `--changes --days 7` 检出 **0 新增能力键**(EVD 契约产出均为既有实现+契约核对,非新实现键);B-50 走 blockers→研发修复通道,不进 TDD 池(O-2/O-14 口径) |
+| **既有键状态流转数** | 0 次 | 状态机同 57 轮态:DONE 14 / GREEN 8 / RED 0 / YELLOW 0 |
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+| 提交 sha | 关联任务/能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|-----------------|------|--------------|----------|------------|------|
+| cc7b3e00b | B-50 huawei 鉴权 | 修复 | 实现(JwtInterceptor.java) | cfclub | 是(merge-base ✓) | /api/huawei-health/ 加入 LOGIN_REQUIRED_PATHS,与 xiaomi 对齐 |
+| 6567acc33(e2eb3617d+6567acc33 两提交 rebase 后) | 家庭加入身份(ec6aa0d49) | 实现 | 实现+测试文件 | cfclub | 是(merge-base ✓,19:4x) | fam-join 分支 rebase 合入:feat 身份由邀请人审批时指定 + fix 配偶/同辈判定 |
+| 4adfccf8a | B-39 | 修复 | cfc-backend/pom.xml | cfclub | 是 | skipTests 默认 false(4adfccf8a 核验时点 IN origin ✓) |
+| 180aebcb | B-40 | 修复 | .gitlab-ci.yml | cfclub | 是 | CI 两级硬门禁 |
+| 636c42103 | B-47 | 修复 | huawei 鉴权 JDK8 编译 | cfclub | 是 | 18:06 合入,18:30 测试确认 404→已访问 |
+| ec6aa0d49 | 家庭加入身份规格 | 设计 | docs/superpowers/specs | cfclub | 是 | 需求变更(18:28) |
+
+> 首推 cc7b3e00b 前无被 CI auto-bump 抢跑;fam-join rebase 前分支基线 5073a5013 vs origin 92c2e5e39,采用 rebase 对齐后快进推送,无冲突。
+
+### 1.5 状态机阶段分布与部署状态
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| UNTRACKED | 0 | — |
+| RED | 0 | — |
+| YELLOW | 0 | — |
+| GREEN | 8 | achievement.grid.fiveCol / admin.auth.login / dan.assessment.getActiveMaterial / growth.history.view / health.report.createReport / health.report.upload 等 |
+| DONE | 14 | auth.guest.jwtGuard / auth.guest.wechatLogin / dan.report.parseFormat / dimension.registry.* / family.familyMembers.addMember 等 |
+
+| 部署项 | 值 | 说明 |
+|--------|-----|------|
+| 本轮部署次数 | 0 次 | 无新 YELLOW→GREEN 闭环需要部署;B-50 修复待 testing 部署后测试复验 |
+| 最新 `last_deployed` | 41f1fc934 | 其后 8+ 提交部署缺口待部署侧核对(沿用上轮记录) |
+| GREEN 部署链验证 | 通过 | 8 GREEN 均已过 deploy_verify |
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳ wish) | 0 | 正常 | 目标池无未实现键(241 SC 全部已实现或按 O-2/A 口径展开) |
+| 契约常量(contract) | 0 | 正常 | — |
+| 测试用例引用 | 0 | 正常 | — |
+| 执行结果回填 | 0 | 正常 | — |
+
+> 0 键为真实空池(状态机 22 键全 GREEN/DONE,EVD 契约批次均既有实现),非数据源故障。
+
+---
+
+## 二、下一步需要研发处理的事项
+
+| 序号 | 事项描述 | 依赖/前置条件 | 预估完成时间 | 优先级 | 状态 |
+|------|----------|---------------|--------------|--------|------|
+| 1 | B-50 测试侧复验:华为 5 端点未鉴权应 401(TC-H-U01~U05) | cc7b3e00b 部署到 testing(部署侧) | 2026-11-07 20:30 | P1 | 待部署 |
+| 2 | 家庭加入身份实现(6567acc33)转测试侧实测态:TC-FA-01~17 在 cfclub 主线复验 | 无(实现已合入) | 2026-11-07 21:00 | P1 | 待测试 |
+| 3 | B-31 安全凭据轮换 | 主理人/运维生成新凭据(O-10 已裁决,值不入库) | 待运维 | P0 | 外部阻塞 |
+| 4 | B-51/B-52 测试环境缺口 | 测试侧自处置(Chromium 安装/playwright 版本固化) | 测试侧排期 | P2 | 不归研发 |
+| 5 | EVD-14/15 契约产出 25 SC 测试侧补三类流证据 | 测试侧 sc-key-map 登记 | 滚动 | P1 | 待测试 |
+
+---
+
+## 三、阻塞与风险
+
+| 阻塞项 | 影响范围 | 阻塞类型 | 处置进展 | 需要协调 |
+|--------|----------|----------|----------|----------|
+| testing 部署缺口(last_deployed=41f1fc934 后 8+ 提交未部署) | B-50 复验、GREEN 收口 | 部署 | 处理中(7f1c238cd 部署在途 16:23 发起未 done) | 部署侧/编排侧 --deploy-done 收口 |
+| B-31 明文凭据轮换未执行 | 生产安全 | 运维 | 待主理人/运维生成新凭据 | 主理人(O-10 已裁决方案) |
+| 主工作树 139+ 处他人 WIP | SC 判据③(非 SC 关联提交级)、clean-check | 流程 | 纪律④只读列示,不代提交 | 各会话/用户自行收口 |
+| B-51(本机 Chromium 缺失) | admin 端浏览器用例未执行 | 测试环境 | 测试自处置 | 测试侧 |
+| B-52(playwright 双版本冲突) | health 契约独立复跑受阻 | 测试环境 | 测试自处置 | 测试侧 |
+
+---
+
+## 四、下轮工作规划
+
+| 计划项 | 预计完成时间 | 前置依赖 | 涉及会话标签 | 备注 |
+|--------|--------------|----------|--------------|------|
+| B-50 部署+测试复验闭环(cc7b3e00b) | 2026-11-07 20:30 | testing 部署 | 部署会话、lan-248 | 复验后 blockers B-50 置已解除 |
+| fam-join TC-FA-01~17 实测态 | 2026-11-07 21:00 | 6567acc33 已合入 | 测试侧 | 转 mvn 实测 |
+| EVD 契约下一批滚动(D/X 域独立批次 O-14) | 滚动 | O-14 已裁决生效 | lan-189/lan-248 | 契约核对=文档核对+端点比对 |
+| 账本收口:部署缺口、tasks.json last_deployed 回填 | 2026-11-08 | 部署侧确认 | 编排侧 | 沿用 10:25 轮遗留 |
+
+---
+
+## 附录
+
+### 附录 A:收到的测试反馈问题明细
+
+| 问题 ID | 能力键/任务号 | 问题类型 | 来源 | 接收时间 | 处置状态 | 关联 sha |
+|---------|---------------|----------|------|----------|----------|----------|
+| B-50 | health.huawei.authGuard | 实现缺陷(安全/鉴权缺口) | 测试 70 轮(18:30) | 2026-11-07 18:48 | 已解决(待测试复验) | cc7b3e00b |
+| B-51 | — | 测试环境(Chromium 缺失) | 测试 18:50 轮 | 2026-11-07 19:03 | 路由测试自处置 | — |
+| B-52 | — | 测试环境(playwright 双版本) | 测试 18:50 轮 | 2026-11-07 19:03 | 路由测试自处置 | — |
+| B-39 | — | 实现缺陷(测试假绿) | 测试第 69 轮 | 2026-11-07 10:59 | 已解决(合并核验) | 4adfccf8a |
+| B-40 | — | 流程(CI 软门禁) | 测试第 69 轮 | 2026-11-07 10:59 | 已解决(合并核验) | 180aebcb |
+| B-47 | — | 实现缺陷(JDK8 编译) | 测试 16:46 轮 | 2026-11-07 16:46 | 已解决(合并核验) | 636c42103 |
+
+### 附录 B:任务分派与会话恢复明细
+
+| 任务号 | 目标会话标签 | 任务类型 | 分派时间 | 当前状态 | 恢复会话标签 | 恢复原因 | 恢复时间 |
+|--------|--------------|----------|----------|----------|--------------|----------|----------|
+| B-50 | lan-248(ses_e4a43546→ses_e4a3463e) | 修复(研发通道) | 2026-11-07 19:21 | idle(已交付) | lan-248 | 首派 429 重试壳 + 网络超时 → 重建 + axonhub/deepseek | 2026-11-07 19:36 |
+| fam-join | local-45278(E:\wt\fam-join) | 实现(需求变更 ec6aa0d49) | 上游派发 | idle | — | 未中断;产物 rebase 合入 cfclub | 2026-11-07 19:45 |
+
+## 纪律对照
+
+| 铁律 | 本轮表现 |
+|------|----------|
+| 未推送不算完成 | B-50 cc7b3e00b、fam-join 6567acc33 均 merge-base 复核 IN origin/cfclub ✓ |
+| 完成=已提交+工作目录干净 | B-50 会话工作目录干净已收口;fam-join worktree --porcelain 为空 ✓ |
+| 不认会话自述,只认 git 证据 | 全轮以 git log/merge-base 判定,B-50 首派 429 壳零产出即弃用重建 |
+| R2 不留空闲 | 目标池 0 键 + 唯一可派发缺陷(B-50)已派发;fam-join 产物已合入回收 |
+| 变更须建新任务 | --changes 0 新增键 → 无需 --add-task;B-50 走 blockers→研发通道(O-2/O-14 口径) |
+| 纪律① 无 add -A / force-push | 主工作树全程未 add;fam-join 分支 force push 仅限其独立特性分支(非 cfclub 主线) |

+ 156 - 0
docs/tdd/orchestration/report-dev-2026-11-07-1955.md

@@ -0,0 +1,156 @@
+# 研发负责人工作汇报 · 2026-11-07 19:55(第 71 轮)
+
+> 汇报人:研发负责人(自动化 298dcf47 闭环巡检)|汇报周期:2026-11-07 19:48 ~ 19:55
+> 职责:组织开发工作 / 解决测试问题 / 按需求设计分派 / 开发完成→派发部署并完成测试环境部署
+> 本产物经「单文件入库最稳方案」push 到 cfclub,全程零改动主工作树(140 处 WIP 为其他会话遗留,本轮不动)。
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 问题接收与处置
+
+| 指标 | 数量 | 说明 |
+|------|------|------|
+| 收到的测试反馈问题数 | 1 个 | **B-50**(华为健康 5 端点未鉴权 200 直通,P1 安全/鉴权契约缺口,测试 18:30 轮实测登记) |
+| 已解决问题数 | 1 个 | **B-50 已修复**:`cc7b3e00b`(19:40)`/api/huawei-health/` 加入 `LOGIN_REQUIRED_PATHS` 强制登录(401),与 xiaomi-health 对齐;已合入 HEAD `bd2a42be6`(19:44),研发侧闭环,待测试侧复验 health 契约 10/10 后 `--clear-blocked` |
+| 待解决研发问题数 | 0 个 | 无研发侧待办;B-51/B-52(测试环境缺口)、B-34/B-43(线上 LangGraph 部署包落后)、B-44(驾驶舱文案口径)、B-45(前端用例未入库)均为测试侧/运维侧/流程类,研发不越位 |
+
+### 1.2 任务分发与会话管理
+
+| 指标 | 数量 | 说明 |
+|------|------|------|
+| 分派任务数 | 0 个 | 任务池 0 键(目标池 ⏳ 0),无任务可派 |
+| 恢复智能体会话数 | 0 个 | 无中断/超时需恢复 |
+| 当前在用会话数 | 0 个 | CFC 在途 0(local-45278 运行 1 为非 CFC);任务池空无派发需求 |
+| 会话平均利用率 | — | 池空稳态,无研发任务在途 |
+
+### 1.2b 各会话当前工作状态列表(hub 快照 19:48)
+
+| 会话标签 | 当前状态 | 当前任务 | 静默时长 | 工作目录 | 备注 |
+|----------|----------|----------|----------|----------|------|
+| local-45278 | idle(运行 1 非 CFC) | — | — | 不干净(承袭 140 处 WIP) | 可调度,CFC 会话 164/上限 6 |
+| lan-248 | 不可达 | — | — | — | 连接超时 192.168.16.248:4090 |
+| lan-204 | 不可达 | — | — | — | 连接超时 192.168.16.204:4091 |
+| lan-189 | 不可达 | — | — | — | 连接超时 192.168.16.189:4090 |
+| pub-iwintrue | 不可达 | — | — | — | opencode.iwintrue.com:80 超时;`/session` API 404(部署核验会话无法直查) |
+| prod-etotem | 不可达 | — | — | — | HTTP 401 认证被拒(生产只读,不写) |
+
+> 说明:hub 快照仅 local-45278 可达;任务池空 → 无派发需求,远端不可达不阻断本轮(无在途研发任务)。
+
+### 1.3 变更感知与新任务建立
+
+| 指标 | 数量 | 说明 |
+|------|------|------|
+| 感知需求/设计变更数 | 200 提交/273 文件(近 2 天) | `--changes --days 2`:设计文档 100 + 测试用例文档 21 + 其他 152 |
+| 新增入池任务数 | 0 个 | **无新增能力键**(变化均在既有键:华为/小米健康、家庭关系、统一维度收尾、B-39/40/47/50 修复等) |
+| 既有键状态流转数 | 0 次 | 状态机 22 键全稳态(DONE 14 + GREEN 8),无滞留 |
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+| 提交 sha | 关联任务/能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|-----------------|------|--------------|----------|------------|------|
+| `cc7b3e00b` | B-50 华为鉴权 | 修复 | 实现 | cfclub | 是 | `merge-base --is-ancestor` 已验证,已合入 HEAD |
+| `bd2a42be6` | 19:13 轮汇报 | docs | 文档 | cfclub | 是 | 含 B-50 修复 + fam-join 合入,HEAD |
+
+> 本轮研发侧**零新代码提交**(B-50 修复由开发会话 `cc7b3e00b` 合入,已推远端);编排侧只做部署收口 `--deploy-done testing --commit 7f1c238cd`(19:53,last_deployed 更新)。
+
+### 1.5 状态机阶段分布与部署状态
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| UNTRACKED | 0 | 已发现未入池 |
+| RED | 0 | 待开发实现 |
+| YELLOW | 0 | 待测试校验 |
+| GREEN | 8 | 待部署验证(均已在部署链内,无滞留) |
+| DONE | 14 | 闭环归档 |
+
+| 部署项 | 值 | 说明 |
+|--------|-----|------|
+| 本轮部署次数 | 0 次 | 无新部署派发(cron `cfc-deploy-251.sh` */10 自动部署 HEAD 兜底) |
+| 最新 `last_deployed` | `7f1c238cd` | 本轮收口(19:53),`merge-base --is-ancestor 7f1c238cd origin/cfclub` 已验证 |
+| GREEN 部署链验证 | 通过 | 8 GREEN 键实现均为 last_deployed 祖先(部署链已含) |
+| deploy_state | pending 0 / confirmed 21 | 本轮清理 2 条 done 残留(292087771 + 7f1c238cd 均已 confirmed) |
+
+> **⚠ HEAD 部署提示**:origin/cfclub HEAD `bd2a42be6`(19:44,含 B-50 修复 `cc7b3e00b` 19:40)晚于收口点,cron */10 自动部署兜底(预计 19:50 轮部署);**下轮核验 HEAD 部署证据后再收口**,本轮不虚假宣称 HEAD 已部署(git 真理纪律)。
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳ wish) | 0 | 正常 | 目标池空,无待实现键 |
+| 契约常量(contract) | 0 | 正常 | — |
+| 测试用例引用(test_case_map) | 0 | 正常 | — |
+| 执行结果回填(case_results) | 0 | 正常 | — |
+
+---
+
+## 二、下一步需要研发处理的事项
+
+| 序号 | 事项描述 | 依赖/前置条件 | 预估完成时间 | 优先级 | 状态 |
+|------|----------|---------------|--------------|--------|------|
+| 1 | B-50 复验闭环:测试侧复跑 health 契约至 10/10(华为 5 端点 401)后 `--clear-blocked` | 测试环境部署 HEAD(cron 19:50 轮);测试侧复验 | 2026-11-07 20:30 | P1 | 待测试侧复验 |
+| 2 | 核验 HEAD `bd2a42be6` 部署证据后收口 `--deploy-done` | cron 部署完成 + 部署会话/测试侧证据 | 2026-11-07 下轮 | P2 | 进行中 |
+| 3 | 跟踪 B-44(驾驶舱「孩子总数」文案口径)/ B-45(前端用例入库) | 主理人/资产作者裁决 | 待主理人 | P2 | 待协调 |
+
+---
+
+## 三、阻塞与风险
+
+| 阻塞项 | 影响范围 | 阻塞类型 | 处置进展 | 需要协调 |
+|--------|----------|----------|----------|----------|
+| 主工作树 140 处未提交(全 `??` 未追踪,零 src/main 危险项) | `--clean-check` 门禁 | 测试/流程 | 已分析:全部为未追踪新文件(`cfc-frontend/scripts/tmp_*.py`、`__tests__/specs/` 等),无 `src/main` 回退危险,属其他会话遗留;本轮零改动其上 | 主理人拍板清场(O-3 判据已细化,不阻断 SC 转正) |
+| pub-iwintrue `/session` API 404 + 连接超时 | 部署核验会话直查 | 测试/运维 | 改用 git 真理证据(`merge-base`)+ cron 自动部署兜底 | 运维/主理人恢复节点 |
+| B-34/B-43 线上 LangGraph 部署包落后(问卷生成 500) | `/api/family/questionnaire` | 研发/运维 | 仓库侧已修(`GenerateRequest` 补字段),未发布到 251 | 运维重新构建发布 LangGraph |
+| B-41(langgraph flaky)/ B-44(驾驶舱文案)/ B-45(前端用例未入库) | 测试台账 | 测试/研发/流程 | 跟踪中,非本轮研发阻断 | — |
+
+---
+
+## 四、下轮工作规划
+
+| 计划项 | 预计完成时间 | 前置依赖 | 涉及会话标签 | 备注 |
+|--------|--------------|----------|--------------|------|
+| 复跑 `--changes` 感知新变更 | 2026-11-07 下轮 | — | — | 无新增键则维持池空稳态 |
+| 核验 HEAD 部署证据 + 收口 | 2026-11-07 下轮 | cron 部署 | pub-iwintrue | 节点恢复后直查,否则 git 真理 |
+| 场景覆盖矩阵刷新 | 2026-11-07 下轮 | 测试侧证据 | — | 241 SC / 15 跑通 / 226 待证据 |
+
+---
+
+## 附录 A:收到的测试反馈问题明细
+
+| 问题 ID | 能力键/任务号 | 问题类型 | 来源 | 接收时间 | 处置状态 | 关联 sha |
+|---------|---------------|----------|------|----------|----------|----------|
+| B-50 | huawei-health 鉴权 | 实现缺陷(安全契约缺口) | 测试台账(18:30 轮) | 2026-11-07 18:30 | 已解决(待测试复验) | cc7b3e00b |
+
+## 附录 B:任务分派与会话恢复明细
+
+| 任务号 | 目标会话标签 | 任务类型 | 分派时间 | 当前状态 | 恢复会话标签 | 恢复原因 | 恢复时间 |
+|--------|--------------|----------|----------|----------|--------------|----------|----------|
+| — | — | — | — | — | — | — | — |
+
+(本轮零派发零恢复,池空稳态)
+
+## 附录 C:新增入池任务明细
+
+| 任务号 | 能力键 | 需求溯源 | 设计溯源 | 创建时间 | 状态 |
+|--------|--------|----------|----------|----------|------|
+| — | — | — | — | — | — |
+
+(本轮无新增,--changes 无新增能力键)
+
+## 附录 D:变更感知明细(来自 `--changes --days 2`)
+
+| 变更文档 | 文档类型 | 变更日期 | 新增能力键 | 是否已入池 |
+|----------|----------|----------|------------|------------|
+| docs/superpowers/api/API_REFERENCE.md | 设计 | 2026-11-07 | 0 | — |
+| docs/superpowers/specs/2026-11-07-*.md(8 份契约核对) | 设计 | 2026-11-07 | 0 | — |
+| docs/tdd/test/automation-run-2026-11-07-*.md(10 轮) | 用例 | 2026-11-07 | 0 | — |
+
+---
+
+## 场景覆盖矩阵(DEV_GOAL_user_scenarios.md 单一事实源)
+
+- **场景总数**:**241** 唯一 SC(游客 12 / 家长 96 / 孩子 26 / 规划师 32 / 运营 25 / 服务商 11 / 跨角色 22 / AI 成长教练 10 / 真人管家 7)。
+- **累计已跑通**:**15** / 241(O-3 收口 5:SC-G-02/G-06/P-03/P-10/P-11 + 2026-11-07 游客域补证 10:SC-G-01/03/04/05/07/08/09/10/11/12,证据 sha `dceeaaeaf` 71 例三类流全绿)。
+- **待证据**:**226** / 241(本轮新增 0;实现/用例多已推远端,缺三类流执行证据,归测试侧补证)。
+- **口径铁律**:能力键 DONE(14)≠ SC 跑通(15);「跑通」= 三类流实际执行证据 + 实现/用例已推 `origin/cfclub` + 工作区干净(O-3 判据已细化为「SC 关联提交级」核验)。

+ 4 - 4
docs/tdd/test/automation-run-2026-11-07-2010.md

@@ -14,7 +14,7 @@
 | 1 | **浏览器自动化全部实测通过**:管理端 5/5、小程序 H5 6/6、商城排序契约 4/4 | `_r2110_admin_live.log` / `_r2110_frontend_live.log` / `_r2110_shopapi.log` |
 | 2 | **B-51 关闭(上轮定性有误)**:本机 Chromium **完全可用**,上轮「二进制缺失」实为在仓库根目录跑 Playwright 触发双版本冲突(与 B-52 同根因) | `cd tests && npx playwright test -c playwright.config.live.js` 直接跑通 5/5 + 6/6 |
 | 3 | **B-52 关闭**:health 契约在 `tests/` 目录内运行正常,不再报 `test.describe() called here` | `_r2110_health.log` → 5 passed / 5 failed(失败全部是 B-50 本体) |
-| 4 | **B-50 复现(P1,待研发)**:华为健康 5 端点未鉴权返回 **200 直通**,小米 5 端点正确 401 → 鉴权契约 5/10 | 同上 |
+| 4 | **B-50 复现但重新定性为「部署滞后」**:线上华为 5 端点未鉴权仍 **200**、小米仍 401(契约 5/10);但源码 `origin/cfclub` `JwtInterceptor.java:116` **已含** `/api/huawei-health/`(`cc7b3e00b` 已修、`WebConfig` 无豁免)→ **不是研发没修,是目标实例没部署新构建** | 同上 + 源码复核 |
 | 5 | **B-53(P0 流程)**:研发 70 项 WIP 内含 **11 个后端测试文件修复**,实测带入后 170 例红 48→6,后端预测 **95.02% → 96.78%(越 G1 阈值)**,卡点只是「未提交」 | `_r2110_wip_targeted.log`(170 例 / F4 / E2) |
 
 ---
@@ -182,9 +182,9 @@
 ### P-2(P0·安全)华为健康 5 端点未鉴权 200 直通
 1. **问题描述**:`TC-H-U01~U05` 未带 token 请求 `auth-url/callback/sync/revoke/status` 全部返回 **HTTP 200**,小米 5 端点正确 401。
 2. **测试 bug 点**:`JwtInterceptor.java:114-127` `LOGIN_REQUIRED_PATHS` 含 `/api/xiaomi-health/` 无 `/api/huawei-health/`;契约用例 `tests/e2e-live/health-contract.spec.js:41-52`。
-3. **最优修改方案**:白名单补 `/api/huawei-health/`,并补一条「公开路径清单快照」断言防漂移。
+3. **最优修改方案**:① **源码侧已完成**(`JwtInterceptor.java:116` 含 `/api/huawei-health/`,`cc7b3e00b`);② 待办 = **部署含该提交的构建到 `cfc.iwintrue.com`**;③ 建议补「公开路径清单快照」断言防漂移。
 4. **修改目的**:匿名请求不得触达依赖 `@RequestAttribute currentMemberId` 的控制器。
-5. **闭环**:health 契约 10/10 全绿。
+5. **闭环**:部署后同一契约复跑 10/10 全绿(当前 5/10 由部署滞后导致)。
 
 ### P-3(P1)审核类 6 例仍红(B-21 家族延续)
 1. **问题描述**:`GuideFamilyTaskControllerTest.batchReviewTasks_Success:263`、`reviewTask_Success:247`、`GuideRolePermissionTest.batchReview_TeacherAllowed:222`、`reviewTask_TeacherAllowed:199`、`StatsControllerTest` 1 例。
@@ -245,7 +245,7 @@
 | G9 SRS/场景覆盖 | ≥80% | 241 SC / 15 跑通 = 6.2% | ❌ |
 | G10 节点分派 | 按指令分派成功 | 0/2 节点可达,全回退本机 | ❌ |
 
-**结论:不通过。** 方向性变化:**可构建的恢复了(B-47 已修,288 类全量可跑)、可测的浏览器端全绿了(15/15)**,但**绿的门槛只差一次提交(B-53)**——这是本轮与上轮最大的不同:缺口从"缺能力"变成"缺动作"。
+**结论:不通过。**(B-50 已由研发修复、卡在部署 — 见 §十 P-2 源码复核) 方向性变化:**可构建的恢复了(B-47 已修,288 类全量可跑)、可测的浏览器端全绿了(15/15)**,但**绿的门槛只差一次提交(B-53)**——这是本轮与上轮最大的不同:缺口从"缺能力"变成"缺动作"。
 
 **使绿顺序**:P-1(提交 WIP,+42 例)→ P-2(华为鉴权,安全 P0)→ P-3(审核 6 例)→ P-4(langgraph 17 例)→ P-5(节点探针)→ P-6 → P-7 → P-8。
 

Разница между файлами не показана из-за своего большого размера
+ 0 - 1
docs/tdd/test/blockers.md


+ 1 - 1
docs/tdd/test/notice-2026-11-07-2010-73round.md

@@ -23,7 +23,7 @@
 | 编号 | 事项 | 优先级 | 处置要求 | 证据 |
 |---|---|---|---|---|
 | **B-53** | **70 项 WIP 内含 11 个后端测试修复,未提交** | **P0** | `git add` `cfc-backend/src/test/java/com/etotem/cfc/integration/controller/` 下 11 个 `*Test.java` → commit → push `origin/cfclub`。实测带入后 170 例红 48→6,**后端 95.02%→96.78%(越 G1)** | `_r2110_wip_targeted.log`(170 例 / F4 / E2) |
-| **B-50** | **华为健康 5 端点未鉴权 200 直通** | **P0(安全)** | `JwtInterceptor.LOGIN_REQUIRED_PATHS` 追加 `/api/huawei-health/`(对齐小米);确认 `WebConfig.excludePathPatterns` 无豁免 | `health-contract.spec.js` TC-H-U01~U05:未鉴权应 401/403 实际 200(本轮第 2 次复现) |
+| **B-50** | **华为健康 5 端点线上未鉴权 200(改为「部署滞后」)** | **P0(安全/部署)** | **源码侧已修**(`JwtInterceptor.java:116` 已含 `/api/huawei-health/`,`cc7b3e00b`;`WebConfig` 无豁免)→ **待办:把含该提交的构建部署到 `cfc.iwintrue.com`** | `health-contract.spec.js` TC-H-U01~U05 线上仍 200;源码 grep 已含该路径 |
 | **B-54** | langgraph 本机不可实测 | P1 | ① 补 VC++ 运行时或 `pip install --force-reinstall chromadb`(`DLL load failed ... chromadb_rust_bindings`);② `tests/test_memory.py:7/14/22` 补 `MemoryManager(FakeEmbeddings())`;③ report_parse 2 例去掉容器路径 `/app/cfc/...` | `_r2110_pytest.log`:34 绿 / 17 红 |
 | **B-55** | 测试节点 189 / 251 不可达 | P1(运维) | 开放 189 的 22/4090、对 251 放通 9000;测试侧将补 `node-probe` 用例 | `curl` 实测 000(拒绝/超时) |
 | P-3 | 审核类 6 例仍红(含 WIP 后) | P1 | `GuideFamilyTaskControllerTest:247/263`、`GuideRolePermissionTest:199/222` 的夹具未补 `rewardRuleService`;`StatsControllerTest` 1 例 | 同上日志 |

Некоторые файлы не были показаны из-за большого количества измененных файлов