userid-vs-memberid-usage-guide.md 10.0 KB

userId / memberId / childId 使用场景梳理

适用模块:cfc-backend(Spring Boot + MyBatis-Plus) 维护日期:2026-09-05 目的:明确系统内三类身份标识的边界,避免新增接口时误用。


一、结论先行:系统里有三类 ID

标识 来源表 主键 含义 生命周期
userId users users.id 登录账号(微信/手机号登录的"人"),认证主体 随账号注册/注销
memberId family_members family_members.id 家庭中的成员(家庭关系网里的一个节点) 随加入/退出家庭
childId children children.id 遗留的孩子表主键(历史数据,正在被 family_members 取代) 遗留,逐步废弃

⚠️ 注意:代码里 memberIdfamilyMemberIdcurrentMemberId 三个名字指的都是同一个东西 —— family_members.id,只是字段命名不统一。childId 则是另一张遗留表 children.id,两者不是一回事。


二、核心实体关系

users (账号)  ──1:1──  family_members (成员)  ──N:1──  families (家庭)
   │                        │
   │  users.id = family_members.userId (外键,可空)
   │
   └── children (遗留表)  children.userId = users.id

关键事实:

  1. family_members.userId 是外键,指向 users.id,且可为 null。

    • 有 userId 的成员 = 已注册登录的真实账号(爸爸/妈妈/孩子本人等)。
    • userId = null 的成员 = 纯档案成员(手动添加、未注册登录的"爷爷/奶奶"等),没有账号、不能登录,只能作为家庭关系节点存在。
  2. userId 与 memberId 不是一一对应。

    • 一个 userId 在某个家庭里有且只有一个 memberId(JwtInterceptoruserId + familyId 反查)。
    • 一个 memberId 可能没有 userId(纯档案成员)。
    • 理论上一个 userId 在不同家庭可有不同 memberId(当前业务基本单家庭,但表结构支持多家庭)。
  3. children 表标注"已废除",但实体 ChildChildMapper 及多个 Service 仍在活跃使用(详见根 AGENTS.md 的说明)。生产库 children 表缺 phone/id_card 两列。处理 children 相关需求时:不要据此删实体,也不要重复加迁移。


三、userId 使用场景("我是谁"——账号/认证维度)

userId 由 JWT 解析而来,JwtInterceptor 写入 request.setAttribute("userId", ...)永远代表"当前登录的账号"

3.1 认证与身份

  • 登录、JWT 签发(JwtConfig.generateToken(userId, role),subject 就是 userId)。
  • 控制器里 @RequestAttribute("userId") Long userId 获取当前操作者。

3.2 账号自身属性(users 表字段)

昵称、头像、手机号、totalPoints(家长积分)、会员等级、佣金(totalCommissionEarned)、推荐关系(referrerId)、服务商资质(vendorType/vendorStatus)、规划师证书(teacherNo/teacherStatus)等。

3.3 权限与归属判断

  • 判断"是不是家庭管理员":families.creator_id == userId
  • 判断"是不是本人":FamilyMemberVO.isSelf
  • 越权校验:FamilyAccessInterceptor 用 userId 算 getEffectiveFamilyIds(userId)

3.4 操作者/交易主体身份(业务表用 user_id 列)

订单(ActivityOrder.userIdPurchaseOrder.userIdPackageOrder.userId)、购物车(Cart.userId)、优惠券(UserCoupon.userId)、收货地址(UserAddress.userId)、平台余额(UserPlatformBalance.userId)、佣金(Commission)、操作日志(UserOperationLog.userId)、通知(UserNotification.userId)等——这些是"账号"维度的数据,用 userId

3.5 家长/规划师/管理员视角的操作

审批任务、审核心愿、录入测评结果、管理家庭、下发任务——都是"账号"做的事,用 userId。


四、memberId 使用场景("我是家庭里的谁"——家庭/成员维度)

memberId = family_members.id,代表家庭关系网中的一个具体成员

4.1 家庭成员管理

  • FamilyMembersController/add /update /kick /switch 都接收 memberId 参数。
  • FamilyMemberService.switchToMember(userId, memberId):家长切换到某个孩子的视角。

4.2 任务执行主体

TaskController 里任务的完成、开始、领取、历史查询都以 memberId 为执行者——任务由"家庭成员"执行,不是"账号"执行。

4.3 成员维度的数据(业务表用 member_id/family_member_id 列)

健康记录(HealthSleepRecordHealthMealRecordHealthWaterRecordHealthExerciseRecordHealthStatus 等)、五维能量(FiveDimensionScore.memberId)、用户画像快照(ProfileSnapshot.memberIdProfileHistory.memberId)、先天画像(InnatePortraitReport.memberId)、家庭关系评分(trustScore/intimacyScore/communicationScore,存于 family_members)、社交圈(SocialCirclePost.memberId)、家庭挑战(ChallengeProgress.memberId)等。

