2026-09-28-registration-zero-fill.md 27 KB

注册零填写 + 信息按需补充 实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 取消小程序注册时的信息填写步骤(注册成功直接进首页),新增接受家庭邀请独立页(真实姓名+家庭身份必填),更新功能→信息字段映射表,添加成员手机号冲突时提示邀请加入。

Architecture: 纯前端为主的最小改动:登录页去掉新用户跳转;user-edit.vue 停用新用户流程只留编辑用途;新增 pages/invite/join-form.vue 承接家庭邀请;join.vue 改跳新页;映射表 profile-field-usage.js 补全;后端 backfill 白名单加 realName;add-member.vue 手机号冲突改邀请提示。

Tech Stack: uni-app Vue 2 (小程序) + Spring Boot 2.7.18 (仅 1 处后端白名单改动)

Spec: docs/superpowers/specs/2026-09-28-registration-zero-fill-design.md

Global Constraints

  • 后端验证唯一方式:cd cfc-backend && mvn clean compile(测试目录有 ~166 个预先存在失败,不要尝试修复)
  • 前端禁止打包(npm run build:mp-weixin 等一律不执行),只做语法/结构校验(node --check 提取 script 块)
  • 接口一律 @PostMapping;DI 用 @Resource;响应统一 Result<T>
  • 小程序限制:禁止可选链 ?.(用 &&)、禁止 CSS Grid(用 flexbox)、禁止 :key 表达式、禁止 new Date(string)(用 parseDate())
  • Vue 2 Options API,禁止 Composition API
  • 迁移幂等;docs/superpowers/ 文件增改必须同步 PROJECT-OVERVIEW.md
  • 提交风格:feat: 注册零填写-xxx
  • 不破坏:服务用户注册流程(pages/guide/register)、个人中心编辑、现有活动报名回写

文件结构

文件 操作 责任
cfc-frontend/pages/login/login.vue 修改 注册零填写:登录成功直接进首页 / 有家庭邀请则跳 join-form
cfc-frontend/pages/user-edit/user-edit.vue 修改 停用新用户流程,仅保留个人中心编辑
cfc-frontend/pages/invite/join-form.vue 新建 接受家庭邀请:真实姓名+家庭身份必填
cfc-frontend/pages.json 修改 注册 join-form 页面
cfc-frontend/pages/invite/join.vue 修改 已登录跳 join-form 而非 user-edit
cfc-frontend/utils/profile-field-usage.js 修改 功能→信息字段映射表补全
cfc-backend/.../service/UserService.java 修改 backfill 白名单加 realName
cfc-frontend/pages/family/add-member.vue 修改 手机号冲突 → 邀请加入提示
docs/superpowers/PROJECT-OVERVIEW.md 修改 状态同步

Task 1: login.vue 注册零填写改造

Files:

  • Modify: cfc-frontend/pages/login/login.vue:174-181

Interfaces:

  • Consumes: uni.getStorageSync('pendingFamilyInviteCode')(join.vue 写入)
  • Produces: 登录成功后的跳转决策(直接进首页 或 跳 join-form)

  • [ ] Step 1: 修改 completeLogin() 跳转逻辑

把 L174-181 的以下代码:

      uni.showToast({ title: '登录成功', icon: 'success' })
      self.loading = false
      // 新用户,或存在待加入的家庭邀请(join.vue 已写入 pendingFamilyInviteCode):
      // 都进入个人信息完善页,填写家庭身份并提交加入申请
      var hasPendingFamilyInvite = !!uni.getStorageSync('pendingFamilyInviteCode')
      if (data.needsRoleSelect || hasPendingFamilyInvite) {
        uni.redirectTo({ url: '/pages/user-edit/user-edit?token=' + data.token + '&userId=' + data.userId + '&autoParent=true' })
      } else {
        self.handlePostLoginRouting()
      }

