|
|
@@ -0,0 +1,180 @@
|
|
|
+# 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<FamilyMember>()
|
|
|
+ .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` 为空时是否已兜底?(未加入家庭的情况)
|