面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(
- [ ])语法来跟踪进度。
目标: 项目不再定义"孩子(child)"特殊角色,统一为"成员(member)"体系——成员分为可登录成员(有 openid,邀请/绑定而来)与不可登录成员(用户创建,无 openid,仅可通过切换使用);切换身份与退出切换需原用户密码保护。
用户决策(2026-09-05):
users.password,不新增独立字段)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 语义判断 |
文件: 修改 cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMember.java
memberType 计算辅助方法在 FamilyMember.java 中新增两个方法(紧邻现有 getAge() 之后):
/**
* 成员类型: 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 注入。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 3:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMember.java
git commit -m "feat(family): FamilyMember 新增 memberType 成员类型字段与可切换判定"
文件: 修改 cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java
[ ] 步骤 1:新增字段
/** 成员类型: login=可登录成员 / local=不可登录成员(用户创建,无 openid) */
private String memberType;
/** 是否可被切换视角(仅不可登录成员 true) */
private Boolean isSwitchable;
[ ] 步骤 2:编译验证
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
文件: 修改 cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java 的 switchToMember(约 298 行)
[ ] 步骤 1:重写 switchToMember 加"仅可切换不可登录成员"校验
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 新增:
/** 是否可被切换(服务端已校验,仅不可登录成员返回 true) */
private Boolean isSwitchable;
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 4:Commit
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 仅允许切换不可登录成员"
文件: 修改 cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java 的 addMember(约 70 行)
在 addMember 中,手机号匹配逻辑(约 130-143 行)替换为:
// 【重构】成员创建:同步创建 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);
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 3:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java
git commit -m "feat(family): addMember 同步创建无 openid 的 user 记录(不可登录成员)"
文件: 修改 cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
在 phoneLogin、wechatPhoneLogin 等登录方法中,手机号匹配用户时补充:若匹配到 openid 为空的用户(预留成员),则直接给该 user 补上 openid 并沿用其 familyId:
// 在登录方法中,查找到 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的当前匹配逻辑再落点。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 3:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
git commit -m "feat(auth): 登录时复用无 openid 的预留成员用户并补充 openid"
文件: 修改 cfc-backend/src/main/java/com/etotem/cfc/controller/auth/AuthController.java、cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
UserService.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;
}
AuthController.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),可考虑合并,但保留独立接口便于前端语义区分。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 4:Commit
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): 新增切换保护密码设置接口"
文件: 修改 cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java
FamilyUserController.java 中约 136 行的废弃方法替换为:
@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 仍是原用户的)。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 3:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java
git commit -m "feat(family): 恢复退出切换接口并接入密码校验"
文件: 修改 cfc-frontend/store/index.js
将 switchToMember mutation 中 child 分支移除(约 90-102 行),统一为 member 语义:
// 通过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 语义。switchToChildWithCheckaction 中的"家庭中暂无孩子"检查改为"家庭中无可切换成员"。
在 switchBackFromMember action 层(非 mutation)增加密码校验调用:
// 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
git add cfc-frontend/store/index.js
git commit -m "refactor(store): 切换视图统一 member 语义,退出切换接入密码校验"
文件: 修改 cfc-frontend/components/FamilyMemberStrip.vue
现有(约 172 行):
return this.actionMember && this.actionMember.userId && this.actionMember.source === 'family_member'
改为(仅不可登录成员可切换):
// 仅不可登录成员(无 openid 绑定、非本人)可切换视角
return this.actionMember && !this.actionMember.isSelf &&
this.actionMember.source === 'family_member' &&
this.actionMember.isSwitchable
[ ] 步骤 2:Commit
git add cfc-frontend/components/FamilyMemberStrip.vue
git commit -m "refactor(component): FamilyMemberStrip 切换入口仅对不可登录成员开放"
文件: 修改切换入口所在页面(切换按钮点击处)
在用户点击"切换到该成员视角"时,若当前用户未设置过密码,弹窗提示设置:
// 切换前检查:进入前提示"切换后需原用户密码才能退出"
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
git add cfc-frontend/pages/...(实际页面路径)
git commit -m "feat(frontend): 切换成员前确认提示,明确退出需密码"
文件: 所有含 executorType="child"、creatorType="child"、memberType="child" 的文件
[ ] 步骤 1:批量注释更新(不改字段值)
# 后端:搜索所有 "child" 业务字段赋值处,补充注释
grep -rn '"child"' cfc-backend/src/main/java/com/etotem/cfc/service/ | grep -E 'executorType|creatorType|memberType'
对每处赋值,在代码上方或行内补充注释:
task.setExecutorType("child"); // 非登录用户(不可登录成员)执行的任务
tasks 表 executor_type 字段注释由 执行者类型: child/parent/member 改为 执行者类型: child=非登录用户/parent=可登录家长/member=其他成员。
[ ] 步骤 3:Commit
git add -u cfc-backend/src/main/java/ cfc-backend/src/main/resources/schema.sql
git commit -m "docs: child 业务字段注释统一为『非登录用户』语义"
文件: 后端含 "child".equals(role) / role="child" 的 31 个文件
[ ] 步骤 1:逐一评估 role='child' 判断
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
cd cfc-backend && mvn clean compile
git add -u cfc-backend/src/main/java/
git commit -m "refactor: role=child 角色判断注释更新,InviteCard 改为创建不可登录成员"
文件: 修改 cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
在 runMigrations() 末尾新增:
// 迁移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);
}
users.role 字段注释由 parent/child/teacher 改为 parent/teacher(child 已废弃,仅历史数据)。
[ ] 步骤 3:编译验证 + Commit
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 账号强制迁移为成员模型"
schema.sql 中已有"迁移 children → family_members"的脚本(2498-2533 行),确认生产库已执行。若未执行,通过 DatabaseInitializer 补充幂等迁移。
[ ] 步骤 1:检查 ChildMapper / Child 引用
grep -rn 'ChildMapper\|childMapper' cfc-backend/src/main/java/ | grep -v 'ChildBadge'
将仍引用 ChildMapper 的业务代码迁移到 FamilyMemberMapper(children 表字段已合并到 family_members)。
Child.java 类注释标注废弃,保留以兼容引用。编译验证通过后提交。
cd cfc-backend && mvn clean compile
git commit -m "refactor: children 表/Child 实体标记废弃,引用迁移至 family_members"
pages/index/index 根据 role 分流到 parent-index / child-index。登录后 role='child' 的账号不再存在(新模型无 child 登录账号),child-index 改为"切换后的成员视图" member-home(或由 currentView='member' 判断跳转)。
[ ] 步骤 1:批量替换前端 effectiveRole === 'child' 判断
grep -rln "effectiveRole === 'child'\|effectiveRole == 'child'\|'child' === .*effectiveRole" cfc-frontend/
逐页确认语义:
m.isSwitchable用于"孩子视角 UI" → 改为 currentView === 'member' 或保留(切换成员后仍显示成员视角)
[ ] 步骤 2:Commit
规格覆盖度:
占位符扫描: 无 TODO/待定,所有步骤含具体代码或命令。
类型一致性: memberType 字段名统一(VO/实体均为 memberType),isSwitchable 统一命名。
users.password),不新增独立字段。set-password 与 set-switch-password 写同一字段。isSwitchedChild / currentChildId 保留为兼容别名,避免一次性破坏所有页面;后续迭代逐步移除。