替换为:

      uni.showToast({ title: '登录成功', icon: 'success' })
      self.loading = false
      // 注册零填写:新用户(needsRoleSelect)不再跳信息填写页,直接进首页
      // 家庭邀请衔接:有 pendingFamilyInviteCode(join.vue 写入)→ 直接进 join-form 填真实姓名+家庭身份
      var pendingInvite = uni.getStorageSync('pendingFamilyInviteCode')
      if (pendingInvite) {
        uni.redirectTo({ url: '/pages/invite/join-form?familyInviteCode=' + encodeURIComponent(pendingInvite) })
      } else {
        self.handlePostLoginRouting()
      }
  • Step 2: 校验改动

node --check 无法直接检查 .vue,改为人工核对:确认 L174-181 区域替换正确、hasPendingFamilyInvite 变量不再被引用、无遗留语法错误(uni.showToast/self.loading 保持原位置)。

  • [ ] Step 3: 提交

    git add cfc-frontend/pages/login/login.vue
    git commit -m "feat: 注册零填写-登录成功直接进首页,家庭邀请改跳join-form"
    

Task 2: user-edit.vue 停用新用户流程

Files:

  • Modify: cfc-frontend/pages/user-edit/user-edit.vue:418-463(onLoad)、:754-768(saveUserInfo 家庭邀请块)、:48-57(家庭身份模板区)、:400-403(hasPendingFamilyInvite computed)

Interfaces:

  • Consumes: 无(不再接收 autoParent/needsRoleSelect 参数)
  • Produces: 纯编辑模式页面(个人中心入口)

  • [ ] Step 1: 替换 onLoad 为新用户流程废弃版

把 L418-463 整个 onLoad(options) {...} 替换为:

	onLoad(options) {
		// 注册零填写改造:本页仅保留「个人中心编辑资料」用途
		// 新用户注册不再跳转本页(autoParent / needsRoleSelect 参数已废弃,不再处理)
		this.showRoleSelect = false
		this.isEdit = true
		this.isNewUserFlow = false
		// 加载已有用户信息
		this.loadUserInfo()
		// 加载会员等级(用于助手解锁)
		var self = this
		getMyMembership().then(function(res) {
			if (res.data && res.data.memberLevel) {
				self.memberLevel = res.data.memberLevel
			}
		}).catch(function() {})
	},

注意:options 参数不再使用,保留形参避免调用方报错。原 L419-449 的 autoParent/needsRoleSelect/phone 测试分支全部删除。

  • Step 2: 删除模板中家庭身份区块(L48-57)

删除以下整段:

			<!-- 家庭身份(接受家庭邀请时置顶显示,方便第一时间选择家庭身份) -->
			<view class="family-section" v-if="isEdit && form.role === 'parent' && isNewUserFlow && hasPendingFamilyInvite">
				<view class="section-title">👨‍👩‍👧‍👦 家庭身份(可选)</view>
				<view class="form-item">
					<text class="label">您在家庭中的身份</text>
					<picker mode="selector" :range="familyRoleOptions" :value="familyRoleIndex" @change="onFamilyRoleChange">
						<view class="picker-input">{{ form.familyRole || '请选择(可跳过)' }}</view>
					</picker>
				</view>
				<text class="hint-text">不选也可先保存,后续在家庭页面发起申请</text>
			</view>
  • Step 3: 删除 hasPendingFamilyInvite computed

删除 L400-403:

    // 是否有待处理的家庭邀请码
    hasPendingFamilyInvite: function() {
      return !!uni.getStorageSync('pendingFamilyInviteCode')
    },
  • Step 4: 简化 saveUserInfo() 家庭邀请分支

L753-778 当前结构(含 if (this.isNewUserFlow && this.hasPendingFamilyInvite) {...} else {...})。由于 isNewUserFlow 恒为 false,将整个 if/else 替换为 else 分支内容:

			uni.showToast({ title: '保存成功', icon: 'success' })
			setTimeout(() => {
				if (this.isNewUserFlow) {
					this.navigateToHome()
				} else {
					uni.navigateBack()
				}
			}, 1500)

