# 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` 取代) | 遗留,逐步废弃 | > ⚠️ 注意:代码里 `memberId`、`familyMemberId`、`currentMemberId` 三个名字指的都是同一个东西 —— **`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(`JwtInterceptor` 用 `userId + familyId` 反查)。 - 一个 memberId 可能没有 userId(纯档案成员)。 - 理论上一个 userId 在不同家庭可有不同 memberId(当前业务基本单家庭,但表结构支持多家庭)。 3. **`children` 表标注"已废除",但实体 `Child`、`ChildMapper` 及多个 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.userId`、`PurchaseOrder.userId`、`PackageOrder.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` 列) 健康记录(`HealthSleepRecord`、`HealthMealRecord`、`HealthWaterRecord`、`HealthExerciseRecord`、`HealthStatus` 等)、五维能量(`FiveDimensionScore.memberId`)、用户画像快照(`ProfileSnapshot.memberId`、`ProfileHistory.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` 迁移,属遗留字段。 大量实体**同时存在 `childId` 与 `userId`/`familyMemberId` 两套字段**,处于过渡期: | 实体 | 现状 | |------|------| | `GrowthRecord` | 同时有 `childId` + `familyMemberId` | | `GrowthPlan` | 同时有 `childId` + `familyMemberId` | | `PointsLog` / `PointsExchangeRecord` | 同时有 `childId` + `familyMemberId` | | `DanAssessmentResult` | 同时有 `childId` + `familyMemberId` | | `AssessmentOrder` / `AssessmentAppointment` / `AssessmentRecord` 等 | 同时有 `childId` + `userId` | | `EnergyLog` / `EnergyBalance` / `Reward` / `Wish` | 只有 `childId`(待迁移) | **处理原则**: - 新增功能优先用 `memberId`(`family_members.id`)或 `userId`,不要再新增 `childId` 引用。 - 读旧数据时可能需要兼容 `childId`,但写入时尽量落新字段。 - 不要据 `children` 表"已废弃"标注而删除 `Child` 实体或相关 Service(仍有活跃使用)。 --- ## 六、关键机制:`currentMemberId` 的自动推导 `JwtInterceptor` 在解析 token 后自动做一次映射(源码约第 163-180 行): ```java // 1. 从 users 表取 familyId,为空时从 family_members 反查兜底 // 2. 用 userId + familyId 从 family_members 查当前用户对应的成员 FamilyMember member = familyMemberMapper.selectOne( new LambdaQueryWrapper() .eq(FamilyMember::getUserId, userId) .eq(FamilyMember::getFamilyId, familyId) .last("LIMIT 1")); request.setAttribute("currentMemberId", member.getId()); // = memberId request.setAttribute("familyMemberId", member.getId()); ``` 控制器里的标准模式: ```java 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.id`;`childId` 指 `children.id`。读写时先确认目标表。 6. **切换视角不改变 currentMemberId**:它是按登录账号固定反查的,切视角必须靠前端显式传 `memberId` 参数,不要试图在后端"改 currentMemberId"。 --- ## 九、新增接口自检清单 开发新接口时,先回答: - [ ] 操作对象是"账号"还是"家庭成员"?(决定用 userId 还是 memberId) - [ ] 是否涉及家长切换视角?→ 需接收可选 `memberId` 参数并回退 `currentMemberId` - [ ] 是否需要权限校验?→ 用 `userId` 判断管理员/本人 - [ ] 是否误用了 `childId`?→ 新功能一律用 `memberId`/`userId` - [ ] `currentMemberId` 为空时是否已兜底?(未加入家庭的情况)