Browse Source

docs(plan): 家庭挑战修复计划按 momus 评审修正(完整方法体/登录角色门控/全员奖励发放/精确paths)

Xiaogang Liao 1 month ago
parent
commit
12bea1e25f
1 changed files with 153 additions and 66 deletions
  1. 153 66
      docs/superpowers/plans/2026-08-06-family-challenge-fix.md

+ 153 - 66
docs/superpowers/plans/2026-08-06-family-challenge-fix.md

@@ -2,7 +2,7 @@
 
 > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
 
-**目标:** 修复家庭挑战功能的 7 个断点(spec: `docs/superpowers/specs/2026-08-06-family-challenge-flow-design.md` §7),使挑战形成闭环:家长创建 → 孩子打卡(限次)→ 到期结算 → 奖励发放,并补齐角色校验与前端展示修复。
+**目标:** 修复家庭挑战功能的 6 个断点(spec: `docs/superpowers/specs/2026-08-06-family-challenge-flow-design.md` §7 的 #1-#6;**#7 autoCreate 无调用方明确排除**,属 spec §8"未来"功能,如需启用需另行 spec),使挑战形成闭环:家长创建 → 孩子打卡(限次)→ 到期结算 → 奖励发放,并补齐角色校验与前端展示修复。
 
 **架构:** 后端 cfc-backend 扩展 `FamilyChallengeService`/`FamilyChallengeController`:加角色校验(复用 JwtInterceptor 已设的 `role`/`familyId`/`currentMemberId` attribute)、打卡每日限次、结算时奖励发放(对接 PointsService 能量点)、列表查询时惰性结算。前端 cfc-frontend:`FamilyChallengeCard.vue` 加打卡按钮 + 修复 cancelled 状态显示、`challenge-manage.vue` 限制家长访问、`api.js` 已有封装不动。
 
@@ -36,6 +36,7 @@
 ```java
 package com.etotem.cfc.service;
 
+import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.FamilyChallenge;
 import com.etotem.cfc.entity.FamilyChallengeProgress;
 import com.etotem.cfc.mapper.FamilyChallengeMapper;
@@ -46,13 +47,15 @@ import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
-import org.springframework.test.util.ReflectionTestUtils;
 
 import java.util.Arrays;
 import java.util.Date;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.*;
 
 public class FamilyChallengeServiceTest {
@@ -61,6 +64,7 @@ public class FamilyChallengeServiceTest {
     @Mock private FamilyChallengeProgressMapper familyChallengeProgressMapper;
     @Mock private FamilyMemberMapper familyMemberMapper;
     @Mock private HealthScoreService healthScoreService;
+    @Mock private PointsService pointsService;
     @InjectMocks private FamilyChallengeService service;
 
     @BeforeEach
@@ -87,15 +91,20 @@ public class FamilyChallengeServiceTest {
 
         when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
         when(familyChallengeProgressMapper.selectOne(any())).thenReturn(existing);
+        // 归属校验通过:childId(100) 属于 familyId(10)
+        when(familyMemberMapper.selectCount(any())).thenReturn(1L);
 
-        var result = service.updateProgress(1L, 100L, 1);
+        Result<Void> result = service.updateProgress(1L, 100L, 1);
 
-        assertEquals("error", result.getCode());
+        // Result.error() 返回 code=500
+        assertEquals(Integer.valueOf(500), result.getCode());
         verify(familyChallengeProgressMapper, never()).updateById(any());
     }
 }
 ```
 
+> 注意:`Result.error()` 返回 `code=500`(Integer),`Result.success()` 返回 `code=200`(见 `common/Result.java`)。`familyMemberMapper.selectCount()` 返回 `Long`,mock 用 `thenReturn(1L)`。
+
 - [ ] **步骤 2:运行测试确认失败**
 
 运行:`cd cfc-backend && mvn test -Dtest=FamilyChallengeServiceTest#updateProgress_shouldRejectDuplicateCheckinSameDay -pl .`
@@ -183,7 +192,7 @@ git commit -m "fix(backend): 家庭挑战打卡每日限次 + memberId 归属校
 
 ---
 
-### 任务 2:后端 — 结算时奖励发放(能量点 + 徽章
+### 任务 2:后端 — 结算时奖励发放(能量点)
 
 **文件:**
 - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/FamilyChallengeService.java:134-182`(`checkAndSettle` 方法)
@@ -224,7 +233,7 @@ void checkAndSettle_shouldGrantPointsWhenCompleted() {
 }
 ```
 
-> 已核实:积分发放使用 `PointsService.awardSystemPoints(Long memberId, int amount, String reason)`(`service/PointsService.java:97`),`@Resource` 注入 `PointsService pointsService`。测试需 `@Mock private PointsService pointsService;`
+> 已核实:积分发放使用 `PointsService.awardSystemPoints(Long memberId, int amount, String reason)`(`service/PointsService.java:97`)。`@Mock private PointsService pointsService;` 已在任务 1 测试类中声明(同包 `com.etotem.cfc.service`,无需 import)
 
 - [ ] **步骤 2:运行测试确认失败**
 
@@ -233,29 +242,41 @@ void checkAndSettle_shouldGrantPointsWhenCompleted() {
 
 - [ ] **步骤 3:实现奖励发放**
 
-在 `checkAndSettle` 中,`isCompleted` 判定通过后、`challenge.setStatus("completed")` 之前插入:
+**3a. 先在 `FamilyChallengeService` 字段区新增注入**(在 `healthScoreService` 字段 L33 之后):
+
+```java
+@Resource
+private PointsService pointsService;
+```
+
+> `PointsService` 与 `FamilyChallengeService` 同包(`com.etotem.cfc.service`),无需 import。字段名 `pointsService` 与类型默认 Bean Name 一致。
+
+**3b. 在 `checkAndSettle` 中,`isCompleted` 判定通过后、`challenge.setStatus("completed")` 之前插入:**
 
 ```java
 // 发放奖励:能量点 + 标记成员完成
 if (isCompleted) {
     int points = challenge.getRewardPoints() != null ? challenge.getRewardPoints() : 0;
     for (FamilyChallengeProgress p : progresses) {
-        if (p.getProgressValue() != null && p.getProgressValue() >= challenge.getTargetValue()) {
-            if (!Boolean.TRUE.equals(p.getCompleted())) {
-                p.setCompleted(true);
-                p.setUpdatedAt(new Date());
-                familyChallengeProgressMapper.updateById(p);
-            }
-            if (points > 0) {
-                // 对接积分服务发放奖励(PointsService.awardSystemPoints,按 memberId 发放)
-                pointsService.awardSystemPoints(p.getChildId(), points,
-                        "家庭挑战完成奖励: " + challenge.getTitle());
-            }
+        if (!Boolean.TRUE.equals(p.getCompleted())) {
+            p.setCompleted(true);
+            p.setUpdatedAt(new Date());
+            familyChallengeProgressMapper.updateById(p);
+        }
+        if (points > 0) {
+            // 对接积分服务发放奖励(PointsService.awardSystemPoints,按 memberId 发放)
+            pointsService.awardSystemPoints(p.getChildId(), points,
+                    "家庭挑战完成奖励: " + challenge.getTitle());
         }
     }
 }
 ```
 
+> **为何对所有参与成员发放(不按个人 progressValue >= targetValue 判断)**:
+> - **aggregate 模式**(如"全家总运动 300 分钟",targetValue=300):挑战达成条件是家庭**合计**达标,个人进度几乎不可能达到 300 → 若按个人目标判断则**无人得奖**,违背"奖励发放"修复意图(spec §7 #3)。挑战整体达成后,所有**参与成员**(有 progress 记录)都应获得奖励。
+> - **all_members 模式**:挑战达成条件本就是**每个成员都达到** targetValue,结算时所有成员必然已达标 → 同样应全员发放。
+> - 两种模式下"有 progress 记录 = 参与了该挑战",统一全员标记完成 + 发放,逻辑一致且简单。
+
 - [ ] **步骤 4:运行测试确认通过**
 
 运行:`cd cfc-backend && mvn test -Dtest=FamilyChallengeServiceTest -pl .`
@@ -321,11 +342,12 @@ for (FamilyChallenge c : active) {
 active = familyChallengeMapper.selectList(wrapper);
 ```
 
-在 `FamilyChallengeController` 各端点加角色校验(模式参考项目其他 controller):
+在 `FamilyChallengeController` 各端点加角色校验。以下为**完整方法体**(基于现有 L19-72 逐方法替换,保留原参数与 Service 调用):
 
 ```java
-// list/history:任何登录角色可看(parent/child),保持现状
-// progress:仅 child 可打卡(parent/teacher 拒绝)
+// list/history:任何登录角色可看(parent/child),保持现状不动(L19-33 不改)
+
+// progress:仅 child 可打卡;强制用 currentMemberId,忽略请求体 memberId(防刷他人进度)
 @PostMapping("/progress")
 public Result<Void> updateProgress(@RequestBody Map<String, Object> params,
                                    @RequestAttribute(value = "role", required = false) String role,
@@ -333,10 +355,19 @@ public Result<Void> updateProgress(@RequestBody Map<String, Object> params,
     if (!"child".equals(role)) {
         return Result.error("仅孩子可打卡");
     }
-    // ... 原逻辑
+    if (currentMemberId == null) {
+        return Result.error("未识别当前成员");
+    }
+    Long challengeId = params.get("challengeId") != null
+            ? Long.valueOf(params.get("challengeId").toString()) : null;
+    int delta = params.get("delta") != null
+            ? Integer.parseInt(params.get("delta").toString()) : 0;
+    return familyChallengeService.updateProgress(challengeId, currentMemberId, delta);
 }
 
-// create/update/delete:仅 parent 可操作
+// templates:任何登录角色可看,保持现状不动(L47-50 不改)
+
+// create:仅 parent
 @PostMapping("/create")
 public Result<Long> createChallenge(@RequestBody Map<String, Object> params,
                                     @RequestAttribute(value = "role", required = false) String role,
@@ -345,11 +376,37 @@ public Result<Long> createChallenge(@RequestBody Map<String, Object> params,
     if (!"parent".equals(role)) {
         return Result.error("仅家长可创建挑战");
     }
-    // ... 原逻辑
+    return familyChallengeService.createChallenge(familyId, userId, params);
+}
+
+// update:仅 parent
+@PostMapping("/update/{id}")
+public Result<Void> updateChallenge(@PathVariable("id") Long challengeId,
+                                    @RequestBody Map<String, Object> params,
+                                    @RequestAttribute(value = "role", required = false) String role,
+                                    @RequestAttribute(value = "familyId", required = false) Long familyId,
+                                    @RequestAttribute("userId") Long userId) {
+    if (!"parent".equals(role)) {
+        return Result.error("仅家长可修改挑战");
+    }
+    return familyChallengeService.updateChallenge(challengeId, userId, familyId, params);
+}
+
+// delete:仅 parent
+@PostMapping("/delete/{id}")
+public Result<Void> deleteChallenge(@PathVariable("id") Long challengeId,
+                                    @RequestAttribute(value = "role", required = false) String role,
+                                    @RequestAttribute(value = "familyId", required = false) Long familyId,
+                                    @RequestAttribute("userId") Long userId) {
+    if (!"parent".equals(role)) {
+        return Result.error("仅家长可删除挑战");
+    }
+    return familyChallengeService.deleteChallenge(challengeId, userId, familyId);
 }
-// update/delete 同理加 "仅家长" 校验
 ```
 
+> 注意:progress 端点不再读请求体 `memberId`(原 L40-41 的逻辑删除),统一用 `currentMemberId`(JwtInterceptor 对 child 角色已自动从 family_member 表查出并注入,见 `JwtInterceptor.java:147-159`)。
+
 - [ ] **步骤 4:运行测试确认通过**
 
 运行:`cd cfc-backend && mvn test -Dtest=FamilyChallengeServiceTest -pl .`
@@ -373,8 +430,8 @@ git commit -m "feat(backend): 家庭挑战到期惰性结算 + 角色校验(
 
 **文件:**
 - 修改:`cfc-frontend/components/FamilyChallengeCard.vue`
-- 修改:`cfc-frontend/pages/index-home/index.vue`(如需传 `childId`
-- 修改:`cfc-frontend/pages/home-pages/parent-index.vue`(如需传 `childId`
+- 修改:`cfc-frontend/pages/index-home/index.vue`(孩子端打卡入口
+- **不改**:`cfc-frontend/pages/home-pages/parent-index.vue`(家长端 L51-53 只传 `:challenge="ch"`,`showCheckin` 默认 false 不显示打卡按钮——后端仅允许 child 角色打卡,家长端不应出现打卡入口;状态显示修复在组件内部自动生效,无需改动此文件
 
 - [ ] **步骤 1:修复 cancelled 状态显示**
 
@@ -394,16 +451,13 @@ git commit -m "feat(backend): 家庭挑战到期惰性结算 + 角色校验(
 </text>
 ```
 
-并在 `statusClass` 计算属性加 cancelled 分支
+`statusClass` 计算属性(L55-57)已存在且通用(返回 `'status-' + status`),**无需修改**。但 style 区缺 `.status-cancelled` 样式(现有 L115-116 只有 `.status-active`/`.status-completed`),在 L116 后新增
 
-```js
-statusClass: function() {
-  var s = this.challenge && this.challenge.status ? this.challenge.status : 'active'
-  return 'status-' + s
-}
+```css
+.status-cancelled { background: #FEE2E2; color: #DC2626; }
 ```
 
-- [ ] **步骤 2:加打卡按钮(孩子端显示)**
+- [ ] **步骤 2:加打卡按钮(仅 child 登录角色显示)**
 
 在卡片底部(`card-detail-row` 之后)加:
 
@@ -418,22 +472,18 @@ statusClass: function() {
 ```js
 props: {
   challenge: { type: Object, default: null },
-  showCheckin: { type: Boolean, default: false },
-  childId: { type: [Number, String], default: null }
+  showCheckin: { type: Boolean, default: false }
 },
 methods: {
   onCheckin: function() {
     var self = this
-    if (!this.childId) {
-      uni.showToast({ title: '请先选择家庭成员', icon: 'none' })
-      return
-    }
     uni.showModal({
       title: '确认打卡',
       content: '确认完成今日打卡吗?',
       success: function(res) {
         if (res.confirm) {
-          self.$emit('checkin', { challengeId: self.challenge.id, childId: self.childId })
+          // 后端已强制使用 JWT 注入的 currentMemberId,无需传 childId
+          self.$emit('checkin', { challengeId: self.challenge.id })
         }
       }
     })
@@ -441,43 +491,57 @@ methods: {
 }
 ```
 
-- [ ] **步骤 3:index-home.vue 传入 childId + 处理打卡事件**
+> **不要加 `childId` prop 守卫**:后端 progress 端点强制使用 JwtInterceptor 注入的 `currentMemberId`(对 `role=child` 自动从 family_member 表查出,`JwtInterceptor.java:147-159`),前端**不需要也不应传** childId。若在卡片加 `if (!this.childId)` 守卫会误拦合法孩子打卡——孩子登录时 `currentChildId` storage 可能为空(`store/index.js` 的 `setUserInfo` 不写该值、`setChildren` 只写 state 不写 storage)。
 
-`cfc-frontend/pages/index-home/index.vue` 第 98-105 行当前嵌入卡片未传 childId。改为:
+- [ ] **步骤 3:index-home.vue 接入打卡事件**
+
+`cfc-frontend/pages/index-home/index.vue` 当前 L98-105 的 section 结构已有 `section-header`("🔥 家庭挑战"标题),**不要改动外层结构**,只改 `FamilyChallengeCard` 标签(原 L104-109):
 
 ```html
-<view class="section challenge-section" v-if="isLoggedIn() && activeChallenges.length > 0">
-  <FamilyChallengeCard
-    v-for="ch in activeChallenges"
-    :key="getItemKey(ch)"
-    :challenge="ch"
-    :showCheckin="true"
-    :childId="currentChildId"
-    @checkin="onChallengeCheckin" />
-</view>
+<FamilyChallengeCard
+  v-for="ch in activeChallenges"
+  :key="ch.id"
+  :challenge="ch"
+  :showCheckin="showCheckinBtn"
+  @checkin="onChallengeCheckin" />
+```
+
+> 保留 `:key="ch.id"`(简单属性访问,合规);**不要**引入 `getItemKey`(index-home 无此方法,也不必要)。卡片已无 `childId` prop(见步骤 2),此处**不传** `:childId`。
+>
+> **打卡按钮必须按登录角色门控**:index-home 是双角色页面(家长登录后可切换到孩子视角,此时 `currentRole='child'` 但 JWT 登录角色仍是 `parent`),而后端 progress 端点仅接受 `role=child`(JwtInterceptor 只对 child 角色注入 `currentMemberId`,家长打卡必被拒)。所以 **不能** 用 `currentRole` 判断,必须用登录角色 `uni.getStorageSync('role')`。
+
+**3a. 补 import**:L379 的 api.js import 行加入 `updateChallengeProgress`(现有行已含 `getChallengeList`,在其后追加):
+
+```js
+import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getTodayTasksByCategory, getChildren, getEnergyOverview, getProductsByDomain, getChallengeList, getMyCircles, discoverCircles, joinGeneralCircle, leaveGeneralCircle, switchToFamilyMember, getMyMembership, updateChallengeProgress } from '../../utils/api.js'
 ```
 
-并在 methods 加(`currentChildId` 从 Vuex/store 取当前孩子成员 id,若为空则用 `currentMemberId` 逻辑):
+**3b. 在 computed 区(L478 起)加** `showCheckinBtn`(登录角色为 child 才显示打卡按钮;家长切换孩子视角时 `currentRole='child'` 但登录角色仍是 `parent`,不可打卡):
+
+```js
+showCheckinBtn: function() {
+  return (uni.getStorageSync('role') || 'parent') === 'child'
+}
+```
+
+**3c. 在 methods 加**:
 
 ```js
 onChallengeCheckin: function(payload) {
   var self = this
-  updateChallengeProgress({ challengeId: payload.challengeId, memberId: payload.childId, delta: 1 })
+  updateChallengeProgress({ challengeId: payload.challengeId, delta: 1 })
     .then(function(res) {
-      if (res && res.code === 0) {
+      if (res && res.code === 200) {
         uni.showToast({ title: '打卡成功', icon: 'success' })
         self.loadChallenges()
       } else {
         uni.showToast({ title: (res && res.message) || '打卡失败', icon: 'none' })
       }
     })
-},
-getItemKey: function(item) {
-  return item.id || item.challengeId || ''
 }
 ```
 
-> 注意:`getItemKey` 若已在页面 methods 中存在(用于其他列表),复用;`currentChildId` 若页面已存在直接复用,否则从 Vuex store 读当前成员
+> **不要加 `currentChildId` 守卫**:孩子登录时 `currentChildId` storage 可能为空(`store/index.js` 的 `setUserInfo` 不写该值、`setChildren` 只写 state 不写 storage),守卫会误拦合法打卡。后端已强制用 JWT 注入的 `currentMemberId`(见任务 3 步骤 3),请求体**不需要**传 `memberId`。`loadChallenges` 是 index-home 现有方法(L703),直接复用
 
 - [ ] **步骤 4:构建验证**
 
@@ -500,7 +564,7 @@ git commit -m "feat(frontend): 家庭挑战孩子端打卡入口 + cancelled 状
 
 - [ ] **步骤 1:加角色判断**
 
-在 `challenge-manage.vue` 的 `onLoad` 中读取角色,非 parent 时提示并返回
+在 `challenge-manage.vue` 的 `onLoad` 开头(现有 `loadFamilyId()` 调用之前)插入角色判断
 
 ```js
 onLoad: function() {
@@ -510,14 +574,18 @@ onLoad: function() {
     setTimeout(function() { uni.navigateBack() }, 1000)
     return
   }
-  // ... 原有加载逻辑
+  this.loadFamilyId()
+  this.loadData()
+  this.loadTemplates()
 },
 getUserRole: function() {
-  // 从 store 读取当前角色
-  return this.$store && this.$store.state && this.$store.state.userInfo && this.$store.state.userInfo.role
+  // store state 顶层有 role 字段(见 store/index.js:15)
+  return this.$store && this.$store.state ? this.$store.state.role : ''
 }
 ```
 
+> 已核实:`challenge-manage.vue` 现有 `onLoad` 只调 `loadFamilyId()/loadData()/loadTemplates()`(L170-174),无其他前置逻辑;`getUserRole` 是新增方法,加入 `methods` 区。store 角色字段是 `state.role`(`store/index.js:15` 初始 `'parent'`),不是 `state.userInfo.role`。
+
 - [ ] **步骤 2:验证现有加载逻辑未被破坏**
 
 读取 `challenge-manage.vue` 的完整 `onLoad`/`onShow` 及 `created` 钩子,确认插入位置正确、原有逻辑保留。
@@ -558,16 +626,25 @@ cd cfc-backend && grep -rn '@Mapping' src/main/java/com/etotem/cfc/controller/ |
 
 - [ ] **步骤 4:最终 Commit(若有遗漏改动)**
 
+> ⚠️ 仓库工作区存在 42 个遗留未提交文件(与本次无关)。**禁止** `git add -A` 或 `git add .`,必须用明确文件路径。
+
 ```bash
-git add -A -- cfc-backend/src/main/java/com/etotem/cfc cfc-frontend/components/FamilyChallengeCard.vue cfc-frontend/pages
+git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyChallengeService.java \
+        cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyChallengeController.java \
+        cfc-backend/src/test/java/com/etotem/cfc/service/FamilyChallengeServiceTest.java \
+        cfc-frontend/components/FamilyChallengeCard.vue \
+        cfc-frontend/pages/index-home/index.vue \
+        cfc-frontend/pages/health/challenge-manage.vue
 git commit -m "fix: 家庭挑战断点修复收尾"
 ```
 
+> 提交前检查:`git diff --cached --stat` 确认 staged 内容仅含上述 6 个文件。
+
 ---
 
 ## 自检
 
-**1. 规格覆盖度(spec §7 断点 ↔ 任务映射):**
+**1. 规格覆盖度(spec §7 断点 ↔ 任务映射,目标为修复 #1-#6,排除 #7):**
 
 | spec §7 # | 断点 | 任务 |
 |---|---|---|
@@ -581,6 +658,16 @@ git commit -m "fix: 家庭挑战断点修复收尾"
 
 **2. 占位符扫描:** 无 TBD/TODO。任务 2 已核实 `PointsService.awardSystemPoints` 签名并写死到计划中,无"实现时确认"残留。
 
-**3. 类型一致性:** `updateProgress(challengeId, childId, delta)` 签名全计划一致;`onCheckin`/`onChallengeCheckin`/`getItemKey` 命名一致;`getItemKey` 已在项目其他页面存在时复用。
+**3. 类型一致性:** `updateProgress(challengeId, childId, delta)` 签名全计划一致;`onCheckin`/`onChallengeCheckin` 命名一致;卡片 `:key="ch.id"` 为简单属性访问(合规,不违反"禁止 :key 表达式"规范);`currentChildId` 为 index-home 现有 data 属性直接复用。
+
+**4. 与代码库事实核对(本计划所有引用均已验证):**
+- `Result.success()` → `code=200`,`Result.error()` → `code=500`(`common/Result.java`),前端统一 `res.code === 200` 判断
+- `PointsService.awardSystemPoints(Long memberId, int amount, String reason)` 存在于 `service/PointsService.java:97`;`FamilyChallengeService` 需新增 `@Resource private PointsService pointsService;`
+- `JwtInterceptor` 已注入 `role`/`familyId`/`currentMemberId` attribute(child 角色自动查 family_member 表,`JwtInterceptor.java:147-159`)
+- `FamilyChallengeCard.vue` 的 `statusClass` 计算属性已存在且通用,仅缺 `.status-cancelled` CSS
+- index-home 现有 `section-header`("🔥 家庭挑战")不重复渲染;`currentChildId` 是现有 data 属性(L458);`loadChallenges` 是现有方法(L703)
+- `challenge-manage.vue` 现有 `onLoad` 仅调三个 load 方法;store 角色字段是 `state.role`(`store/index.js:15`)
+- `FamilyChallengeController` 各端点现有签名已读取(L19-72),替换片段基于真实代码
+- 仓库有 42 个遗留未提交文件,所有 git 命令均为明确路径,禁止 `git add -A`
 
 **执行交接:** 计划已保存到 `docs/superpowers/plans/2026-08-06-family-challenge-fix.md`。