保留 if (this.isNewUserFlow) 判断(恒 false 走 else 返回上一页),不硬删判断以最小化 diff。删除原 var inviteCode = uni.getStorageSync('pendingFamilyInviteCode')、var familyRole = ...、requestJoinByCode try/catch 块、uni.removeStorageSync('pendingFamilyInviteCode')。

  • Step 5: 校验

人工核对:模板中不再引用 hasPendingFamilyInvite;saveUserInfo 无 requestJoinByCode / pendingFamilyInviteCode 引用;isNewUserFlow 仍存在 data 定义(恒 false,其他 v-if 引用无害)。检查 onFamilyRoleChange / familyRoleOptions / familyRoleIndex 若模板无引用则保留方法体不删(无害死代码)。

  • [ ] Step 6: 提交

    git add cfc-frontend/pages/user-edit/user-edit.vue
    git commit -m "feat: 注册零填写-user-edit停用新用户流程仅保留编辑用途"
    

Task 3: 新增 pages/invite/join-form.vue(接受家庭邀请页)

Files:

  • Create: cfc-frontend/pages/invite/join-form.vue
  • Modify: cfc-frontend/pages.json(pages/invite 分包注册)

Interfaces:

  • Consumes: uni.getStorageSync('token')、路由参数 familyInviteCode、checkUserFamily(可不用)、updateUserInfo、requestJoinByCode(均来自 @/utils/api.js)
  • Produces: 接受家庭邀请提交(真实姓名+家庭身份必填);uni.removeStorageSync('pendingFamilyInviteCode') 消费完成

  • [ ] Step 1: 在 pages.json 注册页面

pages.json L1448-1458 的 pages/invite 分包 pages 数组中,join 条目后追加:

        {
          "path": "join-form",
          "style": {
            "navigationBarTitleText": "加入家庭"
          }
        }
  • Step 2: 新建页面文件

创建 cfc-frontend/pages/invite/join-form.vue,完整内容:

<template>
	<view class="join-form-container">
		<view class="header">
			<text class="title">加入家庭</text>
			<text class="subtitle">请填写您的真实姓名和家庭身份,提交后等待管理员审批</text>
		</view>

		<view class="form-section">
			<!-- 真实姓名(必填) -->
			<view class="form-item">
				<text class="label">真实姓名 <text class="required">*</text></text>
				<input type="text" v-model="form.realName" placeholder="请输入真实姓名" class="input" maxlength="20" />
			</view>

			<!-- 家庭身份(必填) -->
			<view class="form-item">
				<text class="label">家庭身份 <text class="required">*</text></text>
				<picker mode="selector" :range="familyRoleOptions" :value="familyRoleIndex" @change="onFamilyRoleChange">
					<view class="picker-input">{{ form.familyRole || '请选择(如:爸爸、妈妈)' }}</view>
				</picker>
			</view>

			<!-- 昵称(选填,默认用户+手机尾号) -->
			<view class="form-item">
				<text class="label">昵称</text>
				<input type="text" v-model="form.nickname" placeholder="选填,默认:用户+手机尾号" class="input" maxlength="20" />
			</view>

			<!-- 头像(选填) -->
			<view class="form-item">
				<text class="label">头像</text>
				<view class="avatar-upload" @click="chooseAvatar">
					<image v-if="form.avatar" :src="form.avatar" class="avatar-preview"></image>
					<view v-else class="avatar-placeholder">
						<text class="plus-icon">+</text>
					</view>
				</view>
			</view>
		</view>

		<view class="actions">
			<button class="btn-primary" @click="submit" :loading="submitting" :disabled="submitting">
				{{ submitting ? '提交中...' : '提交加入申请' }}
			</button>
		</view>
	</view>
</template>

<script>
import { updateUserInfo, requestJoinByCode } from '@/utils/api.js'

