Browse Source

fix(health-plan): 补齐方案删除设计缺口——§4.3方案不存在错误映射 + §5.1失败toast后端消息

sync_gap_tasks 巡检发现 docs/superpowers/specs/2026-10-17-health-plan-deletion-design.md
任务1-6已全栈落地(87fed3047->c2ae3d8d4),本次补齐两处遗留偏差:

后端(§4.3 错误处理):
- HealthPlanServiceImpl.deletePlanCascade 方案不存在抛 IllegalStateException(业务异常),
  GlobalExceptionHandler 映射为 Result.error(400, "方案不存在"),与设计错误表一致
- HealthPlanController / AdminHealthPlanController 捕获 IllegalStateException 转 Result.error
  (对齐 PlanReviewController 既有模式;管理端跨家庭删除不受影响)

小程序(§5.1 失败 toast 后端 message):
- plan-list.vue onDeletePlan catch 改为 toast err.message(后端 message),
  不再用通用「删除失败」覆盖后端提示

测试:新增 HealthPlanDeleteControllerTest 6 用例(planId为空/方案不存在/归属不匹配/成功/管理端)
+ 更新 HealthPlanDeleteCascadeTest 断言为 IllegalStateException;共 12 用例全过

文档:设计规格状态改为「已实现」;plans 任务 1-6 复选框置 [x];PROJECT-OVERVIEW 条目同步
E2E Test Bot 1 week ago
parent
commit
c9f22f66fe

+ 7 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java

@@ -163,8 +163,13 @@ public class HealthPlanController {
                                                   @RequestAttribute(value = "familyId", required = false) Long familyId) {
         Long planId = ParamUtils.getLong(params.get("planId"));
         if (planId == null) return Result.error("planId不能为空");
-        // familyId 从 JWT 注入:方案不属于该家庭时 Service 返回 null → 拒绝删除(IDOR 防护)
-        Map<String, Object> result = healthPlanService.deletePlanCascade(planId, familyId);
+        // familyId 从 JWT 注入:方案不存在 → 业务异常转「方案不存在」;不属于该家庭 → null → 「无权删除该方案」(IDOR 防护)
+        Map<String, Object> result;
+        try {
+            result = healthPlanService.deletePlanCascade(planId, familyId);
+        } catch (IllegalStateException e) {
+            return Result.error(e.getMessage());
+        }
         if (result == null) {
             return Result.error("无权删除该方案");
         }

+ 8 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminHealthPlanController.java

@@ -72,8 +72,14 @@ public class AdminHealthPlanController {
         if (planId == null) {
             return Result.error("planId不能为空");
         }
-        // 管理端跨家庭删除,跳过 familyId 归属校验(Service familyId 传 null)
-        Map<String, Object> result = healthPlanService.deletePlanCascade(planId, null);
+        // 管理端跨家庭删除,跳过 familyId 归属校验(Service familyId 传 null);
+        // 方案不存在 → 业务异常转「方案不存在」
+        Map<String, Object> result;
+        try {
+            result = healthPlanService.deletePlanCascade(planId, null);
+        } catch (IllegalStateException e) {
+            return Result.error(e.getMessage());
+        }
         return Result.success(result);
     }
 }

+ 3 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java

