diff --git a/docs/superpowers/plans/2026-06-24-personality-tag-library.md b/docs/superpowers/plans/2026-06-24-personality-tag-library.md new file mode 100644 index 0000000..13a89e1 --- /dev/null +++ b/docs/superpowers/plans/2026-06-24-personality-tag-library.md @@ -0,0 +1,1307 @@ +# 小程序编辑资料页:性格标签 / 兴趣爱好 标签库扩展 实现计划 + +> **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:** 后端 `UserProfile` 实体新增两个 JSON 字段(`personality_tag_library` / `hobby_library`)存用户专属的完整标签库,原有"已选中标签"字段语义保留不变;前端通过 `stores/app.js` 的 `registrationData` 持有库数据,编辑资料页基于库数据计算有序展示列表,管理页通过 `uni.$emit` 事件将删除结果同步回编辑页。 + +**Tech Stack:** Spring Boot 2.7 + MyBatis-Plus + MySQL 8(后端),UniApp 3 + Vue 3 Composition API + 微信小程序(前端)。 + +**Spec 文档:** `docs/superpowers/specs/2026-06-24-personality-tag-library-design.md` + +--- + +## 文件结构总览 + +### 新增文件 + +| 文件 | 职责 | +|---|---| +| `backend-single/src/main/resources/db/migration/V20260624__add_user_profile_tag_library.sql` | 数据库字段迁移脚本 | +| `mini-program/src/pages/onboarding/tag-manage.vue` | 管理标签独立页面(性格标签 / 兴趣爱好) | + +### 修改文件 + +| 文件 | 修改内容 | +|---|---| +| `backend-single/src/main/java/com/emotion/entity/UserProfile.java` | 新增 `personalityTagLibrary` / `hobbyLibrary` 字段 | +| `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java` | 同上 | +| `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java` | 同上 | +| `backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java` | 同上 | +| `mini-program/src/services/userProfile.js` | 请求/响应转换增加两个库字段 | +| `mini-program/src/stores/app.js` | `registrationData` 增加两个库字段 | +| `mini-program/src/pages.json` | 注册 `tag-manage` 路由 | +| `mini-program/src/pages/onboarding/index.vue` | 标签区 UI 重构、新增标签弹窗、管理入口、事件监听 | + +### 不修改文件 + +| 文件 | 原因 | +|---|---| +| `mini-program/src/pages/main/MineView.vue` | 只读"已选中的标签"(沿用 `hobbies` 字段),不需要感知完整库 | +| `backend-single/src/main/java/com/emotion/service/impl/UserProfileServiceImpl.java` | 使用 `BeanUtils.copyProperties` 自动映射,新字段透传 | +| `backend-single/src/main/java/com/emotion/controller/UserProfileController.java` | 同上 | + +--- + +## Task 1: 后端数据库与实体层 + +**目标:** 给 `t_user_profile` 表新增两个 JSON 字段,同步更新实体类与 3 个 DTO,保证接口透传。 + +**Files:** +- Create: `backend-single/src/main/resources/db/migration/V20260624__add_user_profile_tag_library.sql` +- Modify: `backend-single/src/main/java/com/emotion/entity/UserProfile.java` +- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java` +- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java` +- Modify: `backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java` + +### Step 1: 创建数据库迁移脚本 + +创建 `backend-single/src/main/resources/db/migration/V20260624__add_user_profile_tag_library.sql`: + +```sql +-- 用户档案表新增标签库字段,用于存储用户专属的完整标签列表(含剩余预设 + 自定义) +ALTER TABLE t_user_profile + ADD COLUMN personality_tag_library TEXT DEFAULT NULL COMMENT '用户完整性格标签库(JSON数组)', + ADD COLUMN hobby_library TEXT DEFAULT NULL COMMENT '用户完整兴趣爱好库(JSON数组)'; +``` + +### Step 2: UserProfile 实体新增字段 + +修改 `backend-single/src/main/java/com/emotion/entity/UserProfile.java`。 + +在 `personalityTags` 字段下方追加两个字段(保持与现有 `personalityTags` / `hobbies` 字段风格一致): + +```java + /** + * 性格标签 (JSON字符串) + */ + @TableField("personality_tags") + private String personalityTags; + + /** + * 用户完整性格标签库 (JSON字符串,含剩余预设 + 自定义) + */ + @TableField("personality_tag_library") + private String personalityTagLibrary; + + /** + * 用户完整兴趣爱好库 (JSON字符串,含剩余预设 + 自定义) + */ + @TableField("hobby_library") + private String hobbyLibrary; +``` + +### Step 3: UserProfileUpdateRequest 新增字段 + +修改 `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`。 + +在 `personalityTags` 字段下方追加: + +```java + /** + * 性格标签 (JSON字符串) + */ + private String personalityTags; + + /** + * 用户完整性格标签库 (JSON字符串) + */ + private String personalityTagLibrary; + + /** + * 用户完整兴趣爱好库 (JSON字符串) + */ + private String hobbyLibrary; +``` + +### Step 4: UserProfileCreateRequest 新增字段 + +修改 `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`。 + +在 `personalityTags` 字段下方追加: + +```java + /** + * 性格标签 (JSON字符串) + */ + private String personalityTags; + + /** + * 用户完整性格标签库 (JSON字符串) + */ + private String personalityTagLibrary; + + /** + * 用户完整兴趣爱好库 (JSON字符串) + */ + private String hobbyLibrary; +``` + +### Step 5: UserProfileResponse 新增字段 + +修改 `backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`。 + +在 `personalityTags` 字段下方追加: + +```java + /** + * 性格标签 (JSON字符串) + */ + private String personalityTags; + + /** + * 用户完整性格标签库 (JSON字符串) + */ + private String personalityTagLibrary; + + /** + * 用户完整兴趣爱好库 (JSON字符串) + */ + private String hobbyLibrary; +``` + +### Step 6: 本地编译验证 + +```bash +cd backend-single +mvn clean install -DskipTests -pl :backend-single -am +``` + +预期:`BUILD SUCCESS`。 + +### Step 7: 提交 + +```bash +git add backend-single/src/main/resources/db/migration/V20260624__add_user_profile_tag_library.sql \ + backend-single/src/main/java/com/emotion/entity/UserProfile.java \ + backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java \ + backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java \ + backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java +git commit -m "feat:UserProfile 新增性格标签库与兴趣爱好库字段" +``` + +--- + +## Task 2: 前端数据层(services + stores + pages.json) + +**目标:** 前端请求/响应转换、全局 store、路由注册全部支持新字段。 + +**Files:** +- Modify: `mini-program/src/services/userProfile.js` +- Modify: `mini-program/src/stores/app.js` +- Modify: `mini-program/src/pages.json` + +### Step 1: services/userProfile.js 响应解析增加库字段 + +修改 `mini-program/src/services/userProfile.js`。 + +在 `transformToFrontendFormat` 函数中,解构时增加两个字段,并在返回对象中解析它们: + +原代码: + +```javascript + const { + id, + userId, + nickname, + gender, + zodiac, + profession, + mbti, + hobbies, + childhoodDate, + childhoodContent, + peakDate, + peakContent, + valleyDate, + valleyContent, + futureVision, + idealLife, + city, + industry, + company, + personalityTags, + birthday + } = backendData +``` + +改为: + +```javascript + const { + id, + userId, + nickname, + gender, + zodiac, + profession, + mbti, + hobbies, + childhoodDate, + childhoodContent, + peakDate, + peakContent, + valleyDate, + valleyContent, + futureVision, + idealLife, + city, + industry, + company, + personalityTags, + personalityTagLibrary, + hobbyLibrary, + birthday + } = backendData +``` + +原返回对象末尾: + +```javascript + personalityTags: parseJsonArray(personalityTags), + birthday: normalizeDate(birthday) + } +``` + +改为: + +```javascript + personalityTags: parseJsonArray(personalityTags), + personalityTagLibrary: parseJsonArray(personalityTagLibrary), + hobbyLibrary: parseJsonArray(hobbyLibrary), + birthday: normalizeDate(birthday) + } +``` + +### Step 2: services/userProfile.js 请求提交增加库字段 + +修改 `transformToBackendFormat` 函数。 + +原解构: + +```javascript + const { + id, + nickname, + gender, + zodiac, + profession, + mbti, + hobbies, + childhood, + joy, + low, + future, + city, + industry, + company, + personalityTags, + birthday + } = frontendData +``` + +改为: + +```javascript + const { + id, + nickname, + gender, + zodiac, + profession, + mbti, + hobbies, + childhood, + joy, + low, + future, + city, + industry, + company, + personalityTags, + personalityTagLibrary, + hobbyLibrary, + birthday + } = frontendData +``` + +原返回对象末尾: + +```javascript + personalityTags: Array.isArray(personalityTags) ? JSON.stringify(personalityTags) : personalityTags, + birthday: birthday || null + } +``` + +改为: + +```javascript + personalityTags: Array.isArray(personalityTags) ? JSON.stringify(personalityTags) : personalityTags, + personalityTagLibrary: Array.isArray(personalityTagLibrary) ? JSON.stringify(personalityTagLibrary) : personalityTagLibrary, + hobbyLibrary: Array.isArray(hobbyLibrary) ? JSON.stringify(hobbyLibrary) : hobbyLibrary, + birthday: birthday || null + } +``` + +### Step 3: stores/app.js registrationData 增加库字段 + +修改 `mini-program/src/stores/app.js`。 + +原 `state.registrationData`: + +```javascript + registrationData: { + nickname: '', + gender: '', + mbti: '', + zodiac: '', + profession: '', + hobbies: [], + childhood: { date: '', text: '' }, + joy: { date: '', text: '' }, + low: { date: '', text: '' }, + future: { vision: '', ideal: '' }, + city: '', + industry: '', + company: '', + personalityTags: [], + birthday: '' + } +``` + +改为: + +```javascript + registrationData: { + nickname: '', + gender: '', + mbti: '', + zodiac: '', + profession: '', + hobbies: [], + childhood: { date: '', text: '' }, + joy: { date: '', text: '' }, + low: { date: '', text: '' }, + future: { vision: '', ideal: '' }, + city: '', + industry: '', + company: '', + personalityTags: [], + personalityTagLibrary: [], + hobbyLibrary: [], + birthday: '' + } +``` + +### Step 4: pages.json 注册 tag-manage 路由 + +修改 `mini-program/src/pages.json`。 + +在 `pages/onboarding/index` 条目后面插入新页面: + +```json + { + "path": "pages/onboarding/index", + "style": { + "navigationStyle": "custom" + } + }, + { + "path": "pages/onboarding/tag-manage", + "style": { + "navigationStyle": "custom", + "navigationBarTitleText": "管理标签" + } + }, +``` + +### Step 5: 提交 + +```bash +git add mini-program/src/services/userProfile.js \ + mini-program/src/stores/app.js \ + mini-program/src/pages.json +git commit -m "feat:前端数据层与路由注册支持标签库字段" +``` + +--- + +## Task 3: 编辑资料页数据初始化与兼容 + +**目标:** 编辑资料页 `form` 持有两个库字段,`syncFromStore` 加载库数据并对老用户做兼容初始化。 + +**Files:** +- Modify: `mini-program/src/pages/onboarding/index.vue` + +### Step 1: form reactive 增加库字段 + +修改 `mini-program/src/pages/onboarding/index.vue`。 + +原 `form`: + +```javascript +const form = reactive({ + nickname: '', + gender: '', + zodiac: '', + mbti: '', + profession: '', + city: '', + industry: '', + company: '', + personalityTags: [], + hobbies: [], + childhood: { date: '', text: '' }, + joy: { date: '', text: '' }, + low: { date: '', text: '' }, + future: { vision: '', ideal: '' } +}) +``` + +改为: + +```javascript +const form = reactive({ + nickname: '', + gender: '', + zodiac: '', + mbti: '', + profession: '', + city: '', + industry: '', + company: '', + personalityTags: [], + hobbies: [], + personalityTagLibrary: [], + hobbyLibrary: [], + childhood: { date: '', text: '' }, + joy: { date: '', text: '' }, + low: { date: '', text: '' }, + future: { vision: '', ideal: '' } +}) +``` + +### Step 2: syncFromStore 增加库字段加载与老用户兼容初始化 + +替换原 `syncFromStore` 函数为: + +```javascript +/** + * 合并预设数组与已选数组,去重返回完整库 + */ +const buildInitialLibrary = (preset, selected) => { + const library = [...preset] + if (Array.isArray(selected)) { + selected.forEach((tag) => { + if (!library.includes(tag)) library.push(tag) + }) + } + return library +} + +const syncFromStore = () => { + const source = store.userProfile || store.registrationData || {} + + // 性格标签库:后端有则用后端,否则用预设 + 已选合并(老用户兼容) + const personalityTagLibrary = Array.isArray(source.personalityTagLibrary) && source.personalityTagLibrary.length > 0 + ? [...source.personalityTagLibrary] + : buildInitialLibrary(personalityTags, source.personalityTags) + + // 兴趣爱好库:后端有则用后端,否则用预设 + 已选合并(老用户兼容) + const hobbyLibrary = Array.isArray(source.hobbyLibrary) && source.hobbyLibrary.length > 0 + ? [...source.hobbyLibrary] + : buildInitialLibrary(hobbyOptions, source.hobbies) + + 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] : [], + personalityTagLibrary, + hobbyLibrary, + 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 || '' +} +``` + +### Step 3: 提交 + +```bash +git add mini-program/src/pages/onboarding/index.vue +git commit -m "feat:编辑资料页 form 增加库字段并对老用户兼容初始化" +``` + +--- + +## Task 4: 编辑资料页标签区 UI 重构 + +**目标:** 性格标签、兴趣爱好两个 panel 重构为基于库数据计算的有序展示列表,标题行右侧新增"管理"按钮,末尾"+ 添加"绑定新增弹窗。 + +**Files:** +- Modify: `mini-program/src/pages/onboarding/index.vue` + +### Step 1: 新增两个 computed 计算有序展示列表 + +在 `script setup` 中(`syncFromStore` 函数之后)新增: + +```javascript +/** + * 性格标签有序展示列表: + * 1. 新增的自定义标签(不在预设中)按库顺序排在最前 + * 2. 已选中的其他标签紧随其后 + * 3. 其余未选中的预设标签排在最后 + */ +const displayPersonalityTags = computed(() => { + const selectedSet = new Set(form.personalityTags) + const presetSet = new Set(personalityTags) + + // 自定义标签:不在预设数组中的标签,按库顺序 + const customTags = form.personalityTagLibrary.filter((tag) => !presetSet.has(tag)) + // 已选中的预设标签 + const selectedPreset = form.personalityTagLibrary.filter( + (tag) => presetSet.has(tag) && selectedSet.has(tag) + ) + // 未选中的预设标签 + const unselectedPreset = form.personalityTagLibrary.filter( + (tag) => presetSet.has(tag) && !selectedSet.has(tag) + ) + + return [...customTags, ...selectedPreset, ...unselectedPreset] +}) + +/** + * 兴趣爱好有序展示列表(逻辑同上) + */ +const displayHobbyTags = computed(() => { + const selectedSet = new Set(form.hobbies) + const presetSet = new Set(hobbyOptions) + + const customTags = form.hobbyLibrary.filter((tag) => !presetSet.has(tag)) + const selectedPreset = form.hobbyLibrary.filter( + (tag) => presetSet.has(tag) && selectedSet.has(tag) + ) + const unselectedPreset = form.hobbyLibrary.filter( + (tag) => presetSet.has(tag) && !selectedSet.has(tag) + ) + + return [...customTags, ...selectedPreset, ...unselectedPreset] +}) +``` + +### Step 2: 替换性格标签 panel 模板 + +原性格标签模板(约在 129-145 行): + +```html + + + + 性格标签 + (最多选择5个) + + + {{ tag }} + + 添加标签 + + +``` + +替换为: + +```html + + + + + 性格标签 + (最多选择5个) + + 管理 + + + {{ tag }} + + 添加标签 + + +``` + +### Step 3: 替换兴趣爱好 panel 模板 + +原兴趣爱好模板(约在 147-165 行): + +```html + + + + + 兴趣爱好 + (最多选择5个) + + + 自定义兴趣 + + + {{ tag }} + + +``` + +替换为: + +```html + + + + + 兴趣爱好 + (最多选择5个) + + 管理 + + + {{ tag }} + + 添加兴趣 + + +``` + +### Step 4: 新增管理页跳转与标签新增函数(替换旧 addCustomHobby) + +在 `script setup` 中删除原 `addCustomHobby` 函数,替换为以下三个函数: + +```javascript +/** + * 跳转到管理标签页面 + */ +const goTagManage = (type) => { + uni.navigateTo({ + url: `/pages/onboarding/tag-manage?type=${type}` + }) +} + +/** + * 新增自定义标签(性格标签 / 兴趣爱好通用) + * @param {string} type 'personality' 或 'hobby' + */ +const addCustomTag = (type) => { + const isPersonality = type === 'personality' + const library = isPersonality ? form.personalityTagLibrary : form.hobbyLibrary + const selected = isPersonality ? form.personalityTags : form.hobbies + const title = isPersonality ? '添加性格标签' : '添加兴趣爱好' + + uni.showModal({ + title, + editable: true, + placeholderText: '请输入标签(最多8个字符)', + success: (res) => { + const value = String(res.content || '').trim() + if (!res.confirm) return + if (!value) { + uni.showToast({ title: '请输入标签内容', icon: 'none' }) + return + } + if (value.length > 8) { + uni.showToast({ title: '标签最多8个字符', icon: 'none' }) + return + } + if (library.includes(value)) { + uni.showToast({ title: '标签已存在', icon: 'none' }) + return + } + // 新标签插入到库数组最前面 + library.unshift(value) + // 若已选未满 5 个,自动加入已选;否则仅提示 + if (selected.length >= 5) { + uni.showToast({ title: '已达上限,仅加入标签库', icon: 'none' }) + return + } + selected.push(value) + } + }) +} +``` + +### Step 5: 样式调整 — "管理"链接样式 + +在 ` +``` + +### Step 2: 启动 H5 开发服务器预览 + +```bash +cd mini-program +npm run dev:h5 +``` + +在浏览器中打开 `http://localhost:5173`,登录后进入编辑资料页,验证: +- 性格标签、兴趣爱好 panel 标题右侧显示"管理"链接。 +- 点击"管理"跳转管理页,页面标题显示"管理性格标签"或"管理兴趣爱好"。 +- 每行标签右侧显示 × 按钮,左滑显示"删除"按钮。 +- 点击 × 或滑动后点击删除,弹出确认对话框。 +- 确认后标签从列表消失。 + +### Step 3: 提交 + +```bash +git add mini-program/src/pages/onboarding/tag-manage.vue +git commit -m "feat:新增管理标签独立页面,支持左滑与 × 按钮删除" +``` + +--- + +## Task 7: 端到端浏览器验证 + +**目标:** 在浏览器 H5 模式下覆盖 spec 所有 10 条验收标准。 + +### Step 1: 重启后端(已修改 Java 实体) + +```bash +python dev-services.py restart backend-single +``` + +等待启动完成,查看终端无报错。 + +### Step 2: 启动前端 + +```bash +python dev-services.py start mini-program +``` + +在浏览器中打开 `http://localhost:5284`。 + +### Step 3: 逐条验证验收标准 + +| # | 验收标准 | 验证方法 | +|---|---|---| +| 1 | 标签容器高度自适应撑开,无截断 | 添加多个标签后观察面板是否自动增高 | +| 2 | 新增标签出现在列表最前面 | 点击"+ 添加标签"/"+ 添加兴趣",弹窗输入后观察位置 | +| 3 | 重复输入同名标签,toast 提示"标签已存在" | 输入已存在的标签名,验证 toast | +| 4 | 空输入、超长输入 toast 提示对应原因 | 分别提交空内容、9 字符内容,验证 toast | +| 5 | "管理"按钮跳转管理页,列表展示全部标签 | 点击"管理",验证页面标题与列表 | +| 6 | 管理页左滑或点 × 删除标签,二次确认后消失 | 分别测试左滑、点 ×,确认 modal 弹出 | +| 7 | 删除已在"已选"中的标签,返回编辑资料页后已选同步移除 | 先选中某标签,管理页删除后返回观察 | +| 8 | 保存后重新进入编辑资料页,新增标签仍在最前 | 保存→重新进入,验证顺序 | +| 9 | 老用户首次进入编辑资料页,已选标签保留,预设标签可见 | 使用无新字段用户登录,验证库初始化 | +| 10 | 接口失败时,页面保留原状态并 toast 真实错误 | 断网或后端停止后保存,验证 toast 真实错误 | + +### Step 4: 最终提交 + +所有验收标准通过后,合并所有改动提交(如果之前未 commit): + +```bash +git status +``` + +确认工作区干净,无遗留未提交改动。 + +--- + +## 执行顺序建议 + +1. Task 1(后端)→ 重启后端 +2. Task 2(前端数据层)→ Task 3(编辑页数据初始化)→ Task 4(编辑页 UI)→ Task 5(保存 + 事件)→ Task 6(管理页) +3. Task 7(端到端验证) + +Task 1 必须先完成并重启后端,否则后续前端请求会拿到旧响应结构。Task 2~6 顺序基本线性,每完成一个 task 可启动 H5 预览一次确认无回归。