export default {
	data() {
		return {
			familyInviteCode: '',
			familyRoleOptions: ['爸爸', '妈妈', '爷爷', '奶奶', '姥爷', '姥姥', '其他'],
			form: {
				realName: '',
				familyRole: '',
				nickname: '',
				avatar: ''
			},
			submitting: false
		}
	},
	computed: {
		familyRoleIndex: {
			get() {
				return this.familyRoleOptions.indexOf(this.form.familyRole)
			},
			set(val) {
				this.form.familyRole = this.familyRoleOptions[val] || ''
			}
		}
	},
	onLoad(options) {
		// 接收家庭邀请码:join.vue 传 familyInviteCode;登录页衔接传 pendingFamilyInviteCode 对应值
		var code = (options && options.familyInviteCode) || uni.getStorageSync('pendingFamilyInviteCode') || ''
		this.familyInviteCode = code
		if (code) {
			uni.setStorageSync('pendingFamilyInviteCode', code)
		}
		// 已有资料预填(如有昵称/头像/真实姓名则带出)
		this.prefill()
	},
	methods: {
		prefill() {
			var userInfo = uni.getStorageSync('userInfo') || {}
			if (userInfo.realName) this.form.realName = userInfo.realName
			if (userInfo.nickname) this.form.nickname = userInfo.nickname
			if (userInfo.avatar) this.form.avatar = userInfo.avatar
		},
		chooseAvatar() {
			var self = this
			uni.chooseImage({
				count: 1,
				success: function(res) {
					self.form.avatar = res.tempFilePaths[0]
				}
			})
		},
		onFamilyRoleChange(e) {
			this.form.familyRole = this.familyRoleOptions[e.detail.value] || ''
		},
		async submit() {
			if (this.submitting) return
			if (!this.form.realName || !this.form.realName.trim()) {
				uni.showToast({ title: '请输入真实姓名', icon: 'none' })
				return
			}
			if (!this.form.familyRole) {
				uni.showToast({ title: '请选择家庭身份', icon: 'none' })
				return
			}
			if (!this.familyInviteCode) {
				uni.showToast({ title: '缺少家庭邀请码', icon: 'none' })
				return
			}
			this.submitting = true
			try {
				// 先保存基本信息(真实姓名/昵称/头像),失败不阻塞加入申请
				try {
					await updateUserInfo({
						realName: this.form.realName.trim(),
						nickname: this.form.nickname ? this.form.nickname.trim() : '',
						avatar: this.form.avatar
					})
				} catch (e) {
					console.log('[join-form] 保存基本信息失败,继续提交加入申请', e)
				}
				// 提交加入家庭申请
				await requestJoinByCode(this.familyInviteCode, this.form.familyRole)
				uni.removeStorageSync('pendingFamilyInviteCode')
				uni.showToast({ title: '加入家庭申请已提交,等待管理员审批', icon: 'success' })
				setTimeout(function() {
					uni.reLaunch({ url: '/pages/index-home/index' })
				}, 1500)
			} catch (e) {
				uni.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
			} finally {
				this.submitting = false
			}
		}
	}
}
</script>

