瀏覽代碼

docs: 用户身份标识 userId/memberId 使用指南(rebase 带入的远程文档)

E2E Test Bot 2 周之前
父節點
當前提交
cccfb60326
共有 2 個文件被更改,包括 181 次插入0 次删除
  1. 1 0
      cfc-backend/AGENTS.md
  2. 180 0
      docs/architecture/userid-vs-memberid-usage-guide.md

+ 1 - 0
cfc-backend/AGENTS.md

@@ -49,6 +49,7 @@ Spring Boot 2.7.18 后端服务,MyBatis-Plus ORM,JWT 认证。
   grep -rn '@GetMapping\|@PostMapping\|@PutMapping\|@DeleteMapping' src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u
   ```
 - **Bean名冲突检查:** 新增 Controller/Service 时,确保类名不与其他包中的类重名(Spring 默认 Bean Name 为类名首字母小写)
+- **身份标识:** `userId`=登录账号(`users.id`)、`memberId`/`familyMemberId`/`currentMemberId`=家庭成员(`family_members.id`)、`childId`=遗留表(`children.id`);账号自身/交易/权限判断用 userId,成员/孩子数据(任务/健康/成长/关系/五维能量)用 memberId;`JwtInterceptor` 按 `userId+familyId` 自动反查 `currentMemberId`(始终是登录账号自己),**切换视角不改变 currentMemberId**,靠前端显式传 `memberId` 参数覆盖;新功能禁止新增 `childId` 引用(详见 `docs/architecture/userid-vs-memberid-usage-guide.md`)
 
 ## ANTI-PATTERNS (本项目禁止)
 

+ 180 - 0
docs/architecture/userid-vs-memberid-usage-guide.md

@@ -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` 为空时是否已兜底?(未加入家庭的情况)