@@ -209,7 +209,9 @@ public class HealthPlanServiceImpl implements HealthPlanService {
     public Map<String, Object> deletePlanCascade(Long planId, Long familyId) {
         HealthPlan plan = healthPlanMapper.selectById(planId);
         if (plan == null) {
-            throw new RuntimeException("方案不存在");
+            // 业务异常:GlobalExceptionHandler 将 IllegalStateException 映射为 Result.error(400, "方案不存在"),
+            // 与设计规格 §4.3「方案不存在 → Result.error("方案不存在")」一致
+            throw new IllegalStateException("方案不存在");
         }
         if (familyId != null && !familyId.equals(plan.getFamilyId())) {
             // IDOR 防护:方案不属于该家庭,整体拒绝,不删任何数据

+ 95 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/HealthPlanDeleteControllerTest.java

@@ -0,0 +1,95 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.admin.AdminHealthPlanController;
+import com.etotem.cfc.service.HealthPlanService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * 健康方案删除端点 §4.3 错误处理契约:
+ * - planId 为空 → Result.error("planId不能为空")
+ * - 方案不存在(Service 抛 IllegalStateException)→ Result.error("方案不存在")
+ * - 归属不匹配(Service 返回 null)→ Result.error("无权删除该方案")
+ * - 删除成功 → Result.success({planId, taskCount})
+ */
+class HealthPlanDeleteControllerTest {
+
+    private HealthPlanController userController;
+    private AdminHealthPlanController adminController;
+    private HealthPlanService planService;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        userController = new HealthPlanController();
+        adminController = new AdminHealthPlanController();
+        planService = mock(HealthPlanService.class);
+        set("healthPlanService", userController, planService);
+        set("healthPlanService", adminController, planService);
+    }
+
+    private void set(String name, Object target, Object v) throws Exception {
+        Field f = target.getClass().getDeclaredField(name);
+        f.setAccessible(true);
+        f.set(target, v);
+    }
+
+    private Map<String, Object> params(Object planId) {
+        Map<String, Object> p = new HashMap<>();
+        p.put("planId", planId);
+        return p;
+    }
+
+    @Test
+    void 用户端_planId为空返回错误() {
+        Result<?> r = userController.deletePlan(params(null), 1L, 10L);
+        assertEquals("planId不能为空", r.getMessage());
+        verify(planService, never()).deletePlanCascade(any(), any());
+    }
+
+    @Test
+    void 用户端_方案不存在返回方案不存在() {
+        when(planService.deletePlanCascade(99L, 10L)).thenThrow(new IllegalStateException("方案不存在"));
+        Result<?> r = userController.deletePlan(params(99L), 1L, 10L);
+        assertEquals("方案不存在", r.getMessage());
+    }
+
+    @Test
+    void 用户端_归属不匹配返回无权删除() {
+        when(planService.deletePlanCascade(99L, 20L)).thenReturn(null);
+        Result<?> r = userController.deletePlan(params(99L), 1L, 20L);
+        assertEquals("无权删除该方案", r.getMessage());
+    }
+
+    @Test
+    void 用户端_删除成功返回taskCount() {
+        Map<String, Object> data = new HashMap<>();
+        data.put("planId", 99L);
+        data.put("taskCount", 5);
+        when(planService.deletePlanCascade(99L, 10L)).thenReturn(data);
+        Result<?> r = userController.deletePlan(params(99L), 1L, 10L);
+        assertEquals(200, r.getCode());
+        assertEquals(5, ((Map<?, ?>) r.getData()).get("taskCount"));
+    }
+
+    @Test
+    void 管理端_方案不存在返回方案不存在() {
+        when(planService.deletePlanCascade(99L, null)).thenThrow(new IllegalStateException("方案不存在"));
+        Result<?> r = adminController.deletePlan(params(99L), "admin");
+        assertEquals("方案不存在", r.getMessage());
+    }
+
+    @Test
+    void 管理端_planId为空返回错误() {
+        Result<?> r = adminController.deletePlan(params(null), "admin");
+        assertEquals("planId不能为空", r.getMessage());
+        verify(planService, never()).deletePlanCascade(any(), any());
+    }
+}

+ 3 - 1
cfc-backend/src/test/java/com/etotem/cfc/unit/HealthPlanDeleteCascadeTest.java

@@ -107,7 +107,9 @@ class HealthPlanDeleteCascadeTest {
     void 方案不存在抛方案不存在() {
         when(mapper.selectById(404L)).thenReturn(null);
 
-        RuntimeException ex = assertThrows(RuntimeException.class,
+        // 业务异常(IllegalStateException):Controller 捕获后转 Result.error("方案不存在"),
+        // 与设计规格 §4.3 错误表一致
+        IllegalStateException ex = assertThrows(IllegalStateException.class,
                 () -> service.deletePlanCascade(404L, 10L));
         assertEquals("方案不存在", ex.getMessage());
         verify(mapper, never()).deleteById(any());

+ 3 - 1
cfc-frontend/pages/health/plan-list.vue

@@ -113,7 +113,9 @@ export default {
             }
           }).catch(function(err) {
             console.error('删除方案失败', err)
-            uni.showToast({ title: '删除失败', icon: 'none' })
+            // 失败 toast 后端 message(设计 §5.1:失败 → toast 后端 message);
+            // request 失败时 err 携带后端 message,无则回退通用文案
+            uni.showToast({ title: (err && err.message) || '删除失败', icon: 'none' })
           })
         }
       })

+ 1 - 1
docs/superpowers/PROJECT-OVERVIEW.md