<style scoped>
.join-form-container {
	min-height: 100vh;
	background: #f5f5f5;
	padding: 30rpx;
}
.header {
	margin-bottom: 40rpx;
	text-align: center;
}
.title {
	font-size: 40rpx;
	font-weight: bold;
	color: #333;
	display: block;
	margin-bottom: 10rpx;
}
.subtitle {
	font-size: 26rpx;
	color: #999;
	line-height: 1.6;
	display: block;
}
.form-section {
	background: #fff;
	border-radius: 16rpx;
	padding: 30rpx;
	margin-bottom: 30rpx;
}
.form-item {
	margin-bottom: 30rpx;
}
.form-item:last-child {
	margin-bottom: 0;
}
.label {
	display: block;
	font-size: 28rpx;
	color: #333;
	margin-bottom: 16rpx;
}
.required {
	color: #f44336;
}
.input, .picker-input {
	padding: 20rpx;
	background: #f5f5f5;
	border-radius: 12rpx;
	font-size: 28rpx;
}
.avatar-upload {
	width: 150rpx;
	height: 150rpx;
}
.avatar-preview {
	width: 150rpx;
	height: 150rpx;
	border-radius: 50%;
}
.avatar-placeholder {
	width: 150rpx;
	height: 150rpx;
	background: #f5f5f5;
	border-radius: 50%;
	display: flex;
	align-items: center;
	justify-content: center;
}
.plus-icon {
	font-size: 60rpx;
	color: #ccc;
}
.actions {
	padding-bottom: 40rpx;
}
.btn-primary {
	background: #4CAF50;
	color: #fff;
	border-radius: 40rpx;
	font-size: 30rpx;
}
</style>
  • Step 3: 校验

node --check:从文件中提取 <script> 块存为临时 .js 后运行 node --check(或人工核对模板无 ?.、无 :key 表达式、无 CSS Grid)。

  • [ ] Step 4: 提交

    git add cfc-frontend/pages.json cfc-frontend/pages/invite/join-form.vue
    git commit -m "feat: 注册零填写-新增接受家庭邀请页join-form(真实姓名+家庭身份必填)"
    

Task 4: join.vue 已登录跳转改 join-form

Files:

  • Modify: cfc-frontend/pages/invite/join.vue:280-296

Interfaces:

  • Consumes: this.familyInviteCode
  • Produces: 已登录不在目标家庭 → 跳 /pages/invite/join-form

  • [ ] Step 1: 修改 goUserEdit 为 goJoinForm

把 L280-296 的注释与 goUserEdit() 方法替换:

      // 已登录但不在目标家庭:跳加入家庭表单页。
      // join-form 填真实姓名 + 家庭身份后提交加入申请(requestJoinByCode)
      this.goJoinForm()
    },

    // 已登录用户跳转加入家庭表单页(接受家庭邀请流程)
    goJoinForm() {
      var inviteCode = this.familyInviteCode || uni.getStorageSync('pendingFamilyInviteCode') || ''
      uni.navigateTo({
        url: '/pages/invite/join-form?familyInviteCode=' + encodeURIComponent(inviteCode)
      })
    },

删除原 goUserEdit() 方法(含 token/userId 拼接与 familyInviteCode 参数),模板无直接引用该方法名,仅 goNext() 调用。

  • Step 2: 确认模板无残留引用

grep 确认 join.vue 中不再引用 goUserEdit:grep -n "goUserEdit" cfc-frontend/pages/invite/join.vue 应无输出。

  • [ ] Step 3: 提交

    git add cfc-frontend/pages/invite/join.vue
    git commit -m "feat: 注册零填写-接受邀请已登录跳join-form替代user-edit"
    

Task 5: profile-field-usage.js 映射表补全

Files:

  • Modify: cfc-frontend/utils/profile-field-usage.js(全文件替换)

Interfaces:

  • Consumes: 无
  • Produces: 完整字段→场景映射(供 profile-backfill.js 场景校验 + 后续各功能接入)

  • [ ] Step 1: 全文件替换为补全版

把整个 profile-field-usage.js 内容替换为(新增 realName 及补齐场景):

/**
 * 个人信息使用关联表
 * 记录个人信息的各个字段在哪些功能场景中被使用。
 * 用途:
 *  1. 功能页自查「本场景用到哪些字段、当前人是否缺失」
 *  2. 回写时校验字段确实属于本场景(只从使用处回写)
 *  3. 个人中心卡片统计已填字段数
 * 对齐:docs/superpowers/specs/2026-09-28-registration-zero-fill-design.md 5.3
 */