4.4 关系数据

家庭关系图、关系质量问卷(RelationshipQuestionnaireSnapshot.familyMemberId)、成员变更日志(FamilyMemberLog.memberId)。


五、childId 使用场景(遗留表 children 维度)

childId = children.id,是旧版"孩子"表的主键,目前正在向 family_members 迁移,属遗留字段。

大量实体同时存在 childIduserId/familyMemberId 两套字段,处于过渡期:

实体 现状
GrowthRecord 同时有 childId + familyMemberId
GrowthPlan 同时有 childId + familyMemberId
PointsLog / PointsExchangeRecord 同时有 childId + familyMemberId
DanAssessmentResult 同时有 childId + familyMemberId
AssessmentOrder / AssessmentAppointment / AssessmentRecord 同时有 childId + userId
EnergyLog / EnergyBalance / Reward / Wish 只有 childId(待迁移)

处理原则

  • 新增功能优先用 memberIdfamily_members.id)或 userId,不要再新增 childId 引用。
  • 读旧数据时可能需要兼容 childId,但写入时尽量落新字段。
  • 不要据 children 表"已废弃"标注而删除 Child 实体或相关 Service(仍有活跃使用)。

六、关键机制:currentMemberId 的自动推导

JwtInterceptor 在解析 token 后自动做一次映射(源码约第 163-180 行):

// 1. 从 users 表取 familyId,为空时从 family_members 反查兜底
// 2. 用 userId + familyId 从 family_members 查当前用户对应的成员
FamilyMember member = familyMemberMapper.selectOne(
    new LambdaQueryWrapper<FamilyMember>()
        .eq(FamilyMember::getUserId, userId)
        .eq(FamilyMember::getFamilyId, familyId)
        .last("LIMIT 1"));
request.setAttribute("currentMemberId", member.getId());   // = memberId
request.setAttribute("familyMemberId", member.getId());

控制器里的标准模式:

Long memberId = params.get("memberId") != null
    ? ParamUtils.getLong(params.get("memberId"))
    : currentMemberId;   // 回退到"当前登录账号自己对应的 memberId"

关键理解(容易误解)

  • currentMemberId 永远是"当前登录账号自己"的 memberId,每次请求都从 token 的 userId 重新反查,不会因为"切换视角"而改变
  • 切换视角(家长看孩子)靠的是:前端记住目标成员的 memberId,并在后续请求 body 里显式传 memberId 参数覆盖 currentMemberId

七、判断规则(速查表)

问题 用哪个 ID
这是哪个账号在操作? userId
这是家庭里的哪个成员 memberId
操作的是账号自身属性(积分/会员/佣金/资质/订单/优惠券/余额) userId
操作的是家庭成员/孩子(任务/健康/成长/关系/五维能量) memberId
需要权限判断(是不是管理员/本人) userId
需要切换视角(家长看孩子) 前端传 memberId,后端回退 currentMemberId
涉及旧孩子表历史数据兼容 childId(尽量不新增引用)

八、易踩坑点汇总

  1. memberId ≠ userId:不要拿 userId 去查 family_members 主键,也不要拿 memberId 去查 users 主键。正确桥梁是 family_members.userId

  2. 纯档案成员没有 userId:手动添加的"爷爷/奶奶"等 family_members.userId = null,这类成员没有账号,不能登录,相关逻辑要判空。

  3. currentMemberId 可能为 null:若当前登录用户还没加入任何家庭(familyId = null),JwtInterceptor 不会设置 currentMemberId。控制器里以 @RequestAttribute(value="currentMemberId", required=false) 接收并判空(如 TaskController 返回 "memberId不能为空")。

  4. children 表未真正废弃Child 实体、ChildMapper 及多个 Service 仍活跃使用,生产 children 表缺 phone/id_card 列。勿误删、勿重复加迁移(详见根 AGENTS.md)。

  5. 字段命名不统一memberId / familyMemberId / currentMemberId 三名字都指 family_members.idchildIdchildren.id。读写时先确认目标表。

  6. 切换视角不改变 currentMemberId:它是按登录账号固定反查的,切视角必须靠前端显式传 memberId 参数,不要试图在后端"改 currentMemberId"。


九、新增接口自检清单

开发新接口时,先回答:

  • 操作对象是"账号"还是"家庭成员"?(决定用 userId 还是 memberId)
  • 是否涉及家长切换视角?→ 需接收可选 memberId 参数并回退 currentMemberId
  • 是否需要权限校验?→ 用 userId 判断管理员/本人
  • 是否误用了 childId?→ 新功能一律用 memberId/userId
  • currentMemberId 为空时是否已兜底?(未加入家庭的情况)