@@ -5,7 +5,7 @@
 **状态:** 已确认(v2.1 Phase 2-4 全栈完成)+ 虚拟支付改造(Tasks 1-12 已完成,退款闭环实施中)+ 新用户注册引导(12 Tasks 全栈完成)+ 家庭成员关系条增强(✅ 已实施)+ TabBar 重构(✅ 4Tab + 中间⭐扇形菜单,设计已对齐实现)+ **LIFETIME 终身会员(✅ 11 Tasks 全栈完成)** + **健康数据中心(✅ 全栈完成)** + **SKU 价格单位统一分 + 规格选择响应式修复(✅ 7 Tasks 全栈完成)** + **AI健康教练人格分化与管家自助选择(✅ 11 Tasks 全栈完成)** + **优惠券全链路改为家庭维度(待实施)** + **关系三图重构(关键人拓展章鱼图/珍珠图/能力图,设计规格已完成)** + **注册零填写+信息按需补充(✅ 全栈完成:登录直接进首页 + 家庭邀请独立页真实姓名/家庭身份必填 + 功能→信息字段映射表 + 添加成员手机号冲突邀请加入)** + **健康报告归属重分配(✅ 后端接口 + 管理端页面 + 小程序入口已实现,见 `specs/2026-10-05-health-report-reassign-design.md`)**
 **指标变化曲线·指标速查·指标关联(✅ 全栈完成:后端11任务+小程序M1-M8+管理端W1-W6;HBuilderX人工验收待用户执行,见 `specs/2026-10-09-indicator-change-curve-design.md`)**
 **饮食/睡眠/运动打卡接入任务完成链路(✅ 全栈完成:实体taskId字段 + 三个Controller isDailyProgress分流回写 + RepeatTaskGenerator每日生成打卡任务 + tasks.vue跳转 + 三打卡页taskId透传 + API文档;见 `specs/2026-10-13-health-checkin-task-linkage-design.md` 与 `plans/2026-10-13-health-checkin-task-linkage.md`)**
-**健康方案删除(✅ 全栈完成:后端级联删除接口含每日实例二次删除 + 小程序/管理端删除按钮 + 确认弹窗;见 `specs/2026-10-17-health-plan-deletion-design.md` 与 `plans/2026-10-17-health-plan-deletion.md`)**
+**健康方案删除(✅ 全栈完成:后端级联删除接口含每日实例二次删除 + 小程序/管理端删除按钮 + 确认弹窗;「方案不存在」业务异常转 Result.error + 失败 toast 后端消息;见 `specs/2026-10-17-health-plan-deletion-design.md` 与 `plans/2026-10-17-health-plan-deletion.md`)**
 **菌群知识并入 dan_knowledge_base(🔄 实施中:809 篇文章以 source_type='article' 并入统一知识表 + 后端 sourceType/excludeSourceType 过滤 + langgraph 三源重建 + 管理端统一展示,见 `specs/2026-10-20-knowledge-unify-design.md` 与 `plans/2026-10-20-knowledge-unify.md`)**
 **限时特价(扫码闪购)(✅ 全栈完成:迁移389/390 → SpecialPriceService 7 方法 + 双控制器(公开 info 白名单 + admin 6 接口)→ 特价下单 createSpecialDealOrder + cancel/approveRefund/OrderTimeoutCancelTask 三处 releaseQuota 回滚 + 供应商改价拒绝 + 小程序商品页 sp 解析/倒计时/特价区块/checkout 透传 + cfc-web SpecialPriceManage.vue;5 个测试类 31 case 全过;见 `specs/2026-10-20-limited-time-special-price-design.md` 与 `plans/2026-10-20-limited-time-special-price.md`)**
 **后台多渠道发货(🟡 设计稿待审查:管理端发货对齐微信小程序后台四种方式——快递 logistics_type=1 / 同城配送 2(deliveryMethod 新增编码 6)/ 用户自提 4(不再跳过微信同步)/ 虚拟商品 3(支付成功自动发货 autoShipVirtualOrder,幂等 fail-open);另修正 upload_shipping_info URL 缺 /wxa 前缀;cfc-web 发货对话框按类型切换;见 `specs/2026-10-22-fulfillment-multi-mode-design.md`)**

+ 28 - 28
docs/superpowers/plans/2026-10-17-health-plan-deletion.md

@@ -42,7 +42,7 @@
 - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/HealthPlanService.java`
 - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java`
 
