浏览代码

Merge remote-tracking branch 'origin/cfclub' into task/S-cfc-191

Sisyphus Agent 6 小时之前
父节点
当前提交
03cb65f001

+ 204 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyTaskReviewContractTest.java

@@ -0,0 +1,204 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.guide.GuideFamilyTaskController;
+import com.etotem.cfc.dto.TaskReviewDTO;
+import com.etotem.cfc.entity.Task;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.TaskMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.GuideFamilyAccessGuard;
+import com.etotem.cfc.service.GuideFamilyService;
+import com.etotem.cfc.service.MembershipService;
+import com.etotem.cfc.service.RewardRuleService;
+import com.etotem.cfc.service.TaskService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 契约测试:能力键 {@code teacher.task.review}(SC-T-06 家庭任务审核 / SC-T-18 批量任务下发)。
+ *
+ * <p>锚点:
+ * <ul>
+ *   <li>契约文档 docs/superpowers/specs/2026-11-07-planner-zone-contract.md EVD-10 §2 SC-T-06/18 端点核验表;</li>
+ *   <li>派生用例 tests/test-cases/teacher.md TC-T-09(任务审核:通过给分/驳回重做/<b>越权非绑定家庭被拒</b>/并发双审幂等/超期被拒)
+ *       与 TC-T-18(批量任务:部分失败不影响其他/各自独立落库)、TC-T-09-P(权限流:非绑定家庭数据统一 403)。</li>
+ * </ul></p>
+ *
+ * <p><b>本类聚焦规划师(teacher)侧 {@link GuideFamilyTaskController} 的任务审核归属校验缺口</b>:
+ * 现有实现 {@code reviewTask} / {@code batchReviewTasks} 仅校验「规划师与 familyId 绑定」(requireBound)
+ * 与「任务存在+会员任务校验」(checkMemberOnly),<b>未校验被审任务 task.getFamilyId() 是否等于路径 familyId</b>——
+ * 对比同控制器 {@code updateTask} / {@code deleteTask} 已含「任务不属于该家庭」归属校验。该缺口使
+ * 已绑定家庭 A 的规划师传入属于家庭 B 的 taskId 即可跨家庭越权审核(IDOR),违反 TC-T-09-P
+ * 「非绑定家庭数据统一 403」。</p>
+ *
+ * <p><b>RED 预期</b>:跨家庭任务审核(reviewTask / batchReviewTasks)应被拒且不进入 taskService.reviewTask;
+ * 当前实现缺归属校验 → 会调用 taskService.reviewTask → 断言失败(红)。GREEN 阶段补归属校验后转绿。</p>
+ *
+ * <p>纯 Mockito(无 Spring / 无 DB / 无外部服务),参照同批 RED 契约测试风格(FamilyJoinValidationRulesContractTest、TeamProfitHookContractTest)。</p>
+ */
+class GuideFamilyTaskReviewContractTest {
+
+    private static final Long GUIDE_ID = 1L;
+    /** 规划师已绑定的家庭 ID(requireBound 放行来源) */
+    private static final Long BOUND_FAMILY_ID = 100L;
+    /** 非绑定的其他家庭 ID(跨家庭越权来源) */
+    private static final Long OTHER_FAMILY_ID = 999L;
+
+    @Mock
+    private TaskService taskService;
+
+    @Mock
+    private FamilyMemberMapper familyMemberMapper;
+
+    @Mock
+    private TaskMapper taskMapper;
+
+    @Mock
+    private UserMapper userMapper;
+
+    @Mock
+    private MembershipService membershipService;
+
+    @Mock
+    private GuideFamilyService guideFamilyService;
+
+    @Mock
+    private RewardRuleService rewardRuleService;
+
+    @Mock
+    private GuideFamilyAccessGuard guideFamilyAccessGuard;
+
+    @InjectMocks
+    private GuideFamilyTaskController controller;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        // 规划师与 BOUND_FAMILY_ID 已绑定 → requireBound 放行(检查绑定关系返回 null 表示放行)
+        when(guideFamilyAccessGuard.checkBinding(GUIDE_ID, BOUND_FAMILY_ID)).thenReturn(null);
+    }
+
+    // ==================== 正常流(对照,应全绿) ====================
+
+    /**
+     * 本家庭任务审核:task.familyId == familyId 路径一致,应调用 taskService.reviewTask 进入审核逻辑。
+     */
+    @Test
+    void reviewTask_本家庭任务_进入审核_返回通过() {
+        Long taskId = 5L;
+        when(taskMapper.selectById(taskId)).thenReturn(task(taskId, BOUND_FAMILY_ID, "pending_review"));
+        when(taskService.reviewTask(eq(taskId), any(TaskReviewDTO.class))).thenReturn(true);
+
+        Result<Boolean> result = controller.reviewTask(GUIDE_ID, BOUND_FAMILY_ID, taskId, "teacher", dto(true), null);
+
+        assertTrue(result.getData());
+        verify(taskService).reviewTask(eq(taskId), any(TaskReviewDTO.class));
+    }
+
+    // ==================== 权限流(RED 契约缺口) ====================
+
+    /**
+     * SC-T-06 / TC-T-09-P:规划师越权审核不属于已绑定家庭的子任务 —— 接口应拒绝且不进入 taskService.reviewTask。
+     *
+     * <p>缺口定位:{@code GuideFamilyTaskController#reviewTask} 当前不校验 {@code task.getFamilyId() == familyId},
+     * 导致任何已认证规划师传入跨家庭的 taskId 均可越权审核。</p>
+     */
+    @Test
+    void reviewTask_跨家庭任务_应拒绝且不进入审核() {
+        Long taskId = 5L;
+        // 任务存在,但其 familyId 与路径中的 BOUND_FAMILY_ID 不同(越权场景)
+        when(taskMapper.selectById(taskId)).thenReturn(task(taskId, OTHER_FAMILY_ID, "pending_review"));
+        when(taskService.reviewTask(eq(taskId), any(TaskReviewDTO.class))).thenReturn(true);
+
+        Result<Boolean> result = controller.reviewTask(GUIDE_ID, BOUND_FAMILY_ID, taskId, "teacher", dto(true), null);
+
+        // 契约:跨家庭任务不得进入审核逻辑(TC-T-09-P 非绑定家庭数据统一 403)
+        verify(taskService, never()).reviewTask(anyLong(), any(TaskReviewDTO.class));
+        assertNotEquals(Integer.valueOf(200), result.getCode(), "跨家庭任务审核应被拒(非 200 成功响应)");
+    }
+
+    /**
+     * SC-T-18 / TC-T-18-E:批量审核中包含跨家庭任务时,跨家任务应被跳过不被审核(各自独立不落库)。
+     *
+     * <p>当前实现循环 taskIds,每个 task 单独 checkMemberOnly + reviewTask;缺失跨家庭过滤 → 所有 task 均被处理。</p>
+     */
+    @Test
+    void batchReviewTasks_含跨家庭任务_应跳过跨家庭任务独立处理自身任务() {
+        Long ownTaskId = 1L;
+        Long crossTaskId = 2L;
+
+        when(taskMapper.selectById(ownTaskId)).thenReturn(task(ownTaskId, BOUND_FAMILY_ID, "pending_review"));
+        when(taskMapper.selectById(crossTaskId)).thenReturn(task(crossTaskId, OTHER_FAMILY_ID, "pending_review"));
+        when(taskService.reviewTask(anyLong(), any(TaskReviewDTO.class))).thenReturn(true);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("taskIds", Arrays.asList(ownTaskId, crossTaskId));
+        params.put("approved", true);
+
+        Result<Map<String, Object>> result = controller.batchReviewTasks(GUIDE_ID, BOUND_FAMILY_ID, "teacher", params, null);
+
+        // 契约:仅本家庭任务进入审核;跨家庭任务不得被审核(TC-T-18-E 部分失败不影响其他/TC-T-09-P 越权拒)
+        verify(taskService).reviewTask(eq(ownTaskId), any(TaskReviewDTO.class));
+        verify(taskService, never()).reviewTask(eq(crossTaskId), any(TaskReviewDTO.class));
+    }
+
+    // ==================== 角色校验(对照,应绿) ====================
+
+    /**
+     * 非 teacher/admin 角色访问规划师专属端点 → 应在 role 检查处短路返回 Access denied,不调 use Service。
+     */
+    @Test
+    void reviewTask_非teacher角色_拒绝且不进入审核() {
+        Long taskId = 5L;
+        Result<Boolean> result = controller.reviewTask(GUIDE_ID, BOUND_FAMILY_ID, taskId, "parent", dto(true), null);
+
+        assertEquals("Access denied", result.getMessage());
+        verify(taskService, never()).reviewTask(anyLong(), any(TaskReviewDTO.class));
+    }
+
+    @Test
+    void batchReviewTasks_非teacher角色_拒绝且不进入审核() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("taskIds", Collections.singletonList(1L));
+        params.put("approved", true);
+        Result<Map<String, Object>> result = controller.batchReviewTasks(GUIDE_ID, BOUND_FAMILY_ID, "parent", params, null);
+
+        assertEquals("Access denied", result.getMessage());
+        verify(taskService, never()).reviewTask(anyLong(), any(TaskReviewDTO.class));
+    }
+
+    // ==================== 辅助 ====================
+
+    private Task task(Long id, Long familyId, String status) {
+        Task t = new Task();
+        t.setId(id);
+        t.setFamilyId(familyId);
+        t.setStatus(status);
+        t.setCompletedAt(new Date());
+        return t;
+    }
+
+    private TaskReviewDTO dto(boolean approved) {
+        TaskReviewDTO d = new TaskReviewDTO();
+        d.setApproved(approved);
+        d.setComment("契约测试审核");
+        return d;
+    }
+}

+ 254 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/NutritionPlanTeacherContractTest.java

