Просмотр исходного кода

docs: 新用户注册引导+个人信息按需补充/回写设计稿

Xiaogang Liao 3 месяцев назад
Родитель
Сommit
1e8c39746d

+ 4 - 2
docs/superpowers/PROJECT-OVERVIEW.md

@@ -1,7 +1,7 @@
 # 浠艾福 项目全景 — 阶段性需求与设计汇总
 
-**文档版本:** v2.3
-**日期:** 2026-08-02
+**文档版本:** v2.4
+**日期:** 2026-08-04
 **状态:** 已确认(v2.1 Phase 2-4 全栈完成)+ 虚拟支付改造(已实现·联调中)
 **维护:** 所有需求变更需更新本文档
 
@@ -80,6 +80,7 @@
 | 富沛(我的)页面重构 | Phase 1 | ✅ 已完成(profile/ 6组件 1338行,已移除富模块) | `specs/2026-06-19-profile-page-redesign.md` |
 | 行远页重要关系维护(通讯录) | Phase 1 | ✅ 已完成(action/ 5页面 2116行,含OctopusGraph+问卷+互动日志) | `specs/2026-06-14-action-page-relationship-design.md` |
 | 登录前浏览体验(未登录可见内容) | Phase 1 | ✅ 已完成(多页面游客态+wuxing-sandbox preview模式) | `specs/2026-06-04-pre-login-discovery-design.md` |
+| 新用户注册引导 + 个人信息按需补充/回写 | Phase 1 | 🟡 设计稿(注册解耦最小集 + 个人中心卡片 + 功能场景就地补充自动回写) | `specs/2026-08-04-registration-profile-backfill-design.md` |
 
 ---
 
@@ -330,6 +331,7 @@
 | `2026-07-30-service-role-apply-design.md` | ✅ 已实施 | 服务角色统一申请(规划师/营养师/管家/服务商/文章管理员/活动提供商) |
 | `2026-07-31-compare-table-mobile-redesign.md` | 🟡 设计稿 | 四大饮水方案对比 — 移动端轮播式重设计 |
 | `2026-08-02-virtual-payment-design.md` | 🟢 已实施(offerId/签名按官方 signData 修正) | 小程序虚拟支付合规接入(测评/会员/订阅)|
+| `2026-08-04-registration-profile-backfill-design.md` | 🟡 设计稿 | 新用户注册引导 + 个人信息按需补充/回写 |
 
 ### 实施计划(plans/)
 

+ 318 - 0
docs/superpowers/specs/2026-08-04-registration-profile-backfill-design.md