-- [ ] **步骤 1:编写失败的测试**
+- [x] **步骤 1:编写失败的测试**
 
 创建 `cfc-backend/src/test/java/com/etotem/cfc/unit/HealthPlanDeleteCascadeTest.java`(测试风格对齐同目录 `HealthPlanReviewGuardTest.java`:Mockito + 反射注入字段,无 Spring 上下文):
 
@@ -177,14 +177,14 @@ class HealthPlanDeleteCascadeTest {
 }
 ```
 
-- [ ] **步骤 2:运行测试确认失败**
+- [x] **步骤 2:运行测试确认失败**
 
 运行:`cd cfc-backend && /tmp/apache-maven-3.8.6/bin/mvn test -DskipTests=false -Dtest=HealthPlanDeleteCascadeTest -q`
 预期:**编译失败**(`deletePlanCascade` 方法不存在),错误 `cannot find symbol: method deletePlanCascade`。失败原因正确(接口未声明)。
 
 > ⚠️ 环境注意:项目 pom 默认 `skipTests=true`,`mvn test` 只编译不执行;必须显式 `-DskipTests=false` 才真正运行测试。本机 mvn 不在 PATH,用 `/tmp/apache-maven-3.8.6/bin/mvn`。
 
-- [ ] **步骤 3:在 Service 接口声明方法**
+- [x] **步骤 3:在 Service 接口声明方法**
 
 在 `cfc-backend/src/main/java/com/etotem/cfc/service/HealthPlanService.java` 末尾(`generateFromSelfCheck` 声明之后、类闭合括号之前)追加:
 
@@ -204,7 +204,7 @@ class HealthPlanDeleteCascadeTest {
 
 确认接口头部已有 `java.util.Map` 导入(第 8 行 `import java.util.Map;` 已存在,无需新增)。
 
-- [ ] **步骤 4:编写最少实现代码**
+- [x] **步骤 4:编写最少实现代码**
 
 在 `cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java` 的 `getPlanTasks` 方法之后(第 201 行 `}` 之后)插入实现:
 
@@ -263,12 +263,12 @@ import org.springframework.transaction.annotation.Transactional;
 
 依赖核对:`QueryWrapper`(第 3 行已导入)、`Task`/`HealthPlan`/`List`/`HashMap`/`HashMap`(第 2、7-31 行已导入)、`log`(`@Slf4j`)均已就绪,无需其他新增。
 
-- [ ] **步骤 5:运行测试确认通过**
+- [x] **步骤 5:运行测试确认通过**
 
 运行:`cd cfc-backend && /tmp/apache-maven-3.8.6/bin/mvn test -DskipTests=false -Dtest=HealthPlanDeleteCascadeTest -q`
 预期:**BUILD SUCCESS**,5 个测试全部 PASS(级联删除含 wrapper 语义顺序验证、无任务方案、IDOR 拒绝、方案不存在、管理端跳过校验)。
 
-- [ ] **步骤 6:Commit**
+- [x] **步骤 6:Commit**
 
 ```bash
 cd cfc-backend
@@ -286,7 +286,7 @@ git commit -m "feat(health-plan): 方案级联删除 Service 层(含每日实
 - 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java`
 - 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminHealthPlanController.java`
 
-- [ ] **步骤 1:用户端删除端点**
+- [x] **步骤 1:用户端删除端点**
 
 在 `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java` 的 `planTasks` 方法之后(第 158 行 `}` 之后、"获取可用规划师列表"注释之前)插入:
 
@@ -309,7 +309,7 @@ git commit -m "feat(health-plan): 方案级联删除 Service 层(含每日实
 
 校验:`Map`(第 15 行 `import java.util.Map;`)、`ParamUtils`(第 16 行)、`@PostMapping`(第 11 行通配导入)均已导入;路由 `/delete` 与现有 12 条路由无冲突(已查重)。
 
-- [ ] **步骤 2:管理端删除端点**
+- [x] **步骤 2:管理端删除端点**
 
 在 `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminHealthPlanController.java` 的 `detail` 方法之后(第 61 行 `}` 之后)插入:
 
@@ -333,12 +333,12 @@ git commit -m "feat(health-plan): 方案级联删除 Service 层(含每日实
     }
 ```
 
-- [ ] **步骤 3:编译验证**
+- [x] **步骤 3:编译验证**
 
 运行:`cd cfc-backend && /tmp/apache-maven-3.8.6/bin/mvn clean compile -q`
 预期:**BUILD SUCCESS**,无编译错误。
 
-- [ ] **步骤 4:Commit**
+- [x] **步骤 4:Commit**
 
 ```bash
 cd cfc-backend