module.exports = {
  realName: {
    label: '真实姓名',
    scenes: [
      { scene: 'activity-signup', page: 'pages/activity/activity-detail', usage: '活动报名报名人姓名', backfillTarget: ['user', 'member'] },
      { scene: 'family-join', page: 'pages/invite/join-form', usage: '接受家庭邀请申请人姓名', backfillTarget: ['user'] },
      { scene: 'contact-create', page: 'components/ContactImport', usage: '创建联系人(独立实体,暂不回写)', backfillTarget: [] }
    ]
  },
  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'] },
      { scene: 'member-add', page: 'pages/family/add-member', usage: '添加成员手机号冲突→邀请加入', backfillTarget: ['member'] }
    ]
  },
  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'] },
      { scene: 'nutrition-recommend', page: 'pages/health/nutrition-profile', usage: '饮食推荐年龄相关', backfillTarget: ['member'] }
    ]
  },
  gender: {
    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'] }
    ]
  },
  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'] }
    ]
  }
}

注意:contact-create 的 backfillTarget: [] 表示「只登记场景、不回写」(联系人走独立实体),profile-backfill.js 的 target 校验逻辑对空数组自然跳过(indexOf 不命中即 skip),无需改动 backfill.js。

  • Step 2: 校验

node --check cfc-frontend/utils/profile-field-usage.js 退出码 0。

  • [ ] Step 3: 提交

    git add cfc-frontend/utils/profile-field-usage.js
    git commit -m "feat: 注册零填写-profile-field-usage映射表补全(realName+各字段场景)"
    

Task 6: 后端 backfill 白名单增加 realName

Files:

  • Modify: cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java(USER_BACKFILL_WHITELIST 定义 + applyUserBackfillField 方法)

Interfaces:

  • Consumes: 现有 backfillProfile(Long userId, ProfileBackfillDTO dto)
  • Produces: user 目标可回写 realName(member 目标无 realName 列,白名单不变)

  • [ ] Step 1: 白名单加 realName

找到:

    private static final String[] USER_BACKFILL_WHITELIST = {"phone", "gender", "birthday", "birthHour", "dietPreferences", "address"};

替换为:

    private static final String[] USER_BACKFILL_WHITELIST = {"phone", "gender", "birthday", "birthHour", "dietPreferences", "address", "realName"};
  • Step 2: applyUserBackfillField 加 realName 分支

在 applyUserBackfillField(User user, String field, String value) 的 switch 中,case "address": 之后追加:

            case "realName":
                if (value.length() > 20) throw new RuntimeException("真实姓名不能超过20字");
                user.setRealName(value);
                break;

确认 User 实体有 realName 字段与 setter(存在,updateUserInfo 已用)。MEMBER_BACKFILL_WHITELIST 不加 realName(family_members 表无 real_name 列)。

  • Step 3: 编译验证

Run: cd cfc-backend && mvn clean compile Expected: BUILD SUCCESS,退出码 0。

  • [ ] Step 4: 提交

    git add cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
    git commit -m "feat: 注册零填写-backfill白名单增加realName回写"
    

Task 7: add-member.vue 手机号冲突 → 邀请加入提示

Files:

  • Modify: cfc-frontend/pages/family/add-member.vue:77-81(手机号 hint 文案)、:204-241(submit 校验)、:255-308(doSubmit)

Interfaces:

  • Consumes: checkPhone(@/utils/api.js 已封装 POST /api/auth/check-phone)
  • Produces: 手机号已注册 → 弹确认「邀请加入」;取消则继续添加

  • [ ] Step 1: import checkPhone

把 L110:

import { addFamilyMember, updateFamilyMember, recordConsent } from '../../utils/api.js'

替换为:

import { addFamilyMember, updateFamilyMember, recordConsent, checkPhone } from '../../utils/api.js'
  • Step 2: 修改手机号 hint 文案

把 L80:

<text class="hint">若手机号已注册,将自动关联该账号</text>

替换为:

<text class="hint">若手机号已注册,将提示是否邀请该用户加入家庭</text>
  • Step 3: submit() 加手机号冲突检查