@@ -0,0 +1,318 @@
+# 规格文档:新用户注册引导 + 个人信息按需补充与回写
+
+**日期:** 2026-08-04
+**状态:** 设计稿(3 个默认值待确认)
+**前置文档:**
+- `2026-07-16-first-login-auto-complete.md`(首次登录自动完善,已实施)
+- `2026-08-01-activity-registration-upgrade-design.md`(活动报名升级,已实施)
+- `2026-07-31-activity-registration-form-design.md`(报名表单,已实施)
+
+---
+
+## 1. 概述
+
+新用户注册引导重构,核心诉求:**注册时补充个人信息,而不是创建家庭**;注册可跳过,跳过后需有快捷入口随时补充;各功能用到个人信息时,在功能场景内就地补充并**自动回写**到个人信息。
+
+背景痛点(源于 2026-07-16 文档):信息补全率低于 40%,注册流程强绑定家庭创建导致流失。
+
+三个目标:
+
+1. **注册解耦**:注册流程不再创建家庭,只收集最小集个人信息(头像 + 昵称,均可跳过)
+2. **主动入口**:个人中心常驻「补充个人信息」卡片,显示已填/总字段数
+3. **被动回写**:功能场景「选择用户 → 预填 → 就地补充 → 自动回写」,修改已有值需用户确认
+
+## 2. 用户故事
+
+- 作为新用户,我想注册时只填头像和昵称(可跳过),以便快速进入应用,不被家庭创建流程打断
+- 作为跳过了资料补充的用户,我想在个人中心找到明确的补充入口,以便随时完善个人信息
+- 作为活动报名用户,我想在选择报名人后看到其已有信息自动预填,缺失的手机号在报名处直接补充,以便减少重复输入
+- 作为在功能中补充/修改了个人信息的用户,我想补充的值自动回写进我的资料(修改时需确认),以便资料保持最新
+
+## 3. 现状分析
+
+| 项目 | 现状 | 问题 |
+|------|------|------|
+| 注册流程 | 微信快捷登录即注册;新用户 `needsRoleSelect=true` → 跳转 `user-edit?autoParent=true`(默认 parent → 信息补充页) | 引导页仍展示「创建家庭/加入家庭」三步绑定区 |
+| 家庭创建 | 后端注册时自动创建「我的家庭」并设置 `familyId`(`wechatLogin`/`phoneLogin` 等 5 条注册路径 + `ensureUserHasFamily` 懒创建兜底) | **注册即建家庭**,与「注册不建家庭」诉求冲突 |
+| 资料更新 | `POST /api/user/update`(支持 20 个字段)、`POST /api/family/user/update` | 无字段级完善度统计、无回写语义 |
+| 新手任务 | `COMPLETE_PROFILE` 任务在调用 update 时自动完成(奖励 50 积分 + 5 能量) | 保留,与本次设计不冲突 |
+| 个人中心 | 头像点击 → `user-edit` 编辑页 | 入口不显眼,无完善度提示 |
+| 活动报名 | 家庭成员列表点选预填 name/phone;手动添加外部报名人(memberId=null);手机号非必填 | 「就地补充 + 回写」机制缺失 |
+| 无家庭降级 | 各页自行 `if (!familyId) return` 静默跳过 | 无引导,用户不知道为什么功能不可用 |
+
+## 4. 设计决策记录
+
+| # | 决策 | 结论 |
+|---|------|------|
+| 1 | 注册与家庭解耦程度 | **彻底解耦**:注册不建家庭,`familyId=null`;首次用到家庭功能时引导创建/加入 |
+| 2 | 注册引导字段范围 | **最小集**:头像 + 昵称(可跳过,跳过用默认昵称「用户+手机尾号」);其余字段按需补充 |
+| 3 | 补充机制形式 | **选择用户 → 预填 → 就地补充 → 回写**(非弹窗横幅式提醒) |
+| 4 | 回写字段白名单 | 手机号 / 生日 / 性别 / 饮食偏好 / 出生时辰 / 地址(6 个字段) |
+| 5 | 个人信息使用关联表 | **前端配置文件** `utils/profile-field-usage.js`(字段 → 使用场景映射,只从使用处回写) |
+| 6 | 修改已有值 | 弹确认「是否同步更新个人信息」,用户确认才回写 |
+| 7 | 快捷补充入口 | **个人中心卡片**(显示已填 X/14 字段,点击进编辑页),首页不加 |
+| 8 | 无家庭使用家庭类功能 | **引导创建后继续**:提示「去创建家庭」→ 弹窗输入家庭名 → 调 `create-family` → 返回原功能 |
+| 9 | 外部报名人(memberId=null) | 不回写(无归属主体,仅本次报名使用) |
+| 10 | 回写接口形式 | **统一 backfill 接口 + 后端字段白名单**(防越权写其他字段) |
+
+## 5. 总体架构(三层机制)
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ ① 注册引导(轻量,一次性)                                │
+│    登录/注册 → 最小集表单(头像+昵称,可跳过)→ 首页        │
+└─────────────────────────────────────────────────────────┘
+┌─────────────────────────────────────────────────────────┐
+│ ② 主动补充入口(常驻)                                    │
+│    个人中心卡片:个人信息 已填 X/14 → 点击进编辑页          │
+└─────────────────────────────────────────────────────────┘
+┌─────────────────────────────────────────────────────────┐
+│ ③ 被动补充机制(功能场景,核心)                           │
+│    选择用户(自己/家人/外部) → 已有字段预填                  │
+│    → 缺失字段就地补充 → 提交时自动回写该人的个人信息        │
+│    → 若为「修改已有值」→ 弹确认「是否同步修改个人信息」      │
+└─────────────────────────────────────────────────────────┘
+```
+
+三层互不依赖:注册时跳过的,②随时补;功能用到的,③顺手补。
+
+## 6. 后端改动(cfc-backend)
+
+### 6.1 注册不再创建家庭(核心)
+
+**文件:** `service/UserService.java`
+
+- 5 条注册路径(`wechatLogin`/`phoneLogin`/`directRegister`/`registerWithIdCard`/`registerWithInviteCode`):删除新用户自动创建 `Family` 的逻辑,`user.setFamilyId(null)`
+- `ensureUserHasFamily`(定义 L922,调用点 L143/L227/L289/L325):删除懒创建逻辑,改为**直接 return**(不再兜底建家庭)。注意 L227 注释「P3: admin 创建的用户可能 familyId=0L」,评估该场景:admin 建用户也不应自动建家庭,保持 `familyId` 为空,由前端引导
+- `wechatLogin` L143 前即 `familyId` 相关逻辑一并调整
+
+### 6.2 数据库迁移(迁移 155)
+
+**文件:** `config/DatabaseInitializer.java` + `resources/schema.sql`
+
+```sql
+-- 迁移155: users.family_id 允许 NULL(注册不再自动创建家庭)
+ALTER TABLE users MODIFY COLUMN family_id BIGINT NULL COMMENT '所属家庭ID(注册时可为空,首次使用家庭功能时创建/加入)';
+```
+
+同步 `schema.sql` 中 `users` 表定义:`family_id BIGINT NOT NULL` → `BIGINT NULL`。
+
+迁移编号当前最高 154,新增编号为 155。
+
+### 6.3 登录响应新增标记
+
+**文件:** `dto/LoginResultDTO.java`
+
+```java
+private Boolean needsFamily; // familyId == null 时为 true,前端据此决定是否引导建家庭
+```
+
+在登录/注册成功装配响应处(`UserService` 各 login 方法末尾)设置 `dto.setNeedsFamily(user.getFamilyId() == null)`。
+
+### 6.4 新增统一回写接口
+
+```
+POST /api/user/profile/backfill
+
+请求:
+{
+  "targetType": "user" | "member",   // 自己 or 家庭成员
+  "targetId": 123,                   // targetType=member 时的 family_members.id;user 时可为空
+  "fields": {
+    "phone": "13800138000",
+    "birthday": "2015-06-01",
+    "gender": "male",
+    "birthHour": "巳时",
+    "dietPreferences": "少油少盐",
+    "address": "北京市朝阳区..."
+  }
+}
+
+响应:
+{
+  "code": 200,
+  "message": "success",
+  "data": {
+    "updatedFields": ["phone", "birthday"],
+    "profile": { /* 该人最新完整个人信息,前端刷新缓存用 */ }
+  }
+}
+```
+
+**后端逻辑:**
+
+1. 字段白名单校验:仅允许 `phone / gender / birthday / birthHour / dietPreferences / address` 6 个字段,其余字段拒绝(防越权写 teacherNo 等敏感字段)
+2. `targetType=user`:校验 `targetId == userId`(仅能回写自己),写 `users` 表
+3. `targetType=member`:校验该成员属于当前用户家庭(`family_members.family_id == user.familyId`),写 `family_members` 表
+4. 字段值格式校验(phone 正则、birthday 日期格式、gender 枚举、birthHour 十二时辰枚举)
+5. 回写成功后返回该人最新信息(`User` 或 `FamilyMember` 的完整字段)
+
+**新增文件:** `controller/UserController.java` 内新增方法(或独立 `ProfileBackfillController`)、`dto/ProfileBackfillDTO.java`、`service/UserService.backfillProfile()`。
+
+### 6.5 家庭类接口行为
+
+保持现状(无 familyId 时报错/返回空),由前端负责引导。不改后端各 Service 的 familyId 校验逻辑。
+
+## 7. 前端改动(cfc-frontend)
+
+### 7.1 注册引导最小化
+
+**文件:** `pages/user-edit/user-edit.vue`
+
+- `autoParent=true` 新用户流程:仅展示头像 + 昵称表单,昵称可跳过(跳过则默认「用户+手机尾号」)
+- 移除新用户流程的「创建家庭/加入家庭/跳过」家庭绑定区(家庭创建彻底移出注册)
+- 保存调 `POST /api/user/update`(现有逻辑,触发 `COMPLETE_PROFILE` 任务完成)→ 进首页
+- 保持 `isEdit`(非新用户从个人中心进入)时的完整表单不变
+
+**文件:** `pages/login/login.vue`
+
+- `needsRoleSelect` 跳转保持(user-edit?autoParent=true)
+- 读取新增的 `needsFamily`:暂不强制处理(注册完成直接进首页),由各功能页引导
+
+### 7.2 个人中心补充卡片
+
+**文件:** `pages/profile/profile.vue`(或 `components/ProfileHeader.vue` 下方)
+
+- 新增卡片:`个人信息 已完善 X/14 字段 ›`,点击 → `user-edit`(非 autoParent 编辑模式)
+- X = 14 个可补充字段中已填数量(字段口径见 7.4)
+- 全部填满后卡片隐藏
+- 字段数据来源:登录缓存 `userInfo` + `GET /api/user/info`(现有接口,若无则新增 `POST /api/user/info` 返回完整 User 字段)
+
+### 7.3 个人信息使用关联表
+
+**新增文件:** `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'] }
+    ]
+  }
+}
+```
+
+> **约定:** 功能页接入时在 `scenes` 中登记本场景;回写前先查本场景是否登记了该字段,未登记则不允许回写(只从使用处回写)。
+
+### 7.4 回写工具
+
+**新增文件:** `utils/profile-backfill.js`
+
+```js
+/**
+ * 就地补充 → 自动回写个人信息
+ * @param {Object} opts
+ *   targetType: 'user' | 'member'
+ *   targetId:   memberId(targetType=member 时)
+ *   fields:     { phone: '138...', birthday: '...' }
+ *   scene:      'activity-signup'(必传,校验字段是否属于本场景)
+ * @returns {Promise} 回写结果
+ */
+function backfillProfile(opts) { ... }
+```
+
+逻辑:
+
+1. **场景校验**:对 `opts.fields` 的每个字段,查 `profile-field-usage.js` 中该字段的 `scenes` 是否包含 `opts.scene`,不包含则跳过该字段
+2. **修改确认**:对每个字段,比对当前人已有值(从缓存/接口获取):
+   - 原值为空(首次补充)→ 不打扰,直接回写
+   - 原值非空且与表单值不同(修改)→ `uni.showModal('检测到您修改了{字段名},是否同步更新个人信息?')`,确认才回写
+3. 调 `POST /api/user/profile/backfill`,成功后更新本地缓存(`uni.setStorageSync('userInfo')` / Vuex)
+4. 返回成功/跳过字段列表
+
+### 7.5 功能页接入(试点:活动报名)
+
+**文件:** `pages/activity/activity-detail/activity-detail.vue`
+
+现有结构(保留):家庭成员列表点选预填 name/phone + 手动添加报名人(memberId=null)+ 手机号非必填。
+
+新增:
+
+- 家庭成员点选后,若该成员(memberId 非空)的 phone 为空 → 输入框旁小字提示「将同步到家人资料」
+- 提交报名(`submitRegistration`)时:对每个 `memberId 非空` 的报名人,收集其表单中新增/修改的字段,调 `backfillProfile({ targetType: 'member', targetId, fields, scene: 'activity-signup' })`
+- 手动添加的外部报名人(memberId=null):**不回写**,仅本次报名使用
+- 无家庭用户选「家人」→ 无家庭成员列表 → 走 7.6 引导建家庭
+
+### 7.6 无家庭引导(家庭类功能入口)
+
+**改动范围:** 任务 / 五维能量 / 测评 / 天盘 / 成长档案 / 保险规划等现有 `if (!familyId) return` / toast「请先加入家庭」的页面。
+
+统一引导组件(新增)或逐页改造:
+
+- 检测 `!familyId` 且用户已登录 → 显示轻提示「创建家庭后可开始使用」+ 「去创建家庭」按钮
+- 点击 → 弹窗输入家庭名(默认「我的家庭」)→ 调 `POST /api/family/user/create-family` → 成功 → 刷新 `familyId`(localStorage + Vuex)→ 返回原功能继续操作
+- 同时提供「加入家庭」(输入邀请码,调现有 `join-family`)入口
+- 逐页改造时保持 `mvn clean compile` 与页面可用性
+
+## 8. 试点范围与顺序
+
+| 阶段 | 内容 | 说明 |
+|------|------|------|
+| **P0** | 后端彻底解耦(注册不建家庭 + 迁移 155 + needsFamily + backfill 接口)+ 注册引导最小化 + 个人中心卡片 + profile-field-usage.js/backfill.js | 地基,单独可上线 |
+| **P1** | 活动报名接入「就地补充 + 回写」 | 验证回写模式,含修改确认 |
+| **P2** | 无家庭引导(家庭类功能入口逐页接入) | 依赖 P0 |
+| **P3** | 其他功能页接入(测评 / 天盘 / 商城 / 饮食推荐按关联表铺开) | 每页独立小改动 |
+
+## 9. 验收标准
+
+- [ ] 新用户注册:不再自动创建家庭,`familyId` 为 null;注册引导页仅头像 + 昵称(可跳过)
+- [ ] `users.family_id` 允许 NULL(迁移 155 幂等可重复执行);schema.sql 已同步
+- [ ] `POST /api/user/profile/backfill` 通过字段白名单校验(6 字段);`targetType=member` 校验家庭归属;非法字段被拒绝
+- [ ] 登录响应返回 `needsFamily`(familyId 为空时 true)
+- [ ] 个人中心显示「个人信息 已完善 X/14 字段」卡片,点击进入编辑页,填满后隐藏
+- [ ] 活动报名:点选家庭成员预填手机号;缺失手机号就地补充并回写;修改已有手机号弹确认;外部报名人不回写
+- [ ] 无家庭用户进入家庭类功能:提示 + 「去创建家庭」→ 创建成功 → 返回原功能继续
+- [ ] `mvn clean compile` 通过;小程序构建通过
+
+## 10. 待确认的默认值(实现前需用户确认)
+
+| # | 默认值 | 说明 |
+|---|--------|------|
+| 1 | 统一 backfill 接口 + 后端字段白名单 | 备选:前端直接复用现有 updateUser/updateFamilyMember 接口 |
+| 2 | 外部报名人(memberId=null)不回写 | 仅本次报名使用 |
+| 3 | 个人中心卡片口径 = 14 个字段 | 昵称/头像/真实姓名/性别/生日/出生时辰/民族/血型/最高学历/婚姻状态/地区/兴趣爱好/饮食偏好/手机号 |