@@ -354,7 +354,7 @@ git commit -m "feat(health-plan): 新增方案删除接口(用户端 + 管理
 **文件:**
 - 修改:`docs/superpowers/api/API_REFERENCE.md`
 
-- [ ] **步骤 1:追加 4.49 节**
+- [x] **步骤 1:追加 4.49 节**
 
 在 `docs/superpowers/api/API_REFERENCE.md` 第 1670-1672 行(`*文档更新:2026-10-09(指标统一层新增...)*` 与 `*文档最后更新:2026-10-04*` 之间)插入:
 
@@ -379,7 +379,7 @@ git commit -m "feat(health-plan): 新增方案删除接口(用户端 + 管理
 ---
 ```
 
-- [ ] **步骤 2:Commit**
+- [x] **步骤 2:Commit**
 
 ```bash
 git add docs/superpowers/api/API_REFERENCE.md
@@ -394,7 +394,7 @@ git commit -m "docs(api): 健康方案删除接口记录(4.49)"
 - 修改:`cfc-frontend/utils/api.js`
 - 修改:`cfc-frontend/pages/health/plan-list.vue`
 
-- [ ] **步骤 1:API 封装**
+- [x] **步骤 1:API 封装**
 
 在 `cfc-frontend/utils/api.js` 第 2582 行(`getHealthPlanTasks` 之后)追加:
 
@@ -402,7 +402,7 @@ git commit -m "docs(api): 健康方案删除接口记录(4.49)"
 export const deleteHealthPlan = (data) => request('/api/health/plan/delete', 'POST', data)
 ```
 
-- [ ] **步骤 2:plan-list.vue 模板加删除按钮**
+- [x] **步骤 2:plan-list.vue 模板加删除按钮**
 
 在 `cfc-frontend/pages/health/plan-list.vue` 第 35-37 行,`plan-title` 文本与 `plan-status` 视图之间插入删除按钮(`@click.stop` 阻止冒泡进入详情,符合小程序事件规范):
 
@@ -412,7 +412,7 @@ export const deleteHealthPlan = (data) => request('/api/health/plan/delete', 'PO
           <view class="plan-status" :class="'status-' + plan.status">
 ```
 
-- [ ] **步骤 3:样式**
+- [x] **步骤 3:样式**
 
 在 `cfc-frontend/pages/health/plan-list.vue` 的 `.plan-title` 样式块之后(第 210 行 `}` 之后)插入:
 
@@ -427,7 +427,7 @@ export const deleteHealthPlan = (data) => request('/api/health/plan/delete', 'PO
 }
 ```
 
-- [ ] **步骤 4:methods 实现删除交互**
+- [x] **步骤 4:methods 实现删除交互**
 
 在 `cfc-frontend/pages/health/plan-list.vue` 的 `goDetail` 方法之后(第 90 行 `},` 之后)插入:
 
@@ -458,7 +458,7 @@ export const deleteHealthPlan = (data) => request('/api/health/plan/delete', 'PO
     },
 ```
 
-- [ ] **步骤 5:引入 API 函数**
+- [x] **步骤 5:引入 API 函数**
 
 将 `cfc-frontend/pages/health/plan-list.vue` 第 50 行的 import 改为:
 
