# 实施计划:新用户注册引导 + 个人信息按需补充与回写 - **日期**: 2026-08-04 - **依据**: `docs/superpowers/specs/2026-08-04-registration-profile-backfill-design.md`(3 个默认值按推荐值执行:统一 backfill 接口 / 外部报名人不回写 / 14 字段口径) - **状态**: 待实施 - **验证基线**: 后端 `mvn clean compile`(在 `cfc-backend` 下);前端 `npm run build:mp-weixin`(在 `cfc-frontend` 下) ## 0. 总览 新用户注册不再自动创建家庭(`familyId=null`),注册引导只收集头像+昵称(可跳过);个人中心新增「已完善 X/14 字段」卡片;功能场景(试点:活动报名)实现「选择用户 → 预填 → 就地补充 → 自动回写」,新增统一 backfill 接口(后端字段白名单)。 - 迁移:`users.family_id` 允许 NULL(迁移 155) - 后端:5 条注册路径删除建家庭逻辑;`LoginResultDTO` 新增 `needsFamily`;新增 `POST /api/user/profile/backfill` - 前端:`user-edit.vue` 新用户流程最小化;`profile.vue` 补充卡片;`utils/profile-field-usage.js` + `utils/profile-backfill.js`;活动报名接入回写 - P2:无家庭引导组件 `components/family-guard.vue` 接入 3 个核心页 ## 1. 关键参考代码(实施前必读) | 参考 | 路径 | 用途 | |------|------|------| | 迁移模式 | `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(末尾迁移154块) | 新增迁移155模板 | | 注册路径 | `cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java` `wechatLogin`(L71-168)/`phoneLogin`(L173-247)/`directRegister`(L382-430)/`registerWithIdCard`(L742-809)/`registerWithInviteCode`(L1181+) | 删除建家庭逻辑 | | 懒建家庭 | 同上 `ensureUserHasFamily`(L922-938) + 调用点 L143/L227/L289/L325 | 删除(连同调用点) | | 登录响应装配 | 同上各 login 方法末尾 `LoginResultDTO result = new LoginResultDTO()` | 加 `needsFamily` | | birthday 解析 | 同上 `updateUserInfo`(L962) `SimpleDateFormat("yyyy-MM-dd")` | backfill 复用同款解析 | | 实体字段 | `cfc-backend/src/main/java/com/etotem/cfc/entity/User.java`(birthHour L53/gender L55/dietPreferences L87/address L79/phone L43)、`FamilyMember.java`(phone L30/gender L36/birthday L39/birthHour L42) | 白名单字段 | | 控制器 | `cfc-backend/src/main/java/com/etotem/cfc/controller/UserController.java`(`/api/user`,L66 `/info`、L77 `/update`) | 新增 `/profile/backfill` | | schema.sql | `cfc-backend/src/main/resources/schema.sql` L19 `family_id BIGINT NOT NULL` | 改为 `BIGINT NULL` | | 前端表单 | `cfc-frontend/pages/user-edit/user-edit.vue`(家庭绑定区 L212-233、`saveUserInfo` L463、`skip` L635) | 新用户流程最小化 | | 个人中心 | `cfc-frontend/pages/profile/profile.vue`(L17 ProfileHeader + `@avatar-click="goToEditInfo"`,**goToEditInfo 方法缺失需补**) | 加补充卡片 | | 活动报名 | `cfc-frontend/pages/activity/activity-detail/activity-detail.vue`(`loadFamilyMembers` L570-602、`toggleFamilyMember` L609-622、`addManualEntry` L623-630、`submitRegistration` L671、`doCreateOrder` L708) | 接入回写 | | API 封装 | `cfc-frontend/utils/api.js`(L252 getUserInfo、L264 createFamily、L269 getFamilyMembers) | 加 backfillProfile | | 无家庭页 | `cfc-frontend/pages/wealth-sub/insurance-planning.vue`、`cfc-frontend/pages/profile/components/ProfileMenu.vue`、`cfc-frontend/pages/growth/index.vue`(均有「请先加入家庭」) | P2 接入引导 | **项目约定(必须遵守)**: - 接口一律 `@PostMapping`;DI 用 `@Resource`;响应统一 `Result`;实体 `@TableName` + `@TableId(type = IdType.AUTO)` + `@Data` - 迁移编号递增:本计划新增为 **迁移155**(当前最大 154) - 迁移幂等:try-catch 包裹,注释 `// 迁移155: ...` - schema.sql 必须与 DatabaseInitializer 同步 - 前端禁止可选链 `?.`、禁止 `:key` 表达式、禁止 CSS Grid、禁止 `new Date(string)`(用 `parseDate()`) - 提交信息风格 `feat: 注册引导-xxx` - 后端验证以 `mvn clean compile` 为准(测试目录有 ~166 个预先存在失败,**不要**尝试修复,也不要把本次功能写进现有测试套件) --- ## 任务 1:数据库迁移 155 + schema.sql 同步 ### 1.1 修改 `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` 在 `runMigrations()` 中**最后一个迁移块(迁移154)之后、方法闭合 `}` 之前**追加: ```java // 迁移155: users.family_id 允许 NULL(注册不再自动创建家庭) try { jdbcTemplate.execute("ALTER TABLE users MODIFY COLUMN family_id BIGINT NULL COMMENT '所属家庭ID(注册时可为空,首次使用家庭功能时创建/加入)'"); log.info("已修改users.family_id允许NULL"); } catch (Exception e) { // 忽略 } ``` ### 1.2 同步 `cfc-backend/src/main/resources/schema.sql` 在 `users` 表定义中把(L19): ```sql family_id BIGINT NOT NULL, ``` 改为: ```sql family_id BIGINT NULL COMMENT '所属家庭ID(注册时可为空,首次使用家庭功能时创建/加入)', ``` **验证**: `mvn clean compile`(在 `cfc-backend`)退出码 0。 **提交**: `feat: 注册引导-迁移155 users.family_id可空` --- ## 任务 2:LoginResultDTO 新增 needsFamily ### 2.1 修改 `cfc-backend/src/main/java/com/etotem/cfc/dto/LoginResultDTO.java` 在 `needsRoleSelect` 字段之后(L16 后)追加: ```java private Boolean needsFamily; // familyId 为空或 0 时为 true,前端据此决定是否引导建家庭 ``` ### 2.2 修改 `cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java` 各登录方法 在每个 `LoginResultDTO result = new LoginResultDTO();` 装配块中,于 `result.setNeedsRoleSelect(...)`(无则放 `result.setNickname(...)` 之后)追加: ```java result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L); ``` **涉及装配点(8 处)**: 1. `wechatLogin`(~L153-167) 2. `phoneLogin`(~L235-246) 3. `silentLogin`(~L294-305) 4. `autoLoginByOpenid`(~L329-339) 5. `directRegister`(~L419-429) 6. `registerWithIdCard`(~L800-808) 7. `registerWithInviteCode` 分销分支(~L1212-1221) 8. `registerWithInviteCode` 普通分支(~L1287-1292) **验证**: `mvn clean compile` 退出码 0。 **提交**: `feat: 注册引导-登录响应新增needsFamily标记` --- ## 任务 3:注册路径删除建家庭逻辑(核心解耦) 修改 `cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java`。 ### 3.1 `wechatLogin`(L103-108 + L113) 删除新用户分支中的建家庭代码块: ```java Family family = new Family(); family.setName("我的家庭"); family.setInviteCode(generateInviteCode()); family.setCreatedAt(new Date()); family.setUpdatedAt(new Date()); familyMapper.insert(family); ``` 并把 L113 `user.setFamilyId(family.getId());` 删除(保持默认 null)。 ### 3.2 `phoneLogin`(L192-197 + L202) 删除同样的建家庭代码块(L192-197)和 L202 `user.setFamilyId(family.getId());`。 删除 L216-217 `family.setCreatorId(user.getId()); familyMapper.updateById(family);`(family 变量已不存在)。 ### 3.3 `directRegister`(L384-389 + L395) 删除建家庭代码块(L384-389)和 L395 `user.setFamilyId(family.getId());`。 保留 L408-414 邀请码加入家庭逻辑(`joinFamily(user.getId(), dto.getInviteCode())`)不变。 ### 3.4 `registerWithIdCard`(L775-780 + L785) **仅新用户分支**(`else` 块)删除建家庭代码块(L775-780)和 L785 `user.setFamilyId(family.getId());`。 - L753 `Family family;` 声明删除 - 老用户分支 L764 `family = familyMapper.selectById(user.getFamilyId());` 删除(变量已删) - L802 `result.setFamilyId(user.getFamilyId())` 保留(老用户有 familyId 时正常返回) ### 3.5 `registerWithInviteCode` 分销分支(L1188-1194 + L1199) **仅 distResult 命中分支**删除建家庭代码块(L1189-1194)和 L1199 `user.setFamilyId(family.getId());`,但保留 `distributionService.registerByInviteCode(inviteCode, user.getId())` 与 token 装配。 **普通邀请码分支(L1224+)不动**:该分支语义是「加入已有家庭」(`familyMapper.selectOne(inviteCode)`),新用户 `user.setFamilyId(family.getId())` 保留。 ### 3.6 删除 `ensureUserHasFamily` 方法及 4 个调用点 - 删除方法定义(L917-938,含 javadoc 注释) - 删除调用点:L143(wechatLogin)、L227(phoneLogin else 分支)、L289(silentLogin)、L325(autoLoginByOpenid) **注意**: 调用点在 if/else 块内,删除时保持块结构完整(如 phoneLogin L219-230 的 else 块删除 L227 后仍保留 `user.setUpdatedAt` + `userMapper.updateById`)。 **验证**: `mvn clean compile` 退出码 0。 **提交**: `feat: 注册引导-注册路径不再自动创建家庭` --- ## 任务 4:统一 backfill 接口 ### 4.1 新建 `cfc-backend/src/main/java/com/etotem/cfc/dto/ProfileBackfillDTO.java` ```java package com.etotem.cfc.dto; import lombok.Data; import java.util.Map; @Data public class ProfileBackfillDTO { /** 回写目标类型: user=本人 / member=家庭成员 */ private String targetType; /** targetType=member 时的 family_members.id;user 时可为空 */ private Long targetId; /** 回写字段: { phone: "138...", birthday: "2015-06-01", ... } */ private Map fields; } ``` ### 4.2 修改 `cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java` 新增常量与方法(放在 `getUserInfo` 方法 L363 之后): ```java /** 回写字段白名单(user 目标) */ private static final String[] USER_BACKFILL_WHITELIST = {"phone", "gender", "birthday", "birthHour", "dietPreferences", "address"}; /** 回写字段白名单(member 目标,family_members 表无 dietPreferences/address 列) */ private static final String[] MEMBER_BACKFILL_WHITELIST = {"phone", "gender", "birthday", "birthHour"}; /** * 就地补充信息回写(字段白名单校验 + 目标归属校验) * @return Map: { updatedFields: List<String>, profile: Object(最新完整信息) } */ public Map backfillProfile(Long userId, ProfileBackfillDTO dto) { if (dto == null || dto.getFields() == null || dto.getFields().isEmpty()) { throw new RuntimeException("回写字段不能为空"); } User me = userMapper.selectById(userId); if (me == null) { throw new RuntimeException("用户不存在"); } List updatedFields = new ArrayList<>(); Object profile; if ("user".equals(dto.getTargetType())) { // 仅允许回写自己 if (dto.getTargetId() != null && !dto.getTargetId().equals(userId)) { throw new RuntimeException("仅能回写本人的个人信息"); } for (String field : USER_BACKFILL_WHITELIST) { String value = dto.getFields().get(field); if (value == null || value.isEmpty()) continue; applyUserBackfillField(me, field, value.trim()); updatedFields.add(field); } me.setUpdatedAt(new Date()); userMapper.updateById(me); profile = me; } else if ("member".equals(dto.getTargetType())) { Long memberId = dto.getTargetId(); if (memberId == null) { throw new RuntimeException("回写家庭成员时 targetId 不能为空"); } FamilyMember member = familyMemberMapper.selectById(memberId); if (member == null) { throw new RuntimeException("家庭成员不存在"); } // 归属校验:成员必须属于当前用户家庭 Long myFamilyId = me.getFamilyId(); if (myFamilyId == null || !myFamilyId.equals(member.getFamilyId())) { throw new RuntimeException("该成员不属于您的家庭"); } for (String field : MEMBER_BACKFILL_WHITELIST) { String value = dto.getFields().get(field); if (value == null || value.isEmpty()) continue; applyMemberBackfillField(member, field, value.trim()); updatedFields.add(field); } familyMemberMapper.updateById(member); profile = member; } else { throw new RuntimeException("targetType 仅支持 user/member"); } Map result = new HashMap<>(); result.put("updatedFields", updatedFields); result.put("profile", profile); return result; } private void applyUserBackfillField(User user, String field, String value) { switch (field) { case "phone": if (!value.matches("^1[3-9]\\d{9}$")) throw new RuntimeException("手机号格式不正确"); user.setPhone(value); break; case "gender": if (!"male".equals(value) && !"female".equals(value)) throw new RuntimeException("性别取值非法"); user.setGender(value); break; case "birthday": try { java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd"); user.setBirthday(sdf.parse(value)); } catch (Exception e) { throw new RuntimeException("生日格式不正确,应为 yyyy-MM-dd"); } break; case "birthHour": if (!"子丑寅卯辰巳午未申酉戌亥".contains(value)) throw new RuntimeException("出生时辰取值非法"); user.setBirthHour(value); break; case "dietPreferences": user.setDietPreferences(value); break; case "address": user.setAddress(value); break; default: break; } } private void applyMemberBackfillField(FamilyMember member, String field, String value) { switch (field) { case "phone": if (!value.matches("^1[3-9]\\d{9}$")) throw new RuntimeException("手机号格式不正确"); member.setPhone(value); break; case "gender": if (!"male".equals(value) && !"female".equals(value)) throw new RuntimeException("性别取值非法"); member.setGender(value); break; case "birthday": try { java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd"); member.setBirthday(sdf.parse(value)); } catch (Exception e) { throw new RuntimeException("生日格式不正确,应为 yyyy-MM-dd"); } break; case "birthHour": if (!"子丑寅卯辰巳午未申酉戌亥".contains(value)) throw new RuntimeException("出生时辰取值非法"); member.setBirthHour(value); break; default: break; } } ``` > 确认 UserService 已 import `java.util.HashMap`/`java.util.Map`/`java.util.List`/`java.util.ArrayList`(文件已使用 Map/List)。 ### 4.3 修改 `cfc-backend/src/main/java/com/etotem/cfc/controller/UserController.java` 在 `@PostMapping("/update")`(L105 之后)新增: ```java @PostMapping("/profile/backfill") @Operation(summary = "就地补充信息回写(个人资料按需补充)") public Result> backfillProfile(HttpServletRequest request, @RequestBody ProfileBackfillDTO dto) { Long userId = (Long) request.getAttribute("userId"); try { Map result = userService.backfillProfile(userId, dto); return Result.success(result); } catch (RuntimeException e) { return Result.error(e.getMessage()); } } ``` 确认文件已 import `com.etotem.cfc.dto.ProfileBackfillDTO`、`java.util.Map`(文件已使用 Map)。 **验证**: `mvn clean compile` 退出码 0。 **提交**: `feat: 注册引导-新增统一backfill回写接口` --- ## 任务 5:前端 api.js 新增 backfillProfile 修改 `cfc-frontend/utils/api.js`,在 `updateUserInfo`(L256-258)之后追加: ```js // 就地补充信息回写(活动报名等场景按需补充后自动回写个人资料) export const backfillProfile = (data) => { return request('/api/user/profile/backfill', 'POST', data) } ``` **验证**: `npm run build:mp-weixin` 通过。 **提交**: `feat: 注册引导-前端api新增backfillProfile` --- ## 任务 6:新建 `cfc-frontend/utils/profile-field-usage.js` **新增文件**(完整内容): ```js /** * 个人信息使用关联表 * 记录个人信息的各个字段在哪些功能场景中被使用。 * 用途: * 1. 功能页自查「本场景用到哪些字段、当前人是否缺失」 * 2. 回写时校验字段确实属于本场景(只从使用处回写) * 3. 个人中心卡片统计已填字段数 */ module.exports = { phone: { label: '手机号', scenes: [ { scene: 'activity-signup', page: 'pages/activity/activity-detail', usage: '活动报名联系人手机号', backfillTarget: ['user', 'member'] }, { scene: 'shop-order', page: 'pages/shop/checkout', usage: '订单收货人手机号', backfillTarget: ['user'] }, { scene: 'assessment-order', page: 'pages/assessment/purchase', usage: '测评预约联系人手机号', backfillTarget: ['user'] } ] }, birthday: { label: '生日', scenes: [ { scene: 'assessment', page: 'pages/assessment/*', usage: '测评年龄计算', backfillTarget: ['user', 'member'] }, { scene: 'tianpan', page: 'pages/tianpan/*', usage: '天盘命理计算', backfillTarget: ['user', 'member'] }, { scene: 'health-report', page: 'pages/health/*', usage: '健康报告年龄相关', backfillTarget: ['member'] } ] }, gender: { label: '性别', scenes: [ { scene: 'assessment', page: 'pages/assessment/*', usage: '测评维度计算', backfillTarget: ['user', 'member'] }, { scene: 'tianpan', page: 'pages/tianpan/*', usage: '天盘计算', backfillTarget: ['user', 'member'] } ] }, birthHour: { label: '出生时辰', scenes: [ { scene: 'tianpan', page: 'pages/tianpan/*', usage: '天盘命理计算', backfillTarget: ['user', 'member'] } ] }, dietPreferences: { label: '饮食偏好', scenes: [ { scene: 'nutrition-recommend', page: 'pages/health/nutrition-profile', usage: '饮食推荐个性化', backfillTarget: ['user', 'member'] } ] }, address: { label: '地址', scenes: [ { scene: 'shop-order', page: 'pages/shop/checkout', usage: '收货地址', backfillTarget: ['user'] }, { scene: 'activity-signup', page: 'pages/activity/activity-detail', usage: '活动地点信息', backfillTarget: ['user'] } ] } } ``` **验证**: 文件可被 require(语法正确)。`npm run build:mp-weixin` 通过。 **提交**: `feat: 注册引导-个人信息使用关联表profile-field-usage` --- ## 任务 7:新建 `cfc-frontend/utils/profile-backfill.js` **新增文件**(完整内容): ```js import { backfillProfile as apiBackfill } from './api.js' const fieldUsage = require('./profile-field-usage.js') /** * 就地补充 → 自动回写个人信息 * @param {Object} opts * targetType: 'user' | 'member' * targetId: memberId(targetType=member 时) * fields: { phone: '138...', birthday: '...' } * scene: 'activity-signup'(必传,校验字段是否属于本场景) * prevValues: { phone: '旧值' } 当前人已有值(用于「修改已有值」判定) * @returns {Promise} { updatedFields: [], skippedFields: [] } */ export function backfillProfile(opts) { return new Promise(function(resolve, reject) { var scene = opts.scene var fields = opts.fields || {} var fieldNames = Object.keys(fields) if (!scene || fieldNames.length === 0) { resolve({ updatedFields: [], skippedFields: fieldNames }) return } // 1. 场景校验:只回写本场景登记过的字段 var allowedFields = {} var skippedFields = [] for (var i = 0; i < fieldNames.length; i++) { var name = fieldNames[i] var usage = fieldUsage[name] if (!usage) { skippedFields.push(name) continue } var inScene = false var targetsOk = false for (var j = 0; j < usage.scenes.length; j++) { var sc = usage.scenes[j] if (sc.scene === scene) { inScene = true if (sc.backfillTarget.indexOf(opts.targetType) >= 0) { targetsOk = true } } } if (inScene && targetsOk) { allowedFields[name] = fields[name] } else { skippedFields.push(name) } } if (Object.keys(allowedFields).length === 0) { resolve({ updatedFields: [], skippedFields: skippedFields }) return } // 2. 修改已有值 → 弹确认 var prev = opts.prevValues || {} var changedNames = [] for (var k in allowedFields) { var oldVal = prev[k] var newVal = allowedFields[k] if (oldVal && oldVal !== newVal && !(newVal === '' && oldVal === newVal)) { changedNames.push(fieldUsage[k].label) } } function doBackfill() { apiBackfill({ targetType: opts.targetType, targetId: opts.targetId || null, fields: allowedFields }).then(function(res) { if (res && res.code === 200) { // 3. 成功后更新本地缓存 var profile = (res.data && res.data.profile) || {} if (opts.targetType === 'user') { var userInfo = uni.getStorageSync('userInfo') || {} for (var f in allowedFields) { if (profile[f] !== undefined) { userInfo[f] = profile[f] } } uni.setStorageSync('userInfo', userInfo) } resolve({ updatedFields: (res.data && res.data.updatedFields) || [], skippedFields: skippedFields }) } else { reject(new Error((res && res.message) || '回写失败')) } }).catch(function(err) { reject(err) }) } if (changedNames.length > 0) { uni.showModal({ title: '同步更新资料', content: '检测到您修改了' + changedNames.join('、') + ',是否同步更新' + (opts.targetType === 'member' ? '该家人的' : '您的') + '个人信息?', confirmText: '同步更新', cancelText: '仅本次使用', success: function(modalRes) { if (modalRes.confirm) { doBackfill() } else { resolve({ updatedFields: [], skippedFields: skippedFields }) } } }) } else { doBackfill() } }) } ``` **验证**: 语法正确;`npm run build:mp-weixin` 通过。 **提交**: `feat: 注册引导-回写工具profile-backfill` --- ## 任务 8:user-edit.vue 新用户流程最小化 修改 `cfc-frontend/pages/user-edit/user-edit.vue`。 ### 8.1 隐藏新用户流程的非最小字段 在模板中,给「真实姓名」form-item(L64)所在的整段包裹一个容器并加 `v-if`。具体:在 L62 昵称 form-item 的 `` 之后插入: ```html ``` 并在「饮食偏好」form-item(L155)的 `` 之后、``(L157)之前插入闭合标签 ``。 > 注意:`form.role === 'teacher'` 的字段块(L158-188)不在包裹内(teacher 新用户流程仍需展示规划师字段,且 `isNewUserFlow && form.role === 'teacher'` 时家庭绑定区已用 `form.role !== 'teacher'` 排除)。 ### 8.2 家庭绑定区改造(L212-233) 把整个家庭绑定区(L212-233,含邀请码输入 + 创建/加入家庭按钮)替换为: ```html ``` ### 8.3 `saveUserInfo` 适配新用户流程(L463-501) 替换方法体为: ```js async saveUserInfo() { // 新用户流程:昵称可跳过(跳过时用默认昵称);老用户流程:昵称必填 if (!this.isNewUserFlow && !this.form.nickname) { uni.showToast({ title: '请输入昵称', icon: 'none' }) return } if (this.isNewUserFlow && !this.form.nickname) { var phone = this.form.phone || '' this.form.nickname = phone ? '用户' + phone.substring(phone.length - 4) : '微信用户' } // 从 AddressPicker 读取地址文本 if (this.$refs.addressPicker && this.$refs.addressPicker.fullAddress) { this.form.address = this.$refs.addressPicker.fullAddress } this.loading = true try { await updateUserInfo({ nickname: this.form.nickname, realName: this.form.realName, gender: this.form.gender, idCard: this.form.idCard, birthday: this.form.birthday, avatar: this.form.avatar, mascot: this.form.mascot, ethnicity: this.form.ethnicity, bloodType: this.form.bloodType, highestEducation: this.form.highestEducation, maritalStatus: this.form.maritalStatus, hobbies: this.form.hobbies, dietPreferences: this.form.dietPreferences, address: this.form.address }) uni.showToast({ title: '保存成功', icon: 'success' }) setTimeout(() => { if (this.isNewUserFlow) { this.navigateToHome() } else { uni.navigateBack() } }, 1500) } catch (e) { uni.showToast({ title: '保存失败', icon: 'none' }) } finally { this.loading = false } }, ``` ### 8.4 保留逻辑 - `joinFamilyAction`(L545)与 `createFamilyAction`(L593)方法体保留(非新用户流程/其他入口可能复用),但模板已无引用——保留方法不报错 - `skip()`(L635)与 `navigateToHome()`(L638)不变 **验证**: `npm run build:mp-weixin` 通过。 **提交**: `feat: 注册引导-user-edit新用户流程最小化` --- ## 任务 9:profile.vue 个人中心补充卡片 修改 `cfc-frontend/pages/profile/profile.vue`。 ### 9.1 模板:ProfileHeader 之后加卡片(L17 后) ```html 个人信息 已完善 {{ profileProgress }}/14 个字段,点击继续补充 › ``` ### 9.2 data 新增(L66 data() 内) ```js profileProgress: 0, ``` ### 9.3 onShow 中加载(L110 `this.loadHealthScoreData()` 之后) ```js this.loadProfileProgress() ``` ### 9.4 methods 新增 ```js loadProfileProgress() { var self = this getUserInfo().then(function(res) { if (res && res.code === 200 && res.data) { var user = res.data var fields = ['nickname', 'avatar', 'realName', 'gender', 'birthday', 'birthHour', 'ethnicity', 'bloodType', 'highestEducation', 'maritalStatus', 'address', 'hobbies', 'dietPreferences', 'phone'] var filled = 0 for (var i = 0; i < fields.length; i++) { var v = user[fields[i]] if (v !== null && v !== undefined && v !== '') filled++ } self.profileProgress = filled } }).catch(function() {}) }, // 修复:ProfileHeader 头像点击进入编辑页(原方法缺失) goToEditInfo() { uni.navigateTo({ url: '/pages/user-edit/user-edit' }) }, ``` ### 9.5 import 补充(L46 ProfileHeader import 附近) ```js import { getUserInfo } from '../../utils/api.js' ``` > 若文件已 import getUserInfo 则跳过。同时在 ` ``` ### 11.2 接入 `pages/wealth-sub/insurance-planning.vue` - 模板根节点内(最末尾)加: ```html ``` - script 加 import 与 components 注册: ```js import FamilyGuard from '../../components/family-guard.vue' // components: { FamilyGuard } ``` - 找到现有「请先加入家庭」处理处(`if (!familyId)` / toast),替换为调用引导组件: ```js if (!familyId) { this.$refs.familyGuard.show() return } ``` ### 11.3 接入 `pages/growth/index.vue` - 同样加 ``、import、components 注册 - 将现有 `if (!familyId)` 静默返回处替换为 `this.$refs.familyGuard.show(); return` ### 11.4 接入 `pages/profile/components/ProfileMenu.vue` - 同样加组件、import、注册 - 将「请先加入家庭」toast 处替换为引导弹窗 > 三个页面各自保留原业务逻辑(`familyId` 为空时不再静默,改为弹窗引导),创建成功后 `created` 事件回调里刷新页面数据(`onShow` 已有刷新逻辑则无需额外处理)。 **验证**: `npm run build:mp-weixin` 通过。 **提交**: `feat: 注册引导-无家庭引导组件接入核心页` --- ## 任务 12:验收与文档收尾 1. 后端:`mvn clean compile` 退出码 0 2. 前端:`npm run build:mp-weixin` 通过 3. 检查路由重复: ```bash grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | grep backfill ``` 应仅出现 `/profile/backfill` 一处(无冲突) 4. 检查 Bean 命名:`ProfileBackfillDTO` 为 DTO 无 Bean 冲突;`FamilyGuard` 前端组件名与现有组件无冲突(`grep -rn "FamilyGuard" cfc-frontend/components/`) 5. 更新 `docs/superpowers/PROJECT-OVERVIEW.md`:本计划对应规格条目状态改为「已实施」、追加计划文档索引 6. 提交:`docs: 注册引导实施计划完成` --- ## 自检记录 - **规格覆盖度**:规格第 6.1(任务3)、6.2(任务1)、6.3(任务2)、6.4(任务4)、7.1(任务8)、7.2(任务9)、7.3(任务6)、7.4(任务7)、7.5(任务10)、7.6(任务11)、9 验收标准(任务12)——全部覆盖 - **占位符扫描**:无 TODO/待定;每个任务含完整代码 - **类型一致性**:`backfillProfile` 前后端签名一致(targetType/targetId/fields);`needsFamily` 在 8 个装配点一致;memberId 前缀转换(`p_`/`c_` → 纯数字 targetId)已在任务 10.4 定义并在 10.2-10.3 建立 targetType