Browse Source

docs(plan): 家庭名称编辑与首页问候条/咨询消息实现计划

Agnes Agent 1 week ago
parent
commit
ca58081cea

+ 452 - 0
docs/superpowers/plans/2026-11-01-family-members-rename.md

@@ -0,0 +1,452 @@
+# 家庭成员页可编辑家庭名称 实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 家庭成员页 `pages/profile-extra/family-members.vue` 顶部家庭名称可被家庭管理员编辑。
+
+**架构:** 新增 `POST /api/family/member/rename-family`(`FamilyMembersController` → `FamilyMemberService.renameFamily`),鉴权复用 `isFamilyAdminOperator`(家庭创建者或成员 `is_admin=1`);前端在头部加编辑图标 + 自定义弹窗,保存后回填名称。
+
+**技术栈:** Spring Boot 2.7 + MyBatis-Plus(后端),uni-app 小程序 Vue 2(前端)。
+
+**规格:** `docs/superpowers/specs/2026-11-01-family-members-rename-design.md`
+
+---
+
+### 任务 1:后端 Service `renameFamily` + 单元测试
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java`(在 `isFamilyAdminOperator` 附近新增方法)
+- 创建:`cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyMemberServiceRenameTest.java`
+
+- [ ] **步骤 1:编写失败的测试**
+
+```java
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.FamilyMemberService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class FamilyMemberServiceRenameTest {
+
+    @Mock private FamilyMemberMapper familyMemberMapper;
+    @Mock private UserMapper userMapper;
+    @Mock private FamilyMapper familyMapper;
+
+    @InjectMocks private FamilyMemberService service;
+
+    private User user(Long id, Long familyId) {
+        User u = new User(); u.setId(id); u.setFamilyId(familyId); return u;
+    }
+
+    @Test void rename_管理员_成功更新() {
+        when(userMapper.selectById(10L)).thenReturn(user(10L, 7L));
+        Family f = new Family(); f.setId(7L); f.setName("旧家庭"); f.setCreatorId(10L);
+        when(familyMapper.selectById(7L)).thenReturn(f);
+
+        String out = service.renameFamily(10L, "  新家庭  ");
+        assertEquals("新家庭", out);
+        verify(familyMapper).updateById(argThat(fam -> "新家庭".equals(fam.getName())));
+    }
+
+    @Test void rename_非管理员_抛错() {
+        when(userMapper.selectById(20L)).thenReturn(user(20L, 7L));
+        Family f = new Family(); f.setId(7L); f.setName("旧家庭"); f.setCreatorId(10L);
+        when(familyMapper.selectById(7L)).thenReturn(f);
+        when(familyMemberMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
+
+        assertThrows(RuntimeException.class, () -> service.renameFamily(20L, "新家庭"));
+    }
+
+    @Test void rename_空白名_抛错() {
+        assertThrows(RuntimeException.class, () -> service.renameFamily(10L, "   "));
+    }
+
+    @Test void rename_超长名_抛错() {
+        assertThrows(RuntimeException.class, () -> service.renameFamily(10L, "一二三四五六七八九十一二三四五六七八九十一"));
+    }
+}
+```
+
+- [ ] **步骤 2:运行测试验证失败**
+
+运行:`mvn test -Dtest=FamilyMemberServiceRenameTest -DfailIfNoTests=false`
+预期:FAIL(编译失败——`renameFamily` 方法不存在)
+
+- [ ] **步骤 3:编写最少实现代码**
+
+在 `FamilyMemberService.java` 的 `isFamilyAdminOperator`(约 line 1046)之后新增:
+
+```java
+    /**
+     * 修改家庭名称(仅家庭管理员:家庭创建者或成员记录 is_admin=1)。
+     * 返回更新后的家庭名称。
+     */
+    public String renameFamily(Long userId, String name) {
+        if (userId == null || userId <= 0) {
+            throw new IllegalArgumentException("请先登录");
+        }
+        String newName = name == null ? "" : name.trim();
+        if (newName.isEmpty()) {
+            throw new IllegalArgumentException("家庭名称不能为空");
+        }
+        if (newName.length() > 20) {
+            throw new IllegalArgumentException("家庭名称不能超过20个字");
+        }
+        User operator = userMapper.selectById(userId);
+        if (operator == null) {
+            throw new IllegalArgumentException("用户不存在");
+        }
+        Long familyId = operator.getFamilyId();
+        if (familyId == null) {
+            throw new IllegalArgumentException("请先创建或加入家庭");
+        }
+        if (!isFamilyAdminOperator(userId, familyId)) {
+            throw new IllegalArgumentException("仅家庭管理员可修改家庭名称");
+        }
+        Family family = familyMapper.selectById(familyId);
+        if (family == null) {
+            throw new IllegalArgumentException("家庭不存在");
+        }
+        family.setName(newName);
+        family.setUpdatedAt(new Date());
+        familyMapper.updateById(family);
+        return newName;
+    }
+```
+
+`User`/`Family`/`Date` 已在本文件 import(line 8/15/32),无需新增 import。
+
+- [ ] **步骤 4:运行测试验证通过**
+
+运行:`mvn test -Dtest=FamilyMemberServiceRenameTest -DfailIfNoTests=false`
+预期:PASS
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyMemberServiceRenameTest.java
+git commit -m "feat(family): 新增 renameFamily 服务(仅家庭管理员可改家庭名称)"
+```
+
+---
+
+### 任务 2:后端 Controller 接口 + 测试
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyMembersController.java`(在 `updateMember` 后新增)
+- 创建:`cfc-backend/src/test/java/com/etotem/cfc/controller/family/FamilyMembersControllerRenameFamilyTest.java`
+
+- [ ] **步骤 1:编写失败的测试**
+
+```java
+package com.etotem.cfc.controller.family;
+
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.FamilyMemberService;
+import com.etotem.cfc.service.UnlockGateService;
+import com.etotem.cfc.service.UserService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.MediaType;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class FamilyMembersControllerRenameFamilyTest {
+
+    @Mock private FamilyMemberService familyMemberService;
+    @Mock private UserService userService;
+    @Mock private UnlockGateService unlockGateService;
+    @Mock private UserMapper userMapper;
+
+    private MockMvc mockMvc;
+
+    @BeforeEach
+    public void setUp() {
+        MockitoAnnotations.openMocks(this);
+        FamilyMembersController controller = new FamilyMembersController();
+        ReflectionTestUtils.setField(controller, "familyMemberService", familyMemberService);
+        ReflectionTestUtils.setField(controller, "unlockGateService", unlockGateService);
+        ReflectionTestUtils.setField(controller, "userService", userService);
+        ReflectionTestUtils.setField(controller, "userMapper", userMapper);
+        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+    }
+
+    @Test
+    public void renameFamily_success() throws Exception {
+        when(familyMemberService.renameFamily(1L, "新家庭")).thenReturn("新家庭");
+
+        mockMvc.perform(post("/api/family/member/rename-family")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{\"name\":\"新家庭\"}")
+                        .requestAttr("userId", 1L))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").value("新家庭"));
+
+        verify(familyMemberService).renameFamily(1L, "新家庭");
+    }
+
+    @Test
+    public void renameFamily_notAdmin_returnsError() throws Exception {
+        when(familyMemberService.renameFamily(1L, "新家庭"))
+                .thenThrow(new IllegalArgumentException("仅家庭管理员可修改家庭名称"));
+
+        mockMvc.perform(post("/api/family/member/rename-family")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{\"name\":\"新家庭\"}")
+                        .requestAttr("userId", 1L))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(500))
+                .andExpect(jsonPath("$.message").value("仅家庭管理员可修改家庭名称"));
+    }
+}
+```
+
+- [ ] **步骤 2:运行测试验证失败**
+
+运行:`mvn test -Dtest=FamilyMembersControllerRenameFamilyTest -DfailIfNoTests=false`
+预期:FAIL(路由不存在,404)
+
+- [ ] **步骤 3:编写最少实现代码**
+
+在 `FamilyMembersController.java` 的 `updateMember` 方法之后新增:
+
+```java
+    @Operation(summary = "修改家庭名称(仅家庭管理员)")
+    @PostMapping("/rename-family")
+    public Result<String> renameFamily(@RequestBody Map<String, Object> body,
+                                       @RequestAttribute("userId") Long userId) {
+        if (userId == null || userId <= 0) {
+            return Result.error(401, "请先登录");
+        }
+        String name = body != null && body.get("name") != null ? body.get("name").toString() : null;
+        try {
+            String newName = familyMemberService.renameFamily(userId, name);
+            return Result.success(newName);
+        } catch (IllegalArgumentException e) {
+            return Result.error(e.getMessage());
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+```
+
+`Map`/`Result`/`RequestBody`/`RequestAttribute`/`PostMapping`/`Operation` 均已 import。
+
+- [ ] **步骤 4:运行测试验证通过**
+
+运行:`mvn test -Dtest=FamilyMembersControllerRenameFamilyTest -DfailIfNoTests=false`
+预期:PASS
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyMembersController.java cfc-backend/src/test/java/com/etotem/cfc/controller/family/FamilyMembersControllerRenameFamilyTest.java
+git commit -m "feat(family): 新增 POST /api/family/member/rename-family 接口"
+```
+
+---
+
+### 任务 3:前端 api.js + 家庭成员页弹窗
+
+**文件:**
+- 修改:`cfc-frontend/utils/api.js`(`updateFamilyMember` 之后新增)
+- 修改:`cfc-frontend/pages/profile-extra/family-members.vue`
+
+- [ ] **步骤 1:api.js 新增封装**
+
+在 `utils/api.js` 的 `updateFamilyMember`(约 line 692)之后新增:
+
+```js
+// 修改家庭名称(仅家庭管理员)
+export const renameFamily = (data) => {
+  return request('/api/family/member/rename-family', 'POST', data)
+}
+```
+
+- [ ] **步骤 2:family-members.vue 头部加编辑图标**
+
+模板头部(line 7-10)改为:
+
+```html
+<view class="family-header-text">
+  <view class="family-name-row">
+    <text class="family-name">{{ familyName || '我的家庭' }}</text>
+    <text class="family-name-edit" v-if="isAdmin" @click="openRenameModal">✏️</text>
+  </view>
+  <text class="family-member-count">{{ filteredMembers.length }} 位成员</text>
+</view>
+```
+
+- [ ] **步骤 3:data 新增状态**
+
+在 `data()` 返回对象的 `permGranting: false,` 之后新增:
+
+```js
+      // ===== 家庭名称编辑弹窗状态 =====
+      showRenameModal: false,
+      renameInput: '',
+      renameSaving: false,
+```
+
+- [ ] **步骤 4:methods 新增方法**
+
+在 `closeReportPermission()` 之后新增:
+
+```js
+    // ===== 家庭名称编辑(仅管理员) =====
+    openRenameModal() {
+      this.renameInput = this.familyName || ''
+      this.showRenameModal = true
+    },
+    closeRenameModal() {
+      this.showRenameModal = false
+    },
+    saveRename() {
+      var self = this
+      if (this.renameSaving) return
+      var name = (this.renameInput || '').trim()
+      if (!name) {
+        uni.showToast({ title: '家庭名称不能为空', icon: 'none' })
+        return
+      }
+      this.renameSaving = true
+      renameFamily({ name: name }).then(function(res) {
+        self.renameSaving = false
+        if (res && res.code === 200) {
+          self.familyName = res.data
+          self.showRenameModal = false
+          uni.showToast({ title: '已更新', icon: 'success' })
+        } else {
+          uni.showToast({ title: (res && res.message) || '更新失败', icon: 'none' })
+        }
+      }).catch(function() {
+        self.renameSaving = false
+        uni.showToast({ title: '更新失败', icon: 'none' })
+      })
+    },
+```
+
+并在 import 区(line 180-184 的 `reportAppEnter` 之后)加入:
+
+```js
+  renameFamily,
+```
+
+- [ ] **步骤 5:新增弹窗模板**
+
+在「报告权限管理弹窗」闭合 `</view>`(`showPermModal` 的 `</view>`,约 line 151)之后、底部操作栏之前新增:
+
+```html
+    <!-- ===== 编辑家庭名称弹窗(仅管理员) ===== -->
+    <view class="modal-mask" v-if="showRenameModal" @click="closeRenameModal">
+      <view class="rename-modal-card" @click.stop>
+        <view class="perm-header">
+          <text class="perm-title">✏️ 编辑家庭名称</text>
+          <text class="perm-close" @click="closeRenameModal">×</text>
+        </view>
+        <input class="rename-input" v-model="renameInput" maxlength="20" placeholder="请输入家庭名称" />
+        <view class="rename-actions">
+          <button class="rename-btn rename-cancel" @click="closeRenameModal">取消</button>
+          <button class="rename-btn rename-save" :loading="renameSaving" @click="saveRename">保存</button>
+        </view>
+      </view>
+    </view>
+```
+
+- [ ] **步骤 6:新增样式**
+
+在 `<style>` 中追加:
+
+```scss
+/* ===== 家庭名称编辑 ===== */
+.family-name-row { display: flex; align-items: center; }
+.family-name-edit { margin-left: 10rpx; font-size: calc(26rpx * var(--font-scale, 1)); }
+.rename-modal-card { width: 600rpx; background: $app-surface; border-radius: 24rpx; padding: 40rpx; }
+.rename-input { border: 1rpx solid $facade-border-soft; border-radius: 12rpx; padding: 20rpx; font-size: calc(30rpx * var(--font-scale, 1)); margin-top: 24rpx; }
+.rename-actions { display: flex; gap: 20rpx; margin-top: 32rpx; }
+.rename-btn { flex: 1; border-radius: 24rpx; font-size: calc(28rpx * var(--font-scale, 1)); }
+.rename-cancel { background: $facade-fill-2; color: $facade-text-body; }
+.rename-save { background: $brand-primary; color: $app-surface; }
+```
+
+(`.modal-mask`/`.perm-header`/`.perm-title`/`.perm-close` 已存在,复用。)
+
+- [ ] **步骤 7:前端校验**
+
+运行:`node scripts/verify-vue-compile.js pages/profile-extra/family-members.vue`
+预期:模板/样式/脚本三项 PASS
+运行:`node scripts/audit-mp-antipatterns.js`
+预期:ERROR 0
+
+- [ ] **步骤 8:Commit**
+
+```bash
+git add cfc-frontend/utils/api.js cfc-frontend/pages/profile-extra/family-members.vue
+git commit -m "feat(family): 家庭成员页支持编辑家庭名称(管理员)"
+```
+
+---
+
+### 任务 4:接口文档同步
+
+**文件:**
+- 修改:`docs/superpowers/api/API_REFERENCE.md`
+
+- [ ] **步骤 1:记录新接口**
+
+在 API_REFERENCE.md 家庭成员接口表(新增成员相关接口表格)追加一行:
+
+```markdown
+| 修改家庭名称 | `POST /api/family/member/rename-family` | 仅家庭管理员可改,body `{name}`,返回新名称 |
+```
+
+- [ ] **步骤 2:Commit**
+
+```bash
+git add docs/superpowers/api/API_REFERENCE.md
+git commit -m "docs(api): 记录 /api/family/member/rename-family 接口"
+```
+
+---
+
+### 任务 5:全量编译验证
+
+- [ ] **步骤 1:后端编译**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 2:前端全站校验**
+
+运行:`cd cfc-frontend && node scripts/verify-vue-compile.js`
+预期:全部 PASS
+
+> 注:打包必须由用户在 HBuilderX 完成(AGENTS.md 规范),Agent 不执行 build。

+ 392 - 0
docs/superpowers/plans/2026-11-01-home-greet-shortcut-and-my-messages.md

@@ -0,0 +1,392 @@
+# 首页问候条快捷入口 + 小铃铛进入「我的咨询消息」实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 家长首页问候条可点击跳转家庭成员页;小铃铛进入家长自己的咨询消息列表(修复"只有成长规划师可以查看"报错)。
+
+**架构:** 后端增强已有 `POST /api/guide/messages/my`,按 `teacherId` 回填 `teacherName`;前端新增 `pages/profile/my-messages.vue` 咨询消息列表页,`goNotifications` 改指向该页;问候条整体可点击跳 `/pages/profile-extra/family-members`(铃铛 `@click.stop`)。
+
+**技术栈:** Spring Boot 2.7 + MyBatis-Plus(后端),uni-app 小程序 Vue 2(前端)。
+
+**规格:** `docs/superpowers/specs/2026-11-01-home-greet-shortcut-and-my-messages-design.md`
+
+---
+
+### 任务 1:后端 `TeacherMessage.teacherName` + `/my` 回显
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/TeacherMessage.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/guide/TeacherMessageController.java`
+- 创建:`cfc-backend/src/test/java/com/etotem/cfc/controller/guide/TeacherMessageControllerMyTest.java`
+
+- [ ] **步骤 1:编写失败的测试**
+
+```java
+package com.etotem.cfc.controller.guide;
+
+import com.etotem.cfc.entity.TeacherMessage;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.TeacherMessageService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.MediaType;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.Arrays;
+import java.util.Date;
+
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class TeacherMessageControllerMyTest {
+
+    @Mock private TeacherMessageService teacherMessageService;
+    @Mock private UserMapper userMapper;
+
+    private MockMvc mockMvc;
+
+    @BeforeEach
+    public void setUp() {
+        MockitoAnnotations.openMocks(this);
+        TeacherMessageController controller = new TeacherMessageController();
+        ReflectionTestUtils.setField(controller, "teacherMessageService", teacherMessageService);
+        ReflectionTestUtils.setField(controller, "userMapper", userMapper);
+        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+    }
+
+    @Test
+    public void myMessages_enrichesTeacherName() throws Exception {
+        TeacherMessage msg = new TeacherMessage();
+        msg.setId(1L); msg.setParentId(10L); msg.setTeacherId(5L);
+        msg.setContent("孩子最近注意力不集中"); msg.setReplyContent("建议增加运动量");
+        msg.setStatus("replied"); msg.setCreatedAt(new Date());
+        when(teacherMessageService.getMessagesByParent(10L)).thenReturn(Arrays.asList(msg));
+
+        User teacher = new User(); teacher.setId(5L); teacher.setNickname("王规划师");
+        when(userMapper.selectById(5L)).thenReturn(teacher);
+
+        mockMvc.perform(post("/api/guide/messages/my")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{}")
+                        .requestAttr("userId", 10L))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data[0].teacherName").value("王规划师"));
+
+        verify(teacherMessageService).getMessagesByParent(10L);
+    }
+}
+```
+
+- [ ] **步骤 2:运行测试验证失败**
+
+运行:`mvn test -Dtest=TeacherMessageControllerMyTest -DfailIfNoTests=false`
+预期:FAIL(`teacherName` 为 null,断言失败)
+
+- [ ] **步骤 3:实体新增瞬态字段**
+
+`TeacherMessage.java` 末尾新增:
+
+```java
+    @TableField(exist = false)
+    private String teacherName;   // 规划师昵称(回显用,不入库)
+```
+
+(`@TableField` 已 import。)
+
+- [ ] **步骤 4:Controller `/my` 回填**
+
+`TeacherMessageController.getMyMessages`(约 line 132-136)改为:
+
+```java
+    @Operation(summary = "家庭主账号获取咨询记录")
+    @PostMapping("/my")
+    public Result<List<TeacherMessage>> getMyMessages(@RequestAttribute("userId") Long parentId) {
+        List<TeacherMessage> messages = teacherMessageService.getMessagesByParent(parentId);
+        // 回填规划师昵称(与 /list 填充家庭主账号昵称同模式)
+        for (TeacherMessage msg : messages) {
+            if (msg.getTeacherId() != null) {
+                User teacher = userMapper.selectById(msg.getTeacherId());
+                if (teacher != null) {
+                    msg.setTeacherName(teacher.getNickname());
+                }
+            }
+        }
+        return Result.success(messages);
+    }
+```
+
+`User` 已 import(line 7)。
+
+- [ ] **步骤 5:运行测试验证通过**
+
+运行:`mvn test -Dtest=TeacherMessageControllerMyTest -DfailIfNoTests=false`
+预期:PASS
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/TeacherMessage.java cfc-backend/src/main/java/com/etotem/cfc/controller/guide/TeacherMessageController.java cfc-backend/src/test/java/com/etotem/cfc/controller/guide/TeacherMessageControllerMyTest.java
+git commit -m "feat(guide): /api/guide/messages/my 回填规划师昵称 teacherName"
+```
+
+---
+
+### 任务 2:前端 api.js + 新页面 my-messages.vue + pages.json
+
+**文件:**
+- 修改:`cfc-frontend/utils/api.js`
+- 创建:`cfc-frontend/pages/profile/my-messages.vue`
+- 修改:`cfc-frontend/pages.json`(`pages/profile` 分包内 `message-history` 之后注册)
+
+- [ ] **步骤 1:api.js 新增封装**
+
+在 `utils/api.js` 的 `getGuideMessages`(约 line 1444)之后新增:
+
+```js
+// 家庭主账号获取自己的咨询消息
+export const getMyGuideMessages = () => {
+  return request('/api/guide/messages/my', 'POST')
+}
+```
+
+- [ ] **步骤 2:pages.json 注册页面**
+
+在 `pages/profile` 分包 `message-history` 条目(line 1898-1903)之后新增:
+
+```json
+        {
+          "path": "my-messages",
+          "style": {
+            "navigationBarTitleText": "我的咨询消息"
+          }
+        }
+```
+
+- [ ] **步骤 3:创建页面 `pages/profile/my-messages.vue`**
+
+```html
+<template>
+  <view class="container" :style="{ '--font-scale': fontScale }">
+    <view v-if="loading && messages.length === 0" class="state-box">
+      <text class="state-text">加载中...</text>
+    </view>
+    <view v-else-if="messages.length === 0" class="state-box">
+      <text class="state-icon">💬</text>
+      <text class="state-text">暂无咨询消息</text>
+    </view>
+    <view v-else class="msg-list">
+      <view class="msg-item" v-for="msg in messages" :key="msg.id">
+        <view class="msg-head">
+          <text class="msg-teacher">{{ msg.teacherName || '规划师' }}</text>
+          <text class="msg-status" :class="msg.status === 'replied' ? 'st-replied' : 'st-pending'">
+            {{ msg.status === 'replied' ? '已回复' : '待回复' }}
+          </text>
+        </view>
+        <view class="msg-body">
+          <text class="msg-label">我的咨询</text>
+          <text class="msg-content">{{ msg.content }}</text>
+        </view>
+        <view class="msg-reply" v-if="msg.replyContent">
+          <text class="msg-label">规划师回复</text>
+          <text class="msg-content reply">{{ msg.replyContent }}</text>
+        </view>
+        <text class="msg-time">{{ formatTime(msg.createdAt) }}</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getMyGuideMessages } from '../../utils/api.js'
+import { parseDate } from '../../utils/format.js'
+import fontScaleMixin from '@/mixins/font-scale.js'
+
+export default {
+  mixins: [fontScaleMixin],
+  data() {
+    return {
+      loading: true,
+      messages: []
+    }
+  },
+  onLoad() {
+    this.loadMessages()
+  },
+  methods: {
+    async loadMessages() {
+      try {
+        var res = await getMyGuideMessages()
+        this.messages = (res && res.code === 200 && res.data instanceof Array) ? res.data : []
+      } catch (e) {
+        this.messages = []
+      } finally {
+        this.loading = false
+      }
+    },
+    formatTime(dateStr) {
+      if (!dateStr) return ''
+      var d = parseDate(dateStr)
+      if (!d) return ''
+      var now = new Date()
+      var diff = now - d
+      var minutes = Math.floor(diff / 60000)
+      if (minutes < 1) return '刚刚'
+      if (minutes < 60) return minutes + '分钟前'
+      var hours = Math.floor(minutes / 60)
+      if (hours < 24) return hours + '小时前'
+      var days = Math.floor(hours / 24)
+      if (days === 1) return '昨天'
+      if (days < 7) return days + '天前'
+      return (d.getMonth() + 1) + '月' + d.getDate() + '日'
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.container { min-height: 100vh; padding: $uni-space-page; background: $facade-fill-2; }
+.state-box { display: flex; flex-direction: column; align-items: center; padding: 120rpx 0; }
+.state-icon { font-size: calc(80rpx * var(--font-scale, 1)); margin-bottom: 20rpx; }
+.state-text { font-size: calc(26rpx * var(--font-scale, 1)); color: $facade-text-muted; }
+.msg-list { display: flex; flex-direction: column; gap: 20rpx; }
+.msg-item { background: $app-surface; border-radius: 20rpx; padding: 28rpx; }
+.msg-head { display: flex; align-items: center; justify-content: space-between; }
+.msg-teacher { font-size: calc(30rpx * var(--font-scale, 1)); font-weight: bold; color: $facade-text-strong; }
+.msg-status { font-size: calc(22rpx * var(--font-scale, 1)); padding: 4rpx 16rpx; border-radius: 20rpx; }
+.st-replied { color: $status-success; background: rgba(16,185,129,0.12); }
+.st-pending { color: $status-warning; background: rgba(251,191,36,0.14); }
+.msg-body { margin-top: 16rpx; }
+.msg-label { font-size: calc(22rpx * var(--font-scale, 1)); color: $facade-text-muted; display: block; margin-bottom: 6rpx; }
+.msg-content { font-size: calc(28rpx * var(--font-scale, 1)); color: $facade-text-body; display: block; line-height: 1.6; }
+.msg-reply { margin-top: 16rpx; padding: 16rpx; background: $facade-fill-ghost; border-radius: 12rpx; }
+.msg-content.reply { color: $facade-text-strong; }
+.msg-time { font-size: calc(22rpx * var(--font-scale, 1)); color: $facade-text-muted; display: block; margin-top: 12rpx; }
+</style>
+```
+
+- [ ] **步骤 4:前端校验**
+
+运行:`node scripts/verify-vue-compile.js pages/profile/my-messages.vue`
+预期:模板/样式/脚本三项 PASS
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-frontend/utils/api.js cfc-frontend/pages/profile/my-messages.vue cfc-frontend/pages.json
+git commit -m "feat(profile): 新增我的咨询消息页 my-messages"
+```
+
+---
+
+### 任务 3:index-home 问候条点击 + 小铃铛改向
+
+**文件:**
+- 修改:`cfc-frontend/pages/index-home/index.vue`
+
+- [ ] **步骤 1:问候条整体可点击 + 铃铛阻断**
+
+`greet-bar`(line 55)改为:
+
+```html
+<view class="greet-bar" @click="goFamilyMembers">
+```
+
+`greet-bell`(line 78)改为:
+
+```html
+<view class="greet-bell" @click.stop="goNotifications">
+```
+
+- [ ] **步骤 2:问候语区加箭头**
+
+`greet-main`(line 74-77)改为(包一层 `.greet-text` 列 + 右侧箭头,避免绝对定位与文字重叠):
+
+```html
+<view class="greet-main">
+  <view class="greet-text">
+    <text class="greet-title">{{ greetTitle }}</text>
+    <text class="greet-line">{{ greetLine }}</text>
+  </view>
+  <text class="greet-arrow">›</text>
+</view>
+```
+
+- [ ] **步骤 3:goFamilyMembers 方法 + goNotifications 改向**
+
+`goNotifications`(line 1811-1813)改为:
+
+```js
+    goNotifications: function() {
+      uni.navigateTo({ url: '/pages/profile/my-messages' })
+    },
+    // 问候条点击 → 家庭成员页
+    goFamilyMembers: function() {
+      this.navTo('/pages/profile-extra/family-members')
+    },
+```
+
+- [ ] **步骤 4:新增样式**
+
+`.greet-main`(line 3011)改为:
+
+```scss
+.greet-main { flex: 1; min-width: 0; display: flex; align-items: center; }
+.greet-text { flex: 1; min-width: 0; }
+.greet-arrow { margin-left: 8rpx; font-size: calc(36rpx * var(--font-scale, 1)); color: $facade-text-muted; }
+```
+
+- [ ] **步骤 5:前端校验**
+
+运行:`node scripts/verify-vue-compile.js pages/index-home/index.vue`
+预期:模板/样式/脚本三项 PASS
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-frontend/pages/index-home/index.vue
+git commit -m "feat(home): 问候条可点击进家庭成员页,小铃铛进我的咨询消息"
+```
+
+---
+
+### 任务 4:接口文档同步 + 全量验证
+
+**文件:**
+- 修改:`docs/superpowers/api/API_REFERENCE.md`
+
+- [ ] **步骤 1:记录 `/my` 增强**
+
+在 API_REFERENCE.md 中 `/api/guide/messages` 相关章节补充:
+
+```markdown
+- **`POST /api/guide/messages/my`**:家庭主账号获取自己的咨询记录。响应 `TeacherMessage` 列表,每条含 `teacherName`(规划师昵称,按 `teacherId` 从 `users` 表回填)。
+```
+
+- [ ] **步骤 2:后端全量编译**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 3:前端全站校验**
+
+运行:`cd cfc-frontend && node scripts/verify-vue-compile.js`
+预期:全部 PASS
+运行:`cd cfc-frontend && node scripts/audit-mp-antipatterns.js`
+预期:ERROR 0
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add docs/superpowers/api/API_REFERENCE.md
+git commit -m "docs(api): 记录 /api/guide/messages/my teacherName 回显"
+```
+
+> 注:打包必须由用户在 HBuilderX 完成(AGENTS.md 规范),Agent 不执行 build。