在 submit() 的 L222-225(手机号格式校验)之后、L227(儿童弹窗检查)之前插入:

if (this.form.phone && !this.isEdit) {
  try {
    var phoneCheck = await checkPhone(this.form.phone)
    if (phoneCheck && phoneCheck.code === 200 && phoneCheck.data === true) {
      var self = this
      var confirmed = await new Promise(function(resolve) {
        uni.showModal({
          title: '手机号已注册',
          content: '该手机号已被其他用户注册,是否邀请该用户加入家庭?',
          confirmText: '去邀请',
          cancelText: '继续添加',
          success: function(res2) {
            resolve(res2.confirm)
          }
        })
      })
      if (confirmed) {
        // 跳转家庭邀请分享页(生成邀请卡片/链接发给该用户)
        uni.navigateTo({ url: '/pages/guide/invite/share' })
        return
      }
      // 取消:继续以不可登录用户添加(手机号照常保存,后端不做关联)
    }
  } catch (e) {
    console.log('手机号存在性检查失败,忽略', e)
  }
}

注意:submit() 已是 async(L204 async submit()),await 可直接使用。检查仅在新增(!this.isEdit)时执行,编辑成员跳过。

  • Step 4: 校验

人工核对:submit() 中新增块的位置在手机号格式校验之后;self 变量不与外层冲突(submit 内无 var self 声明,新增块内 var self = this 仅用于 modal 回调);无可选链。

  • [ ] Step 5: 提交

    git add cfc-frontend/pages/family/add-member.vue
    git commit -m "feat: 注册零填写-添加成员手机号已注册时提示邀请加入"
    

Task 8: 验证与文档收尾

Files:

  • Modify: docs/superpowers/PROJECT-OVERVIEW.md

  • [ ] Step 1: 后端编译

Run: cd cfc-backend && mvn clean compile Expected: BUILD SUCCESS。

  • [ ] Step 2: 前端静态校验

  • node --check cfc-frontend/utils/profile-field-usage.js 退出码 0

  • 对 join-form.vue / login.vue / user-edit.vue / join.vue / add-member.vue 提取 <script> 块逐一 node --check

  • 全局 grep 确认无残留:grep -rn "goUserEdit\|autoParent=true\|hasPendingFamilyInvite" cfc-frontend/pages/ | grep -v "pendingFamilyInviteCode"(允许 login.vue 中 pendingFamilyInviteCode 读取代码存在;join.vue 的 storage 写入保留)

  • [ ] Step 3: 更新 PROJECT-OVERVIEW.md

把 2026-09-28-registration-zero-fill-design.md 条目状态 🟡 设计稿(待确认) → ✅ 已实施(注册零填写+接收邀请页+映射表+手机号冲突邀请);同时更新头部 v2.13 状态行中该条描述为已实施。

  • [ ] Step 4: 提交

    git add docs/superpowers/PROJECT-OVERVIEW.md
    git commit -m "docs: 注册零填写实施完成,PROJECT-OVERVIEW同步"
    
  • [ ] Step 5: 推送

Run: git push Expected: 推送成功。


自检记录

  • 规格覆盖度:5.1(Task 1-2)、5.2(Task 3-4)、5.3(Task 5-6)、5.4(Task 7)、7 验收标准(Task 8)——全部覆盖
  • 占位符扫描:无 TBD/TODO;每个任务含完整代码或明确操作
  • 类型一致性:checkPhone(phone) 返回 {code, data: boolean}(AuthController L253 返回 Result<Boolean>);requestJoinByCode(inviteCode, familyRole)(api.js L1390);updateUserInfo(payload) 透传 realName/nickname/avatar;User.setRealName(String) 存在
  • 风险点:user-edit.vue 保留的 dead 方法(joinFamilyAction/createFamilyAction/confirmRole/selectRole)不引用即无副作用;join-form 页面首次访问需 HBuilderX 重新打包(产物在 unpackage/,本计划不打包)