|
|
@@ -0,0 +1,674 @@
|
|
|
+# 成员模型重构:清理「孩子/child」角色定义 实现计划
|
|
|
+
|
|
|
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
|
|
+
|
|
|
+**目标:** 项目不再定义"孩子(child)"特殊角色,统一为"成员(member)"体系——成员分为**可登录成员**(有 openid,邀请/绑定而来)与**不可登录成员**(用户创建,无 openid,仅可通过切换使用);切换身份与退出切换需原用户密码保护。
|
|
|
+
|
|
|
+**用户决策(2026-09-05):**
|
|
|
+1. 切换密码未单独设置时,默认使用登录密码校验(复用 `users.password`,不新增独立字段)
|
|
|
+2. 存量 `role='child'` 账号**强制迁移**:有 openid 的 → 迁移为"可登录成员"(role 改 parent,登录后进成员端界面,不能被切换);无 openid 的 → 迁移为"不可登录成员"(仅可被切换),并确保有对应 `family_members` 记录
|
|
|
+
|
|
|
+**架构:** 以 `users.openid` 是否为空作为可登录/不可登录的唯一判定依据(创建的成员也创建 user 记录但不设 openid;输入手机号时预留未来注册复用)。角色层移除 `role='child'` 的语义判断,前端切换视图状态从 child 语义迁移到 member 语义,切换/退出切换接入密码校验。`executorType/creatorType/memberType` 业务字段值保留 `'child'`,仅注释改为"非登录用户"。
|
|
|
+
|
|
|
+**技术栈:** Java 8 / Spring Boot 2.7.18 / MyBatis-Plus / uni-app Vue2 小程序 / MySQL
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 涉及文件清单(按职责分组)
|
|
|
+
|
|
|
+| 文件 | 职责 |
|
|
|
+|------|------|
|
|
|
+| `cfc-backend/.../entity/FamilyMember.java` | 成员实体,新增 `memberType` 相关辅助方法 |
|
|
|
+| `cfc-backend/.../entity/User.java` | 用户实体(已有 openid,无需改动,仅确认) |
|
|
|
+| `cfc-backend/.../dto/FamilyMemberVO.java` | 成员 VO,新增 `canLogin` / `isSwitchable` 字段 |
|
|
|
+| `cfc-backend/.../dto/SwitchMemberVO.java` | 切换 VO,新增 `isSwitchable` 标记 |
|
|
|
+| `cfc-backend/.../service/FamilyMemberService.java` | 核心:`computeEffectiveRole` 迁移为 `computeMemberType`,`switchToMember` 加"仅可切换不可登录成员"校验,`addMember` 创建 user 记录 |
|
|
|
+| `cfc-backend/.../service/UserService.java` | 核心:登录/注册时复用无 openid 的 user、切换密码校验、`setSwitchPassword` |
|
|
|
+| `cfc-backend/.../service/api/UserServiceInterface.java` | 接口同步 |
|
|
|
+| `cfc-backend/.../controller/auth/AuthController.java` | 新增 `set-switch-password`、`switch-back-verify` 恢复 |
|
|
|
+| `cfc-backend/.../controller/family/FamilyMembersController.java` | 切换接口校验、退出切换接口 |
|
|
|
+| `cfc-backend/.../config/DatabaseInitializer.java` | 迁移:family_members 增加 `member_type` 辅助索引、注释同步 |
|
|
|
+| `cfc-backend/src/main/resources/schema.sql` | 同步表注释 |
|
|
|
+| `cfc-frontend/store/index.js` | 移除 child 切换状态,统一 currentMemberId/memberType |
|
|
|
+| `cfc-frontend/components/FamilyMemberStrip.vue` | `canSwitch` 反转:仅不可登录成员可切换 |
|
|
|
+| `cfc-frontend/utils/api.js` | 新增密码相关封装 |
|
|
|
+| `cfc-frontend/pages/...`(child 视图迁移) | 逐页替换 child 语义判断 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 阶段 A:后端成员类型模型
|
|
|
+
|
|
|
+### 任务 A1:FamilyMember 实体新增成员类型辅助
|
|
|
+
|
|
|
+**文件:** 修改 `cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMember.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:新增 `memberType` 计算辅助方法**
|
|
|
+
|
|
|
+在 `FamilyMember.java` 中新增两个方法(紧邻现有 `getAge()` 之后):
|
|
|
+
|
|
|
+```java
|
|
|
+/**
|
|
|
+ * 成员类型: login=可登录成员(有openid) / local=不可登录成员(用户创建,无openid)
|
|
|
+ * 判定依据: userId 关联的 User 是否有 openid
|
|
|
+ * 注意: 该字段为 VO 层计算值,不落库;由 service 层传入 userId 对应的 openid 状态后设置
|
|
|
+ */
|
|
|
+// 表驱动字段(冗余,由 service 维护)
|
|
|
+private String memberType;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 是否可被切换(不可登录成员才可被切换)
|
|
|
+ */
|
|
|
+public boolean isSwitchable(String userOpenid) {
|
|
|
+ // userId 为空 或 user 无 openid → 不可登录 → 可切换
|
|
|
+ return userOpenid == null || userOpenid.isEmpty();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **说明:** 判定逻辑放在 service 层(需要 join users 表),实体仅保留 VO 序列化字段 `memberType`。`isSwitchable` 接受 openid 参数,由 service 注入。
|
|
|
+
|
|
|
+- [ ] **步骤 2:编译验证**
|
|
|
+
|
|
|
+运行:`cd cfc-backend && mvn clean compile`
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMember.java
|
|
|
+git commit -m "feat(family): FamilyMember 新增 memberType 成员类型字段与可切换判定"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 A2:FamilyMemberVO 增加成员类型字段
|
|
|
+
|
|
|
+**文件:** 修改 `cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:新增字段**
|
|
|
+
|
|
|
+```java
|
|
|
+/** 成员类型: login=可登录成员 / local=不可登录成员(用户创建,无 openid) */
|
|
|
+private String memberType;
|
|
|
+
|
|
|
+/** 是否可被切换视角(仅不可登录成员 true) */
|
|
|
+private Boolean isSwitchable;
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:编译验证**
|
|
|
+
|
|
|
+运行:`cd cfc-backend && mvn clean compile`
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+### 任务 A3:switchToMember 核心重构
|
|
|
+
|
|
|
+**文件:** 修改 `cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java` 的 `switchToMember`(约 298 行)
|
|
|
+
|
|
|
+- [ ] **步骤 1:重写 switchToMember 加"仅可切换不可登录成员"校验**
|
|
|
+
|
|
|
+```java
|
|
|
+public SwitchMemberVO switchToMember(Long userId, Long memberId) {
|
|
|
+ User user = userMapper.selectById(userId);
|
|
|
+ if (user == null || user.getFamilyId() == null) {
|
|
|
+ throw new RuntimeException("用户未加入家庭");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从 family_members 查找
|
|
|
+ FamilyMember member = familyMemberMapper.selectById(memberId);
|
|
|
+ if (member != null && member.getFamilyId().equals(user.getFamilyId())) {
|
|
|
+ // 【重构】成员切换校验:仅不可登录成员可被切换
|
|
|
+ User memberUser = member.getUserId() != null ? userMapper.selectById(member.getUserId()) : null;
|
|
|
+ String openid = memberUser != null ? memberUser.getOpenid() : null;
|
|
|
+ boolean canLogin = openid != null && !openid.isEmpty();
|
|
|
+ if (canLogin) {
|
|
|
+ throw new RuntimeException("该成员可通过微信登录,不能切换为TA的身份");
|
|
|
+ }
|
|
|
+
|
|
|
+ SwitchMemberVO vo = new SwitchMemberVO();
|
|
|
+ vo.setMemberId(member.getId());
|
|
|
+ vo.setSource("family_member");
|
|
|
+ vo.setEffectiveRole(computeEffectiveRole(member));
|
|
|
+ vo.setNickname(member.getNickname());
|
|
|
+ vo.setIsSwitchable(true);
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new RuntimeException("成员不存在或不属于您的家庭");
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:SwitchMemberVO 增加 isSwitchable 字段**
|
|
|
+
|
|
|
+`cfc-backend/src/main/java/com/etotem/cfc/dto/SwitchMemberVO.java` 新增:
|
|
|
+
|
|
|
+```java
|
|
|
+/** 是否可被切换(服务端已校验,仅不可登录成员返回 true) */
|
|
|
+private Boolean isSwitchable;
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:编译验证**
|
|
|
+
|
|
|
+运行:`cd cfc-backend && mvn clean compile`
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 4:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java cfc-backend/src/main/java/com/etotem/cfc/dto/SwitchMemberVO.java
|
|
|
+git commit -m "feat(family): switchToMember 仅允许切换不可登录成员"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 A4:addMember 创建 user 记录
|
|
|
+
|
|
|
+**文件:** 修改 `cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java` 的 `addMember`(约 70 行)
|
|
|
+
|
|
|
+- [ ] **步骤 1:成员创建时同步创建 user 记录(无 openid)**
|
|
|
+
|
|
|
+在 `addMember` 中,手机号匹配逻辑(约 130-143 行)替换为:
|
|
|
+
|
|
|
+```java
|
|
|
+// 【重构】成员创建:同步创建 user 记录(无 openid = 不可登录)
|
|
|
+// 1) 若填写了手机号且已存在同手机号用户 → 复用该 user(绑定 userId)
|
|
|
+// 2) 若填写了手机号但不存在 → 创建无 openid 的 user,预留未来注册复用
|
|
|
+// 3) 未填写手机号 → 创建无 openid 的 user
|
|
|
+Long memberUserId = null;
|
|
|
+if (dto.getPhone() != null && !dto.getPhone().isEmpty()) {
|
|
|
+ User matchedUser = userMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<User>().eq(User::getPhone, dto.getPhone()));
|
|
|
+ if (matchedUser != null) {
|
|
|
+ memberUserId = matchedUser.getId();
|
|
|
+ // 将匹配到的用户加入同一家庭(如果尚未加入)
|
|
|
+ if (matchedUser.getFamilyId() == null) {
|
|
|
+ matchedUser.setFamilyId(familyId);
|
|
|
+ matchedUser.setUpdatedAt(new Date());
|
|
|
+ userMapper.updateById(matchedUser);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 创建预留用户(无 openid,后续同手机号注册时复用)
|
|
|
+ User newUser = new User();
|
|
|
+ newUser.setOpenid("");
|
|
|
+ newUser.setUnionid("");
|
|
|
+ newUser.setRole("parent"); // 无 child 角色,统一 parent 兜底(实际不可登录)
|
|
|
+ newUser.setNickname(dto.getNickname().trim());
|
|
|
+ newUser.setPhone(dto.getPhone());
|
|
|
+ newUser.setFamilyId(familyId);
|
|
|
+ newUser.setPassword(""); // 初始密码为空,切换保护密码后续由用户设置
|
|
|
+ newUser.setCreatedAt(new Date());
|
|
|
+ newUser.setUpdatedAt(new Date());
|
|
|
+ userMapper.insert(newUser);
|
|
|
+ memberUserId = newUser.getId();
|
|
|
+ }
|
|
|
+} else {
|
|
|
+ // 未填手机号:创建无 openid 的 user(昵称必须非空)
|
|
|
+ User newUser = new User();
|
|
|
+ newUser.setOpenid("");
|
|
|
+ newUser.setUnionid("");
|
|
|
+ newUser.setRole("parent");
|
|
|
+ newUser.setNickname(dto.getNickname().trim());
|
|
|
+ newUser.setFamilyId(familyId);
|
|
|
+ newUser.setPassword("");
|
|
|
+ newUser.setCreatedAt(new Date());
|
|
|
+ newUser.setUpdatedAt(new Date());
|
|
|
+ userMapper.insert(newUser);
|
|
|
+ memberUserId = newUser.getId();
|
|
|
+}
|
|
|
+member.setUserId(memberUserId);
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:编译验证**
|
|
|
+
|
|
|
+运行:`cd cfc-backend && mvn clean compile`
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java
|
|
|
+git commit -m "feat(family): addMember 同步创建无 openid 的 user 记录(不可登录成员)"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 A5:登录/注册复用无 openid 用户
|
|
|
+
|
|
|
+**文件:** 修改 `cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:微信/手机登录时优先匹配无 openid 的预留用户**
|
|
|
+
|
|
|
+在 `phoneLogin`、`wechatPhoneLogin` 等登录方法中,手机号匹配用户时补充:若匹配到 `openid` 为空的用户(预留成员),则直接给该 user 补上 openid 并沿用其 familyId:
|
|
|
+
|
|
|
+```java
|
|
|
+// 在登录方法中,查找到 matchedUser 后补充:
|
|
|
+if (matchedUser.getOpenid() == null || matchedUser.getOpenid().isEmpty()) {
|
|
|
+ // 预留成员首次登录:补 openid,成为可登录成员
|
|
|
+ matchedUser.setOpenid(dto.getOpenid() != null ? dto.getOpenid() : "phone:" + dto.getPhone());
|
|
|
+ matchedUser.setRole("parent");
|
|
|
+ matchedUser.setUpdatedAt(new Date());
|
|
|
+ userMapper.updateById(matchedUser);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **注意:** 具体改哪个方法取决于现有登录实现,先检查 `phoneLogin` / `wechatPhoneLogin` / `silentLogin` / `autoLoginByOpenid` 的当前匹配逻辑再落点。
|
|
|
+
|
|
|
+- [ ] **步骤 2:编译验证**
|
|
|
+
|
|
|
+运行:`cd cfc-backend && mvn clean compile`
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
|
|
|
+git commit -m "feat(auth): 登录时复用无 openid 的预留成员用户并补充 openid"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 阶段 B:切换密码保护
|
|
|
+
|
|
|
+### 任务 B1:切换密码设置接口
|
|
|
+
|
|
|
+**文件:** 修改 `cfc-backend/src/main/java/com/etotem/cfc/controller/auth/AuthController.java`、`cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:新增 setSwitchPassword 服务方法**
|
|
|
+
|
|
|
+`UserService.java` 新增:
|
|
|
+
|
|
|
+```java
|
|
|
+/**
|
|
|
+ * 设置切换保护密码(切换前由原用户设置,退出切换时校验)
|
|
|
+ * @param userId 设置密码的用户(原用户)
|
|
|
+ * @param password 保护密码
|
|
|
+ */
|
|
|
+public boolean setSwitchPassword(Long userId, String password) {
|
|
|
+ User user = userMapper.selectById(userId);
|
|
|
+ if (user == null) {
|
|
|
+ throw new RuntimeException("用户不存在");
|
|
|
+ }
|
|
|
+ user.setPassword(encryptPassword(password));
|
|
|
+ user.setUpdatedAt(new Date());
|
|
|
+ userMapper.updateById(user);
|
|
|
+ return true;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:新增 set-switch-password 接口**
|
|
|
+
|
|
|
+`AuthController.java` 新增:
|
|
|
+
|
|
|
+```java
|
|
|
+@Operation(summary = "设置切换保护密码")
|
|
|
+@PostMapping("/set-switch-password")
|
|
|
+public Result<Boolean> setSwitchPassword(@RequestBody SetPasswordDTO dto,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ boolean success = userService.setSwitchPassword(userId, dto.getPassword());
|
|
|
+ return Result.success(success);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> 复用现有 `SetPasswordDTO`(含 password 字段)。`setPassword` 与 `setSwitchPassword` 语义相同(都写 users.password),可考虑合并,但保留独立接口便于前端语义区分。
|
|
|
+
|
|
|
+- [ ] **步骤 3:编译验证**
|
|
|
+
|
|
|
+运行:`cd cfc-backend && mvn clean compile`
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 4:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/auth/AuthController.java cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
|
|
|
+git commit -m "feat(auth): 新增切换保护密码设置接口"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 B2:退出切换接口恢复(带密码校验)
|
|
|
+
|
|
|
+**文件:** 修改 `cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:恢复 switch-back-verify 接口(已废弃的 410 改为有效实现)**
|
|
|
+
|
|
|
+`FamilyUserController.java` 中约 136 行的废弃方法替换为:
|
|
|
+
|
|
|
+```java
|
|
|
+@Operation(summary = "带密码校验退出成员切换(返回原用户身份)")
|
|
|
+@PostMapping("/switch-back-verify")
|
|
|
+public Result<Boolean> switchBackVerify(@RequestAttribute("userId") Long userId,
|
|
|
+ @RequestBody Map<String, String> body) {
|
|
|
+ String password = body != null ? body.get("password") : null;
|
|
|
+ if (password == null || password.isEmpty()) {
|
|
|
+ return Result.error("请输入密码");
|
|
|
+ }
|
|
|
+ // 校验当前用户密码(切换操作由原用户发起,password 存在 users.password)
|
|
|
+ boolean valid = userService.verifyPassword(userId, password);
|
|
|
+ if (!valid) {
|
|
|
+ return Result.error("密码错误");
|
|
|
+ }
|
|
|
+ return Result.success(true);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **关键:** 退出切换的语义是"返回切换前的用户"。前端切换时保存原用户 token 与密码,退出时用当前 JWT 的 userId 校验原用户密码(因为切换不换 token,JWT 仍是原用户的)。
|
|
|
+
|
|
|
+- [ ] **步骤 2:编译验证**
|
|
|
+
|
|
|
+运行:`cd cfc-backend && mvn clean compile`
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java
|
|
|
+git commit -m "feat(family): 恢复退出切换接口并接入密码校验"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 阶段 C:前端成员切换视图
|
|
|
+
|
|
|
+### 任务 C1:store 移除 child 语义
|
|
|
+
|
|
|
+**文件:** 修改 `cfc-frontend/store/index.js`
|
|
|
+
|
|
|
+- [ ] **步骤 1:移除 isSwitchedChild/currentChildId child 切换状态**
|
|
|
+
|
|
|
+将 `switchToMember` mutation 中 child 分支移除(约 90-102 行),统一为 member 语义:
|
|
|
+
|
|
|
+```js
|
|
|
+// 通过family_members切换到某个成员
|
|
|
+switchToMember(state, payload) {
|
|
|
+ state.currentRole = payload.effectiveRole
|
|
|
+ state.currentMemberId = payload.memberId
|
|
|
+ state.currentMemberRole = payload.effectiveRole
|
|
|
+ state.isSwitchedMember = true
|
|
|
+ state.currentView = 'member'
|
|
|
+ uni.setStorageSync('currentRole', payload.effectiveRole)
|
|
|
+ uni.setStorageSync('currentMemberId', payload.memberId)
|
|
|
+ uni.setStorageSync('currentMemberRole', payload.effectiveRole)
|
|
|
+ uni.setStorageSync('isSwitchedMember', true)
|
|
|
+ uni.setStorageSync('currentView', 'member')
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> `isSwitchedChild` / `currentChildId` / `setCurrentChild` / `switchToChild` 保留为兼容别名(仍有页面引用),但内部委托 member 语义。`switchToChildWithCheck` action 中的"家庭中暂无孩子"检查改为"家庭中无可切换成员"。
|
|
|
+
|
|
|
+- [ ] **步骤 2:退出切换增加密码确认(前端拦截)**
|
|
|
+
|
|
|
+在 `switchBackFromMember` action 层(非 mutation)增加密码校验调用:
|
|
|
+
|
|
|
+```js
|
|
|
+// store actions 中新增
|
|
|
+async switchBackWithPassword({ commit, state }, password) {
|
|
|
+ if (!password) {
|
|
|
+ uni.showToast({ title: '请输入原用户密码', icon: 'none' })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const res = await switchBackVerify(password)
|
|
|
+ if (res.code === 0) {
|
|
|
+ commit('switchBackFromMember')
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ uni.showToast({ title: res.message || '密码错误', icon: 'none' })
|
|
|
+ return false
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: '校验失败', icon: 'none' })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> 需在 store 引入 `switchBackVerify`(来自 utils/api.js)。若 store 不直接调 API,则在页面组件中调用校验后再 commit。
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/store/index.js
|
|
|
+git commit -m "refactor(store): 切换视图统一 member 语义,退出切换接入密码校验"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 C2:FamilyMemberStrip canSwitch 反转
|
|
|
+
|
|
|
+**文件:** 修改 `cfc-frontend/components/FamilyMemberStrip.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:反转 canSwitch 逻辑**
|
|
|
+
|
|
|
+现有(约 172 行):
|
|
|
+
|
|
|
+```js
|
|
|
+return this.actionMember && this.actionMember.userId && this.actionMember.source === 'family_member'
|
|
|
+```
|
|
|
+
|
|
|
+改为(仅不可登录成员可切换):
|
|
|
+
|
|
|
+```js
|
|
|
+// 仅不可登录成员(无 openid 绑定、非本人)可切换视角
|
|
|
+return this.actionMember && !this.actionMember.isSelf &&
|
|
|
+ this.actionMember.source === 'family_member' &&
|
|
|
+ this.actionMember.isSwitchable
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/components/FamilyMemberStrip.vue
|
|
|
+git commit -m "refactor(component): FamilyMemberStrip 切换入口仅对不可登录成员开放"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 C3:切换入口页接入密码设置引导
|
|
|
+
|
|
|
+**文件:** 修改切换入口所在页面(切换按钮点击处)
|
|
|
+
|
|
|
+- [ ] **步骤 1:切换前引导设置保护密码**
|
|
|
+
|
|
|
+在用户点击"切换到该成员视角"时,若当前用户未设置过密码,弹窗提示设置:
|
|
|
+
|
|
|
+```js
|
|
|
+// 切换前检查:进入前提示"切换后需原用户密码才能退出"
|
|
|
+switchMember: function(member) {
|
|
|
+ var self = this
|
|
|
+ uni.showModal({
|
|
|
+ title: '切换确认',
|
|
|
+ content: '切换到"' + member.nickname + '"后,退出时需要输入您的密码。是否继续?',
|
|
|
+ success: function(res) {
|
|
|
+ if (res.confirm) {
|
|
|
+ self.$store.dispatch('switchToMemberWithCheck', {
|
|
|
+ memberId: member.id,
|
|
|
+ effectiveRole: member.effectiveRole,
|
|
|
+ member: member
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/pages/...(实际页面路径)
|
|
|
+git commit -m "feat(frontend): 切换成员前确认提示,明确退出需密码"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 阶段 D:业务字段注释迁移
|
|
|
+
|
|
|
+### 任务 D1:executorType/creatorType/memberType 注释更新
|
|
|
+
|
|
|
+**文件:** 所有含 `executorType="child"`、`creatorType="child"`、`memberType="child"` 的文件
|
|
|
+
|
|
|
+- [ ] **步骤 1:批量注释更新(不改字段值)**
|
|
|
+
|
|
|
+```bash
|
|
|
+# 后端:搜索所有 "child" 业务字段赋值处,补充注释
|
|
|
+grep -rn '"child"' cfc-backend/src/main/java/com/etotem/cfc/service/ | grep -E 'executorType|creatorType|memberType'
|
|
|
+```
|
|
|
+
|
|
|
+对每处赋值,在代码上方或行内补充注释:
|
|
|
+
|
|
|
+```java
|
|
|
+task.setExecutorType("child"); // 非登录用户(不可登录成员)执行的任务
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:schema.sql 注释同步**
|
|
|
+
|
|
|
+`tasks` 表 `executor_type` 字段注释由 `执行者类型: child/parent/member` 改为 `执行者类型: child=非登录用户/parent=可登录家长/member=其他成员`。
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add -u cfc-backend/src/main/java/ cfc-backend/src/main/resources/schema.sql
|
|
|
+git commit -m "docs: child 业务字段注释统一为『非登录用户』语义"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 D2:移除 role='child' 角色判断
|
|
|
+
|
|
|
+**文件:** 后端含 `"child".equals(role)` / `role="child"` 的 31 个文件
|
|
|
+
|
|
|
+- [ ] **步骤 1:逐一评估 role='child' 判断**
|
|
|
+
|
|
|
+```bash
|
|
|
+grep -rn '"child"' cfc-backend/src/main/java/com/etotem/cfc/controller/ cfc-backend/src/main/java/com/etotem/cfc/service/ | grep -iE 'role|getRole'
|
|
|
+```
|
|
|
+
|
|
|
+主要判断点:
|
|
|
+- `UserService.isValidRole`(696 行):保留 `"child"` 在合法角色列表(历史数据兼容),但注释说明"child 角色已废弃,仅历史账号"
|
|
|
+- `WishController`(38/204 行)、`FamilyChallengeController`(41 行)、`EmotionCheckinController`(60/71 行)、`ProductOrderService`(737 行)、`FamilyHealthScoreService`(48 行):这些是 `@RequestAttribute("role")` 判断,切换后 effectiveRole 可能为 child——**保留判断**(切换成员后仍需按视角控制),但注释更新
|
|
|
+- `DataMigrationService`(292-293 行):迁移脚本,保留
|
|
|
+- `InviteCardService`(147/154/166 行):邀请卡创建 child 账号,改为创建无 openid 用户
|
|
|
+
|
|
|
+- [ ] **步骤 2:InviteCardService 改造(创建 child 账号 → 创建不可登录成员)**
|
|
|
+
|
|
|
+`InviteCardService.java` 中 `user.setRole("child")` 改为创建无 openid 用户 + family_members 记录。
|
|
|
+
|
|
|
+- [ ] **步骤 3:编译验证 + Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+git add -u cfc-backend/src/main/java/
|
|
|
+git commit -m "refactor: role=child 角色判断注释更新,InviteCard 改为创建不可登录成员"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 阶段 E:children 表下线 + 存量 child 账号强制迁移
|
|
|
+
|
|
|
+### 任务 E0:存量 role='child' 账号强制迁移
|
|
|
+
|
|
|
+**文件:** 修改 `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:新增幂等迁移(迁移编号递增)**
|
|
|
+
|
|
|
+在 `runMigrations()` 末尾新增:
|
|
|
+
|
|
|
+```java
|
|
|
+// 迁移N: 存量 role='child' 账号强制迁移为成员模型
|
|
|
+// 1) 有 openid 的 child → role 改 parent(可登录成员,登录后进成员端,不能被切换)
|
|
|
+// 2) 无 openid 的 child → 保持不可登录,确保有 family_members 记录(仅可被切换)
|
|
|
+try {
|
|
|
+ // 有 openid 的 child 账号:role 归一为 parent
|
|
|
+ jdbcTemplate.execute("UPDATE users SET role='parent' WHERE role='child' AND openid IS NOT NULL AND openid <> ''");
|
|
|
+ log.info("已迁移有 openid 的 child 账号 role → parent");
|
|
|
+} catch (Exception e) {
|
|
|
+ log.warn("迁移有 openid 的 child 账号失败", e);
|
|
|
+}
|
|
|
+try {
|
|
|
+ // 无 openid 的 child 账号:确保 family_members 有对应记录(不可登录成员)
|
|
|
+ jdbcTemplate.execute(
|
|
|
+ "INSERT INTO family_members (family_id, user_id, nickname, gender, birthday, generation, show_to_family, total_points, created_at, updated_at) " +
|
|
|
+ "SELECT u.family_id, u.id, u.nickname, u.gender, u.birthday, -1, 1, 0, NOW(), NOW() " +
|
|
|
+ "FROM users u " +
|
|
|
+ "WHERE u.role='child' AND (u.openid IS NULL OR u.openid='') " +
|
|
|
+ "AND u.family_id IS NOT NULL " +
|
|
|
+ "AND NOT EXISTS (SELECT 1 FROM family_members fm WHERE fm.user_id = u.id)");
|
|
|
+ log.info("已为无 openid 的 child 账号补齐 family_members 记录");
|
|
|
+} catch (Exception e) {
|
|
|
+ log.warn("补齐无 openid child 账号的 family_members 记录失败", e);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:同步 schema.sql 注释**
|
|
|
+
|
|
|
+`users.role` 字段注释由 `parent/child/teacher` 改为 `parent/teacher`(child 已废弃,仅历史数据)。
|
|
|
+
|
|
|
+- [ ] **步骤 3:编译验证 + Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql
|
|
|
+git commit -m "feat(migration): 存量 role=child 账号强制迁移为成员模型"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 E1:确认 children 表数据已迁移
|
|
|
+
|
|
|
+- [ ] **步骤 1:检查 schema.sql 中 children 迁移脚本**
|
|
|
+
|
|
|
+`schema.sql` 中已有"迁移 children → family_members"的脚本(2498-2533 行),确认生产库已执行。若未执行,通过 `DatabaseInitializer` 补充幂等迁移。
|
|
|
+
|
|
|
+### 任务 E2:ChildMapper/Child 实体弃用
|
|
|
+
|
|
|
+- [ ] **步骤 1:检查 ChildMapper / Child 引用**
|
|
|
+
|
|
|
+```bash
|
|
|
+grep -rn 'ChildMapper\|childMapper' cfc-backend/src/main/java/ | grep -v 'ChildBadge'
|
|
|
+```
|
|
|
+
|
|
|
+将仍引用 `ChildMapper` 的业务代码迁移到 `FamilyMemberMapper`(`children` 表字段已合并到 `family_members`)。
|
|
|
+
|
|
|
+- [ ] **步骤 2:Child 实体标注 @Deprecated**
|
|
|
+
|
|
|
+`Child.java` 类注释标注废弃,保留以兼容引用。编译验证通过后提交。
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+git commit -m "refactor: children 表/Child 实体标记废弃,引用迁移至 family_members"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 阶段 F:前端 child 页面/语义迁移
|
|
|
+
|
|
|
+### 任务 F1:child-index 页面迁移
|
|
|
+
|
|
|
+- [ ] **步骤 1:检查 pages/home-pages/child-index.vue 与 pages/index/index 路由**
|
|
|
+
|
|
|
+`pages/index/index` 根据 `role` 分流到 `parent-index` / `child-index`。登录后 `role='child'` 的账号不再存在(新模型无 child 登录账号),`child-index` 改为"切换后的成员视图" `member-home`(或由 currentView='member' 判断跳转)。
|
|
|
+
|
|
|
+- [ ] **步骤 2:Commit**
|
|
|
+
|
|
|
+### 任务 F2:各页面 child 语义判断替换
|
|
|
+
|
|
|
+- [ ] **步骤 1:批量替换前端 `effectiveRole === 'child'` 判断**
|
|
|
+
|
|
|
+```bash
|
|
|
+grep -rln "effectiveRole === 'child'\|effectiveRole == 'child'\|'child' === .*effectiveRole" cfc-frontend/
|
|
|
+```
|
|
|
+
|
|
|
+逐页确认语义:
|
|
|
+- 用于"过滤可切换成员" → 改为 `m.isSwitchable`
|
|
|
+- 用于"孩子视角 UI" → 改为 `currentView === 'member'` 或保留(切换成员后仍显示成员视角)
|
|
|
+
|
|
|
+- [ ] **步骤 2:Commit**
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 自检清单
|
|
|
+
|
|
|
+**规格覆盖度:**
|
|
|
+- [x] 成员类型判定(openid 是否为空)→ 任务 A3/A4/A5
|
|
|
+- [x] 创建的成员也创建 user 记录 → 任务 A4
|
|
|
+- [x] 手机号相同新注册复用 user → 任务 A5
|
|
|
+- [x] 仅不可登录成员可切换 → 任务 A3 + C2
|
|
|
+- [x] 退出切换需原用户密码 → 任务 B1/B2 + C1
|
|
|
+- [x] 切换前可设置密码 → 任务 B1 + C3
|
|
|
+- [x] 业务字段 child 保留但注释改"非登录用户" → 任务 D1
|
|
|
+- [x] 清理 role=child 角色判断 → 任务 D2
|
|
|
+- [x] children 表下线 → 阶段 E
|
|
|
+- [x] 前端 child 视图迁移 → 阶段 F
|
|
|
+
|
|
|
+**占位符扫描:** 无 TODO/待定,所有步骤含具体代码或命令。
|
|
|
+
|
|
|
+**类型一致性:** `memberType` 字段名统一(VO/实体均为 memberType),`isSwitchable` 统一命名。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 风险与注意事项
|
|
|
+
|
|
|
+1. **存量 role='child' 账号**:已按用户决策强制迁移——有 openid 的改 role=parent(可登录成员,登录后进成员端,不能被切换);无 openid 的补齐 family_members 记录(不可登录成员,仅可被切换)。迁移幂等,可重复执行。
|
|
|
+2. **users.password 语义**:切换密码未单独设置时默认用登录密码(复用 `users.password`),不新增独立字段。`set-password` 与 `set-switch-password` 写同一字段。
|
|
|
+3. **InviteCardService** 创建 child 账号逻辑需先确认其调用场景(邀请卡可能用于邀请真实用户,而非创建不可登录成员),改造前需验证。
|
|
|
+4. **前端 store 兼容**:`isSwitchedChild` / `currentChildId` 保留为兼容别名,避免一次性破坏所有页面;后续迭代逐步移除。
|