适用模块:
cfc-backend(Spring Boot + MyBatis-Plus) 维护日期:2026-09-05 目的:明确系统内三类身份标识的边界,避免新增接口时误用。
| 标识 | 来源表 | 主键 | 含义 | 生命周期 |
|---|---|---|---|---|
| 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
关键事实:
family_members.userId 是外键,指向 users.id,且可为 null。
userId = null 的成员 = 纯档案成员(手动添加、未注册登录的"爷爷/奶奶"等),没有账号、不能登录,只能作为家庭关系节点存在。userId 与 memberId 不是一一对应。
JwtInterceptor 用 userId + familyId 反查)。children 表标注"已废除",但实体 Child、ChildMapper 及多个 Service 仍在活跃使用(详见根 AGENTS.md 的说明)。生产库 children 表缺 phone/id_card 两列。处理 children 相关需求时:不要据此删实体,也不要重复加迁移。
userId 由 JWT 解析而来,JwtInterceptor 写入 request.setAttribute("userId", ...),永远代表"当前登录的账号"。
JwtConfig.generateToken(userId, role),subject 就是 userId)。@RequestAttribute("userId") Long userId 获取当前操作者。users 表字段)昵称、头像、手机号、totalPoints(家长积分)、会员等级、佣金(totalCommissionEarned)、推荐关系(referrerId)、服务商资质(vendorType/vendorStatus)、规划师证书(teacherNo/teacherStatus)等。
families.creator_id == userIdFamilyMemberVO.isSelfFamilyAccessInterceptor 用 userId 算 getEffectiveFamilyIds(userId)user_id 列)订单(ActivityOrder.userId、PurchaseOrder.userId、PackageOrder.userId)、购物车(Cart.userId)、优惠券(UserCoupon.userId)、收货地址(UserAddress.userId)、平台余额(UserPlatformBalance.userId)、佣金(Commission)、操作日志(UserOperationLog.userId)、通知(UserNotification.userId)等——这些是"账号"维度的数据,用 userId。
审批任务、审核心愿、录入测评结果、管理家庭、下发任务——都是"账号"做的事,用 userId。
memberId = family_members.id,代表家庭关系网中的一个具体成员。
FamilyMembersController:/add /update /kick /switch 都接收 memberId 参数。FamilyMemberService.switchToMember(userId, memberId):家长切换到某个孩子的视角。TaskController 里任务的完成、开始、领取、历史查询都以 memberId 为执行者——任务由"家庭成员"执行,不是"账号"执行。
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)等。
家庭关系图、关系质量问卷(RelationshipQuestionnaireSnapshot.familyMemberId)、成员变更日志(FamilyMemberLog.memberId)。
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 行):
// 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 参数覆盖 currentMemberId。| 问题 | 用哪个 ID |
|---|---|
| 这是哪个账号在操作? | userId |
| 这是家庭里的哪个成员? | memberId |
| 操作的是账号自身属性(积分/会员/佣金/资质/订单/优惠券/余额) | userId |
| 操作的是家庭成员/孩子(任务/健康/成长/关系/五维能量) | memberId |
| 需要权限判断(是不是管理员/本人) | userId |
| 需要切换视角(家长看孩子) | 前端传 memberId,后端回退 currentMemberId |
| 涉及旧孩子表历史数据兼容 | childId(尽量不新增引用) |
memberId ≠ userId:不要拿 userId 去查 family_members 主键,也不要拿 memberId 去查 users 主键。正确桥梁是 family_members.userId。
纯档案成员没有 userId:手动添加的"爷爷/奶奶"等 family_members.userId = null,这类成员没有账号,不能登录,相关逻辑要判空。
currentMemberId 可能为 null:若当前登录用户还没加入任何家庭(familyId = null),JwtInterceptor 不会设置 currentMemberId。控制器里以 @RequestAttribute(value="currentMemberId", required=false) 接收并判空(如 TaskController 返回 "memberId不能为空")。
children 表未真正废弃:Child 实体、ChildMapper 及多个 Service 仍活跃使用,生产 children 表缺 phone/id_card 列。勿误删、勿重复加迁移(详见根 AGENTS.md)。
字段命名不统一:memberId / familyMemberId / currentMemberId 三名字都指 family_members.id;childId 指 children.id。读写时先确认目标表。
切换视角不改变 currentMemberId:它是按登录账号固定反查的,切视角必须靠前端显式传 memberId 参数,不要试图在后端"改 currentMemberId"。
开发新接口时,先回答:
memberId 参数并回退 currentMemberIduserId 判断管理员/本人childId?→ 新功能一律用 memberId/userIdcurrentMemberId 为空时是否已兜底?(未加入家庭的情况)