docs/superpowers/specs/2026-08-04-registration-profile-backfill-design.md(3 个默认值按推荐值执行:统一 backfill 接口 / 外部报名人不回写 / 14 字段口径)mvn clean compile(在 cfc-backend 下);前端 npm run build:mp-weixin(在 cfc-frontend 下)新用户注册不再自动创建家庭(familyId=null),注册引导只收集头像+昵称(可跳过);个人中心新增「已完善 X/14 字段」卡片;功能场景(试点:活动报名)实现「选择用户 → 预填 → 就地补充 → 自动回写」,新增统一 backfill 接口(后端字段白名单)。
users.family_id 允许 NULL(迁移 155)LoginResultDTO 新增 needsFamily;新增 POST /api/user/profile/backfilluser-edit.vue 新用户流程最小化;profile.vue 补充卡片;utils/profile-field-usage.js + utils/profile-backfill.js;活动报名接入回写components/family-guard.vue 接入 3 个核心页| 参考 | 路径 | 用途 |
|---|---|---|
| 迁移模式 | cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java(末尾迁移154块) |
新增迁移155模板 |
| 注册路径 | cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java wechatLogin(L71-168)/phoneLogin(L173-247)/directRegister(L382-430)/registerWithIdCard(L742-809)/registerWithInviteCode(L1181+) |
删除建家庭逻辑 |
| 懒建家庭 | 同上 ensureUserHasFamily(L922-938) + 调用点 L143/L227/L289/L325 |
删除(连同调用点) |
| 登录响应装配 | 同上各 login 方法末尾 LoginResultDTO result = new LoginResultDTO() |
加 needsFamily |
| birthday 解析 | 同上 updateUserInfo(L962) SimpleDateFormat("yyyy-MM-dd") |
backfill 复用同款解析 |
| 实体字段 | cfc-backend/src/main/java/com/etotem/cfc/entity/User.java(birthHour L53/gender L55/dietPreferences L87/address L79/phone L43)、FamilyMember.java(phone L30/gender L36/birthday L39/birthHour L42) |
白名单字段 |
| 控制器 | cfc-backend/src/main/java/com/etotem/cfc/controller/UserController.java(/api/user,L66 /info、L77 /update) |
新增 /profile/backfill |
| schema.sql | cfc-backend/src/main/resources/schema.sql L19 family_id BIGINT NOT NULL |
改为 BIGINT NULL |
| 前端表单 | cfc-frontend/pages/user-edit/user-edit.vue(家庭绑定区 L212-233、saveUserInfo L463、skip L635) |
新用户流程最小化 |
| 个人中心 | cfc-frontend/pages/profile/profile.vue(L17 ProfileHeader + @avatar-click="goToEditInfo",goToEditInfo 方法缺失需补) |
加补充卡片 |
| 活动报名 | cfc-frontend/pages/activity/activity-detail/activity-detail.vue(loadFamilyMembers L570-602、toggleFamilyMember L609-622、addManualEntry L623-630、submitRegistration L671、doCreateOrder L708) |
接入回写 |
| API 封装 | cfc-frontend/utils/api.js(L252 getUserInfo、L264 createFamily、L269 getFamilyMembers) |
加 backfillProfile |
| 无家庭页 | cfc-frontend/pages/wealth-sub/insurance-planning.vue、cfc-frontend/pages/profile/components/ProfileMenu.vue、cfc-frontend/pages/growth/index.vue(均有「请先加入家庭」) |
P2 接入引导 |
项目约定(必须遵守):
@PostMapping;DI 用 @Resource;响应统一 Result<T>;实体 @TableName + @TableId(type = IdType.AUTO) + @Data// 迁移155: ...?.、禁止 :key 表达式、禁止 CSS Grid、禁止 new Date(string)(用 parseDate())feat: 注册引导-xxxmvn clean compile 为准(测试目录有 ~166 个预先存在失败,不要尝试修复,也不要把本次功能写进现有测试套件)cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java在 runMigrations() 中最后一个迁移块(迁移154)之后、方法闭合 } 之前追加:
// 迁移155: users.family_id 允许 NULL(注册不再自动创建家庭)
try {
jdbcTemplate.execute("ALTER TABLE users MODIFY COLUMN family_id BIGINT NULL COMMENT '所属家庭ID(注册时可为空,首次使用家庭功能时创建/加入)'");
log.info("已修改users.family_id允许NULL");
} catch (Exception e) {
// 忽略
}
cfc-backend/src/main/resources/schema.sql在 users 表定义中把(L19):
family_id BIGINT NOT NULL,
改为:
family_id BIGINT NULL COMMENT '所属家庭ID(注册时可为空,首次使用家庭功能时创建/加入)',
验证: mvn clean compile(在 cfc-backend)退出码 0。
提交: feat: 注册引导-迁移155 users.family_id可空
cfc-backend/src/main/java/com/etotem/cfc/dto/LoginResultDTO.java在 needsRoleSelect 字段之后(L16 后)追加:
private Boolean needsFamily; // familyId 为空或 0 时为 true,前端据此决定是否引导建家庭
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java 各登录方法在每个 LoginResultDTO result = new LoginResultDTO(); 装配块中,于 result.setNeedsRoleSelect(...)(无则放 result.setNickname(...) 之后)追加:
result.setNeedsFamily(user.getFamilyId() == null || user.getFamilyId() == 0L);
涉及装配点(8 处):
wechatLogin(~L153-167)phoneLogin(~L235-246)silentLogin(~L294-305)autoLoginByOpenid(~L329-339)directRegister(~L419-429)registerWithIdCard(~L800-808)registerWithInviteCode 分销分支(~L1212-1221)registerWithInviteCode 普通分支(~L1287-1292)验证: mvn clean compile 退出码 0。
提交: feat: 注册引导-登录响应新增needsFamily标记
修改 cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java。
wechatLogin(L103-108 + L113)删除新用户分支中的建家庭代码块:
Family family = new Family();
family.setName("我的家庭");
family.setInviteCode(generateInviteCode());
family.setCreatedAt(new Date());
family.setUpdatedAt(new Date());
familyMapper.insert(family);
并把 L113 user.setFamilyId(family.getId()); 删除(保持默认 null)。
phoneLogin(L192-197 + L202)删除同样的建家庭代码块(L192-197)和 L202 user.setFamilyId(family.getId());。
删除 L216-217 family.setCreatorId(user.getId()); familyMapper.updateById(family);(family 变量已不存在)。
directRegister(L384-389 + L395)删除建家庭代码块(L384-389)和 L395 user.setFamilyId(family.getId());。
保留 L408-414 邀请码加入家庭逻辑(joinFamily(user.getId(), dto.getInviteCode()))不变。
registerWithIdCard(L775-780 + L785)仅新用户分支(else 块)删除建家庭代码块(L775-780)和 L785 user.setFamilyId(family.getId());。
Family family; 声明删除family = familyMapper.selectById(user.getFamilyId()); 删除(变量已删)result.setFamilyId(user.getFamilyId()) 保留(老用户有 familyId 时正常返回)registerWithInviteCode 分销分支(L1188-1194 + L1199)仅 distResult 命中分支删除建家庭代码块(L1189-1194)和 L1199 user.setFamilyId(family.getId());,但保留 distributionService.registerByInviteCode(inviteCode, user.getId()) 与 token 装配。
普通邀请码分支(L1224+)不动:该分支语义是「加入已有家庭」(familyMapper.selectOne(inviteCode)),新用户 user.setFamilyId(family.getId()) 保留。
ensureUserHasFamily 方法及 4 个调用点注意: 调用点在 if/else 块内,删除时保持块结构完整(如 phoneLogin L219-230 的 else 块删除 L227 后仍保留 user.setUpdatedAt + userMapper.updateById)。
验证: mvn clean compile 退出码 0。
提交: feat: 注册引导-注册路径不再自动创建家庭
cfc-backend/src/main/java/com/etotem/cfc/dto/ProfileBackfillDTO.javapackage com.etotem.cfc.dto;
import lombok.Data;
import java.util.Map;
@Data
public class ProfileBackfillDTO {
/** 回写目标类型: user=本人 / member=家庭成员 */
private String targetType;
/** targetType=member 时的 family_members.id;user 时可为空 */
private Long targetId;
/** 回写字段: { phone: "138...", birthday: "2015-06-01", ... } */
private Map<String, String> fields;
}
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java新增常量与方法(放在 getUserInfo 方法 L363 之后):
/** 回写字段白名单(user 目标) */
private static final String[] USER_BACKFILL_WHITELIST = {"phone", "gender", "birthday", "birthHour", "dietPreferences", "address"};
/** 回写字段白名单(member 目标,family_members 表无 dietPreferences/address 列) */
private static final String[] MEMBER_BACKFILL_WHITELIST = {"phone", "gender", "birthday", "birthHour"};
/**
* 就地补充信息回写(字段白名单校验 + 目标归属校验)
* @return Map: { updatedFields: List<String>, profile: Object(最新完整信息) }
*/
public Map<String, Object> backfillProfile(Long userId, ProfileBackfillDTO dto) {
if (dto == null || dto.getFields() == null || dto.getFields().isEmpty()) {
throw new RuntimeException("回写字段不能为空");
}
User me = userMapper.selectById(userId);
if (me == null) {
throw new RuntimeException("用户不存在");
}
List<String> updatedFields = new ArrayList<>();
Object profile;
if ("user".equals(dto.getTargetType())) {
// 仅允许回写自己
if (dto.getTargetId() != null && !dto.getTargetId().equals(userId)) {
throw new RuntimeException("仅能回写本人的个人信息");
}
for (String field : USER_BACKFILL_WHITELIST) {
String value = dto.getFields().get(field);
if (value == null || value.isEmpty()) continue;
applyUserBackfillField(me, field, value.trim());
updatedFields.add(field);
}
me.setUpdatedAt(new Date());
userMapper.updateById(me);
profile = me;
} else if ("member".equals(dto.getTargetType())) {
Long memberId = dto.getTargetId();
if (memberId == null) {
throw new RuntimeException("回写家庭成员时 targetId 不能为空");
}
FamilyMember member = familyMemberMapper.selectById(memberId);
if (member == null) {
throw new RuntimeException("家庭成员不存在");
}
// 归属校验:成员必须属于当前用户家庭
Long myFamilyId = me.getFamilyId();
if (myFamilyId == null || !myFamilyId.equals(member.getFamilyId())) {
throw new RuntimeException("该成员不属于您的家庭");
}
for (String field : MEMBER_BACKFILL_WHITELIST) {
String value = dto.getFields().get(field);
if (value == null || value.isEmpty()) continue;
applyMemberBackfillField(member, field, value.trim());
updatedFields.add(field);
}
familyMemberMapper.updateById(member);
profile = member;
} else {
throw new RuntimeException("targetType 仅支持 user/member");
}
Map<String, Object> result = new HashMap<>();
result.put("updatedFields", updatedFields);
result.put("profile", profile);
return result;
}
private void applyUserBackfillField(User user, String field, String value) {
switch (field) {
case "phone":
if (!value.matches("^1[3-9]\\d{9}$")) throw new RuntimeException("手机号格式不正确");
user.setPhone(value);
break;
case "gender":
if (!"male".equals(value) && !"female".equals(value)) throw new RuntimeException("性别取值非法");
user.setGender(value);
break;
case "birthday":
try {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
user.setBirthday(sdf.parse(value));
} catch (Exception e) {
throw new RuntimeException("生日格式不正确,应为 yyyy-MM-dd");
}
break;
case "birthHour":
if (!"子丑寅卯辰巳午未申酉戌亥".contains(value)) throw new RuntimeException("出生时辰取值非法");
user.setBirthHour(value);
break;
case "dietPreferences":
user.setDietPreferences(value);
break;
case "address":
user.setAddress(value);
break;
default:
break;
}
}
private void applyMemberBackfillField(FamilyMember member, String field, String value) {
switch (field) {
case "phone":
if (!value.matches("^1[3-9]\\d{9}$")) throw new RuntimeException("手机号格式不正确");
member.setPhone(value);
break;
case "gender":
if (!"male".equals(value) && !"female".equals(value)) throw new RuntimeException("性别取值非法");
member.setGender(value);
break;
case "birthday":
try {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
member.setBirthday(sdf.parse(value));
} catch (Exception e) {
throw new RuntimeException("生日格式不正确,应为 yyyy-MM-dd");
}
break;
case "birthHour":
if (!"子丑寅卯辰巳午未申酉戌亥".contains(value)) throw new RuntimeException("出生时辰取值非法");
member.setBirthHour(value);
break;
default:
break;
}
}
确认 UserService 已 import
java.util.HashMap/java.util.Map/java.util.List/java.util.ArrayList(文件已使用 Map/List)。
cfc-backend/src/main/java/com/etotem/cfc/controller/UserController.java在 @PostMapping("/update")(L105 之后)新增:
@PostMapping("/profile/backfill")
@Operation(summary = "就地补充信息回写(个人资料按需补充)")
public Result<Map<String, Object>> backfillProfile(HttpServletRequest request, @RequestBody ProfileBackfillDTO dto) {
Long userId = (Long) request.getAttribute("userId");
try {
Map<String, Object> result = userService.backfillProfile(userId, dto);
return Result.success(result);
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
}
确认文件已 import com.etotem.cfc.dto.ProfileBackfillDTO、java.util.Map(文件已使用 Map)。
验证: mvn clean compile 退出码 0。
提交: feat: 注册引导-新增统一backfill回写接口
修改 cfc-frontend/utils/api.js,在 updateUserInfo(L256-258)之后追加:
// 就地补充信息回写(活动报名等场景按需补充后自动回写个人资料)
export const backfillProfile = (data) => {
return request('/api/user/profile/backfill', 'POST', data)
}
验证: npm run build:mp-weixin 通过。
提交: feat: 注册引导-前端api新增backfillProfile
cfc-frontend/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'] }
]
}
}
验证: 文件可被 require(语法正确)。npm run build:mp-weixin 通过。
提交: feat: 注册引导-个人信息使用关联表profile-field-usage
cfc-frontend/utils/profile-backfill.js新增文件(完整内容):
import { backfillProfile as apiBackfill } from './api.js'
const fieldUsage = require('./profile-field-usage.js')
/**
* 就地补充 → 自动回写个人信息
* @param {Object} opts
* targetType: 'user' | 'member'
* targetId: memberId(targetType=member 时)
* fields: { phone: '138...', birthday: '...' }
* scene: 'activity-signup'(必传,校验字段是否属于本场景)
* prevValues: { phone: '旧值' } 当前人已有值(用于「修改已有值」判定)
* @returns {Promise<Object>} { updatedFields: [], skippedFields: [] }
*/
export function backfillProfile(opts) {
return new Promise(function(resolve, reject) {
var scene = opts.scene
var fields = opts.fields || {}
var fieldNames = Object.keys(fields)
if (!scene || fieldNames.length === 0) {
resolve({ updatedFields: [], skippedFields: fieldNames })
return
}
// 1. 场景校验:只回写本场景登记过的字段
var allowedFields = {}
var skippedFields = []
for (var i = 0; i < fieldNames.length; i++) {
var name = fieldNames[i]
var usage = fieldUsage[name]
if (!usage) {
skippedFields.push(name)
continue
}
var inScene = false
var targetsOk = false
for (var j = 0; j < usage.scenes.length; j++) {
var sc = usage.scenes[j]
if (sc.scene === scene) {
inScene = true
if (sc.backfillTarget.indexOf(opts.targetType) >= 0) {
targetsOk = true
}
}
}
if (inScene && targetsOk) {
allowedFields[name] = fields[name]
} else {
skippedFields.push(name)
}
}
if (Object.keys(allowedFields).length === 0) {
resolve({ updatedFields: [], skippedFields: skippedFields })
return
}
// 2. 修改已有值 → 弹确认
var prev = opts.prevValues || {}
var changedNames = []
for (var k in allowedFields) {
var oldVal = prev[k]
var newVal = allowedFields[k]
if (oldVal && oldVal !== newVal && !(newVal === '' && oldVal === newVal)) {
changedNames.push(fieldUsage[k].label)
}
}
function doBackfill() {
apiBackfill({
targetType: opts.targetType,
targetId: opts.targetId || null,
fields: allowedFields
}).then(function(res) {
if (res && res.code === 200) {
// 3. 成功后更新本地缓存
var profile = (res.data && res.data.profile) || {}
if (opts.targetType === 'user') {
var userInfo = uni.getStorageSync('userInfo') || {}
for (var f in allowedFields) {
if (profile[f] !== undefined) {
userInfo[f] = profile[f]
}
}
uni.setStorageSync('userInfo', userInfo)
}
resolve({
updatedFields: (res.data && res.data.updatedFields) || [],
skippedFields: skippedFields
})
} else {
reject(new Error((res && res.message) || '回写失败'))
}
}).catch(function(err) {
reject(err)
})
}
if (changedNames.length > 0) {
uni.showModal({
title: '同步更新资料',
content: '检测到您修改了' + changedNames.join('、') + ',是否同步更新' + (opts.targetType === 'member' ? '该家人的' : '您的') + '个人信息?',
confirmText: '同步更新',
cancelText: '仅本次使用',
success: function(modalRes) {
if (modalRes.confirm) {
doBackfill()
} else {
resolve({ updatedFields: [], skippedFields: skippedFields })
}
}
})
} else {
doBackfill()
}
})
}
验证: 语法正确;npm run build:mp-weixin 通过。
提交: feat: 注册引导-回写工具profile-backfill
修改 cfc-frontend/pages/user-edit/user-edit.vue。
在模板中,给「真实姓名」form-item(L64)所在的整段包裹一个容器并加 v-if。具体:在 L62 昵称 form-item 的 </view> 之后插入:
<!-- 新用户流程:仅展示头像+昵称,其余字段按需在功能页补充 -->
<view v-if="!isNewUserFlow">
并在「饮食偏好」form-item(L155)的 </view> 之后、<!-- 成长规划师额外字段 -->(L157)之前插入闭合标签 </view>。
注意:
form.role === 'teacher'的字段块(L158-188)不在包裹内(teacher 新用户流程仍需展示规划师字段,且isNewUserFlow && form.role === 'teacher'时家庭绑定区已用form.role !== 'teacher'排除)。
把整个家庭绑定区(L212-233,含邀请码输入 + 创建/加入家庭按钮)替换为:
<!-- 新用户流程:保存资料 / 跳过(不再创建或加入家庭) -->
<view class="family-section" v-if="isEdit && form.role !== 'teacher' && isNewUserFlow">
<view class="family-actions">
<button class="btn btn-primary" @click="saveUserInfo" :loading="loading">
保存资料
</button>
<button class="btn btn-skip" @click="skip">
跳过
</button>
</view>
</view>
saveUserInfo 适配新用户流程(L463-501)替换方法体为:
async saveUserInfo() {
// 新用户流程:昵称可跳过(跳过时用默认昵称);老用户流程:昵称必填
if (!this.isNewUserFlow && !this.form.nickname) {
uni.showToast({ title: '请输入昵称', icon: 'none' })
return
}
if (this.isNewUserFlow && !this.form.nickname) {
var phone = this.form.phone || ''
this.form.nickname = phone ? '用户' + phone.substring(phone.length - 4) : '微信用户'
}
// 从 AddressPicker 读取地址文本
if (this.$refs.addressPicker && this.$refs.addressPicker.fullAddress) {
this.form.address = this.$refs.addressPicker.fullAddress
}
this.loading = true
try {
await updateUserInfo({
nickname: this.form.nickname,
realName: this.form.realName,
gender: this.form.gender,
idCard: this.form.idCard,
birthday: this.form.birthday,
avatar: this.form.avatar,
mascot: this.form.mascot,
ethnicity: this.form.ethnicity,
bloodType: this.form.bloodType,
highestEducation: this.form.highestEducation,
maritalStatus: this.form.maritalStatus,
hobbies: this.form.hobbies,
dietPreferences: this.form.dietPreferences,
address: this.form.address
})
uni.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => {
if (this.isNewUserFlow) {
this.navigateToHome()
} else {
uni.navigateBack()
}
}, 1500)
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none' })
} finally {
this.loading = false
}
},
joinFamilyAction(L545)与 createFamilyAction(L593)方法体保留(非新用户流程/其他入口可能复用),但模板已无引用——保留方法不报错skip()(L635)与 navigateToHome()(L638)不变验证: npm run build:mp-weixin 通过。
提交: feat: 注册引导-user-edit新用户流程最小化
修改 cfc-frontend/pages/profile/profile.vue。
<!-- 个人信息完善卡片(全部填满后隐藏) -->
<view class="profile-card" v-if="isLoggedIn && profileProgress < 14" @click="goToEditInfo">
<view class="profile-card-left">
<text class="profile-card-title">个人信息</text>
<text class="profile-card-desc">已完善 {{ profileProgress }}/14 个字段,点击继续补充</text>
</view>
<text class="profile-card-arrow">›</text>
</view>
profileProgress: 0,
this.loadHealthScoreData() 之后) this.loadProfileProgress()
loadProfileProgress() {
var self = this
getUserInfo().then(function(res) {
if (res && res.code === 200 && res.data) {
var user = res.data
var fields = ['nickname', 'avatar', 'realName', 'gender', 'birthday', 'birthHour', 'ethnicity', 'bloodType', 'highestEducation', 'maritalStatus', 'address', 'hobbies', 'dietPreferences', 'phone']
var filled = 0
for (var i = 0; i < fields.length; i++) {
var v = user[fields[i]]
if (v !== null && v !== undefined && v !== '') filled++
}
self.profileProgress = filled
}
}).catch(function() {})
},
// 修复:ProfileHeader 头像点击进入编辑页(原方法缺失)
goToEditInfo() {
uni.navigateTo({ url: '/pages/user-edit/user-edit' })
},
import { getUserInfo } from '../../utils/api.js'
若文件已 import getUserInfo 则跳过。同时在
<style scoped>末尾追加卡片样式:
.profile-card {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 20rpx 30rpx;
padding: 28rpx 32rpx;
background: #fff;
border-radius: 16rpx;
}
.profile-card-left {
display: flex;
flex-direction: column;
}
.profile-card-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.profile-card-desc {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.profile-card-arrow {
font-size: 40rpx;
color: #ccc;
}
验证: npm run build:mp-weixin 通过。
提交: feat: 注册引导-个人中心个人信息完善卡片
修改 cfc-frontend/pages/activity/activity-detail/activity-detail.vue。
import { backfillProfile } from '../../../utils/profile-backfill.js'
确认相对路径:activity-detail.vue 位于
pages/activity/activity-detail/,到 utils 为../../../utils/。
loadFamilyMembers 成员对象补充 targetType 标记(L579-597)parents 分支(L578-585)的 push 对象追加:
targetType: 'user',
children 分支(L590-597)的 push 对象追加:
targetType: 'member',
toggleFamilyMember 记录原始值(L616-621)push 对象追加 prevValues:
this.registrants.push({
memberId: member.id,
name: member.name,
phone: member.phone,
childId: member.childId,
targetType: member.targetType,
prevPhone: member.phone
})
submitRegistration 后触发回写(L671-707 校验之后、调用 doCreateOrder 之前)在校验循环(L678-689)之后、if (self.hasFee...)(L691)之前插入:
// 就地补充的手机号自动回写(仅家庭成员;外部报名人 memberId=null 不回写)
var backfillTasks = []
for (var b = 0; b < self.registrants.length; b++) {
var entryB = self.registrants[b]
if (!entryB.memberId) continue
var phoneB = (entryB.phone || '').trim()
if (phoneB === '') continue
backfillTasks.push(backfillProfile({
targetType: entryB.targetType,
targetId: entryB.memberId,
fields: { phone: phoneB },
scene: 'activity-signup',
prevValues: { phone: entryB.prevPhone }
}))
}
// 回写失败不阻塞报名主流程,仅静默记录
if (backfillTasks.length > 0) {
Promise.all(backfillTasks).catch(function(err) {
console.warn('[backfill] 活动报名信息回写失败', err)
})
}
memberId 格式说明:
loadFamilyMembers中 parents 的 id 为'p_' + parents[i].id(对应 users.id),children 的 id 为'c_' + children[j].id(对应 family_members.id)。backfillProfile会把memberId直接作为targetId传给后端——需要前端转换:targetType='user'时 targetId 应为去掉p_前缀的纯数字。因此把 10.4 中的targetId改为:
var rawId = String(entryB.memberId)
var realId = rawId.indexOf('_') >= 0 ? rawId.substring(2) : rawId
并把 targetId: entryB.memberId 替换为 targetId: realId。
验证: npm run build:mp-weixin 通过。
提交: feat: 注册引导-活动报名接入就地补充回写
cfc-frontend/components/family-guard.vue<template>
<view v-if="visible" class="family-guard-mask" @click="close">
<view class="family-guard-panel" @click.stop>
<text class="family-guard-title">创建家庭后即可使用</text>
<text class="family-guard-desc">家庭是任务、测评、成长档案等功能的基础,创建后邀请家人加入</text>
<view class="family-guard-input-wrap">
<input class="family-guard-input" v-model="familyName" placeholder="请输入家庭名称(默认:我的家庭)" maxlength="20" />
</view>
<view class="family-guard-actions">
<button class="family-guard-btn family-guard-btn-primary" @click="doCreate" :loading="loading">去创建家庭</button>
<button class="family-guard-btn family-guard-btn-plain" @click="close">暂不创建</button>
</view>
</view>
</view>
</template>
<script>
import { createFamily } from '../utils/api.js'
export default {
name: 'FamilyGuard',
data() {
return {
visible: false,
loading: false,
familyName: ''
}
},
methods: {
show() {
this.familyName = ''
this.visible = true
},
close() {
this.visible = false
},
doCreate() {
var self = this
var name = (this.familyName || '').trim() || '我的家庭'
this.loading = true
createFamily(name).then(function(res) {
self.loading = false
if (res && res.code === 200 && res.data) {
uni.setStorageSync('familyId', res.data)
uni.setStorageSync('currentFamilyId', res.data)
// 同步 userInfo 缓存
var userInfo = uni.getStorageSync('userInfo') || {}
userInfo.familyId = res.data
uni.setStorageSync('userInfo', userInfo)
self.visible = false
uni.showToast({ title: '家庭创建成功', icon: 'success' })
if (self.$listeners && self.$listeners.created) {
self.$emit('created', res.data)
}
} else {
uni.showToast({ title: (res && res.message) || '创建失败', icon: 'none' })
}
}).catch(function() {
self.loading = false
uni.showToast({ title: '网络异常', icon: 'none' })
})
}
}
}
</script>
<style scoped>
.family-guard-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
}
.family-guard-panel {
width: 600rpx;
background: #fff;
border-radius: 20rpx;
padding: 40rpx 36rpx;
display: flex;
flex-direction: column;
}
.family-guard-title {
font-size: 34rpx;
font-weight: bold;
color: #333;
text-align: center;
}
.family-guard-desc {
font-size: 26rpx;
color: #999;
margin-top: 16rpx;
line-height: 1.6;
text-align: center;
}
.family-guard-input-wrap {
margin-top: 30rpx;
}
.family-guard-input {
height: 80rpx;
background: #f5f7fa;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
}
.family-guard-actions {
margin-top: 36rpx;
display: flex;
flex-direction: column;
}
.family-guard-btn {
height: 84rpx;
border-radius: 42rpx;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
margin-top: 16rpx;
}
.family-guard-btn-primary {
background: #F97316;
color: #fff;
}
.family-guard-btn-plain {
background: #f5f7fa;
color: #666;
}
</style>
pages/wealth-sub/insurance-planning.vue模板根节点内(最末尾)加:
<family-guard ref="familyGuard" />
script 加 import 与 components 注册:
import FamilyGuard from '../../components/family-guard.vue'
// components: { FamilyGuard }
找到现有「请先加入家庭」处理处(if (!familyId) / toast),替换为调用引导组件:
if (!familyId) {
this.$refs.familyGuard.show()
return
}
pages/growth/index.vue<family-guard ref="familyGuard" />、import、components 注册if (!familyId) 静默返回处替换为 this.$refs.familyGuard.show(); returnpages/profile/components/ProfileMenu.vue三个页面各自保留原业务逻辑(
familyId为空时不再静默,改为弹窗引导),创建成功后created事件回调里刷新页面数据(onShow已有刷新逻辑则无需额外处理)。
验证: npm run build:mp-weixin 通过。
提交: feat: 注册引导-无家庭引导组件接入核心页
mvn clean compile 退出码 0npm run build:mp-weixin 通过检查路由重复:
grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | grep backfill
应仅出现 /profile/backfill 一处(无冲突)
检查 Bean 命名:ProfileBackfillDTO 为 DTO 无 Bean 冲突;FamilyGuard 前端组件名与现有组件无冲突(grep -rn "FamilyGuard" cfc-frontend/components/)
更新 docs/superpowers/PROJECT-OVERVIEW.md:本计划对应规格条目状态改为「已实施」、追加计划文档索引
提交:docs: 注册引导实施计划完成
backfillProfile 前后端签名一致(targetType/targetId/fields);needsFamily 在 8 个装配点一致;memberId 前缀转换(p_/c_ → 纯数字 targetId)已在任务 10.4 定义并在 10.2-10.3 建立 targetType