日期: 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(报名表单,已实施)新用户注册引导重构,核心诉求:注册时补充个人信息,而不是创建家庭;注册可跳过,跳过后需有快捷入口随时补充;各功能用到个人信息时,在功能场景内就地补充并自动回写到个人信息。
背景痛点(源于 2026-07-16 文档):信息补全率低于 40%,注册流程强绑定家庭创建导致流失。
三个目标:
| 项目 | 现状 | 问题 |
|---|---|---|
| 注册流程 | 微信快捷登录即注册;新用户 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 静默跳过 |
无引导,用户不知道为什么功能不可用 |
| # | 决策 | 结论 |
|---|---|---|
| 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 接口 + 后端字段白名单(防越权写其他字段) |
┌─────────────────────────────────────────────────────────┐
│ ① 注册引导(轻量,一次性) │
│ 登录/注册 → 最小集表单(头像+昵称,可跳过)→ 首页 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ ② 主动补充入口(常驻) │
│ 个人中心卡片:个人信息 已填 X/14 → 点击进编辑页 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ ③ 被动补充机制(功能场景,核心) │
│ 选择用户(自己/家人/外部) → 已有字段预填 │
│ → 缺失字段就地补充 → 提交时自动回写该人的个人信息 │
│ → 若为「修改已有值」→ 弹确认「是否同步修改个人信息」 │
└─────────────────────────────────────────────────────────┘
三层互不依赖:注册时跳过的,②随时补;功能用到的,③顺手补。
文件: service/UserService.java
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 相关逻辑一并调整文件: config/DatabaseInitializer.java + resources/schema.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。
文件: dto/LoginResultDTO.java
private Boolean needsFamily; // familyId == null 时为 true,前端据此决定是否引导建家庭
在登录/注册成功装配响应处(UserService 各 login 方法末尾)设置 dto.setNeedsFamily(user.getFamilyId() == null)。
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": { /* 该人最新完整个人信息,前端刷新缓存用 */ }
}
}
后端逻辑:
phone / gender / birthday / birthHour / dietPreferences / address 6 个字段,其余字段拒绝(防越权写 teacherNo 等敏感字段)targetType=user:校验 targetId == userId(仅能回写自己),写 users 表targetType=member:校验该成员属于当前用户家庭(family_members.family_id == user.familyId),写 family_members 表User 或 FamilyMember 的完整字段)新增文件: controller/UserController.java 内新增方法(或独立 ProfileBackfillController)、dto/ProfileBackfillDTO.java、service/UserService.backfillProfile()。
保持现状(无 familyId 时报错/返回空),由前端负责引导。不改后端各 Service 的 familyId 校验逻辑。
文件: 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:暂不强制处理(注册完成直接进首页),由各功能页引导文件: pages/profile/profile.vue(或 components/ProfileHeader.vue 下方)
个人信息 已完善 X/14 字段 ›,点击 → user-edit(非 autoParent 编辑模式)userInfo + POST /api/user/info(现有接口,返回完整 User 字段)新增文件: utils/profile-field-usage.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中登记本场景;回写前先查本场景是否登记了该字段,未登记则不允许回写(只从使用处回写)。
新增文件: utils/profile-backfill.js
/**
* 就地补充 → 自动回写个人信息
* @param {Object} opts
* targetType: 'user' | 'member'
* targetId: memberId(targetType=member 时)
* fields: { phone: '138...', birthday: '...' }
* scene: 'activity-signup'(必传,校验字段是否属于本场景)
* @returns {Promise} 回写结果
*/
function backfillProfile(opts) { ... }
逻辑:
opts.fields 的每个字段,查 profile-field-usage.js 中该字段的 scenes 是否包含 opts.scene,不包含则跳过该字段uni.showModal('检测到您修改了{字段名},是否同步更新个人信息?'),确认才回写POST /api/user/profile/backfill,成功后更新本地缓存(uni.setStorageSync('userInfo') / Vuex)文件: pages/activity/activity-detail/activity-detail.vue
现有结构(保留):家庭成员列表点选预填 name/phone + 手动添加报名人(memberId=null)+ 手机号非必填。
新增:
submitRegistration)时:对每个 memberId 非空 的报名人,收集其表单中新增/修改的字段,调 backfillProfile({ targetType: 'member', targetId, fields, scene: 'activity-signup' })改动范围: 任务 / 五维能量 / 测评 / 天盘 / 成长档案 / 保险规划等现有 if (!familyId) return / toast「请先加入家庭」的页面。
统一引导组件(新增)或逐页改造:
!familyId 且用户已登录 → 显示轻提示「创建家庭后可开始使用」+ 「去创建家庭」按钮POST /api/family/user/create-family → 成功 → 刷新 familyId(localStorage + Vuex)→ 返回原功能继续操作join-family)入口mvn clean compile 与页面可用性| 阶段 | 内容 | 说明 |
|---|---|---|
| P0 | 后端彻底解耦(注册不建家庭 + 迁移 155 + needsFamily + backfill 接口)+ 注册引导最小化 + 个人中心卡片 + profile-field-usage.js/backfill.js | 地基,单独可上线 |
| P1 | 活动报名接入「就地补充 + 回写」 | 验证回写模式,含修改确认 |
| P2 | 无家庭引导(家庭类功能入口逐页接入) | 依赖 P0 |
| P3 | 其他功能页接入(测评 / 天盘 / 商城 / 饮食推荐按关联表铺开) | 每页独立小改动 |
familyId 为 null;注册引导页仅头像 + 昵称(可跳过)users.family_id 允许 NULL(迁移 155 幂等可重复执行);schema.sql 已同步POST /api/user/profile/backfill 通过字段白名单校验(6 字段);targetType=member 校验家庭归属;非法字段被拒绝needsFamily(familyId 为空时 true)mvn clean compile 通过;小程序构建通过| # | 默认值 | 说明 |
|---|---|---|
| 1 | 统一 backfill 接口 + 后端字段白名单 | 备选:前端直接复用现有 updateUser/updateFamilyMember 接口 |
| 2 | 外部报名人(memberId=null)不回写 | 仅本次报名使用 |
| 3 | 个人中心卡片口径 = 14 个字段 | 昵称/头像/真实姓名/性别/生日/出生时辰/民族/血型/最高学历/婚姻状态/地区/兴趣爱好/饮食偏好/手机号 |