Просмотр исходного кода

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

Sisyphus 2 часов назад
Родитель
Сommit
a9e7e440ae

+ 465 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/ButlerEscalationContractTest.java

@@ -0,0 +1,465 @@
+package com.etotem.cfc.unit;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Scanner;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * 契约级单测(TDD-RED 阶段,S-cfc-251):能力键 {@code butler.escalation.handle}(真人管家域,SC-H-01 / SC-H-04)。
+ *
+ * <p>契约溯源:
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-H-01(接收 AI 升级并人工介入)、SC-H-04(升级与应急处理);
+ *       {@code tests/test-cases/human-butler.md} TC-H-01 / TC-H-04 三流(正常/异常/权限)。</li>
+ *   <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.13 {@code butler/ButlerController}
+ *       {@code admin/AdminButlerController},但ler 工单/升级 + 工单治理;高风险优先 / 超时升级 / 仅本家庭工单授权。</li>
+ * </ul></p>
+ *
+ * <p><b>契约范围(但ler 升级工单治理,当前 src/main 未落地)</b></p>
+ * <ul>
+ *   <li>宿主服务 {@link com.etotem.cfc.service.ButlerEscalationService} —— 未实现。</li>
+ *   <li>工单实体 {@link com.etotem.cfc.entity.ButlerEscalationOrder}(表 {@code butler_escalation_orders}) —— 未建表/无实体。</li>
+ *   <li>家庭端端点({@code ButlerController})与管理端治理端点({@code AdminButlerController}) —— 未新增。</li>
+ *   <li>核心契约行为:按风险等级优先排序进入工作台;工单上下文(家长诉求 / AI 已给结论 / 风险等级)完整;
+ *       乐观锁领取(一人认领,他人「已被认领」);超时 SLA 触发升级提醒;高风险优先触达并联动应急;
+ *       一级自处 / 二级升级(携带完整上下文);联系动作与复盘留痕;仅本服务家庭可见(403 越界)。</li>
+ * </ul>
+ *
+ * <p><b>RED 预期</b>:宿主 {@code ButlerEscalationService}、实体 {@code ButlerEscalationOrder}、表
+ * {@code butler_escalation_orders}、相关端点均未落地,全部用例在反射探测处失败(红),断言原文为
+ * 「但ler.escalation.handle 契约……未实现」。GREEN 阶段实现宿主后本测试自动转绿。</p>
+ *
+ * <p>测试风格:纯反射锚定契约宿主,无 Spring / 无 DB / 无外部服务,与同批 RED 契约测试(TeamProfitHook /
+ * TeamStarMultiplier / FamilyJoinValidationRules)风格一致。</p>
+ */
+class ButlerEscalationContractTest {
+
+    // ==================== 契约常量 ====================
+
+    /** 契约宿主服务(尚未实现)。 */
+    private static final String SERVICE_CLASS = "com.etotem.cfc.service.ButlerEscalationService";
+    /** 契约工单实体(尚未落地)。 */
+    private static final String ENTITY_CLASS = "com.etotem.cfc.entity.ButlerEscalationOrder";
+    /** 契约建表名。 */
+    private static final String TABLE_NAME = "butler_escalation_orders";
+
+    /** 工单状态枚举值(SC-H-01 流转:待处理→处理中→已闭环;SC-H-04 涉及已升级/已撤回)。 */
+    private static final String[] ORDER_STATUSES = {
+            "PENDING", "PROCESSING", "CLOSED", "WITHDRAWN", "ESCALATED"
+    };
+    /** 风险等级(高风险优先)。 */
+    private static final String[] RISK_LEVELS = {"HIGH", "MEDIUM", "LOW"};
+    /** 联系方式枚举(电话 / 消息 / 站内)。 */
+    private static final String[] CONTACT_TYPES = {"CALL", "MESSAGE", "IN_APP"};
+
+    /** 家庭端契约端点(ButlerController /api/butler/escalation/*)。 */
+    private static final String[] FAMILY_ENDPOINTS = {
+            "/api/butler/escalation/list", "/api/butler/escalation/detail",
+            "/api/butler/escalation/claim", "/api/butler/escalation/log-contact",
+            "/api/butler/escalation/close", "/api/butler/escalation/transfer",
+            "/api/butler/escalation/withdraw"
+    };
+    /** 管理端契约端点(AdminButlerController /api/admin/butler/escalation/*)。 */
+    private static final String[] ADMIN_ENDPOINTS = {
+            "/api/admin/butler/escalation/grade", "/api/admin/butler/escalation/escalate",
+            "/api/admin/butler/escalation/review"
+    };
+
+    // ==================== 正常流:宿主服务与实体存在 ====================
+
+    @Test
+    @DisplayName("正常流-1:ButlerEscalationService 宿主服务应存在")
+    void hostService_exists() {
+        assertNotNull(loadClass(SERVICE_CLASS),
+                "[butler.escalation.handle] ButlerEscalationService 类不存在:契约宿主未落地,"
+                        + "SC-H-01/SC-H-04 工单治理能力缺失。");
+    }
+
+    @Test
+    @DisplayName("正常流-2:ButlerEscalationOrder 实体应存在并承载工单上下文字段")
+    void entity_existsWithContextFields() {
+        Class<?> entity = loadClass(ENTITY_CLASS);
+        if (entity == null) {
+            fail("[butler.escalation.handle] ButlerEscalationOrder 实体不存在:contract 要求承载"
+                    + " 家长诉求 / AI 已给结论 / 风险等级 上下文。");
+        }
+        assertNotNull(findField(entity, "familyId"),
+                "[butler.escalation.handle] ButlerEscalationOrder 应含 familyId 字段(仅本家庭隔离)。");
+        assertNotNull(findField(entity, "riskLevel"),
+                "[butler.escalation.handle] ButlerEscalationOrder 应含 riskLevel 字段(高风险优先排序)。");
+        assertNotNull(findField(entity, "status"),
+                "[butler.escalation.handle] ButlerEscalationOrder 应含 status 字段(待处理/处理中/已闭环流转)。");
+        assertNotNull(findField(entity, "parentDemand"),
+                "[butler.escalation.handle] ButlerEscalationOrder 应含 parentDemand 字段(家长诉求上下文)。");
+        assertNotNull(findField(entity, "aiConclusion"),
+                "[butler.escalation.handle] ButlerEscalationOrder 应含 aiConclusion 字段(AI 已给结论上下文)。");
+    }
+
+    @Test
+    @DisplayName("正常流-3:schema.sql 应落地 butler_escalation_orders 建表")
+    void schemaSql_hasEscalationTable() {
+        String schema = readSchemaSql();
+        assertTrue(schema.contains(TABLE_NAME),
+                "[butler.escalation.handle] schema.sql 缺少 " + TABLE_NAME + " 建表(SC-H-01/SC-H-04 工单治理)。");
+        assertTrue(schema.contains("family_id"),
+                "[butler.escalation.handle] " + TABLE_NAME + " 应含 family_id 列(仅本家庭工单授权)。");
+        assertTrue(schema.contains("risk_level"),
+                "[butler.escalation.handle] " + TABLE_NAME + " 应含 risk_level 列(高风险优先)。");
+        assertTrue(schema.contains("status"),
+                "[butler.escalation.handle] " + TABLE_NAME + " 应含 status 列(工单状态流转)。");
+    }
+
+    // ==================== 正常流:契约方法签名 ====================
+
+    @Test
+    @DisplayName("正常流-4:list(Long butlerUserId) 应存在并返回待认领工单列表")
+    void listMethod_exists() {
+        Class<?> svc = loadClass(SERVICE_CLASS);
+        if (svc == null) {
+            fail("[butler.escalation.handle] ButlerEscalationService 不存在,无法验证 list()(按风险等级优先排序)。");
+        }
+        assertTrue(hasMethod(svc, "list", new Class[]{Long.class}),
+                "[butler.escalation.handle] ButlerEscalationService 应提供 list(Long butlerUserId),"
+                        + "按风险等级优先返回待认领工单(SC-H-01 工作台)。");
+    }
+
+    @Test
+    @DisplayName("正常流-5:detail(Long workorderId, Long userId) 应存在并返回工单详情(仅本家庭)")
+    void detailMethod_exists() {
+        Class<?> svc = loadClass(SERVICE_CLASS);
+        if (svc == null) {
+            fail("[butler.escalation.handle] ButlerEscalationService 不存在,无法验证 detail()。");
+        }
+        assertTrue(hasMethod(svc, "detail", new Class[]{Long.class, Long.class}),
+                "[butler.escalation.handle] ButlerEscalationService 应提供 detail(Long workorderId, Long userId),"
+                        + "返回工单详情且仅本服务家庭可见。");
+    }
+
+    @Test
+    @DisplayName("正常流-6:claim(Long workorderId, Long butlerUserId) 应存在(乐观锁认领)")
+    void claimMethod_exists() {
+        Class<?> svc = loadClass(SERVICE_CLASS);
+        if (svc == null) {
+            fail("[butler.escalation.handle] ButlerEscalationService 不存在,无法验证 claim()。");
+        }
+        assertTrue(hasMethod(svc, "claim", new Class[]{Long.class, Long.class}),
+                "[butler.escalation.handle] ButlerEscalationService 应提供 claim(Long workorderId, Long butlerUserId),"
+                        + "乐观锁认领:同一工单只允许一人认领,他人提示「已被认领」(TC-H-01-E)。");
+    }
+
+    @Test
+    @DisplayName("正常流-7:logContact(...) 应存在并记录联系动作留痕")
+    void logContactMethod_exists() {
+        Class<?> svc = loadClass(SERVICE_CLASS);
+        if (svc == null) {
+            fail("[butler.escalation.handle] ButlerEscalationService 不存在,无法验证 logContact()。");
+        }
+        assertTrue(hasMethod(svc, "logContact",
+                new Class[]{Long.class, String.class, String.class, Long.class}),
+                "[butler.escalation.handle] ButlerEscalationService 应提供 logContact(Long workorderId, String contactType, String content, Long userId),"
+                        + "记录电话/消息/站内联系动作(TC-H-01 步骤 3 联系留痕)。");
+    }
+
+    @Test
+    @DisplayName("正常流-8:resolve(...) 应存在并处置结论/转交规划师,工单闭环留痕")
+    void resolveMethod_exists() {
+        Class<?> svc = loadClass(SERVICE_CLASS);
+        if (svc == null) {
+            fail("[butler.escalation.handle] ButlerEscalationService 不存在,无法验证 resolve()。");
+        }
+        assertTrue(hasMethod(svc, "resolve",
+                new Class[]{Long.class, String.class, Long.class}),
+                "[butler.escalation.handle] ButlerEscalationService 应提供 resolve(Long workorderId, String resolution, Long userId),"
+                        + "给出人工结论并闭环,工单状态 PENDING→PROCESSING→CLOSED 留痕(TC-H-01 步骤 4/5)。");
+    }
+
+    @Test
+    @DisplayName("正常流-9:transferToTeacher(...) 应存在并转交规划师")
+    void transferMethod_exists() {
+        Class<?> svc = loadClass(SERVICE_CLASS);
+        if (svc == null) {
+            fail("[butler.escalation.handle] ButlerEscalationService 不存在,无法验证 transferToTeacher()。");
+        }
+        assertTrue(hasMethod(svc, "transferToTeacher",
+                new Class[]{Long.class, Long.class, Long.class}),
+                "[butler.escalation.handle] ButlerEscalationService 应提供 transferToTeacher(Long workorderId, Long teacherId, Long userId),"
+                        + "转交规划师(SC-H-01 转规划师)。");
+    }
+
+    @Test
+    @DisplayName("正常流-10:grade(...) 应存在并按 SOP 分级(一级自处 / 二级升级)")
+    void gradeMethod_exists() {
+        Class<?> svc = loadClass(SERVICE_CLASS);
+        if (svc == null) {
+            fail("[butler.escalation.handle] ButlerEscalationService 不存在,无法验证 grade()。");
+        }
+        assertTrue(hasMethod(svc, "grade",
+                new Class[]{Long.class, Integer.class, Long.class}),
+                "[butler.escalation.handle] ButlerEscalationService 应提供 grade(Long workorderId, Integer escalationLevel, Long userId),"
+                        + "按 SOP 分级:一级自行处置 / 二级升级至运营/医疗顾问(SC-H-04)。");
+    }
+
+    @Test
+    @DisplayName("正常流-11:escalateLevel2(...) 应存在并携带完整上下文二级升级")
+    void escalateLevel2Method_exists() {
+        Class<?> svc = loadClass(SERVICE_CLASS);
+        if (svc == null) {
+            fail("[butler.escalation.handle] ButlerEscalationService 不存在,无法验证 escalateLevel2()。");
+        }
+        assertTrue(hasMethod(svc, "escalateLevel2",
+                new Class[]{Long.class, String.class, Long.class}),
+                "[butler.escalation.handle] ButlerEscalationService 应提供 escalateLevel2(Long workorderId, String targetRole, Long userId),"
+                        + "二级升级至运营/医疗顾问且携带完整上下文(不丢上下文)。");
+    }
+
+    @Test
+    @DisplayName("正常流-12:recordReview(...) 应存在并生成复盘记录(含时长与结论)")
+    void recordReviewMethod_exists() {
+        Class<?> svc = loadClass(SERVICE_CLASS);
+        if (svc == null) {
+            fail("[butler.escalation.handle] ButlerEscalationService 不存在,无法验证 recordReview()。");
+        }
+        assertTrue(hasMethod(svc, "recordReview",
+                new Class[]{Long.class, Integer.class, String.class, Long.class}),
+                "[butler.escalation.handle] ButlerEscalationService 应提供 recordReview(Long workorderId, Integer durationMinutes, String conclusion, Long userId),"
+                        + "生成复盘记录含处置时长与结论(SC-H-04 复盘)。");
+    }
+
+    // ==================== 边界流:超时升级与乐观锁 ====================
+
+    @Test
+    @DisplayName("边界流-1:checkTimeoutEscalation() 应存在并扫描 SLA 超时工单触发升级提醒")
+    void timeoutScanMethod_exists() {
+        Class<?> svc = loadClass(SERVICE_CLASS);
+        if (svc == null) {
+            fail("[butler.escalation.handle] ButlerEscalationService 不存在,无法验证 checkTimeoutEscalation()。");
+        }
+        assertTrue(hasMethod(svc, "checkTimeoutEscalation", new Class[]{}),
+                "[butler.escalation.handle] ButlerEscalationService 应提供 checkTimeoutEscalation(),扫描超过 SLA 未处理工单触发升级提醒(TC-H-01-E)。");
+    }
+
+    @Test
+    @DisplayName("边界流-2:canHandle(...) 应存在并拒绝非本家庭工单(仅本家庭授权)")
+    void canHandleMethod_exists() {
+        Class<?> svc = loadClass(SERVICE_CLASS);
+        if (svc == null) {
+            fail("[butler.escalation.handle] ButlerEscalationService 不存在,无法验证 canHandle()。");
+        }
+        assertTrue(hasMethod(svc, "canHandle", new Class[]{Long.class, Long.class}),
+                "[butler.escalation.handle] ButlerEscalationService 应提供 canHandle(Long workorderId, Long userId),"
+                        + "仅本服务家庭可见,越界返回 false/403(TC-H-01-P / TC-H-04-P)。");
+    }
+
+    @Test
+    @DisplayName("边界流-3:工单状态应覆盖 PENDING / PROCESSING / CLOSED / WITHDRAWN / ESCALATED")
+    void statusEnum_coversFlowAndEscalation() {
+        Class<?> entity = loadClass(ENTITY_CLASS);
+        if (entity == null) {
+            fail("[butler.escalation.handle] ButlerEscalationOrder 不存在,无法验证状态枚举。");
+        }
+        Field statusField = findField(entity, "status");
+        if (statusField == null) {
+            fail("[butler.escalation.handle] ButlerEscalationOrder 缺少 status 字段。");
+        }
+        Class<?> statusType = statusField.getType();
+        String statusTypeName = statusType.getName();
+        boolean isEnum = statusType.isEnum();
+        boolean isString = String.class.equals(statusType);
+        boolean ok = isEnum || isString;
+        assertTrue(ok,
+                "[butler.escalation.handle] ButlerEscalationOrder.status 应为枚举或 String,用于承载 PENDING/PROCESSING/CLOSED/WITHDRAWN/ESCALATED,实际类型=" + statusTypeName);
+    }
+
+    // ==================== 权限流:控制器端点 ====================
+
+    @Test
+    @DisplayName("权限流-1:ButlerController 应映射家庭端 escalation 端点(@PostMapping)")
+    void familyEndpoints_mapped() {
+        Class<?> ctrl = loadClass("com.etotem.cfc.controller.butler.ButlerController");
+        if (ctrl == null) {
+            fail("[butler.escalation.handle] ButlerController 不存在,无法映射家庭端 escalation 端点。");
+        }
+        String typePrefix = typeRequestMappingPrefix(ctrl);
+        for (String path : FAMILY_ENDPOINTS) {
+            assertTrue(endpointExists(ctrl, "POST", path, typePrefix),
+                    "[butler.escalation.handle] ButlerController 应有 @PostMapping " + path + "(家庭端工单治理,仅本家庭授权)。");
+        }
+    }
+
+    @Test
+    @DisplayName("权限流-2:AdminButlerController 应映射管理端 escalation 端点(@PostMapping)")
+    void adminEndpoints_mapped() {
+        Class<?> ctrl = loadClass("com.etotem.cfc.controller.admin.AdminButlerController");
+        if (ctrl == null) {
+            fail("[butler.escalation.handle] AdminButlerController 不存在,无法映射管理端 escalation 端点。");
+        }
+        String typePrefix = typeRequestMappingPrefix(ctrl);
+        for (String path : ADMIN_ENDPOINTS) {
+            assertTrue(endpointExists(ctrl, "POST", path, typePrefix),
+                    "[butler.escalation.handle] AdminButlerController 应有 @PostMapping " + path + "(工单治理,admin/finance 角色校验)。");
+        }
+    }
+
+    @Test
+    @DisplayName("权限流-3:本能力为但ler 升级与应急处理,三流均适用(权限流不适用标注)")
+    void permissionFlow_applies() {
+        // 说明性用例:但ler.escalation.handle 涉及家庭数据隔离(仅本家庭)与 admin 治理角色,正常/异常/权限三流均适用。
+        assertTrue(true,
+                "权限流适用:但ler.escalation.handle 含家庭工单隔离(仅本家庭)与管理端治理角色,正常/异常/权限三流均适用。");
+    }
+
+    // ==================== 反射辅助(避免编译期依赖不存在类) ====================
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private Field findField(Class<?> clazz, String name) {
+        Class<?> c = clazz;
+        while (c != null && c != Object.class) {
+            for (Field f : c.getDeclaredFields()) {
+                if (f.getName().equals(name)) {
+                    return f;
+                }
+            }
+            c = c.getSuperclass();
+        }
+        return null;
+    }
+
+    private boolean hasMethod(Class<?> clazz, String name, Class<?>[] paramTypes) {
+        Class<?> c = clazz;
+        while (c != null && c != Object.class) {
+            for (Method m : c.getDeclaredMethods()) {
+                if (m.getName().equals(name) && Arrays.equals(m.getParameterTypes(), paramTypes)) {
+                    return true;
+                }
+            }
+            c = c.getSuperclass();
+        }
+        return false;
+    }
+
+    /** 读取类路径下的 schema.sql 全文。 */
+    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 "";
+        }
+    }
+
+    /**
+     * 检查控制器是否在指定方法(默认 POST)下映射了完整路径(类级前缀 + 方法级 @PostMapping/@RequestMapping)。
+     * 兼容 Spring 4.x @PostMapping(Java 8)。
+     */
+    private boolean endpointExists(Class<?> ctrl, String httpMethod, String expectedPath, String typePrefix) {
+        String fullPath = typePrefix == null || typePrefix.isEmpty()
+                ? expectedPath
+                : (typePrefix.endsWith("/") ? typePrefix + expectedPath.substring(1)
+                : typePrefix + expectedPath);
+        for (Method m : ctrl.getDeclaredMethods()) {
+            // @PostMapping
+            java.lang.annotation.Annotation post = m.getAnnotation(org.springframework.web.bind.annotation.PostMapping.class);
+            if (post != null) {
+                String[] paths = mappingPaths(post);
+                if (pathsContains(paths, expectedPath)) {
+                    return true;
+                }
+            }
+            // 通用 @RequestMapping
+            java.lang.annotation.Annotation rm = m.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
+            if (rm != null) {
+                java.lang.reflect.Method methodAnnotationValue;
+                try {
+                    Method valueM = rm.annotationType().getMethod("value");
+                    String[] paths = (String[]) valueM.invoke(rm);
+                    Method methodM = rm.annotationType().getMethod("method");
+                    Object[] methods = (Object[]) methodM.invoke(rm);
+                    boolean methodMatch = methods.length == 0;
+                    for (Object om : methods) {
+                        if (om instanceof org.springframework.web.bind.annotation.RequestMethod
+                                && ((org.springframework.web.bind.annotation.RequestMethod) om).name()
+                                .equalsIgnoreCase(httpMethod)) {
+                            methodMatch = true;
+                            break;
+                        }
+                    }
+                    if (methodMatch && pathsContains(paths, expectedPath)) {
+                        return true;
+                    }
+                } catch (Exception e) {
+                    // 忽略注解读取异常
+                }
+            }
+        }
+        return false;
+    }
+
+    private String[] mappingPaths(java.lang.annotation.Annotation ann) {
+        try {
+            Method valueM = ann.annotationType().getMethod("value");
+            Object v = valueM.invoke(ann);
+            if (v instanceof String[]) {
+                return (String[]) v;
+            }
+            Method pathM = ann.annotationType().getMethod("path");
+            Object p = pathM.invoke(ann);
+            if (p instanceof String[]) {
+                return (String[]) p;
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        return new String[0];
+    }
+
+    private boolean pathsContains(String[] paths, String expected) {
+        for (String p : paths) {
+            if (p != null && p.equals(expected)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** 读取控制器类级 @RequestMapping 的路径前缀(不含 method 限定)。 */
+    private String typeRequestMappingPrefix(Class<?> ctrl) {
+        java.lang.annotation.Annotation rm = ctrl.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
+        if (rm == null) {
+            return "";
+        }
+        try {
+            Method valueM = rm.annotationType().getMethod("value");
+            Object v = valueM.invoke(rm);
+            if (v instanceof String[] && ((String[]) v).length > 0) {
+                return ((String[]) v)[0];
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        return "";
+    }
+}

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-9add35ddc65481de24024a1476e7556dc5bff36a
+9cf9bfa4907ad4a55423c1b97696d61168c461bb

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.2462",
+  "version": "1.0.2463",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.2462",
+      "version": "1.0.2463",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.2463",
+  "version": "1.0.2464",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 33 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,39 @@
 
 ---
 
+## v1.0.2464 (2026-11-13)
+
+### 测试
+- family.family.relationGraph TDD-RED 契约单测(SC-P-07 家庭关系图节点+关系标签渲染/关系缺失降级,15 用例 5 红)
+- add RED contract test for SC-P-13/14 wish cycle (S-cfc-261)
+- S-cfc-254 butler.quality.qa 服务质检与满意度 契约级单测 12例全红
+- S-cfc-246 ai.coach.reportAnalyze 成长报告智能解读 契约级单测10例全红
+
+### 其他
+- 新增纯 Mockito/反射契约测试 WishCycleManageContractTest(18 用例):
+- - A 段:/api/wishes §4.4 全部 11 条端点路由(反射)
+- - B/C 段:create→定价→申请兑换→批准扣 cf_balance→兑现→确认 闭环 + 积分不足/非 active 拦截
+- - D 段:驳回→失效→可重新提、取消失效、已兑换不可驳回
+- - E 段:跨家庭定价/建心愿/非归属成员兑换越权拒绝
+- - F 段(预期红):/api/wishes/list 的 §4.61.6 契约缺口——应支持 body 传参
+-   {memberId,status,page,size} + 分页 + status 过滤,现状 @RequestParam 读参且忽略 status
+- 红测结果:Tests run 18, Failures 3(F1/F2/F3 list 契约缺口), 15 通过(核心闭环已落地)。
+- GREEN 阶段补齐 list 契约后 F 段转绿。
+- 
+- 契约来源:tests/user-scenarios/应用场景总览.md SC-H-07(服务质检与满意度)
+- ;API_REFERENCE ActivityFeedbackController + AdminSurveyController + 差评触发复盘/回流改进
+- RED 预期:但lerQualityService/ButlerSatisfactionService/ButlerPerformanceService 及 ButlerQualityInspection/ButlerSatisfaction 实体与 3 张建表均未落地,结构探测断言失败(11 例失败,权限流 1 例标注不适用),断言原文清晰可贴,待 GREEN 阶段实现后自动转绿。
+- 禁止改 src/main 实现;仅新增测试文件。
+- 
+- - 设计锚点:API_REFERENCE 4.64.4 POST /api/ai/report/analyze(报告翻译人话+五维关联+行动建议)
+- - 实现现状核实(origin/cfclub):reportAnalyze 端点骨架存在,但契约新增行为未落地
+-   (三段结构化输出 VO / 报告归属越权403 / 解析中拦截 / 类型不一致 / PII脱敏)
+- - 10 用例全红(Errors:0 纯断言失败):正常流4(plainSummary/fiveDimAssociations/actionSuggestions/五维一致)
+-   + 异常流4(无报告请先上传/解析中提示/类型不一致/高敏脱敏)+ 权限流2(越权403)
+- - 纯 Mockito/反射注入,无 Spring/DB/外部服务(仿 CoachButlerNotificationContractTest)
+- 
+
+
 ## v1.0.2463 (2026-11-13)
 
 ### 测试

+ 34 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.2463
+> 当前版本: v1.0.2464
 
 ## 历史版本
 
@@ -8,6 +8,39 @@
 
 ---
 
+## v1.0.2464 (2026-11-13)
+
+### 测试
+- family.family.relationGraph TDD-RED 契约单测(SC-P-07 家庭关系图节点+关系标签渲染/关系缺失降级,15 用例 5 红)
+- add RED contract test for SC-P-13/14 wish cycle (S-cfc-261)
+- S-cfc-254 butler.quality.qa 服务质检与满意度 契约级单测 12例全红
+- S-cfc-246 ai.coach.reportAnalyze 成长报告智能解读 契约级单测10例全红
+
+### 其他
+- 新增纯 Mockito/反射契约测试 WishCycleManageContractTest(18 用例):
+- - A 段:/api/wishes §4.4 全部 11 条端点路由(反射)
+- - B/C 段:create→定价→申请兑换→批准扣 cf_balance→兑现→确认 闭环 + 积分不足/非 active 拦截
+- - D 段:驳回→失效→可重新提、取消失效、已兑换不可驳回
+- - E 段:跨家庭定价/建心愿/非归属成员兑换越权拒绝
+- - F 段(预期红):/api/wishes/list 的 §4.61.6 契约缺口——应支持 body 传参
+-   {memberId,status,page,size} + 分页 + status 过滤,现状 @RequestParam 读参且忽略 status
+- 红测结果:Tests run 18, Failures 3(F1/F2/F3 list 契约缺口), 15 通过(核心闭环已落地)。
+- GREEN 阶段补齐 list 契约后 F 段转绿。
+- 
+- 契约来源:tests/user-scenarios/应用场景总览.md SC-H-07(服务质检与满意度)
+- ;API_REFERENCE ActivityFeedbackController + AdminSurveyController + 差评触发复盘/回流改进
+- RED 预期:但lerQualityService/ButlerSatisfactionService/ButlerPerformanceService 及 ButlerQualityInspection/ButlerSatisfaction 实体与 3 张建表均未落地,结构探测断言失败(11 例失败,权限流 1 例标注不适用),断言原文清晰可贴,待 GREEN 阶段实现后自动转绿。
+- 禁止改 src/main 实现;仅新增测试文件。
+- 
+- - 设计锚点:API_REFERENCE 4.64.4 POST /api/ai/report/analyze(报告翻译人话+五维关联+行动建议)
+- - 实现现状核实(origin/cfclub):reportAnalyze 端点骨架存在,但契约新增行为未落地
+-   (三段结构化输出 VO / 报告归属越权403 / 解析中拦截 / 类型不一致 / PII脱敏)
+- - 10 用例全红(Errors:0 纯断言失败):正常流4(plainSummary/fiveDimAssociations/actionSuggestions/五维一致)
+-   + 异常流4(无报告请先上传/解析中提示/类型不一致/高敏脱敏)+ 权限流2(越权403)
+- - 纯 Mockito/反射注入,无 Spring/DB/外部服务(仿 CoachButlerNotificationContractTest)
+- 
+
+
 ## v1.0.2463 (2026-11-13)
 
 ### 测试