@@ -0,0 +1,254 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.diet.DietRecommendationController;
+import com.etotem.cfc.mapper.DietRecommendationMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.MealConfigMapper;
+import com.etotem.cfc.service.AiGateway;
+import com.etotem.cfc.service.BeijingNutritionService;
+import com.etotem.cfc.service.DietRecommendationService;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * TDD-RED 契约级单测:成长规划师「精准营养方案制定」(能力键 {@code teacher.nutrition.plan},
+ * SC-T-26 / API_REFERENCE §4.57 / 用例场景 04-teacher.md T-26 / 契约 EVD-10 77026df42)。
+ *
+ * <p><b>契约来源</b>(用户故事 + 主流程 + 边界):
+ * <blockquote>
+ *   角色:规划师。前置:客户设定了营养目标(或由规划师设定)。<br>
+ *   主流程:设目标(<b>热量 / 宏量营养素</b>)→ 生成方案 → 关联食谱。<br>
+ *   异常/边界:<b>目标非法(负 / 超上限)拦截</b>。
+ * </blockquote></p>
+ *
+ * <p><b>端点载体</b>(EVD-10 §4.57 回填):{@code DietRecommendationController /api/diet/recommendation/*}
+ * (generate 为「生成方案」入口)。规划师经 {@code familyId}(拦截器注入)作用于其绑定家庭,
+ * 三类流中的权限流(家庭隔离)由 {@code JwtInterceptor} + {@code familyId} 作用域承载,
+ * 在本控制器单测粒度不重复建模,以说明性用例标注(不适用)。</p>
+ *
+ * <p><b>实现现状(TDD-RED,实测 origin/cfclub)</b>:本能力契约所需的「营养目标(宏量营养素)」
+ * 链路<b>未落地</b>——
+ * <ul>
+ *   <li>{@code DietRecommendationService.aggregateNutrition} 将 {@code protein/carbs/fat}
+ *       <b>恒置 0</b>(源码注释原文:「protein/carbs/fat 恒 0 不在本轮范围」),
+ *       且 {@code toDish} 归一化时丢弃 AI 菜品的宏量营养素,仅保留热量 →
+ *       生成的方案 nutritionSummary 无法承载「宏量营养素」目标;</li>
+ *   <li>{@code generateRecommendation} 仅读取 {@code date / meal_type / selected_foods /
+ *       participant_member_ids / view_key},<b>不读取 / 不校验营养目标</b>,
+ *       非法(负 / 超上限)目标被静默忽略并照常返回成功,无「拦截」。</li>
+ * </ul>
+ * 因此本测试以<b>纯 Mockito 行为级探测</b>(无 Spring / 无 DB / 无外部 AI)断言契约缺失:
+ * 能编译、能运行、断言失败(红),失败断言原文清晰可贴。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者在 {@code DietRecommendationService} 侧——
+ * ① 让 {@code toDish} 保留并让 {@code aggregateNutrition} 真实聚合 AI 菜品的
+ * {@code protein/carbs/fat}(占位菜 / 汤无营养数据时贡献 0);
+ * ② 在方案生成契约中读取营养目标(本测试以 {@code nutrition_goals} 承载,
+ * 形如 {@code {calories, protein, carbs, fat}}),并对非法目标(负值 / 超上限)
+ * 返回<b>拒绝</b>(非 200,如 badRequest 400),而非静默放行——本测试自动转绿。</p>
+ */
+class NutritionPlanTeacherContractTest extends AssessmentPlanContractTestBase {
+
+    private static final ObjectMapper OM = new ObjectMapper();
+
+    private static final Long FAMILY_ID = 7L;
+    private static final String DATE = "2026-11-13";
+
+    /**
+     * 模拟 AI 返回的午餐 + 晚餐菜单(每餐 2 道主菜,均带 nutrition 宏量营养素)。
+     *
+     * <p>N=2 → DishCountRule.dishCount(2)=4 主菜,normalizeMenu 会把每餐补齐到
+     * 4 主菜 + 1 汤;补齐的占位菜/汤 nutrition 为空 → 宏量营养素贡献 0。
+     * 故真实菜品宏量营养素合计:protein=30+5+28+3=66、carbs=5+20+10+30=65、
+     * fat=8+2+12+1=23、calories=200+100+250+150=700(热量当前已可聚合,作对照)。</p>
+     */
+    private static final String AI_MENU_JSON = "{"
+            + "\"meals\":["
+            + "{\"type\":\"lunch\",\"name\":\"午餐\",\"dishes\":["
+            +   "{\"name\":\"鸡胸肉\",\"type\":\"dish\",\"nutrition\":{\"calories\":200,\"protein\":30,\"carbs\":5,\"fat\":8},"
+            +     "\"ingredients\":[{\"name\":\"鸡胸肉\",\"grams\":150}],\"cooking_method\":\"煎\"},"
+            +   "{\"name\":\"西兰花\",\"type\":\"dish\",\"nutrition\":{\"calories\":100,\"protein\":5,\"carbs\":20,\"fat\":2},"
+            +     "\"ingredients\":[{\"name\":\"西兰花\",\"grams\":100}],\"cooking_method\":\"煮\"}"
+            + "]},"
+            + "{\"type\":\"dinner\",\"name\":\"晚餐\",\"dishes\":["
+            +   "{\"name\":\"牛肉\",\"type\":\"dish\",\"nutrition\":{\"calories\":250,\"protein\":28,\"carbs\":10,\"fat\":12},"
+            +     "\"ingredients\":[{\"name\":\"牛肉\",\"grams\":120}],\"cooking_method\":\"炒\"},"
+            +   "{\"name\":\"米饭\",\"type\":\"dish\",\"nutrition\":{\"calories\":150,\"protein\":3,\"carbs\":30,\"fat\":1},"
+            +     "\"ingredients\":[{\"name\":\"米饭\",\"grams\":100}],\"cooking_method\":\"蒸\"}"
+            + "]}"
+            + "]}";
+
+    private DietRecommendationMapper recMapper;
+    private MealConfigMapper mealConfigMapper;
+    private FamilyMemberMapper familyMemberMapper;
+    private AiGateway aiGateway;
+    private BeijingNutritionService beijingNutritionService;
+    private DietRecommendationService service;
+    private DietRecommendationController controller;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        recMapper = mock(DietRecommendationMapper.class);
+        mealConfigMapper = mock(MealConfigMapper.class);
+        familyMemberMapper = mock(FamilyMemberMapper.class);
+        aiGateway = mock(AiGateway.class);
+        beijingNutritionService = mock(BeijingNutritionService.class);
+
+        // 真实 Service(注入 mock 依赖,无 Spring / 无 DB)
+        service = new DietRecommendationService();
+        inject(service, "dietRecommendationMapper", recMapper);
+        inject(service, "mealConfigMapper", mealConfigMapper);
+        inject(service, "familyMemberMapper", familyMemberMapper);
+        inject(service, "aiGateway", aiGateway);
+        inject(service, "beijingNutritionService", beijingNutritionService);
+
+        // 真实 Controller(注入真实 Service)
+        controller = new DietRecommendationController();
+        inject(controller, "dietRecommendationService", service);
+
+        // MyBatis-Plus TableInfo 预注册(使 LambdaQueryWrapper 在无 Spring 上下文可解析列名)
+        initTable(com.etotem.cfc.entity.DietRecommendation.class);
+        initTable(com.etotem.cfc.entity.MealConfig.class);
+        initTable(com.etotem.cfc.entity.FamilyMember.class);
+
+        // AI 网关桩:返回带宏量营养素的可用菜单(含 lunch + dinner,跳过规则降级)
+        when(aiGateway.generateMenu(anyString(), anyString(), anyString(), anyInt()))
+                .thenReturn(AI_MENU_JSON);
+        // 食材可用性未桩:generateIngredientList 返回 null → availableIngredientNames 兜底空列表,
+        // 占位菜/汤贡献 0 宏量营养素(与 GREEN 语义一致)。
+    }
+
+    // ------------------------------------------------------------------
+    // 正常流:生成方案应聚合宏量营养素(热量/蛋白/碳水/脂肪)到 nutritionSummary
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:生成方案 nutritionSummary 应聚合宏量营养素 protein/carbs/fat(非恒 0)")
+    void generate_正常_宏量营养素应聚合() throws Exception {
+        Map<String, Object> request = baseRequest();
+
+        Result<Map> r = controller.generateRecommendation(FAMILY_ID, request);
+        assertEquals(200, r.getCode(), "方案生成应成功(AI 桩返回可用菜单)");
+
+        Map<String, Object> summary = readSummary(r);
+        // 对照(应通过):热量当前已聚合 —— 证明 mock 装配与归一化链路正确,
+        // 下方宏量营养素断言失败是「契约缺口」而非「测试装配错误」。
+        assertEquals(700, intOf(summary.get("calories")),
+                "对照:nutritionSummary.calories 应聚合真实菜品热量(占位菜/汤贡献 0)");
+        // RED 断言:宏量营养素应从 AI 菜品聚合,当前 aggregateNutrition 恒置 0。
+        assertEquals(66, intOf(summary.get("protein")),
+                "[teacher.nutrition.plan] nutritionSummary.protein 应聚合 AI 菜品蛋白质(30+5+28+3=66),"
+                        + "当前 aggregateNutrition 恒置 0、toDish 归一化丢弃宏量营养素 —— "
+                        + "契约要求的「宏量营养素」目标未承载。");
+        assertEquals(65, intOf(summary.get("carbs")),
+                "[teacher.nutrition.plan] nutritionSummary.carbs 应聚合 AI 菜品碳水(5+20+10+30=65),当前恒 0。");
+        assertEquals(23, intOf(summary.get("fat")),
+                "[teacher.nutrition.plan] nutritionSummary.fat 应聚合 AI 菜品脂肪(8+2+12+1=23),当前恒 0。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:营养目标非法(负 / 超上限)应被拦截(拒绝,非 200)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:营养目标 calories 为负(-500)应被拦截,返回非 200")
+    void generate_边界_负热量目标应拦截() {
+        Map<String, Object> request = baseRequest();
+        request.put("nutrition_goals", goals(-500, 60, 80, 40));
+
+        Result<Map> r = controller.generateRecommendation(FAMILY_ID, request);
+        assertTrue(r.getCode() != 200,
+                "[teacher.nutrition.plan] 营养目标 calories=-500(负值)非法,契约要求「拦截」"
+                        + "(返回拒绝态非 200,如 badRequest 400),而非静默忽略并照常返回成功。"
+                        + "实测 code=" + r.getCode() + ",当前实现未读取/未校验营养目标,非法目标被放行。");
+    }
+
+    @Test
+    @DisplayName("边界流-2:营养目标 calories 超上限(999999)应被拦截,返回非 200")
+    void generate_边界_超上限热量目标应拦截() {
+        Map<String, Object> request = baseRequest();
+        request.put("nutrition_goals", goals(999999, 60, 80, 40));
+
+        Result<Map> r = controller.generateRecommendation(FAMILY_ID, request);
+        assertTrue(r.getCode() != 200,
+                "[teacher.nutrition.plan] 营养目标 calories=999999(超上限)非法,契约要求「拦截」"
+                        + "(返回拒绝态非 200),而非静默忽略并照常返回成功。"
+                        + "实测 code=" + r.getCode() + ",当前实现未读取/未校验营养目标。");
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流(不适用):家庭隔离由 JwtInterceptor + familyId 作用域承载
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:规划师经 familyId 作用于绑定家庭,家庭隔离由拦截器承载,本控制器单测粒度不适用")
+    void permissionFlow_家庭隔离由拦截器承载_不适用() {
+        // 说明性用例:teacher.nutrition.plan 的权限维度 = 规划师仅能作用于其绑定家庭,
+        // 由 JwtInterceptor 注入 familyId + 全局数据隔离承载;本控制器方法仅按 familyId 过滤,
+        // 不重复建模越权分支(与 TeamProfitHookContractTest 的「权限流不适用」标注一致)。
+        // 无家庭上下文时 controller 已返回 Result.noFamily(5001),为既有行为,此处不产生断言失败。
+        assertTrue(true, "权限流不适用:家庭隔离由 JwtInterceptor + familyId 作用域承载。");
+    }
+
+    // ------------------------------------------------------------------
+    // helpers
+    // ------------------------------------------------------------------
+
+    /** 基础请求:date + meal_type + 共餐人(避免回落 meal_config 兜底查询) */
+    private static Map<String, Object> baseRequest() {
+        Map<String, Object> request = new HashMap<>();
+        request.put("date", DATE);
+        request.put("meal_type", "lunch");
+        request.put("participant_member_ids", Arrays.asList(1, 2));
+        return request;
+    }
+
+    /** 营养目标载荷:{calories, protein, carbs, fat} */
+    private static Map<String, Object> goals(int calories, int protein, int carbs, int fat) {
+        Map<String, Object> goals = new HashMap<>();
+        goals.put("calories", calories);
+        goals.put("protein", protein);
+        goals.put("carbs", carbs);
+        goals.put("fat", fat);
+        return goals;
+    }
+
+    /** 从 Result.data 读取并解析 nutritionSummary(safeWrite 后为 JSON 串) */
+    private static Map<String, Object> readSummary(Result<Map> r) throws Exception {
+        Map<String, Object> data = r.getData();
+        if (data == null) {
+            fail("[teacher.nutrition.plan] 方案生成结果 data 为空,无法读取 nutritionSummary。");
+        }
+        Object summaryObj = data.get("nutritionSummary");
+        if (summaryObj == null) {
+            fail("[teacher.nutrition.plan] 方案生成结果缺 nutritionSummary。");
+        }
+        JsonNode node = OM.readTree(summaryObj.toString());
+        Map<String, Object> summary = new HashMap<>();
+        summary.put("calories", node.has("calories") ? node.get("calories").asInt() : null);
+        summary.put("protein", node.has("protein") ? node.get("protein").asInt() : null);
+        summary.put("carbs", node.has("carbs") ? node.get("carbs").asInt() : null);
+        summary.put("fat", node.has("fat") ? node.get("fat").asInt() : null);
+        return summary;
+    }
+
+    private static int intOf(Object v) {
+        return v instanceof Number ? ((Number) v).intValue() : 0;
+    }
+}

+ 249 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherDataIsolationScT13ContractTest.java

@@ -0,0 +1,249 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.guide.PlanReviewController;
+import com.etotem.cfc.entity.TaskPlanInstance;
+import com.etotem.cfc.mapper.TaskPlanInstanceMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.GuideFamilyAccessGuard;
+import com.etotem.cfc.service.GuideFamilyService;
+import com.etotem.cfc.service.PlanActivationService;
+import com.etotem.cfc.service.PlannerCertificationGuard;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 契约级单测:规划师视角数据隔离(能力键 {@code teacher.data.isolation},SC-T-13)。
+ *
+ * <p><b>契约锚点</b></p>
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-T-13(规划师视角数据隔离——仅见绑定家庭;
+ *       边界:非绑定家庭 403;模块:控制器内 role 检查)</li>
+ *   <li>设计:{@code docs/superpowers/specs/2026-11-07-planner-zone-contract.md}(EVD-10 §2 SC-T-13 端点核验表 +
+ *       §5.1 权限流建议:teacher 越权访问非绑定家庭 {@code {familyId}} 端点应 403/拒绝)</li>
+ *   <li>API:{@code docs/superpowers/api/API_REFERENCE.md} §4.57 SC-T-13——授权源
+ *       {@code guide_families.status='binding'}({@link GuideFamilyService#isBound})+ 角色隔离;
+ *       {@code PlanReviewController family-list}(teacher 按 JWT familyId 过滤)</li>
+ * </ul>
+ *
+ * <p><b>实测缺口(RED 依据,origin/cfclub @ e03ec423a 工作树)</b></p>
+ * <p>{@code PlanReviewController#listFamilyPlans}({@code POST /api/guide/plans/family-list})对
+ * teacher 的处理是「body.familyId 优先、JWT familyId 兜底」,<b>无任何 isBound 校验</b>——
+ * 控制器仅注入 {@code planMapper/userMapper/planActivationService/plannerCertificationGuard},
+ * 未注入全项目唯一授权判定点 {@link GuideFamilyAccessGuard}(其 javadoc 自述「全项目唯一判定点,
+ * 避免各控制器散写导致口径漂移」)。因此 teacher 在 body 中传任意非绑定 familyId 即可列出该家庭全部方案
+ * (IDOR 数据泄漏),与契约「teacher 按 JWT familyId 过滤 / 非绑定家庭 403」相悖。SC-T-13 其余三个载体
+ * ({@code GuideFamilyQueryController /my-families}、{@code GuideFamilyTaskController /bound-families +
+ * /{familyId}/*} 的 isBound 校验)已由 {@code GuideBindingAccessContractTest} / {@code PlanReviewTeacherContractTest}
+ * 覆盖,本类专补 family-list 隔离缺口。</p>
+ *
+ * <p><b>RED 预期</b>:权限流(越权 403 / 不泄漏非绑定家庭方案 / 越权不查询 planMapper)与
+ * 结构流(注入统一授权守卫)在实现补齐前应失败(红);正常流/异常流/观察为基线钉住(绿)。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者在 {@code PlanReviewController} 注入
+ * {@code GuideFamilyAccessGuard}(或 {@code GuideFamilyService}),在 {@code listFamilyPlans} 的
+ * teacher 分支先做 {@code checkFamilyAccess(userId, filterFamilyId)} 校验(非绑定 → 403),
+ * 并将家族过滤收敛到绑定/JWT 家庭集合后,本类自动转绿。</p>
+ */
+class TeacherDataIsolationScT13ContractTest extends AssessmentPlanContractTestBase {
+
+    private static final Long TEACHER = 7L;
+    /** JWT familyId(默认家庭上下文,按契约即 teacher 的绑定/JWT 家庭) */
+    private static final Long OWN_FAMILY = 7L;
+    /** 非绑定家庭:teacher 越权目标(契约要求 403/拒绝) */
+    private static final Long FOREIGN_FAMILY = 99L;
+
+    private TaskPlanInstanceMapper planMapper;
+    private PlanReviewController controller;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        // 无 Spring 上下文:预注册 TableInfo,使 listFamilyPlans 内 LambdaQueryWrapper 可用
+        initTable(TaskPlanInstance.class);
+        planMapper = mock(TaskPlanInstanceMapper.class);
+        controller = new PlanReviewController();
+        inject(controller, "planMapper", planMapper);
+        inject(controller, "userMapper", mock(UserMapper.class));
+        inject(controller, "planActivationService", mock(PlanActivationService.class));
+        inject(controller, "plannerCertificationGuard", mock(PlannerCertificationGuard.class));
+    }
+
+    // ==================== 正常流 / 异常流(基线钉住,当前实现已符合契约) ====================
+
+    @Test
+    @DisplayName("SC-T-13 正常流-1:admin 指定任务家庭查方案列表 → 200 放行(运营兜底)")
+    void familyList_正常_admin放行() {
+        when(planMapper.selectList(any())).thenReturn(foreignPlans());
+
+        Result<List<TaskPlanInstance>> r =
+                controller.listFamilyPlans(bodyWithFamily(FOREIGN_FAMILY), 1L, "admin", 2L, null);
+
+        assertEquals(200, r.getCode());
+        assertNotNull(r.getData());
+    }
+
+    @Test
+    @DisplayName("SC-T-13 异常流-1:teacher 无 familyId 且无 JWT 家庭 → 5001 请先创建或加入家庭")
+    void familyList_异常_teacher无家庭上下文() {
+        Result<List<TaskPlanInstance>> r =
+                controller.listFamilyPlans(new HashMap<String, Object>(), TEACHER, "teacher", null, null);
+
+        assertEquals(5001, r.getCode());
+        assertTrue(r.getMessage().contains("请先创建或加入家庭"));
+    }
+
+    @Test
+    @DisplayName("SC-T-13 异常流-2:非 teacher/admin 角色 → 拒绝(无权限)")
+    void familyList_异常_非teacher角色拒绝() {
+        Result<List<TaskPlanInstance>> r =
+                controller.listFamilyPlans(bodyWithFamily(OWN_FAMILY), 1L, "parent", 7L, null);
+
+        assertEquals(500, r.getCode());
+        assertTrue(r.getMessage().contains("无权限"));
+    }
+
+    // ==================== 权限流(RED:family-list 数据隔离缺口,当前实现越权可及) ====================
+
+    @Test
+    @DisplayName("SC-T-13 权限流-1:teacher 越权访问非绑定家庭(99L) family-list → 应 403 拒绝")
+    void familyList_权限_teacher越权非绑定家庭_403() {
+        when(planMapper.selectList(any())).thenReturn(foreignPlans());
+
+        Result<List<TaskPlanInstance>> r =
+                controller.listFamilyPlans(bodyWithFamily(FOREIGN_FAMILY), TEACHER, "teacher", OWN_FAMILY, null);
+
+        // 契约(EVD-10 §5.1 / API_REFERENCE §4.57 SC-T-13):teacher 越权访问非绑定家庭应 403/拒绝。
+        // 当前实现无 isBound 校验,直接以 body.familyId 查询并返回该家庭方案(200 + data)→ 断言失败(红)。
+        assertEquals(403, r.getCode(),
+                "[teacher.data.isolation] family-list 应拒绝 teacher 越权访问非绑定家庭(99L):"
+                        + "需按 guide_families(status='binding') isBound 校验(SC-T-13)。");
+    }
+
+    @Test
+    @DisplayName("SC-T-13 权限流-2:teacher 携带 body.familyId 不得返回非绑定家庭方案(按 JWT 家庭隔离)")
+    void familyList_权限_不泄漏跨家庭方案() {
+        when(planMapper.selectList(any())).thenReturn(foreignPlans());
+
+        Map<String, Object> body = bodyWithFamily(FOREIGN_FAMILY);
+        Result<List<TaskPlanInstance>> r =
+                controller.listFamilyPlans(body, TEACHER, "teacher", OWN_FAMILY, null);
+
+        // 契约:teacher 查询范围 = 绑定/JWT 家庭;99L 非绑定 → 结果不得含 99L 方案。
+        // 当前实现 body.familyId 直接透传 → 返回 foreignPlans(含 99L)→ 断言失败(红)。
+        assertEquals(200, r.getCode());
+        assertNotNull(r.getData());
+        for (TaskPlanInstance plan : r.getData()) {
+            assertTrue(!FOREIGN_FAMILY.equals(plan.getFamilyId()),
+                    "[teacher.data.isolation] family-list 泄漏非绑定家庭(99L)方案:"
+                            + "teacher 应按绑定/JWT 家庭过滤,禁止跨家庭返回(SC-T-13)。");
+        }
+    }
+
+    @Test
+    @DisplayName("SC-T-13 权限流-3:teacher 越权非绑定家庭时 planMapper.selectList 不得被查询(拒绝先行)")
+    void familyList_权限_越权不查询planMapper() {
+        when(planMapper.selectList(any())).thenReturn(foreignPlans());
+
+        Map<String, Object> body = bodyWithFamily(FOREIGN_FAMILY);
+        controller.listFamilyPlans(body, TEACHER, "teacher", OWN_FAMILY, null);
+
+        // 契约(GuideFamilyAccessGuard 全项目唯一判定点语义):隔离校验先行,
+        // 非绑定家庭不触达方案查询层。当前实现无校验、直接 selectList → verify(never) 失败(红)。
+        verify(planMapper, never()).selectList(any());
+    }
+
+    // ==================== 结构流(RED:family-list 未接入统一授权判定点) ====================
+
+    @Test
+    @DisplayName("SC-T-13 结构流-1:PlanReviewController 应注入 GuideFamilyAccessGuard/GuideFamilyService(统一判定点)")
+    void familyList_结构_应注入统一授权守卫() {
+        boolean hasGuard = false;
+        for (Field f : PlanReviewController.class.getDeclaredFields()) {
+            String type = f.getType().getName();
+            if (GuideFamilyAccessGuard.class.getName().equals(type)
+                    || GuideFamilyService.class.getName().equals(type)) {
+                hasGuard = true;
+                break;
+            }
+        }
+        assertTrue(hasGuard,
+                "[teacher.data.isolation] PlanReviewController 未注入 GuideFamilyAccessGuard/GuideFamilyService:"
+                        + "契约要求 family-list 对 teacher 复用全项目唯一授权判定点(guide_families status='binding' "
+                        + "isBound 校验,SC-T-13 / EVD-10 §5.1),越权访问非绑定家庭应 403。当前未落地。");
+    }
+
+    @Test
+    @DisplayName("SC-T-13 结构流-2(基线):授权源 guide_families 表 + status='binding' 语义应存在于 schema")
+    void familyList_结构_授权源表存在() {
+        String schema = readSchemaSql();
+        assertTrue(schema.contains("guide_families"),
+                "[teacher.data.isolation] schema.sql 应含 guide_families 表(授权源,SC-T-13 契约 §2)。"
+                        + "GREEN 实现依赖该表 status='binding' 做 isBound 判定。");
+        assertTrue(schema.contains("binding"),
+                "[teacher.data.isolation] schema.sql 的 guide_families 应含 status='binding' 状态语义"
+                        + "(GuideFamilyService.isBound 授权源,SC-T-13)。");
+    }
+
+    // ==================== 观察(钉住现状,不修实现) ====================
+
+    @Test
+    @DisplayName("SC-T-13 观察-1:planDetail 无角色/归属校验入参(观察钉住,SC-T-13 契约面为 family-list)")
+    void pinsObserved_planDetail无隔离校验() throws Exception {
+        Method m = PlanReviewController.class.getDeclaredMethod("planDetail", Map.class);
+        assertEquals(1, m.getParameterCount(),
+                "planDetail 仅接收 body(无 role/userId 归属校验入参)。观察钉住现状:"
+                        + "SC-T-13 契约面为 family-list(teacher 按 JWT familyId 过滤),detail 无隔离不在本键断言范围。");
+    }
+
+    // ==================== 辅助构造 ====================
+
+    private Map<String, Object> bodyWithFamily(Long familyId) {
+        Map<String, Object> body = new HashMap<String, Object>();
+        body.put("familyId", familyId);
+        return body;
+    }
+
+    private List<TaskPlanInstance> foreignPlans() {
+        TaskPlanInstance p = new TaskPlanInstance();
+        p.setId(999L);
+        p.setFamilyId(FOREIGN_FAMILY);
+        p.setName("非绑定家庭方案");
+        p.setStatus("generated");
+        p.setCreatedAt(new Date());
+        return Collections.singletonList(p);
+    }
+
+    private String readSchemaSql() {
+        try (java.io.InputStream is =
+                     getClass().getClassLoader().getResourceAsStream("schema.sql")) {
+            if (is == null) {
+                return "";
+            }
+            try (java.util.Scanner scanner =
+                         new java.util.Scanner(is, java.nio.charset.StandardCharsets.UTF_8.name())) {
+                scanner.useDelimiter("\\A");
+                return scanner.hasNext() ? scanner.next() : "";
+            }
+        } catch (Exception e) {
+            return "";
+        }
+    }
+}

+ 295 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherDimensionScoreContractTest.java

@@ -0,0 +1,295 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.controller.family.ManualDimensionScoreController;
+import com.etotem.cfc.dto.ManualScoreSubmitDTO;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.FiveDimensionScore;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.FiveDimensionScoreMapper;
+import com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper;
+import com.etotem.cfc.service.FiveDimensionSelfCheckService;
+import com.etotem.cfc.service.impl.ManualDimensionScoreServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 契约级单测:成长规划师五维变化录入/打分(能力键 {@code teacher.dimension.score})。
+ *
+ * <p><b>需求/设计锚点(EVD-10 契约,2026-11-07)</b>:
+ * {@code docs/superpowers/specs/2026-11-07-planner-zone-contract.md} SC-T-30/31/32
+ * + {@code docs/superpowers/api/API_REFERENCE.md} §4.57 成长规划师域 + sc-key-map.md
+ * (SC-T-30/31/32 均「未跑通」,本测试为其补契约 JUnit 证据)。</p>
+ *
+ * <p><b>被校验的契约行为</b></p>
+ * <ul>
+ *   <li>SC-T-30 测评录入触发五维维度变化:{@code DanAssessmentController /result/record}
+ *       → {@code AssessmentService.recordResult} → {@link com.etotem.cfc.service.DimensionScoreService#refreshFromDanAssessment}</li>
+ *   <li>SC-T-31 手动维度打分覆盖/调整:{@code ManualDimensionScoreController /api/family/manual-score/*}
+ *       (submit/latest/history)→ 落 {@code five_dimension_scores}(source_type=manual)</li>
+ *   <li>SC-T-32 五维自评录入与变化:{@code FiveDimensionSelfCheckController /api/family/self-check/*}
+ *       (questions/submit/latest/history/status/ignore/generate-plan/trend-analysis/ai-generate-questions)
+ *       → <b>自评分数应桥接落五维变化</b>(契约原文「五维自评 → 变化落 EnergyController」)</li>
+ * </ul>
+ *
+ * <p><b>实现现状(origin/cfclub 实测核实)</b>:
+ * SC-T-30/31 载体已落地(测评桥接钩子、手动打分 controller+service 均存在);
+ * <b>SC-T-32 的「自评 → 五维变化桥接」未落地</b>——{@code FiveDimensionSelfCheckService}
+ * 仅声明 {@code selfCheckMapper/familyMemberMapper/wuxingSourcingService/onboardingService/
+ * ignoreMapper/selfCheckAnalysisService/selfCheckTrendService} 等字段,<b>无任何五维变化落点
+ * (DimensionScoreService / EnergyService / FiveDimensionScoreMapper)引用</b>;
+ * schema.sql 中 {@code five_dimension_scores.source_type} 枚举注释
+ * (periodic/task/assessment/manual)也<b>无 selfcheck 来源</b>。故 SC-T-32 桥接用例预期红。</p>
+ *
+ * <p><b>测试范围</b>:纯契约级单测,Mockito + 反射结构探测,无 Spring / 无 DB / 无外部服务。</p>
+ */
+class TeacherDimensionScoreContractTest {
+
+    /** SC-T-31 五维编码(契约口径 body/mind/wisdom/action/wealth) */
+    private static final String[] DIMENSIONS = {"body", "mind", "wisdom", "action", "wealth"};
+
+    // ==================== SC-T-31 行为级绿测试的 mock ====================
+
+    @Mock
+    private FiveDimensionScoreMapper fiveDimensionScoreMapper;
+
+    @Mock
+    private FiveDimensionSelfCheckMapper selfCheckMapper;
+
+    @Mock
+    private FamilyMemberMapper familyMemberMapper;
+
+    @InjectMocks
+    private ManualDimensionScoreServiceImpl manualScoreService;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+    }
+
+    // ------------------------------------------------------------------
+    // 正常流-1(SC-T-31):手动维度打分端点 submit/latest/history 应存在
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1(SC-T-31):ManualDimensionScoreController 应暴露 submit/latest/history 端点")
+    void manualScore_controllerExposesSubmitLatestHistory() {
+        Class<?> clazz = ManualDimensionScoreController.class;
+        String[] requiredPaths = {"/submit", "/latest", "/history"};
+        for (String path : requiredPaths) {
+            boolean found = false;
+            for (Method m : clazz.getDeclaredMethods()) {
+                org.springframework.web.bind.annotation.PostMapping pm =
+                        m.getAnnotation(org.springframework.web.bind.annotation.PostMapping.class);
+                if (pm != null && java.util.Arrays.asList(pm.value()).contains(path)) {
+                    found = true;
+                    break;
+                }
+            }
+            assertTrue(found,
+                    "[teacher.dimension.score] ManualDimensionScoreController 应暴露 POST "
+                            + path + " 端点(契约 SC-T-31 手动维度打分覆盖/调整)。");
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // 正常流-2(SC-T-31):手动打分 submit 应落 five_dimension_scores(source_type=manual)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-2(SC-T-31):手动打分 submit 应写 five_dimension_scores(source_type=manual)")
+    void manualScore_submitPersistsManualSourceRecords() {
+        FamilyMember member = new FamilyMember();
+        member.setId(88001L);
+        member.setFamilyId(9001L);
+        member.setNickname("契约之子");
+
+        Map<String, Integer> scores = new LinkedHashMap<>();
+        scores.put("body", 80);
+        scores.put("mind", 60);
+        scores.put("wisdom", 70);
+        scores.put("action", 50);
+        scores.put("wealth", 90);
+
+        ManualScoreSubmitDTO dto = new ManualScoreSubmitDTO();
+        dto.setMemberId(88001L);
+        dto.setScores(scores);
+
+        when(familyMemberMapper.selectById(88001L)).thenReturn(member);
+
+        manualScoreService.submit(dto, 10001L);
+
+        ArgumentCaptor<FiveDimensionScore> captor = ArgumentCaptor.forClass(FiveDimensionScore.class);
+        verify(fiveDimensionScoreMapper, times(5)).insert(captor.capture());
+        Set<String> codes = new HashSet<>();
+        for (FiveDimensionScore record : captor.getAllValues()) {
+            codes.add(record.getDimensionCode());
+            assertTrue("manual".equals(record.getSourceType()),
+                    "[teacher.dimension.score] 手动打分应落 source_type=manual,实际="
+                            + record.getSourceType() + "(契约 SC-T-31)。");
+        }
+        for (String dim : DIMENSIONS) {
+            assertTrue(codes.contains(dim),
+                    "[teacher.dimension.score] 手动打分应覆盖五维编码 " + dim + "(契约 SC-T-31)。");
+        }
+        // 自检记录同步落表(复用 five_dimension_self_checks 记录手动填分)
+        verify(selfCheckMapper, times(1)).insert(any());
+    }
+
+    // ------------------------------------------------------------------
+    // 正常流-3(SC-T-30):测评录入应触发五维维度变化(桥接钩子存在)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-3(SC-T-30):DimensionScoreService 应提供 refreshFromDanAssessment 桥接")
+    void dimensionScoreService_exposesDanAssessmentRefreshHook() throws Exception {
+        Class<?> ds = Class.forName("com.etotem.cfc.service.DimensionScoreService");
+        boolean hasHook = false;
+        for (Method m : ds.getDeclaredMethods()) {
+            if ("refreshFromDanAssessment".equals(m.getName())) {
+                hasHook = true;
+                break;
+            }
+        }
+        assertTrue(hasHook,
+                "[teacher.dimension.score] DimensionScoreService 应提供 refreshFromDanAssessment"
+                        + "(契约 SC-T-30 测评录入触发五维维度变化)。");
+
+        // AssessmentService 应注入该桥接(recordResult 触发点)
+        Class<?> as = Class.forName("com.etotem.cfc.service.AssessmentService");
+        boolean injected = false;
+        for (Field f : as.getDeclaredFields()) {
+            if ("com.etotem.cfc.service.DimensionScoreService".equals(f.getType().getName())) {
+                injected = true;
+                break;
+            }
+        }
+        assertTrue(injected,
+                "[teacher.dimension.score] AssessmentService 应注入 DimensionScoreService"
+                        + "(recordResult 录入测评结果时触发五维维度变化,契约 SC-T-30)。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流-1(SC-T-32,预期红):五维自评应桥接落五维变化(契约「自评 → 变化落 EnergyController」)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1(SC-T-32,红):五维自评提交应桥接五维分数/能量变化(当前未落地)")
+    void selfCheck_submitShouldBridgeDimensionScoreChange() {
+        Class<?> clazz = FiveDimensionSelfCheckService.class;
+        String[] bridgeTypes = {
+                "com.etotem.cfc.service.DimensionScoreService", // 维度分桥接(refactor 同 SC-T-30)
+                "com.etotem.cfc.service.EnergyService",          // 能量落点(契约「变化落 EnergyController」)
+                "com.etotem.cfc.mapper.FiveDimensionScoreMapper" // 直接落 five_dimension_scores
+        };
+        String found = null;
+        for (Field f : clazz.getDeclaredFields()) {
+            String type = f.getType().getName();
+            for (String t : bridgeTypes) {
+                if (t.equals(type)) {
+                    found = t;
+                    break;
+                }
+            }
+            if (found != null) {
+                break;
+            }
+        }
+        assertNotNull(found,
+                "[teacher.dimension.score][SC-T-32] 五维自评提交后应触发五维变化落点:"
+                        + "FiveDimensionSelfCheckService 未注入任何维度分/能量桥接服务(DimensionScoreService / "
+                        + "EnergyService / FiveDimensionScoreMapper 均无)。契约要求「五维自评 → 变化落 "
+                        + "EnergyController」,当前自评只写 five_dimension_self_checks 表,"
+                        + "未桥接 five_dimension_scores / 五维能量。实现未落地,测试预期红。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流-2(SC-T-32,预期红):five_dimension_scores.source_type 应有自评来源
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-2(SC-T-32,红):five_dimension_scores.source_type 应含 selfcheck 来源(当前缺)")
+    void schema_sourceType_shouldSupportSelfCheck() {
+        String schema = readSchemaSql();
+        assertTrue(schema.contains("five_dimension_scores"),
+                "[teacher.dimension.score] schema.sql 缺少 five_dimension_scores 建表。");
+
+        // 定位 five_dimension_scores 建表段的 source_type 枚举注释
+        int idx = schema.indexOf("CREATE TABLE IF NOT EXISTS five_dimension_scores");
+        assertTrue(idx >= 0,
+                "[teacher.dimension.score] schema.sql 未找到 five_dimension_scores 建表语句。");
+        int end = schema.indexOf("ENGINE=InnoDB", idx);
+        String ddl = end > idx ? schema.substring(idx, end) : schema.substring(idx);
+        assertTrue(ddl.contains("selfcheck"),
+                "[teacher.dimension.score][SC-T-32] five_dimension_scores.source_type 枚举注释"
+                        + "(periodic/task/assessment/manual)缺少五维自评来源(selfcheck):"
+                        + "契约要求「五维自评 → 变化落 EnergyController」,自评分数应能写为五维分数变化,"
+                        + "当前建表定义无对应来源。实现未落地,测试预期红。");
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流:teacher.dimension.score 为双角色业务域能力,无独立角色断言
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:手动打分/自评端点走 JWT 登录态,规划师双角色无独立角色断言")
+    void permissionFlow_teacherDualRoleNoSeparateRoleAssertion() {
+        // 说明性用例:SC-T-30/31/32 端点经 JwtInterceptor 登录态保护(@RequestAttribute("userId")),
+        // 规划师(role=teacher)与家长共用同端点;契约未定义独立 teacher-only 角色分支(与
+        // FamilyJoinValidationRulesContractTest 权限流口径一致)。不产生断言失败。
+        assertTrue(true,
+                "权限流说明:teacher.dimension.score 端点面向登录用户(含 role=teacher 双角色),"
+                        + "无独立角色门禁断言。");
+    }
+
+    // ------------------------------------------------------------------
+    // 辅助
+    // ------------------------------------------------------------------
+
+    private String readSchemaSql() {
+        try (InputStream is = getClass().getClassLoader().getResourceAsStream("schema.sql")) {
+            if (is == null) {
+                return "";
+            }
+            try (Scanner scanner = new Scanner(is, StandardCharsets.UTF_8.name())) {
+                scanner.useDelimiter("\\A");
+                return scanner.hasNext() ? scanner.next() : "";
+            }
+        } catch (Exception e) {
+            return "";
+        }
+    }
+
+    @SuppressWarnings("unused")
+    private Method findMethod(Class<?> clazz, String name) {
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals(name)) {
+                return m;
+            }
+        }
+        return null;
+    }
+}

+ 257 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherMindRecordContractTest.java

@@ -0,0 +1,257 @@
+package com.etotem.cfc.unit;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * 契约级单测:规划师舌诊/情绪测评录入(能力键 {@code teacher.mind.record},SC-T-25)。
+ *
+ * <p><b>需求/设计溯源</b></p>
+ * <ul>
+ *   <li>需求:sc-key-map.md SC-T-25「舌诊/情绪测评录入(规划师)」(成长规划师域,EVD-10)</li>
+ *   <li>契约:{@code docs/superpowers/specs/2026-11-07-planner-zone-contract.md} §2(SC-T-25 端点核验)
+ *       + §5.5(舌诊链入库语义:parse-preview{@code type=tongue} → confirm{@code type=tongue} 入库,
+ *       <b>非 tongue 类型不落入舌诊链</b>)</li>
+ *   <li>设计:API_REFERENCE §4.57 建议能力键映射 {@code teacher.mind.record}(T-25)——“统一
+ *       {@code MindAssessmentController} 舌诊/情绪测评录入(规划师)”;模块结构参考
+ *       {@code docs/backup/superpowers/plans/2026-06-02-care-family-club-design.md}(mind 域
+ *       {@code controller/mind/MindAssessmentController} + {@code service/MindAssessmentService}
+ *       + {@code entity/MindPsychologicalAssessment},设计方法
+ *       {@code createAssessment(assessment)} 插入入库、{@code getAssessments(...)} 按
+ *       {@code assessmentDate} 倒序)</li>
+ * </ul>
+ *
+ * <p><b>被校验的契约行为(统一录入链路,规划师代客户录入)</b></p>
+ * <ul>
+ *   <li>{@code MindAssessmentController}(package {@code controller.mind})应作为规划师端
+ *       舌诊/情绪测评录入统一入口:提供 {@code create}(代录,受评客户按 memberId 归属)与
+ *       {@code list}(按客户成员查看)端点;</li>
+ *   <li>{@code MindAssessmentService} 应桥接既有双类载体(舌诊链 {@code TongueDiagnosisService}
+ *       + 情绪链 {@code EmotionCheckinService}),提供 {@code createAssessment} 入库与
+ *       {@code getAssessments} 查询;</li>
+ *   <li>{@code MindPsychologicalAssessment} 实体应含受评客户归属({@code memberId},遵循
+ *       用户模型统一口径,不用 userId/childId)、录入规划师归属({@code guideId})、测评类型
+ *       ({@code assessmentType} = tongue/emotion 双类)、状态机({@code status},默认
+ *       {@code draft} → {@code confirmed},对齐舌诊链)与测评时间({@code assessmentDate})。</li>
+ * </ul>
+ *
+ * <p><b>实现现状</b>:本分支 src/main 中 {@code MindAssessmentController} /
+ * {@code MindAssessmentService} / {@code MindPsychologicalAssessment} 均<b>未落地</b>
+ * (三者全库 0 引用,grep 仅命中 backup 设计文档;舌诊/情绪双类现有载体
+ * {@code HealthReportController tongue 链} 与 {@code EmotionCheckinController} 为分散端点,
+ * 尚无统一规划师录入入口)。因此本测试以「反射结构契约」探测实现缺失:能编译、能运行、
+ * 断言失败(红),失败断言原文清晰可贴。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者按设计落地 {@code MindAssessmentController}(create/list)、
+ * {@code MindAssessmentService}(createAssessment/getAssessments)、
+ * {@code MindPsychologicalAssessment} 实体(含 memberId/guideId/assessmentType/status/
+ * assessmentDate 字段,status 默认 draft)后,本测试自动转绿;届时可将结构探测升级为
+ * 行为级 Mockito 验证(verify 入库 + state 流转 + 非 tongue 不落舌诊链)。</p>
+ *
+ * <p><b>权限流说明</b>:规划师(role=teacher)角色隔离由 {@code JwtInterceptor} +
+ * 既有 SC-T-13「规划师视角数据隔离」契约(guide_families isBound)覆盖,本键专注统一录入
+ * 链路自身契约,权限流以说明性用例标注「不适用」,不重复断言。</p>
+ *
+ * <p><b>RED 预期</b>:三个契约宿主类均未实现,全部用例在类探测/方法探测/字段探测处失败(红),
+ * 断言原文为「[teacher.mind.record] ... 未实现(实现未落地)」及同类文案。</p>
+ */
+class TeacherMindRecordContractTest {
+
+    /** 统一录入控制器(设计锚点:controller/mind 包,API_REFERENCE §4.57)。 */
+    private static final String HOST_CONTROLLER = "com.etotem.cfc.controller.mind.MindAssessmentController";
+
+    /** 统一录入服务(桥接 TongueDiagnosisService 舌诊链 + EmotionCheckinService 情绪链)。 */
+    private static final String HOST_SERVICE = "com.etotem.cfc.service.MindAssessmentService";
+
+    /** 心理测评记录实体(含受评客户/录入规划师归属与状态机)。 */
+    private static final String HOST_ENTITY = "com.etotem.cfc.entity.MindPsychologicalAssessment";
+
+    // ------------------------------------------------------------------
+    // 正常流:统一录入控制器存在并提供 代录/查看 端点
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:MindAssessmentController 类应存在(规划师舌诊/情绪测评录入统一入口)")
+    void controller_mindAssessmentController_classExists() {
+        assertNotNull(loadClass(HOST_CONTROLLER),
+                "[teacher.mind.record] MindAssessmentController 类不存在(controller/mind 包):"
+                        + "契约要求规划师端舌诊/情绪测评录入统一入口(API_REFERENCE §4.57 / EVD-10 SC-T-25)。"
+                        + "实现未落地。");
+    }
+
+    @Test
+    @DisplayName("正常流-2:MindAssessmentController 应提供 create 方法(规划师代录,受评客户按 memberId 归属)")
+    void controller_createEndpoint_exists() {
+        Class<?> clazz = loadClass(HOST_CONTROLLER);
+        if (clazz == null) {
+            fail("[teacher.mind.record] MindAssessmentController 类不存在,无法验证 create 代录入口。");
+        }
+        assertTrue(hasMethod(clazz, "create"),
+                "[teacher.mind.record] MindAssessmentController 应提供 create 方法:"
+                        + "规划师代客户录入舌诊/情绪测评(受评客户按 memberId 归属,不落 userId/childId)。");
+    }
+
+    @Test
+    @DisplayName("正常流-3:MindAssessmentController 应提供 list 方法(规划师按客户成员查看测评记录)")
+    void controller_listEndpoint_exists() {
+        Class<?> clazz = loadClass(HOST_CONTROLLER);
+        if (clazz == null) {
+            fail("[teacher.mind.record] MindAssessmentController 类不存在,无法验证 list 查看入口。");
+        }
+        assertTrue(hasMethod(clazz, "list"),
+                "[teacher.mind.record] MindAssessmentController 应提供 list 方法:"
+                        + "规划师按受评客户成员查看舌诊/情绪测评记录。");
+    }
+
+    // ------------------------------------------------------------------
+    // 正常流:统一录入服务存在并提供 入库/查询 能力
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-4:MindAssessmentService 类应存在(统一录入服务,桥接舌诊链 + 情绪链)")
+    void service_mindAssessmentService_classExists() {
+        assertNotNull(loadClass(HOST_SERVICE),
+                "[teacher.mind.record] MindAssessmentService 类不存在:契约要求统一录入服务"
+                        + "桥接 TongueDiagnosisService 舌诊链 + EmotionCheckinService 情绪链"
+                        + "(API_REFERENCE §4.57 / EVD-10 SC-T-25)。实现未落地。");
+    }
+
+    @Test
+    @DisplayName("正常流-5:MindAssessmentService 应提供 createAssessment 方法(测评记录入库)")
+    void service_createAssessment_methodExists() {
+        Class<?> clazz = loadClass(HOST_SERVICE);
+        if (clazz == null) {
+            fail("[teacher.mind.record] MindAssessmentService 类不存在,无法验证 createAssessment 入库。");
+        }
+        assertTrue(hasMethod(clazz, "createAssessment"),
+                "[teacher.mind.record] MindAssessmentService 应提供 createAssessment(...) 方法:"
+                        + "测评记录入库(设计 createAssessment(assessment) → mapper.insert)。");
+    }
+
+    @Test
+    @DisplayName("正常流-6:MindAssessmentService 应提供 getAssessments 方法(按 assessmentDate 倒序查询)")
+    void service_getAssessments_methodExists() {
+        Class<?> clazz = loadClass(HOST_SERVICE);
+        if (clazz == null) {
+            fail("[teacher.mind.record] MindAssessmentService 类不存在,无法验证 getAssessments 查询。");
+        }
+        assertTrue(hasMethod(clazz, "getAssessments"),
+                "[teacher.mind.record] MindAssessmentService 应提供 getAssessments(...) 方法:"
+                        + "按受评客户成员查询测评记录,orderByDesc(assessmentDate)(设计口径)。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:测评记录实体承载归属/类型/状态机(含默认值契约)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:MindPsychologicalAssessment 实体应存在(心理测评记录载体)")
+    void entity_mindPsychologicalAssessment_classExists() {
+        assertNotNull(loadClass(HOST_ENTITY),
+                "[teacher.mind.record] MindPsychologicalAssessment 实体不存在:契约要求测评记录实体"
+                        + "承载归属、类型与状态机(设计 mind 域实体)。实现未落地。");
+    }
+
+    @Test
+    @DisplayName("边界流-2:实体应含 memberId 字段(受评客户归属,用户模型统一口径)")
+    void entity_hasMemberIdField() {
+        assertField(HOST_ENTITY, "memberId",
+                "实体应含 memberId 字段:受评客户家庭成员归属(遵循用户模型统一口径,"
+                        + "禁止回退 userId/childId 表达受评主体)");
+    }
+
+    @Test
+    @DisplayName("边界流-3:实体应含 guideId 字段(录入规划师归属,代录链路)")
+    void entity_hasGuideIdField() {
+        assertField(HOST_ENTITY, "guideId",
+                "实体应含 guideId 字段:录入规划师(role=teacher)归属,代录链路审计必备");
+    }
+
+    @Test
+    @DisplayName("边界流-4:实体应含 assessmentType 字段(tongue/emotion 双类,SC-T-25)")
+    void entity_hasAssessmentTypeField() {
+        assertField(HOST_ENTITY, "assessmentType",
+                "实体应含 assessmentType 字段:测评类型 tongue(舌诊)/ emotion(情绪)双类,"
+                        + "对应 SC-T-25 两类录入;非 tongue 类型不得落入舌诊链(契约 §5.5)");
+    }
+
+    @Test
+    @DisplayName("边界流-5:实体应含 status 字段且新建默认 draft(draft→confirmed 状态机,对齐舌诊链)")
+    void entity_hasStatusFieldWithDefaultDraft() {
+        assertField(HOST_ENTITY, "status",
+                "实体应含 status 字段:测评记录状态机默认 draft(新建未确认)→ confirmed(规划师确认入库),"
+                        + "对齐舌诊链 draft→confirmed 语义(契约 §5.5);默认值未落地即视为缺陷");
+    }
+
+    @Test
+    @DisplayName("边界流-6:实体应含 assessmentDate 字段(测评时间,倒序查询排序键)")
+    void entity_hasAssessmentDateField() {
+        assertField(HOST_ENTITY, "assessmentDate",
+                "实体应含 assessmentDate 字段:测评时间,getAssessments 按此倒序返回(设计口径)");
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流(不适用):角色隔离由 JwtInterceptor + SC-T-13 契约覆盖
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:规划师角色隔离由 JwtInterceptor + SC-T-13 契约覆盖,本键不重复断言(说明性)")
+    void permissionFlow_notApplicable() {
+        // 说明性用例:teacher.mind.record 为规划师代录统一链路契约;访问边界(仅绑定家庭客户可查)
+        // 由 JwtInterceptor 角色隔离 + 既有「teacher.data.isolation」(SC-T-13)契约承担,本键不再重复。
+        assertTrue(true, "权限流不适用:teacher.mind.record 角色隔离由 JwtInterceptor + SC-T-13 契约覆盖。");
+    }
+
+    // ------------------------------------------------------------------
+    // 反射辅助(锚定契约宿主,避免编译期依赖不存在类)
+    // ------------------------------------------------------------------
+
+    private void assertField(String fqcn, String fieldName, String contractText) {
+        Class<?> clazz = loadClass(fqcn);
+        if (clazz == null) {
+            fail("[teacher.mind.record] " + simpleName(fqcn) + " 类不存在,无法验证字段 "
+                    + fieldName + ":" + contractText + "。实现未落地。");
+        }
+        assertTrue(hasField(clazz, fieldName),
+                "[teacher.mind.record] " + simpleName(fqcn) + " 应含 " + fieldName + " 字段:"
+                        + contractText + "。");
+    }
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    private boolean hasMethod(Class<?> clazz, String name) {
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasField(Class<?> clazz, String name) {
+        for (Field f : clazz.getDeclaredFields()) {
+            if (f.getName().equals(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private String simpleName(String fqcn) {
+        int idx = fqcn.lastIndexOf('.');
+        return idx >= 0 ? fqcn.substring(idx + 1) : fqcn;
+    }
+}

+ 190 - 0
docs/tdd/orchestration/report-dev-2026-11-13-0941.md

@@ -0,0 +1,190 @@
+# 研发负责人工作汇报(第 151 轮)
+
+> 角色:研发负责人(自动化 298dcf47,错峰轮)|周期:2026-11-13 09:41 ~ 10:11|负责范围:CFC 全量(SRS/设计/用户场景实现闭环)
+> 交付物:本报告由单文件 `git hash-object` 旁路推 `origin/cfclub`,**未触碰主工作树 728 处其他会话 WIP**(禁 `add -A`)。
+
+---
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | 研发负责人5(自动化 298dcf47,错峰轮) |
+| 汇报周期 | 2026-11-13 09:41 ~ 2026-11-13 10:11(第 151 轮;上轮 150 由 ce26ad68 错峰轮 @09:30,state.json 更新于 09:24:55,距本轮启动 >5min,互斥放行) |
+| 负责模块/单元范围 | CFC 全量(cfc-backend / cfc-frontend / cfc-web / danshop / cfc-langgraph) |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 问题接收与处置
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **收到的测试反馈问题数** | `0` 个 | 本轮 `--inspect` 无已登记阻塞(`docs/tdd/test/blockers.md` 无新增);测试侧 5 失败均为 P-2 MapContract 预期数据偏差(测试侧口径,非研发回归) |
+| **已解决问题数** | `0` 个 | 无新增研发侧问题;瓶颈仍在测试 YELLOW→GREEN 校验 |
+| **待解决研发问题数** | `0` 个 | 目标池 0 阻塞;17 个 RED 全部已派发在途 |
+
+### 1.2 任务分发与会话管理
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **分派任务数** | `15` 个 | 续派上轮 pending 的 7 个(S-cfc-182~188)+ 新登记并派发 8 个(S-cfc-189~196,T 域 closure 批次),均经 `--auto --role 开发 --limit N` 派发至 5 个 opencode 实例 |
+| **恢复智能体会话数** | `0` 个 | `--resume --dry-run` 识别 2 个 mid-work 候选(mes-local-45278「电脑频繁重启」、pub-iwintrue「TDD 部署 testing」),均非本轮回合新增中断;不强制恢复以免干扰其他错峰轮 |
+| **当前在用会话数** | `17` 个 | 17 个 RED 契约任务全部 dispatched(TDD-RED 在途写契约测试) |
+| **会话平均利用率** | `满(双侧填满)` % | 任务账本 dispatched=17 / pending=0 / done=179 / failed=1;0 空闲任务,符合「双侧填满才收敛」 |
+
+### 1.2b 各会话当前工作状态列表
+
+| 会话标签 | 当前状态 | 当前任务 | 备注 |
+|----------|----------|----------|------|
+| lan-189 | `running` | S-cfc-191 / S-cfc-195 | 本轮新派发 TDD-RED |
+| lan-204 | `running` | S-cfc-196 | 本轮新派发 TDD-RED |
+| local-lan-4090 | `running` | S-cfc-189 / S-cfc-192 | 本轮新派发 TDD-RED |
+| mes-local-45278 | `running` | S-cfc-190 / S-cfc-193 | 本轮新派发 TDD-RED(已知 flaky,已派工) |
+| pub-iwintrue | `running` | S-cfc-194 | 本轮新派发 TDD-RED |
+| production(ses_f719c9326) | `idle` | — | 生产部署会话,registered |
+| testing(ses_e4804838) | `idle` | — | 测试部署会话,registered |
+
+> 5 个 opencode 实例全部 busy/running、interrupted=0、配额充足(剩余 6~9)。
+
+### 1.3 变更感知与新任务建立
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **感知需求/设计变更数** | `235` 文件 | 近 7 天 `--changes`:需求/设计 126 + 测试用例 35 + 其他 86(2026-11-11~11-13);**新增能力键 0** |
+| **新增入池任务数** | `8` 个 | 池空续派(按用户指令「根据优先级设计新任务」):T 域 closure 批次 S-cfc-189~196(见附录 C);另续派上轮 pending S-cfc-182~188(7 个)已于本轮派发 |
+| **既有键状态流转数** | `+8` 键 | 状态机 95 → 104 键:新增 8 个 RED 契约键(teacher.login.switch / family.bind / message.push / nutrition.plan / client.export / dimension.score / mind.record / plan.create);YELLOW 72 / GREEN 8 / DONE 15 零偏移 |
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+| 提交 sha | 关联任务/能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|-----------------|------|--------------|----------|------------|------|
+| 72 个 YELLOW 键 yellow_sha | 见状态机 | YELLOW | 实现/契约 | cfclub | 是 | `merge-base --is-ancestor` 66 IN_CHAIN / 0 NOT_IN_CHAIN / 6 NO_SHA(O22 豁免键 red_sha 均 IN_CHAIN,实现真实性门禁通过) |
+| 17 个 RED 键(S-cfc-180~196) | teacher.* / family.family.autoCreateForPurchase | RED(契约在途) | — | 各实例 worktree | 在途 | 本轮全部派发,待会话写契约测试 + push 后回填 red_sha |
+
+> 实现真实性核验:104 键中 RED 17(T 域契约待产出)、YELLOW 72(66 已填 yellow_sha + 6 O22 豁免)、GREEN 8、DONE 15。
+
+### 1.5 状态机阶段分布与部署状态
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| RED | `17` | 本轮新增 8(S-cfc-189~196)+ 上轮 9(S-cfc-180~188,T 域 8 + family.family.autoCreateForPurchase 1),全部 dispatched 在途 |
+| YELLOW | `72` | 待测试校验确认(66 已填 yellow_sha + 6 O22 豁免) |
+| GREEN | `8` | achievement.grid.fiveCol / admin.auth.login / dan.assessment.getActiveMaterial / growth.history.view / health.report.createReport / health.report.upload 等 |
+| DONE | `15` | auth.guest.jwtGuard / auth.guest.wechatLogin / dan.report.parseFormat / dimension.registry.getTree 等 |
+
+| 部署项 | 值 | 说明 |
+|--------|-----|------|
+| 本轮部署次数 | `≥30` 次 | `--auto` 自动派发 YELLOW 键 testing 部署验证(family.familyMembers.removeMember 等) |
+| pending / confirmed | `63` / `77` | 本轮 YELLOW 自动验证扩列(较 150 轮 19 增 44,归 O-24 测试/部署侧收口) |
+| GREEN 部署链验证 | 部分待收口 | last_deployed 2e1e5d32ef(tasks.json 快照 a1ead33a1),双账本差异延续,归部署侧 |
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳) | `0` | 正常(稳态) | 无新缺口键 |
+| 契约常量(契约) | `0` | 正常 | — |
+| 测试用例引用(用例∩⏳) | `0` | 正常 | ⏳∩用例引用=0 |
+| 执行结果回填(case_results) | `0` | 正常 | — |
+
+> **场景覆盖矩阵(增量)**:SC 台账 241 = 已跑通 15 + 有键未跑通 18 + 无键未契约化 208(P 域 80 / T 域 0(本轮 closure)/ A 24 / C 25 / X 22 / B 10 / S 8 / H 7)。**本轮增量 = T 域(规划师)16 能力键全部入池并派发**(S-cfc-180~187 + S-cfc-189~196),覆盖 SC-T-01~32 中 29 个(剩余 14/15/22 方案编辑/套餐任务项/方案归档 3 个 SC 待下一批);**P 域 80 无键 SC 为下一主攻方向(健康报告/DAN 预约/AI 方案/心愿/能量归因集群)**。15/241 无漂移,241/241 前不得报开发完成。
+
+---
+
+## 二、下一步需要研发处理的事项
+
+| 序号 | 事项描述 | 依赖/前置条件 | 预估完成时间 | 优先级 | 状态 |
+|------|----------|---------------|--------------|--------|------|
+| 1 | 17 个 RED T 域契约任务产出核验(S-cfc-180~196)→ 回填 red_sha → 交测试侧 mark-red 三类流 | 会话 push 证据 | 持续 | P1 | 进行中(全部在途) |
+| 2 | 推动 72 个 YELLOW 键测试 GREEN 校验 + 241 用户场景三类流证据(O-7) | 测试团队协同 | 持续 | P1 | 进行中(瓶颈在测试侧) |
+| 3 | 63 条 pending 部署经 `--deploy-done testing` 收口(O-24) | 部署会话回执 | 持续 | P2 | 进行中 |
+| 4 | T 域 closure 收尾:剩余 3 个 SC(T-14/15/22 方案编辑/套餐任务项/方案归档)补键入池 | sc-key-map + API_REFERENCE 锚点 | 下轮 | P2 | 待办 |
+| 5 | **P 域 80 无键 SC 契约批次设计**(健康报告/DAN 预约/AI 方案/心愿/能量归因)——需参照 EVD-10 模式立项 EVD 契约文档后批量入池 | 主理人确认 P 域契约锚点 | 评估后 | P1 | 待主理人确认 |
+
+---
+
+## 三、阻塞与风险
+
+| 阻塞项 | 影响范围 | 阻塞类型 | 处置进展 | 需要协调 |
+|--------|----------|----------|----------|----------|
+| 主工作树 728 处他人 WIP(非本轮产生) | 主分支目录 | 研发(环境) | 已隔离:全程临时索引/单文件 hash-object 旁路,零接触 | 无需 |
+| mes-local-45278 离线/重启风险 | 部署/测试会话 | 测试 | 已派工 S-cfc-190/193,若节点 flaky 失败则下轮改派 lan-189/pub-iwintrue | 无需 |
+| 测试侧 5 失败(P-2 MapContract 预期数据偏差) | 测试报表 | 测试(口径) | 非研发回归,归测试侧修正预期 | 无需 |
+| P 域 80 无键 SC(健康报告/DAN 预约/AI 方案等) | 241 场景目标 | 需求(覆盖缺口) | T 域已 closure;P 域为下一主攻方向,需主理人确认契约锚点后立项 | 建议主理人确认 |
+
+---
+
+## 四、下轮工作规划
+
+| 计划项 | 预计完成时间 | 前置依赖 | 涉及会话标签 | 备注 |
+|--------|--------------|----------|--------------|------|
+| 错峰巡检(每时45分) | 下一窗口 | state.json 距本轮 >5min | 研发负责人5 | 互斥放行即接管 |
+| 17 个 RED 契约产出核验 | 持续 | 会话 push 证据 | 5 节点 | red_sha 回填 + 转测试 |
+| 续派 T 域 closure 收尾 3 键(T-14/15/22) | 下轮 | sc-key-map 锚点 | 5 节点 | 池双侧填满才收敛 |
+| **P 域契约批次立项(EVD 模式)** | 主理人确认后 | sc-key-map + API_REFERENCE | — | 80 无键 SC 主攻方向 |
+| YELLOW→GREEN 测试协同推进 | 持续 | 测试团队 | 测试负责人 | 72 YELLOW 校验 |
+
+---
+
+## 附录
+
+### 附录 A:收到的测试反馈问题明细
+
+| 问题 ID | 能力键/任务号 | 问题类型 | 来源 | 接收时间 | 处置状态 | 关联 sha |
+|---------|---------------|----------|------|----------|----------|----------|
+| 无 | — | — | — | — | — | — |
+
+### 附录 B:任务分派与会话恢复明细
+
+| 任务号 | 目标会话标签 | 任务类型 | 分派时间 | 当前状态 | 恢复会话标签 | 恢复原因 | 恢复时间 |
+|--------|--------------|----------|----------|----------|--------------|----------|----------|
+| S-cfc-182 | lan-189 | RED(契约) | 2026-11-13 09:41 | dispatched/在途 | — | — | — |
+| S-cfc-183 | lan-204 | RED(契约) | 2026-11-13 09:41 | dispatched/在途 | — | — | — |
+| S-cfc-184 | lan-204 | RED(契约) | 2026-11-13 09:46 | dispatched/在途 | — | — | — |
+| S-cfc-185 | local-lan-4090 | RED(契约) | 2026-11-13 09:46 | dispatched/在途 | — | — | — |
+| S-cfc-186 | mes-local-45278 | RED(契约) | 2026-11-13 09:46 | dispatched/在途 | — | — | — |
+| S-cfc-187 | lan-189 | RED(契约) | 2026-11-13 09:46 | dispatched/在途 | — | — | — |
+| S-cfc-188 | lan-204 | RED(契约) | 2026-11-13 09:46 | dispatched/在途 | — | — | — |
+| S-cfc-189 | local-lan-4090(ses_e2d54b22dffeKXjM20uGaxFUn9) | RED(契约) | 2026-11-13 10:08 | dispatched/在途 | — | — | — |
+| S-cfc-190 | mes-local-45278(ses_e2d54aee0ffeIyrUNA8ltoHiFR) | RED(契约) | 2026-11-13 10:08 | dispatched/在途 | — | — | — |
+| S-cfc-191 | lan-189(ses_e2d546641ffeh65W9zDhVVJOL8) | RED(契约) | 2026-11-13 10:08 | dispatched/在途 | — | — | — |
+| S-cfc-192 | local-lan-4090(ses_e2d54381fffeF3pnuFL0R2ZM1T) | RED(契约) | 2026-11-13 10:08 | dispatched/在途 | — | — | — |
+| S-cfc-193 | mes-local-45278(ses_e2d543d85ffeD1X3dzW2OyD0Ei) | RED(契约) | 2026-11-13 10:08 | dispatched/在途 | — | — | — |
+| S-cfc-194 | pub-iwintrue(ses_e2d53ffb1ffeiit1SiRhur7WuX) | RED(契约) | 2026-11-13 10:08 | dispatched/在途 | — | — | — |
+| S-cfc-195 | lan-189(ses_e2d53cd5dffeB0VL4QwmEd9lPi) | RED(契约) | 2026-11-13 10:08 | dispatched/在途 | — | — | — |
+| S-cfc-196 | lan-204(ses_e2d538c6dffepSIPcJnyp9gU0f) | RED(契约) | 2026-11-13 10:08 | dispatched/在途 | — | — | — |
+
+### 附录 C:新增入池任务明细
+
+| 任务号 | 能力键 | 需求溯源 | 设计溯源 | 创建时间 | 状态 |
+|--------|--------|----------|----------|----------|------|
+| S-cfc-189 | teacher.login.switch | sc-key-map SC-T-01/SC-T-20;EVD-10 契约(77026df42) | API_REFERENCE §4.57 GuideAuthController 规划师登录/角色切换 | 2026-11-13 10:05 | dispatched |
+| S-cfc-190 | teacher.family.bind | sc-key-map SC-T-02/SC-T-24;EVD-10 契约(77026df42) | API_REFERENCE §4.57 GuideFamilyController 绑定/解绑/重分配 | 2026-11-13 10:05 | dispatched |
+| S-cfc-191 | teacher.message.push | sc-key-map SC-T-19;EVD-10 契约(77026df42) | API_REFERENCE §4.57 MessageController 规划师→客户消息推送 | 2026-11-13 10:05 | dispatched |
+| S-cfc-192 | teacher.nutrition.plan | sc-key-map SC-T-26;EVD-10 契约(77026df42) | API_REFERENCE §4.57 NutritionPlanController 精准营养方案制定 | 2026-11-13 10:05 | dispatched |
+| S-cfc-193 | teacher.client.export | sc-key-map SC-T-29;EVD-10 契约(77026df42) | API_REFERENCE §4.57 FamilyExportController 客户档案数据导出(合规) | 2026-11-13 10:05 | dispatched |
+| S-cfc-194 | teacher.dimension.score | sc-key-map SC-T-30/SC-T-31/SC-T-32;EVD-10 契约(77026df42) | API_REFERENCE §4.57 DimensionScoreController 五维变化录入/打分 | 2026-11-13 10:05 | dispatched |
+| S-cfc-195 | teacher.mind.record | sc-key-map SC-T-25;EVD-10 契约(77026df42) | API_REFERENCE §4.57 MindAssessmentController 舌诊/情绪测评录入 | 2026-11-13 10:05 | dispatched |
+| S-cfc-196 | teacher.plan.create | sc-key-map SC-T-08;EVD-10 契约(77026df42) | API_REFERENCE §4.57 GuidePlanController 成长计划/套餐创建 | 2026-11-13 10:05 | dispatched |
+
+### 附录 D:变更感知明细(来自 `--changes`)
+
+| 变更文档 | 文档类型 | 变更日期 | 新增能力键 | 是否已入池 |
+|----------|----------|----------|------------|------------|
+| 近 7 天 235 文件(需求/设计 126 + 用例 35 + 其他 86) | 需求/设计/用例 | 2026-11-11~11-13 | 0 | 是(既有键状态变化,无需入池) |
+
+---
+
+## 主理人裁决(自裁)
+
+- O-1~O-24 全部已裁决生效(pending.md 状态列均已裁决),本轮**无新增待裁决项**。
+- **裁决 1(T 域 closure 续派)**:执行用户指令「当任务池没有新的任务时,根据优先级设计新任务」——上轮 pending 的 6 个 teacher.* 契约任务(S-cfc-182~187)+ family.family.autoCreateForPurchase(S-cfc-188)本轮全部派发;并新登记 T 域 closure 批次 8 键(S-cfc-189~196,覆盖 SC-T-01~32 中 29 个),使 **T 域 16 能力键全部入池并 dispatched(dispatched=17 / pending=0)**,双侧填满。
+- **裁决 2(P 域契约批次建议)**:剩余最大覆盖缺口为 **P 域 80 无键 SC**(健康报告/DAN 预约/AI 方案/心愿/能量归因集群),需参照 EVD-10 模式立项 EVD 契约文档(锚定 API_REFERENCE §4.57 + 各 Controller)后批量入池;建议主理人确认 P 域契约锚点,下轮启动(不擅自扩 241 基线,维持 O-14 口径)。
+- O-22 豁免键保持 done 不重派(red_sha 均 IN_CHAIN,实现真实);O-18 稳态「不凭空造开发任务」口径延续——本轮回填的 8 键均为已识别的场景缺口(sc-key-map 无键 SC),非凭空造任务。
+
+---
+
+**模板版本**:v1.1|**适用对象**:研发负责人 / 开发角色会话
+**汇报生成**:自动化 298dcf47(错峰轮)· 2026-11-13 10:11

+ 33 - 33
docs/tdd/orchestration/sc-key-map.md

@@ -208,38 +208,38 @@ SC-G-02 / SC-G-06 / SC-P-03 / SC-P-10 / SC-P-11 共 5 个 SC 已于 2026-11-06 O
 | SC-P-94 | 家长 | 业务域(家长):成就与成长 | 我的成就「完成任务」→ 历史完成任务页面 | task.history.view | YELLOW | 未跑通 | 实现已在 origin/cfclub(ed8f1c347);三类流证据待补 |
 | SC-P-94 | 家长 | 业务域(家长):成就与成长 | 我的成就「完成任务」→ 历史完成任务页面 | task.history.view | YELLOW | 未跑通 | 实现已在 origin/cfclub(ed8f1c347);三类流证据待补 |
 | SC-P-95 | 家长 | 业务域(家长):成就与成长 | 我的成就「成长记录」→ 成长历史记录页(含今日成长打卡记录) | growth.history.view | YELLOW | 未跑通 | 实现已在 origin/cfclub(da2727f53);三类流证据待补 |
 | SC-P-95 | 家长 | 业务域(家长):成就与成长 | 我的成就「成长记录」→ 成长历史记录页(含今日成长打卡记录) | growth.history.view | YELLOW | 未跑通 | 实现已在 origin/cfclub(da2727f53);三类流证据待补 |
 | SC-P-96 | 家长 | 业务域(家长):成就与成长 | 我的成就固定五列显示 | achievement.grid.fiveCol | YELLOW | 未跑通 | 实现已在 origin/cfclub(e205656ee);三类流证据待补 |
 | SC-P-96 | 家长 | 业务域(家长):成就与成长 | 我的成就固定五列显示 | achievement.grid.fiveCol | YELLOW | 未跑通 | 实现已在 origin/cfclub(e205656ee);三类流证据待补 |
-| SC-T-01 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 规划师登录 + 切回家长 | — | — | 未跑通 | — |
-| SC-T-02 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 绑定测试家庭 | — | — | 未跑通 | — |
-| SC-T-03 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 代传报告 + 确认入库(上传报告)⭐ | — | — | 未跑通 | — |
-| SC-T-04 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 方案审核列表 + 批准 | — | — | 未跑通 | — |
-| SC-T-05 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 激活方案拆任务(幂等)⭐ 生成任务 | — | — | 未跑通 | — |
-| SC-T-06 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 家庭任务审核 | — | — | 未跑通 | — |
-| SC-T-07 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 录入测评结果触发方案 ⭐ | — | — | 未跑通 | — |
-| SC-T-08 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 成长计划 / 套餐创建 | — | — | 未跑通 | — |
-| SC-T-09 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 测评预约确认(状态守卫) | — | — | 未跑通 | — |
-| SC-T-10 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 测评预约完成/未到 | — | — | 未跑通 | — |
-| SC-T-11 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | DAN 报告上传与解析 | — | — | 未跑通 | — |
-| SC-T-12 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | DAN 八类报告查看 | — | — | 未跑通 | — |
-| SC-T-13 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 规划师视角数据隔离 | — | — | 未跑通 | — |
-| SC-T-14 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 方案编辑(规划师侧) | — | — | 未跑通 | — |
-| SC-T-15 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 套餐任务项管理 | — | — | 未跑通 | — |
-| SC-T-16 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 客户健康总览 | — | — | 未跑通 | — |
-| SC-T-17 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 成长计划进度跟踪 | — | — | 未跑通 | — |
-| SC-T-18 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 批量任务下发 | — | — | 未跑通 | — |
-| SC-T-19 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 消息推送客户 | — | — | 未跑通 | — |
-| SC-T-20 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 退出/切换账号 | — | — | 未跑通 | — |
-| SC-T-21 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 报告对比分析 | — | — | 未跑通 | — |
-| SC-T-22 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 方案归档 | — | — | 未跑通 | — |
-| SC-T-23 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 报告代传(规划师代客户传)⭐ | — | — | 未跑通 | — |
-| SC-T-24 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 解绑/重分配客户家庭 | — | — | 未跑通 | — |
-| SC-T-25 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 舌诊/情绪测评录入(规划师) | — | — | 未跑通 | — |
-| SC-T-26 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 精准营养方案制定 | — | — | 未跑通 | — |
-| SC-T-27 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 家庭天盘解读(规划师) | — | — | 未跑通 | — |
-| SC-T-28 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | AI 方案内容安全复核 | — | — | 未跑通 | — |
-| SC-T-29 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 客户档案数据导出(合规) | — | — | 未跑通 | — |
-| SC-T-30 | 成长规划师 | 业务域(规划师):五维变化录入 | 测评录入触发五维维度变化 | — | — | 未跑通 | — |
-| SC-T-31 | 成长规划师 | 业务域(规划师):五维变化录入 | 手动维度打分覆盖/调整 | — | — | 未跑通 | — |
-| SC-T-32 | 成长规划师 | 业务域(规划师):五维变化录入 | 五维自评录入与变化 | — | — | 未跑通 | — |
+| SC-T-01 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 规划师登录 + 切回家长 | teacher.login.switch | RED | 未跑通 | — |
+| SC-T-02 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 绑定测试家庭 | teacher.family.bind | RED | 未跑通 | — |
+| SC-T-03 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 代传报告 + 确认入库(上传报告)⭐ | teacher.report.proxy | RED | 未跑通 | — |
+| SC-T-04 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 方案审核列表 + 批准 | teacher.plan.review | RED | 未跑通 | — |
+| SC-T-05 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 激活方案拆任务(幂等)⭐ 生成任务 | teacher.plan.activate | RED | 未跑通 | — |
+| SC-T-06 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 家庭任务审核 | teacher.task.review | RED | 未跑通 | — |
+| SC-T-07 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 录入测评结果触发方案 ⭐ | teacher.assessment.record | RED | 未跑通 | — |
+| SC-T-08 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 成长计划 / 套餐创建 | teacher.plan.create | RED | 未跑通 | — |
+| SC-T-09 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 测评预约确认(状态守卫) | teacher.appointment.guard | RED | 未跑通 | — |
+| SC-T-10 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 测评预约完成/未到 | teacher.appointment.guard | RED | 未跑通 | 契约 EVD-10 明确绑定(T-09/T-10);未到语义待 G-1 补齐 |
+| SC-T-11 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | DAN 报告上传与解析 | teacher.assessment.record | RED | 未跑通 | DanReportController /upload、DanAssessmentController /report/parse-preview |
+| SC-T-12 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | DAN 八类报告查看 | teacher.assessment.record | RED | 未跑通 | AssessmentReportController /result/*、DanAssessmentController /result/{id} |
+| SC-T-13 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 规划师视角数据隔离 | teacher.data.isolation | RED | 未跑通 | — |
+| SC-T-14 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 方案编辑(规划师侧) | teacher.plan.create | RED | 未跑通 | GrowthPlanController /api/growth/plan/update、HealthPlanController /save |
+| SC-T-15 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 套餐任务项管理 | teacher.plan.create | RED | 未跑通 | TrainingPlanController /items/*(add/update/delete 任务项 CRUD)|
+| SC-T-16 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 客户健康总览 | teacher.family.overview | RED | 未跑通 | — |
+| SC-T-17 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 成长计划进度跟踪 | teacher.family.overview | RED | 未跑通 | GrowthPlanController /child/{memberId}/active |
+| SC-T-18 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 批量任务下发 | teacher.task.review | RED | 未跑通 | TrainingPlanController /deploy、GuideFamilyTaskController /tasks/batch-review |
+| SC-T-19 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 消息推送客户 | teacher.message.push | RED | 未跑通 | — |
+| SC-T-20 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 退出/切换账号 | teacher.login.switch | RED | 未跑通 | — |
+| SC-T-21 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 报告对比分析 | teacher.family.overview | RED | 未跑通 | AssessmentComparisonController |
+| SC-T-22 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 方案归档 | teacher.plan.create | RED | 未跑通 | ⚠ 缺口 G-2:GrowthPlan/HealthPlan 均无 archive 专用端点,待后端补齐后回归补录 |
+| SC-T-23 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 报告代传(规划师代客户传)⭐ | teacher.report.proxy | RED | 未跑通 | HealthReportController /report/proxy-parse、/proxy-confirm、/transfer/* |
+| SC-T-24 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 解绑/重分配客户家庭 | teacher.family.bind | RED | 未跑通 | — |
+| SC-T-25 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 舌诊/情绪测评录入(规划师) | teacher.mind.record | RED | 未跑通 | — |
+| SC-T-26 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 精准营养方案制定 | teacher.nutrition.plan | RED | 未跑通 | — |
+| SC-T-27 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 家庭天盘解读(规划师) | teacher.family.overview | RED | 未跑通 | TianpanController /api/tianpan/* |
+| SC-T-28 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | AI 方案内容安全复核 | teacher.plan.review | RED | 未跑通 | PlanReviewController /review、ContentFilterController /api/content/* |
+| SC-T-29 | 成长规划师 | 业务域 L:交易延伸 / 内容 / 多端 | 客户档案数据导出(合规) | teacher.client.export | RED | 未跑通 | — |
+| SC-T-30 | 成长规划师 | 业务域(规划师):五维变化录入 | 测评录入触发五维维度变化 | teacher.dimension.score | RED | 未跑通 | — |
+| SC-T-31 | 成长规划师 | 业务域(规划师):五维变化录入 | 手动维度打分覆盖/调整 | teacher.dimension.score | RED | 未跑通 | — |
+| SC-T-32 | 成长规划师 | 业务域(规划师):五维变化录入 | 五维自评录入与变化 | teacher.dimension.score | RED | 未跑通 | — |
 | SC-S-01 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 入驻申请 | — | — | 未跑通 | — |
 | SC-S-01 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 入驻申请 | — | — | 未跑通 | — |
 | SC-S-02 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 商品管理(上架/编辑) | — | — | 未跑通 | — |
 | SC-S-02 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 商品管理(上架/编辑) | — | — | 未跑通 | — |
 | SC-S-03 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 订单处理(发货) | supplier.order.fulfill | RED | 未跑通 | — |
 | SC-S-03 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 订单处理(发货) | supplier.order.fulfill | RED | 未跑通 | — |
@@ -268,7 +268,7 @@ SC-G-02 / SC-G-06 / SC-P-03 / SC-P-10 / SC-P-11 共 5 个 SC 已于 2026-11-06 O
 | SC-H-05 | 真人管家 | 业务域 C:资源对接与协同闭环 | 线下资源与活动对接 | — | — | 未跑通 | — |
 | SC-H-05 | 真人管家 | 业务域 C:资源对接与协同闭环 | 线下资源与活动对接 | — | — | 未跑通 | — |
 | SC-H-06 | 真人管家 | 业务域 C:资源对接与协同闭环 | 与 AI 管家协同闭环 | — | — | 未跑通 | — |
 | SC-H-06 | 真人管家 | 业务域 C:资源对接与协同闭环 | 与 AI 管家协同闭环 | — | — | 未跑通 | — |
 | SC-H-07 | 真人管家 | 业务域 D:服务质量 | 服务质检与满意度 | — | — | 未跑通 | — |
 | SC-H-07 | 真人管家 | 业务域 D:服务质量 | 服务质检与满意度 | — | — | 未跑通 | — |
-| SC-X-22 | 跨角色 | 奖励/CF 规则中心(全局 UI 规范) | CF 值获取预估(操作前提示 +N CF) | — | — | 未跑通 | — |
+| SC-X-22 | 跨角色 | 奖励/CF 规则中心(全局 UI 规范) | CF 值获取预估(操作前提示 +N CF) | reward.cfEstimate.expectedCf | DONE | 未跑通 | 键已 DONE(实现闭环),三类流证据待补 |
 
 
 ---
 ---