|
|
@@ -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 个字段 | 昵称/头像/真实姓名/性别/生日/出生时辰/民族/血型/最高学历/婚姻状态/地区/兴趣爱好/饮食偏好/手机号 |
|