@@ -466,7 +466,7 @@ export const deleteHealthPlan = (data) => request('/api/health/plan/delete', 'PO
 import { getHealthPlanList, deleteHealthPlan } from '@/utils/api.js'
 ```
 
-- [ ] **步骤 6:语法校验(不打包)**
+- [x] **步骤 6:语法校验(不打包)**
 
 `node --check` 默认按 CommonJS 解析 `.js`,而本文件是 ESM(`import`/`export`)会误报语法错——统一先落成 `.mjs` 再校验:
 
@@ -486,7 +486,7 @@ node scripts/audit-duplicate-api-calls.js
 
 预期:退出码 0,无 ERROR 级违规。
 
-- [ ] **步骤 7:Commit**
+- [x] **步骤 7:Commit**
 
 ```bash
 cd cfc-frontend
@@ -502,7 +502,7 @@ git commit -m "feat(health): 方案列表支持删除(确认弹窗 + 级联删
 - 修改:`cfc-web/src/api/healthPlan.js`
 - 修改:`cfc-web/src/views/admin/HealthPlanManage.vue`
 
-- [ ] **步骤 1:API 封装**
+- [x] **步骤 1:API 封装**
 
 在 `cfc-web/src/api/healthPlan.js` 末尾追加:
 
@@ -518,7 +518,7 @@ export function adminDeleteHealthPlan(data) {
 }
 ```
 
-- [ ] **步骤 2:操作列加删除按钮**
+- [x] **步骤 2:操作列加删除按钮**
 
 在 `cfc-web/src/views/admin/HealthPlanManage.vue` 第 59-63 行,操作列(`width="100"` 改 `"140"` 容纳两按钮)内,`详情` 按钮后插入:
 
@@ -531,7 +531,7 @@ export function adminDeleteHealthPlan(data) {
     </el-table-column>
 ```
 
-- [ ] **步骤 3:import 与方法**
+- [x] **步骤 3:import 与方法**
 
 将 `cfc-web/src/views/admin/HealthPlanManage.vue` 第 131 行改为:
 
@@ -569,7 +569,7 @@ import { adminListHealthPlans, adminGetHealthPlanDetail, adminDeleteHealthPlan }
     },
 ```
 
-- [ ] **步骤 4:语法校验(不打包,ESM 需 .mjs)**
+- [x] **步骤 4:语法校验(不打包,ESM 需 .mjs)**
 
 ```bash
 cd cfc-web
@@ -581,7 +581,7 @@ cp src/api/healthPlan.js /tmp/healthPlan.check.mjs && node --check /tmp/healthPl
 预期:两条 `node --check` 均无输出即通过,最后输出 `SYNTAX OK`。
 (说明:两文件均为 ESM,落为 `.mjs` 后 `--check` 只做语法解析、不解析模块路径,能覆盖语法错误;实际编译由 HBuilderX 打包期兜底。)
 
-- [ ] **步骤 5:Commit**
+- [x] **步骤 5:Commit**
 
 ```bash
 cd cfc-web
@@ -596,7 +596,7 @@ git commit -m "feat(admin): 健康方案管理支持删除(确认弹窗 + 级
 **文件:**
 - 修改:`docs/superpowers/PROJECT-OVERVIEW.md`
 
-- [ ] **步骤 1:状态行追加条目**
+- [x] **步骤 1:状态行追加条目**
 
 在 `docs/superpowers/PROJECT-OVERVIEW.md` 第 7 行(饮食/睡眠/运动打卡条目)之后插入新行:
 
@@ -606,7 +606,7 @@ git commit -m "feat(admin): 健康方案管理支持删除(确认弹窗 + 级
 
 同步将第 3 行 `**文档版本:** v2.17` 改为 `**文档版本:** v2.18`,第 4 行 `**日期:** 2026-10-09` 改为 `**日期:** 2026-10-17`。
 
-- [ ] **步骤 2:全量回归验证(后端)**
+- [x] **步骤 2:全量回归验证(后端)**
 
 ```bash
 cd cfc-backend && /tmp/apache-maven-3.8.6/bin/mvn clean compile -q && /tmp/apache-maven-3.8.6/bin/mvn test -DskipTests=false -Dtest=HealthPlanDeleteCascadeTest -q
@@ -614,7 +614,7 @@ cd cfc-backend && /tmp/apache-maven-3.8.6/bin/mvn clean compile -q && /tmp/apach
 
 预期:`BUILD SUCCESS`,5 个测试 PASS。
 
-- [ ] **步骤 3:前端审计**
+- [x] **步骤 3:前端审计**
 
 ```bash
 node scripts/audit-duplicate-api-calls.js
@@ -622,7 +622,7 @@ node scripts/audit-duplicate-api-calls.js
 
 预期:退出码 0。
 
-- [ ] **步骤 4:Commit**
+- [x] **步骤 4:Commit**
 
 ```bash
 git add docs/superpowers/PROJECT-OVERVIEW.md

+ 1 - 1
docs/superpowers/specs/2026-10-17-health-plan-deletion-design.md

@@ -1,7 +1,7 @@
 # 健康方案删除(级联删除方案任务)设计
 
 - 日期:2026-10-17
-- 状态:已批准(设计评审通过,待实现)
+- 状态:已实现(2026-10-30 复核:任务 1-6 全栈落地;补齐 §4.3「方案不存在」错误映射、§5.1 失败 toast 后端消息)
 
 ## 1. 背景与目标