2026-08-04-registration-profile-backfill-design.md 17 KB

规格文档:新用户注册引导 + 个人信息按需补充与回写

日期: 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 → 信息补充页) 引导页仍展示「创建家庭/加入家庭」三步绑定区
家庭创建 后端注册时自动创建「我的家庭」并设置 familyIdwechatLogin/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

-- 迁移155: users.family_id 允许 NULL(注册不再自动创建家庭)
ALTER TABLE users MODIFY COLUMN family_id BIGINT NULL COMMENT '所属家庭ID(注册时可为空,首次使用家庭功能时创建/加入)';

同步 schema.sqlusers 表定义:family_id BIGINT NOT NULLBIGINT NULL

迁移编号当前最高 154,新增编号为 155。

6.3 登录响应新增标记

文件: dto/LoginResultDTO.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. 回写成功后返回该人最新信息(UserFamilyMember 的完整字段)

新增文件: controller/UserController.java 内新增方法(或独立 ProfileBackfillController)、dto/ProfileBackfillDTO.javaservice/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 个可补充字段中已填数量(字段口径见第 10 节待确认默认值 #3
  • 全部填满后卡片隐藏
  • 字段数据来源:登录缓存 userInfo + POST /api/user/info(现有接口,返回完整 User 字段)

7.3 个人信息使用关联表

新增文件: 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 中登记本场景;回写前先查本场景是否登记了该字段,未登记则不允许回写(只从使用处回写)。

7.4 回写工具

新增文件: 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) { ... }

逻辑:

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