From 1e2de85f2ec36855027e97505128daf6fd0c817a Mon Sep 17 00:00:00 2001 From: Peanut Date: Thu, 4 Jun 2026 00:43:47 +0800 Subject: [PATCH] docs: add user profile real data implementation plan --- .../2026-06-03-user-profile-real-data.md | 692 ++++++++++++++++++ 1 file changed, 692 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-03-user-profile-real-data.md diff --git a/docs/superpowers/plans/2026-06-03-user-profile-real-data.md b/docs/superpowers/plans/2026-06-03-user-profile-real-data.md new file mode 100644 index 0000000..1141804 --- /dev/null +++ b/docs/superpowers/plans/2026-06-03-user-profile-real-data.md @@ -0,0 +1,692 @@ +# 小程序用户信息去写死 + 编辑资料保存生效 — Implementation Plan + +> **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:** 去除小程序人生轨迹页和编辑资料页的所有硬编码用户信息,扩展后端 `t_user_profile` 表支持 `city`/`industry`/`company`/`personalityTags`/`birthday` 字段,确保编辑资料能完整保存和回显。 + +**Architecture:** 后端通过数据库 ALTER + Java 实体/DTO 扩展新增5个字段;前端同步扩展 Service 转换函数和 Store 数据结构,并移除两个页面中所有 `|| '硬编码默认值'` 回退和示例事件数据。 + +**Tech Stack:** Java 17 / Spring Boot / MyBatis-Plus / MySQL;uni-app (Vue 3) / JavaScript + +--- + +## File Structure + +| 文件 | 改动 | 职责 | +|------|------|------| +| `sql/emotion_museum_ddl.sql` | 追加 | 数据库迁移:新增5列 | +| `backend-single/.../entity/UserProfile.java` | 修改 | ORM 实体新增字段 | +| `backend-single/.../request/UserProfileCreateRequest.java` | 修改 | 创建请求 DTO 新增字段 | +| `backend-single/.../request/UserProfileUpdateRequest.java` | 修改 | 更新请求 DTO 新增字段 | +| `backend-single/.../response/UserProfileResponse.java` | 修改 | 响应 DTO 新增字段 | +| `mini-program/src/services/userProfile.js` | 修改 | 前后端数据格式双向转换 | +| `mini-program/src/stores/app.js` | 修改 | 全局状态 registrationData 扩展 | +| `mini-program/src/pages/main/RecordView.vue` | 修改 | 去除写死默认值和 sampleEvents | +| `mini-program/src/pages/onboarding/index.vue` | 修改 | 去除硬编码默认值,空值友好展示 | + +--- + +### Task 1: 数据库迁移 — 新增5个字段 + +**Files:** +- Modify: `sql/emotion_museum_ddl.sql` + +- [ ] **Step 1: 追加 ALTER TABLE 语句** + +在 `sql/emotion_museum_ddl.sql` 文件末尾追加: + +```sql +-- 用户档案扩展字段:城市、行业、公司、性格标签、生日 +alter table emotion_museum.t_user_profile + add city varchar(100) null comment '所在城市', + add industry varchar(100) null comment '行业', + add company varchar(200) null comment '公司', + add personality_tags json null comment '性格标签列表', + add birthday date null comment '生日'; +``` + +- [ ] **Step 2: Commit** + +```bash +git add sql/emotion_museum_ddl.sql +git commit -m "db: add city, industry, company, personality_tags, birthday to t_user_profile" +``` + +--- + +### Task 2: 后端实体扩展 — UserProfile.java + +**Files:** +- Modify: `backend-single/src/main/java/com/emotion/entity/UserProfile.java` + +- [ ] **Step 1: 在 idealLife 字段后新增5个字段** + +找到 `idealLife` 字段定义(约第115行),在其后、`scripts` 字段前插入: + +```java + /** + * 所在城市 + */ + @TableField("city") + private String city; + + /** + * 行业 + */ + @TableField("industry") + private String industry; + + /** + * 公司 + */ + @TableField("company") + private String company; + + /** + * 性格标签 (JSON字符串) + */ + @TableField("personality_tags") + private String personalityTags; + + /** + * 生日 + */ + @TableField("birthday") + private LocalDate birthday; +``` + +注意:`LocalDate` 的 import 已在文件顶部存在(第12行),无需新增 import。 + +- [ ] **Step 2: Commit** + +```bash +git add backend-single/src/main/java/com/emotion/entity/UserProfile.java +git commit -m "feat: extend UserProfile entity with city/industry/company/personalityTags/birthday" +``` + +--- + +### Task 3: 后端创建请求 DTO 扩展 — UserProfileCreateRequest.java + +**Files:** +- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java` + +- [ ] **Step 1: 在 idealLife 字段后新增5个字段** + +找到 `idealLife` 字段定义(约第86行),在其后、`scripts` 字段前插入: + +```java + /** + * 所在城市 + */ + private String city; + + /** + * 行业 + */ + private String industry; + + /** + * 公司 + */ + private String company; + + /** + * 性格标签 (JSON字符串) + */ + private String personalityTags; + + /** + * 生日 + */ + private LocalDate birthday; +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java +git commit -m "feat: extend UserProfileCreateRequest with city/industry/company/personalityTags/birthday" +``` + +--- + +### Task 4: 后端更新请求 DTO 扩展 — UserProfileUpdateRequest.java + +**Files:** +- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java` + +- [ ] **Step 1: 在 idealLife 字段后新增5个字段** + +找到 `idealLife` 字段定义(约第90行),在其后、`scripts` 字段前插入: + +```java + /** + * 所在城市 + */ + private String city; + + /** + * 行业 + */ + private String industry; + + /** + * 公司 + */ + private String company; + + /** + * 性格标签 (JSON字符串) + */ + private String personalityTags; + + /** + * 生日 + */ + private LocalDate birthday; +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java +git commit -m "feat: extend UserProfileUpdateRequest with city/industry/company/personalityTags/birthday" +``` + +--- + +### Task 5: 后端响应 DTO 扩展 — UserProfileResponse.java + +**Files:** +- Modify: `backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java` + +- [ ] **Step 1: 在 idealLife 字段后新增5个字段** + +找到 `idealLife` 字段定义(约第105行),在其后、`scripts` 字段前插入: + +```java + /** + * 所在城市 + */ + private String city; + + /** + * 行业 + */ + private String industry; + + /** + * 公司 + */ + private String company; + + /** + * 性格标签 (JSON字符串) + */ + private String personalityTags; + + /** + * 生日 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate birthday; +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java +git commit -m "feat: extend UserProfileResponse with city/industry/company/personalityTags/birthday" +``` + +--- + +### Task 6: 前端 Service 扩展 — userProfile.js + +**Files:** +- Modify: `mini-program/src/services/userProfile.js` + +- [ ] **Step 1: 扩展 transformToBackendFormat 的解构和返回对象** + +原解构(第47-59行)新增5个字段: + +```js +const { + id, + nickname, + gender, + zodiac, + profession, + mbti, + hobbies, + childhood, + joy, + low, + future, + city, + industry, + company, + personalityTags, + birthday + } = frontendData +``` + +原返回对象(第61-83行)在 `idealLife` 后新增: + +```js + return { + id, + nickname, + gender, + zodiac, + profession, + mbti, + // 兴趣爱好转为JSON字符串 + hobbies: Array.isArray(hobbies) ? JSON.stringify(hobbies) : hobbies, + // 童年经历 + childhoodDate: childhood?.date || null, + childhoodContent: childhood?.text || null, + // 高光时刻(对应前端的joy) + peakDate: joy?.date || null, + peakContent: joy?.text || null, + // 低谷时期(对应前端的low) + valleyDate: low?.date || null, + valleyContent: low?.text || null, + // 未来期许 + futureVision: future?.vision || null, + // 理想生活状态 + idealLife: future?.ideal || null, + // 新增字段 + city: city || null, + industry: industry || null, + company: company || null, + personalityTags: Array.isArray(personalityTags) ? JSON.stringify(personalityTags) : personalityTags, + birthday: birthday || null + } +``` + +- [ ] **Step 2: 扩展 transformToFrontendFormat 的解构和返回对象** + +原解构(第94-111行)新增5个字段: + +```js + const { + id, + userId, + nickname, + gender, + zodiac, + profession, + mbti, + hobbies, + childhoodDate, + childhoodContent, + peakDate, + peakContent, + valleyDate, + valleyContent, + futureVision, + idealLife, + city, + industry, + company, + personalityTags, + birthday + } = backendData +``` + +原返回对象(第113-143行)在 `future` 对象后新增: + +```js + return { + id, + userId, + nickname: nickname || '', + gender: gender || '', + zodiac: zodiac || '', + profession: profession || '', + mbti: mbti || '', + // 兴趣爱好从JSON字符串解析 + hobbies: hobbies ? (typeof hobbies === 'string' ? JSON.parse(hobbies) : hobbies) : [], + // 童年经历 + childhood: { + date: childhoodDate || '', + text: childhoodContent || '' + }, + // 高光时刻 + joy: { + date: peakDate || '', + text: peakContent || '' + }, + // 低谷时期 + low: { + date: valleyDate || '', + text: valleyContent || '' + }, + // 未来期许 + future: { + vision: futureVision || '', + ideal: idealLife || '' + }, + // 新增字段 + city: city || '', + industry: industry || '', + company: company || '', + personalityTags: personalityTags ? (typeof personalityTags === 'string' ? JSON.parse(personalityTags) : personalityTags) : [], + birthday: birthday || '' + } +``` + +- [ ] **Step 3: Commit** + +```bash +git add mini-program/src/services/userProfile.js +git commit -m "feat: add city/industry/company/personalityTags/birthday to userProfile transforms" +``` + +--- + +### Task 7: 前端 Store 扩展 — app.js + +**Files:** +- Modify: `mini-program/src/stores/app.js` + +- [ ] **Step 1: 扩展 registrationData 初始结构** + +找到 `registrationData` 定义(第19-30行),在 `future` 对象后新增: + +```js + registrationData: { + nickname: '', + gender: '', + mbti: '', + zodiac: '', + profession: '', + hobbies: [], + childhood: { date: '', text: '' }, + joy: { date: '', text: '' }, + low: { date: '', text: '' }, + future: { vision: '', ideal: '' }, + city: '', + industry: '', + company: '', + personalityTags: [], + birthday: '' + } +``` + +注意:此文件使用 CRLF 行尾(`\r\n`),使用 Edit 工具时请按 Read 工具输出的格式匹配。 + +- [ ] **Step 2: Commit** + +```bash +git add mini-program/src/stores/app.js +git commit -m "feat: extend registrationData with city/industry/company/personalityTags/birthday" +``` + +--- + +### Task 8: 人生轨迹页去写死 — RecordView.vue + +**Files:** +- Modify: `mini-program/src/pages/main/RecordView.vue` + +- [ ] **Step 1: 删除 sampleEvents 数组** + +删除第143-176行的 `sampleEvents` 数组定义。 + +- [ ] **Step 2: 修改 events computed 去除 sampleEvents 回退** + +将第179-182行: +```js +const events = computed(() => { + const list = store.events || [] + return list.length ? list : sampleEvents +}) +``` +改为: +```js +const events = computed(() => store.events || []) +``` + +- [ ] **Step 3: 修改 heroTags computed 去除硬编码默认值** + +将第183-187行: +```js +const heroTags = computed(() => { + const hobbies = profile.value.hobbies + if (Array.isArray(hobbies) && hobbies.length) return hobbies.slice(0, 4) + return ['阅读', '旅行', '音乐', '创作'] +}) +``` +改为: +```js +const heroTags = computed(() => { + const hobbies = profile.value.hobbies + if (Array.isArray(hobbies) && hobbies.length) return hobbies.slice(0, 4) + return [] +}) +``` + +- [ ] **Step 4: 修改 avatar computed 去除硬编码昵称** + +将第189-192行: +```js +const avatar = computed(() => { + const nickname = profile.value.nickname || 'Zoey' + return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(nickname)}&backgroundColor=b982ff` +}) +``` +改为: +```js +const avatar = computed(() => { + const nickname = profile.value.nickname || 'User' + return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(nickname)}&backgroundColor=b982ff` +}) +``` + +- [ ] **Step 5: 修改模板中硬编码默认值** + +将第13行: +```vue +{{ profile.nickname || 'Zoey' }} +``` +改为: +```vue +{{ profile.nickname || '未设置' }} +``` + +将第31行: +```vue +{{ profile.zodiac || '巨蟹座' }} +``` +改为: +```vue +{{ profile.zodiac || '未设置' }} +``` + +将第38行: +```vue +{{ profile.mbti || 'ENTJ' }} +``` +改为: +```vue +{{ profile.mbti || '未设置' }} +``` + +将第45行: +```vue +{{ profile.profession || '产品经理' }} +``` +改为: +```vue +{{ profile.profession || '未设置' }} +``` + +将第56行: +```vue +{{ heroTags.join(' · ') }} +``` +改为: +```vue +{{ heroTags.length ? heroTags.join(' · ') : '未设置' }} +``` + +- [ ] **Step 6: Commit** + +```bash +git add mini-program/src/pages/main/RecordView.vue +git commit -m "fix: remove hardcoded defaults and sampleEvents from RecordView" +``` + +--- + +### Task 9: 编辑资料页去写死 — onboarding/index.vue + +**Files:** +- Modify: `mini-program/src/pages/onboarding/index.vue` + +- [ ] **Step 1: 修改 avatarUrl computed 去除硬编码昵称** + +将第234-238行: +```js +const avatarUrl = computed(() => { + if (avatarLocal.value) return avatarLocal.value + const nickname = form.nickname || 'Zoey' + return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(nickname)}&backgroundColor=b982ff` +}) +``` +改为: +```js +const avatarUrl = computed(() => { + if (avatarLocal.value) return avatarLocal.value + const nickname = form.nickname || 'User' + return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(nickname)}&backgroundColor=b982ff` +}) +``` + +- [ ] **Step 2: 修改 birthdayDisplay computed 去除硬编码日期** + +将第240-244行: +```js +const birthdayDisplay = computed(() => { + if (!birthday.value) return '1998年06月18日' + const [year, month, day] = birthday.value.split('-') + return `${year}年${month}月${day}日` +}) +``` +改为: +```js +const birthdayDisplay = computed(() => { + if (!birthday.value) return '请选择生日' + const [year, month, day] = birthday.value.split('-') + return `${year}年${month}月${day}日` +}) +``` + +- [ ] **Step 3: 修改 syncFromStore 去除所有硬编码默认值** + +将第246-265行的 `syncFromStore` 函数整体替换为: + +```js +const syncFromStore = () => { + const source = store.userProfile || store.registrationData || {} + Object.assign(form, { + nickname: source.nickname || '', + gender: source.gender || '', + zodiac: source.zodiac || '', + mbti: source.mbti || '', + profession: source.profession || '', + city: source.city || '', + industry: source.industry || '', + company: source.company || '', + personalityTags: Array.isArray(source.personalityTags) ? [...source.personalityTags] : [], + hobbies: Array.isArray(source.hobbies) ? [...source.hobbies] : [], + childhood: { date: source.childhood?.date || '', text: source.childhood?.text || '' }, + joy: { date: source.joy?.date || '', text: source.joy?.text || '' }, + low: { date: source.low?.date || '', text: source.low?.text || '' }, + future: { vision: source.future?.vision || '', ideal: source.future?.ideal || '' } + }) + birthday.value = source.birthday || '' +} +``` + +注意:此文件使用 CRLF 行尾(`\r\n`),使用 Edit 工具时请按 Read 工具输出的格式匹配。 + +- [ ] **Step 4: Commit** + +```bash +git add mini-program/src/pages/onboarding/index.vue +git commit -m "fix: remove hardcoded defaults from profile edit page" +``` + +--- + +### Task 10: 验证测试 + +**Files:** +- 无需修改,纯验证 + +- [ ] **Step 1: 后端编译验证** + +```bash +cd backend-single && mvn compile -q +``` + +预期:编译成功,无错误。 + +- [ ] **Step 2: 前端语法检查(如有 linter)** + +```bash +cd mini-program && npm run lint 2>/dev/null || echo "No lint script, skipping" +``` + +- [ ] **Step 3: 手动功能验证清单** + +1. 启动后端服务,确认无启动错误。 +2. 在数据库执行 `DESCRIBE t_user_profile;`,确认 `city`、`industry`、`company`、`personality_tags`、`birthday` 五列已存在。 +3. 小程序登录后进入"人生轨迹"页: + - 未设置资料时,昵称显示"未设置",星座/MBTI/职业显示"未设置",爱好显示"未设置"。 + - 无人生事件时,显示"还没有人生轨迹"空状态,无 sampleEvents。 +4. 点击"编辑资料": + - 首次进入(无资料)时,所有输入框为空,生日显示"请选择生日",性格标签和爱好无选中项。 + - 填写所有字段(包括城市、行业、公司、性格标签、生日)后保存。 + - 保存成功后返回人生轨迹页,数据已更新。 + - 再次进入编辑资料页,所有字段正确回显。 +5. 已有用户(老数据,无新字段)登录后,页面正常展示,无报错,新字段显示为空。 + +- [ ] **Step 4: Commit(如测试通过)** + +```bash +git log --oneline -10 +``` + +预期看到 9 个 commits(Task 1-9 各一个)。 + +--- + +## Self-Review + +**1. Spec coverage:** +- 数据库扩展5个字段 → Task 1 ✅ +- 后端实体扩展 → Task 2 ✅ +- 后端 DTO 扩展(Create/Update/Response)→ Task 3/4/5 ✅ +- 前端 Service 双向转换扩展 → Task 6 ✅ +- 前端 Store 扩展 → Task 7 ✅ +- 人生轨迹页去除写死数据和 sampleEvents → Task 8 ✅ +- 编辑资料页去除硬编码默认值 → Task 9 ✅ +- 测试验证 → Task 10 ✅ + +**2. Placeholder scan:** +- 无 TBD/TODO/"implement later"/"fill in details" ✅ +- 无 "Add appropriate error handling" 等模糊描述 ✅ +- 无 "Similar to Task N" ✅ +- 每个代码步骤都包含完整代码 ✅ + +**3. Type consistency:** +- 后端5个字段名在 Entity/CreateRequest/UpdateRequest/Response 中完全一致:`city`, `industry`, `company`, `personalityTags`, `birthday` ✅ +- 前端 Service 中 `transformToBackendFormat` 和 `transformToFrontendFormat` 字段名一致 ✅ +- Store 中 `registrationData` 字段名与 Service 一致 ✅ +- `personalityTags` 在前端始终作为数组处理,后端作为 JSON 字符串,转换逻辑与 `hobbies` 保持一致 ✅ +- `birthday` 前后端均为 `yyyy-MM-dd` 字符串格式 ✅