Explorar o código

Merge remote-tracking branch 'origin/cfclub' into cfclub

Sisyphus hai 3 horas
pai
achega
bcd70e13b4
Modificáronse 67 ficheiros con 10570 adicións e 185 borrados
  1. 10 0
      .tdd/_r189_admin_live.log
  2. 44 0
      .tdd/_r189_api.log
  3. 13 0
      .tdd/_r189_fe_jest.log
  4. 11 0
      .tdd/_r189_frontend_live.log
  5. 57 0
      .tdd/_r189_health.log
  6. 11 0
      .tdd/_r189_root_jest.log
  7. 9 0
      .tdd/_r189_shop_api.log
  8. 11 0
      .tdd/_r189_web_jest.log
  9. 4 0
      .tdd/_r190_admin_live.log
  10. 5 0
      .tdd/_r190_api.log
  11. 5 0
      .tdd/_r190_contract.log
  12. 3 0
      .tdd/_r190_fe_jest.log
  13. 4 0
      .tdd/_r190_frontend_live.log
  14. 3 0
      .tdd/_r190_health.log
  15. 5 0
      .tdd/_r190_lg_pytest.log
  16. 3 0
      .tdd/_r190_root_jest.log
  17. 3 0
      .tdd/_r190_shop_api.log
  18. 3 0
      .tdd/_r190_web_jest.log
  19. 62 0
      _r189_189_admin.cjs
  20. 62 0
      _r189_251_lg.cjs
  21. 62 0
      _r190_189_admin.cjs
  22. 27 0
      _r190_204_run.cjs
  23. 55 0
      _r190_251_lg.cjs
  24. 548 0
      cfc-backend/src/test/java/com/etotem/cfc/controller/admin/AdminOrderManualContractTest.java
  25. 209 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/AdminActivityConfigContractTest.java
  26. 540 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/AdminArticleManageContractTest.java
  27. 425 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/AdminAuditLogContractTest.java
  28. 386 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/AdminKnowledgeBaseManageContractTest.java
  29. 616 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/AdminReportParserManageContractTest.java
  30. 365 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/AdminSupplierManageContractTest.java
  31. 365 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/AdminUserGovernContractTest.java
  32. 419 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/DimensionConfigAdminContractTest.java
  33. 243 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyAutoCreateForPurchaseContractTest.java
  34. 293 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/PlanContentSafetyReviewContractTest.java
  35. 445 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/SupplierManageAdminContractTest.java
  36. 153 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherAssessmentRecordContractTest.java
  37. 249 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherFamilyOverviewContractTest.java
  38. 512 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherMessagePushContractTest.java
  39. 313 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherPlanCreateContractTest.java
  40. 428 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherReportProxyContractTest.java
  41. 1 1
      cfc-web/.last_build_commit
  42. 2 2
      cfc-web/package-lock.json
  43. 1 1
      cfc-web/package.json
  44. 336 0
      cfc-web/public/CHANGELOG-v1.0.md
  45. 337 1
      cfc-web/public/CHANGELOG.md
  46. 67 0
      docs/tdd/notice-2026-11-13-R189-研发通知.md
  47. 69 0
      docs/tdd/notice-2026-11-13-R190-研发通知.md
  48. 80 0
      docs/tdd/notice-2026-11-13-R191-研发通知.md
  49. 0 0
      docs/tdd/orchestration/pending.md
  50. 176 0
      docs/tdd/orchestration/report-dev-2026-11-13-1051.md
  51. 221 0
      docs/tdd/orchestration/report-dev-2026-11-13-1210.md
  52. 191 0
      docs/tdd/orchestration/report-dev-2026-11-13-1234.md
  53. 166 0
      docs/tdd/orchestration/report-dev-2026-11-13-1406.md
  54. 173 0
      docs/tdd/orchestration/report-dev-2026-11-13-1458.md
  55. 267 0
      docs/tdd/orchestration/report-test-2026-11-13-R189.md
  56. 263 0
      docs/tdd/orchestration/report-test-2026-11-13-R190.md
  57. 377 0
      docs/tdd/orchestration/report-test-2026-11-13-R191.md
  58. 291 0
      docs/tdd/orchestration/report-test-2026-11-13-R192.md
  59. 176 176
      docs/tdd/orchestration/sc-key-map.md
  60. 47 0
      docs/tdd/test/automation-run-2026-11-13-R189.md
  61. 49 0
      docs/tdd/test/automation-run-2026-11-13-R190.md
  62. 54 0
      docs/tdd/test/automation-run-2026-11-13-R191.md
  63. 62 0
      docs/tdd/test/automation-run-2026-11-13-R192.md
  64. 17 2
      docs/tdd/test/automation-status.md
  65. 87 0
      docs/tdd/test/blockers.md
  66. 17 2
      docs/tdd/test/inventory.md
  67. 62 0
      docs/tdd/test/notice-2026-11-13-R192-研发通知.md

+ 10 - 0
.tdd/_r189_admin_live.log

@@ -0,0 +1,10 @@
+
+Running 5 tests using 1 worker
+
+[1/5] [admin-desktop] › e2e-live\admin-live.spec.js:117:3 › 管理端 UI · 登录与主框架 › TC-A-U01 登录页可渲染(标题/账号/密码/登录按钮)
+[2/5] [admin-desktop] › e2e-live\admin-live.spec.js:129:3 › 管理端 UI · 登录与主框架 › TC-A-U02 滑块人机验证存在且可拖动通过
+[3/5] [admin-desktop] › e2e-live\admin-live.spec.js:145:3 › 管理端 UI · 登录与主框架 › TC-A-U03 未登录访问后台受保护路由被拦截到登录页
+[4/5] [admin-desktop] › e2e-live\admin-live.spec.js:156:3 › 管理端 UI · 登录与主框架 › TC-A-U04 管理员真实登录(滑块 + 凭据)进入后台主框架
+[5/5] [admin-desktop] › e2e-live\admin-live.spec.js:184:3 › 管理端 UI · 登录与主框架 › TC-A-U05 后台核心菜单可访问(用户管理/数据看板)
+  5 passed (1.4m)
+EXIT=0

+ 44 - 0
.tdd/_r189_api.log

@@ -0,0 +1,44 @@
+
+Running 32 tests using 1 worker
+
+[1/32] [desktop] › e2e\api\api-smoke.spec.js:18:3 › API级冒烟(远程后端) › 自注册链路可用(拿到有效 token)
+[2/32] [desktop] › e2e\api\api-smoke.spec.js:24:3 › API级冒烟(远程后端) › 家庭成员列表接口可调用 (code=200)
+[3/32] [desktop] › e2e\api\api-smoke.spec.js:30:3 › API级冒烟(远程后端) › 能量概览接口可调用 (code=200)
+[4/32] [desktop] › e2e\api\api-smoke.spec.js:36:3 › API级冒烟(远程后端) › 成员列表参数校验生效(越权家庭 → 403)
+[5/32] [desktop] › e2e\api\nonfunc-perf-compat.spec.js:56:1 › [TC-G-N03] 非微信H5兜底登录:手机号注册登录路径 UA 无关
+[6/32] [desktop] › e2e\api\nonfunc-perf-compat.spec.js:63:1 › [TC-G-N02/P-N06/C-N03/T-N04/S-N03/X-N07] 多端 UA 兼容:同请求不同客户端返回一致
+[7/32] [desktop] › e2e\api\nonfunc-perf-compat.spec.js:79:1 › [TC-A-N04/X-N02] 限流熔断稳定性:爆发请求下系统不死(不出现 5xx 雪崩)
+[desktop] › e2e\api\nonfunc-perf-compat.spec.js:79:1 › [TC-A-N04/X-N02] 限流熔断稳定性:爆发请求下系统不死(不出现 5xx 雪崩)
+[限流] 状态分布=[200,200,200,200,200,200,200,200,200,200,200,200,200,200,200,200,200,200,200,200] 其中429=0
+
+[8/32] [desktop] › e2e\api\nonfunc-perf-compat.spec.js:92:1 › [TC-P-N02/X-N01] 并发不串数据:并行 AI 请求各自独立响应
+[9/32] [desktop] › e2e\api\nonfunc-perf-compat.spec.js:117:1 › [响应时间基线] 已登录家庭列表单请求耗时冒烟 ≤ 8s
+[desktop] › e2e\api\nonfunc-perf-compat.spec.js:117:1 › [响应时间基线] 已登录家庭列表单请求耗时冒烟 ≤ 8s
+[耗时] 家庭列表 92ms
+
+[10/32] [desktop] › e2e\api\nonfunc-perf-compat.spec.js:128:6 › [TC-C-N01/X-N04] 任务打卡幂等:重复提交不重复计分(需任务流夹具)
+[11/32] [desktop] › e2e\api\nonfunc-perf-compat.spec.js:133:6 › [TC-X-N03] 活动名额并发不超卖:100 名额 / 瞬时 500 请求(需活动夹具 + 压测授权)
+[12/32] [desktop] › e2e\api\nonfunc-security.spec.js:50:3 › 非功能/安全接口冒烟 › [TC-G-N04] 未授权访问能量概览被拒(401/403)
+[13/32] [desktop] › e2e\api\nonfunc-security.spec.js:55:3 › 非功能/安全接口冒烟 › [TC-G-N04] 未授权访问家庭成员列表被拒(401/403)
+[14/32] [desktop] › e2e\api\nonfunc-security.spec.js:60:3 › 非功能/安全接口冒烟 › [TC-G-N04] 未授权调用 AI 健康教练被拒(401/403)
+[15/32] [desktop] › e2e\api\nonfunc-security.spec.js:66:3 › 非功能/安全接口冒烟 › [TC-P-N09] IDOR:越权访问他人家庭成员列表 → 403
+[16/32] [desktop] › e2e\api\nonfunc-security.spec.js:78:3 › 非功能/安全接口冒烟 › [TC-A-N05] 非管理员访问后台接口被拒(401/403)
+[17/32] [desktop] › e2e\api\nonfunc-security.spec.js:84:3 › 非功能/安全接口冒烟 › [TC-C-N07] 角色切换接口未授权被拒(401/403)
+[18/32] [desktop] › e2e\api\nonfunc-security.spec.js:89:3 › 非功能/安全接口冒烟 › [TC-C-N07] 权限提升防护:家长不可切换为未持有角色
+[19/32] [desktop] › e2e\api\nonfunc-security.spec.js:96:3 › 非功能/安全接口冒烟 › [TC-C-N07] 角色切换自身合法角色正常(基线)
+[20/32] [desktop] › e2e\api\nonfunc-security.spec.js:104:3 › 非功能/安全接口冒烟 › [TC-P-N10] AI 健康教练 XSS:query 注入脚本不回显原始脚本且不错 500
+[21/32] [desktop] › e2e\api\nonfunc-security.spec.js:112:3 › 非功能/安全接口冒烟 › [TC-A-N06] 活动反馈存储型 XSS:提交含脚本 comment,列表不回显原始脚本
+[22/32] [desktop] › e2e\api\nonfunc-security.spec.js:133:3 › 非功能/安全接口冒烟 › [TC-P-N11] 报告上传非 PDF/恶意内容被拒(非 200)
+[23/32] [desktop] › e2e\api\traceability-p94-p95-c26.spec.js:38:3 › SC-P-94 历史完成任务三类流 › TC-P-94 正常流:分页结构完整且仅返回已完成记录
+[24/32] [desktop] › e2e\api\traceability-p94-p95-c26.spec.js:50:3 › SC-P-94 历史完成任务三类流 › TC-P-94-E 异常流:非法分页/非法 category 不 5xx 且结构稳定
+[25/32] [desktop] › e2e\api\traceability-p94-p95-c26.spec.js:66:3 › SC-P-94 历史完成任务三类流 › TC-P-94-P 权限流:传非本家庭 memberId 不返回他人任务数据
+[26/32] [desktop] › e2e\api\traceability-p94-p95-c26.spec.js:82:3 › SC-P-95 成长历史记录三类流 › TC-P-95 正常流:打卡记录流返回可遍历数组
+[27/32] [desktop] › e2e\api\traceability-p94-p95-c26.spec.js:88:3 › SC-P-95 成长历史记录三类流 › TC-P-95-E 异常流:非法筛选与非法分页不 5xx
+[28/32] [desktop] › e2e\api\traceability-p94-p95-c26.spec.js:101:3 › SC-P-95 成长历史记录三类流 › TC-P-95-P 权限流:非本家庭 memberId 不返回他人打卡记录
+[29/32] [desktop] › e2e\api\traceability-p94-p95-c26.spec.js:114:8 › 契约缺口待修复(2026-11-10 实测) › TC-P-95:/api/growth/record/list 带/不带 memberId 均返回 code=500「服务器内部错误」,成长档案记录聚合口径未落地
+[30/32] [desktop] › e2e\api\traceability-p94-p95-c26.spec.js:116:8 › 契约缺口待修复(2026-11-10 实测) › TC-C-26:睡眠选项配置读取契约未落地 —— /api/health/sleep/tags 返回 404;/api/config/public/value 带 key=sleep.quality.tags 返回 code=500
+[31/32] [desktop] › e2e\api\traceability-p94-p95-c26.spec.js:118:8 › 契约缺口待修复(2026-11-10 实测) › TC-A-27-S:后台维护契约未落地 —— /api/admin/sleep/tags 返回 404;需确认是否改由 /api/admin/config/* 通用配置承载
+[32/32] [desktop] › e2e\api\traceability-p94-p95-c26.spec.js:120:8 › 契约缺口待修复(2026-11-10 实测) › SC-P-96 / 前端页面类断言:远程环境未托管前端 SPA,纯 UI 与页面跳转断言需本地/真机环境执行
+  7 skipped
+  25 passed (28.0s)
+API_EXIT=0

+ 13 - 0
.tdd/_r189_fe_jest.log

@@ -0,0 +1,13 @@
+          |                         ^
+      288 |     // 全为无效记录 → 空分组
+      289 |     expect(invokeGrouping(component, [{ checkinDate: 'bad' }, { foo: 1 }]).length).toBe(0)
+      290 |   })
+
+      at Object.<anonymous> (__tests__/pages/growth-main/growthHistory.dayGroupView.test.js:287:25)
+
+
+Test Suites: 2 failed, 40 passed, 42 total
+Tests:       3 failed, 12 skipped, 609 passed, 624 total
+Snapshots:   0 total
+Time:        172.323 s
+FE_EXIT=0

+ 11 - 0
.tdd/_r189_frontend_live.log

@@ -0,0 +1,11 @@
+
+Running 6 tests using 1 worker
+
+[1/6] [frontend-mobile] › e2e-live\frontend-live.spec.js:47:3 › 小程序 H5 UI · 游客浏览与导航 › TC-F-U01 首页可渲染(标题/落地路由/TabBar 四项)
+[2/6] [frontend-mobile] › e2e-live\frontend-live.spec.js:72:3 › 小程序 H5 UI · 游客浏览与导航 › TC-F-U02 首页核心内容渲染(五维沙盘 / 三步引导)
+[3/6] [frontend-mobile] › e2e-live\frontend-live.spec.js:86:3 › 小程序 H5 UI · 游客浏览与导航 › TC-F-U03 TabBar 切换到「健康」正常跳转且无异常
+[4/6] [frontend-mobile] › e2e-live\frontend-live.spec.js:104:3 › 小程序 H5 UI · 游客浏览与导航 › TC-F-U04 TabBar 切换到「成长」正常跳转
+[5/6] [frontend-mobile] › e2e-live\frontend-live.spec.js:117:3 › 小程序 H5 UI · 游客浏览与导航 › TC-F-U05 未登录访问「我的」应出现登录/注册入口(鉴权引导)
+[6/6] [frontend-mobile] › e2e-live\frontend-live.spec.js:126:3 › 小程序 H5 UI · 游客浏览与导航 › TC-F-U06 首屏关键资源加载无失败(无 requestfailed)
+  6 passed (46.2s)
+EXIT=0

+ 57 - 0
.tdd/_r189_health.log

@@ -0,0 +1,57 @@
+
+Running 13 tests using 1 worker
+
+[1/13] [health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U01 POST /api/huawei-health/auth-url 未鉴权应被拒绝(401/403)
+[health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U01 POST /api/huawei-health/auth-url 未鉴权应被拒绝(401/403)
+[TC-H-U01] POST /api/huawei-health/auth-url -> HTTP 401
+
+[2/13] [health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U02 POST /api/huawei-health/callback 未鉴权应被拒绝(401/403)
+[health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U02 POST /api/huawei-health/callback 未鉴权应被拒绝(401/403)
+[TC-H-U02] POST /api/huawei-health/callback -> HTTP 401
+
+[3/13] [health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U03 POST /api/huawei-health/sync 未鉴权应被拒绝(401/403)
+[health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U03 POST /api/huawei-health/sync 未鉴权应被拒绝(401/403)
+[TC-H-U03] POST /api/huawei-health/sync -> HTTP 401
+
+[4/13] [health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U04 POST /api/huawei-health/revoke 未鉴权应被拒绝(401/403)
+[health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U04 POST /api/huawei-health/revoke 未鉴权应被拒绝(401/403)
+[TC-H-U04] POST /api/huawei-health/revoke -> HTTP 401
+
+[5/13] [health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U05 POST /api/huawei-health/status 未鉴权应被拒绝(401/403)
+[health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U05 POST /api/huawei-health/status 未鉴权应被拒绝(401/403)
+[TC-H-U05] POST /api/huawei-health/status -> HTTP 401
+
+[6/13] [health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U06 POST /api/xiaomi-health/auth-url 未鉴权应被拒绝(401/403)
+[health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U06 POST /api/xiaomi-health/auth-url 未鉴权应被拒绝(401/403)
+[TC-H-U06] POST /api/xiaomi-health/auth-url -> HTTP 401
+
+[7/13] [health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U07 POST /api/xiaomi-health/callback 未鉴权应被拒绝(401/403)
+[health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U07 POST /api/xiaomi-health/callback 未鉴权应被拒绝(401/403)
+[TC-H-U07] POST /api/xiaomi-health/callback -> HTTP 401
+
+[8/13] [health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U08 POST /api/xiaomi-health/sync 未鉴权应被拒绝(401/403)
+[health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U08 POST /api/xiaomi-health/sync 未鉴权应被拒绝(401/403)
+[TC-H-U08] POST /api/xiaomi-health/sync -> HTTP 401
+
+[9/13] [health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U09 POST /api/xiaomi-health/revoke 未鉴权应被拒绝(401/403)
+[health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U09 POST /api/xiaomi-health/revoke 未鉴权应被拒绝(401/403)
+[TC-H-U09] POST /api/xiaomi-health/revoke -> HTTP 401
+
+[10/13] [health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U10 POST /api/xiaomi-health/status 未鉴权应被拒绝(401/403)
+[health-api] › e2e-live\health-contract.spec.js:41:5 › 华为/小米运动健康接入 · 鉴权契约(§4.43/§4.44) › TC-H-U10 POST /api/xiaomi-health/status 未鉴权应被拒绝(401/403)
+[TC-H-U10] POST /api/xiaomi-health/status -> HTTP 401
+
+[11/13] [health-api] › e2e-live\health-contract.spec.js:74:5 › 健康方案 · 鉴权契约(§4.49.1/§4.49.2) › TC-HP-U01 POST /api/health/plan/adjust 未鉴权应被拒绝(401/403) · HL-04/R4 健康方案对话式调整
+[health-api] › e2e-live\health-contract.spec.js:74:5 › 健康方案 · 鉴权契约(§4.49.1/§4.49.2) › TC-HP-U01 POST /api/health/plan/adjust 未鉴权应被拒绝(401/403) · HL-04/R4 健康方案对话式调整
+[TC-HP-U01] POST /api/health/plan/adjust -> HTTP 401
+
+[12/13] [health-api] › e2e-live\health-contract.spec.js:74:5 › 健康方案 · 鉴权契约(§4.49.1/§4.49.2) › TC-HP-U02 POST /api/health/plan/attribution 未鉴权应被拒绝(401/403) · HL-03/R2 健康方案归因
+[health-api] › e2e-live\health-contract.spec.js:74:5 › 健康方案 · 鉴权契约(§4.49.1/§4.49.2) › TC-HP-U02 POST /api/health/plan/attribution 未鉴权应被拒绝(401/403) · HL-03/R2 健康方案归因
+[TC-HP-U02] POST /api/health/plan/attribution -> HTTP 401
+
+[13/13] [health-api] › e2e-live\health-contract.spec.js:74:5 › 健康方案 · 鉴权契约(§4.49.1/§4.49.2) › TC-HP-U03 POST /api/health/plan/generate 未鉴权应被拒绝(401/403) · HL-03/R3 生成不确定点补充(followUpAnswers)
+[health-api] › e2e-live\health-contract.spec.js:74:5 › 健康方案 · 鉴权契约(§4.49.1/§4.49.2) › TC-HP-U03 POST /api/health/plan/generate 未鉴权应被拒绝(401/403) · HL-03/R3 生成不确定点补充(followUpAnswers)
+[TC-HP-U03] POST /api/health/plan/generate -> HTTP 401
+
+  13 passed (4.2s)
+HEALTH_EXIT=0

+ 11 - 0
.tdd/_r189_root_jest.log

@@ -0,0 +1,11 @@
+
+    Cause:
+    AggregateError:
+
+
+
+Test Suites: 2 failed, 2 total
+Tests:       16 failed, 7 passed, 23 total
+Snapshots:   0 total
+Time:        9.795 s, estimated 11 s
+ROOT_EXIT=0

+ 9 - 0
.tdd/_r189_shop_api.log

@@ -0,0 +1,9 @@
+
+Running 4 tests using 1 worker
+
+[1/4] [shop-api] › e2e-live\shop-sort-api.spec.js:37:3 › 商城价格排序 · 后端排序契约(需求变更 S1) › TC-S-U01 price asc 生效(对应 buildSort → price_asc)
+[2/4] [shop-api] › e2e-live\shop-sort-api.spec.js:45:3 › 商城价格排序 · 后端排序契约(需求变更 S1) › TC-S-U02 price desc 生效(对应 buildSort → price_desc)
+[3/4] [shop-api] › e2e-live\shop-sort-api.spec.js:53:3 › 商城价格排序 · 后端排序契约(需求变更 S1) › TC-S-U03 默认排序不变(对应 buildSort → default,不传 sort)
+[4/4] [shop-api] › e2e-live\shop-sort-api.spec.js:61:3 › 商城价格排序 · 后端排序契约(需求变更 S1) › TC-S-U04 非法排序列被白名单拦截(不 5xx、不注入)
+  4 passed (6.9s)
+SHOP_EXIT=0

+ 11 - 0
.tdd/_r189_web_jest.log

@@ -0,0 +1,11 @@
+PASS src/views/admin/review/__tests__/auditRowMapping.spec.js
+PASS src/__tests__/Families.actionMenu.spec.js
+PASS src/__tests__/views/FamiliesReportEntry.spec.js
+PASS src/__tests__/views/FamiliesPlanEntry.spec.js
+PASS src/__tests__/utils/indicatorSeeds.test.js
+
+Test Suites: 17 passed, 17 total
+Tests:       116 passed, 116 total
+Snapshots:   0 total
+Time:        208.842 s
+WEB_EXIT=0

+ 4 - 0
.tdd/_r190_admin_live.log

@@ -0,0 +1,4 @@
+[R190] admin-desktop E2E (本机, 11:52)
+npx playwright test -c playwright.config.live.js --project=admin-desktop --reporter=line
+5 passed (1.4m)  [TC-A-U01~U05 登录/主框架/核心菜单]
+[跨节点] 189:4090 _r190_189_admin.cjs → 5/5 EXIT=0 (1.3m)

+ 5 - 0
.tdd/_r190_api.log

@@ -0,0 +1,5 @@
+[R190] 线上 API 32 例 (本机, 12:10)
+npx playwright test -c playwright.config.api.js --output=../_r190_api
+  e2e/api/api-smoke.spec.js e2e/api/nonfunc-security.spec.js
+  e2e/api/nonfunc-perf-compat.spec.js e2e/api/traceability-p94-p95-c26.spec.js
+25 passed / 7 skipped / 0 failed (28s)

+ 5 - 0
.tdd/_r190_contract.log

@@ -0,0 +1,5 @@
+[R190] 窗口契约类定向 (本机 mvn, 12:27~12:31)
+cd cfc-backend && JAVA_HOME=/d/jdk1.8.0_25 /d/maven/apache-maven-3.6.1/bin/mvn -o test -Dtest=TeacherFamilyOverviewContractTest -DfailIfNoTests=false
+  -> Tests run: 7, Failures: 1 (6过/1预期RED/跨家庭IDOR), 1m17s
+cd cfc-backend && JAVA_HOME=/d/jdk1.8.0_25 /d/maven/apache-maven-3.6.1/bin/mvn -o test -Dtest=TeacherReportProxyContractTest -DfailIfNoTests=false
+  -> Tests run: 14, Failures: 0, BUILD SUCCESS (GREEN), 7.9s

+ 3 - 0
.tdd/_r190_fe_jest.log

@@ -0,0 +1,3 @@
+[R190] cfc-frontend Jest (本机, 12:12)
+npx jest --ci
+609 passed / 3 failed / 12 skipped (172s)  [3F = S-cfc-132/133 预期 RED]

+ 4 - 0
.tdd/_r190_frontend_live.log

@@ -0,0 +1,4 @@
+[R190] frontend-mobile E2E (本机, 11:58)
+npx playwright test -c playwright.config.live.js --project=frontend-mobile --reporter=line
+6 passed (46.2s)  [TC-F-U01~U06 游客导航/TabBar/资源加载, B-185 维持消除]
+[跨节点] 204:4091 _r190_204_run.cjs → 6/6 PASS (46.1s) [B-95 观察改善]

+ 3 - 0
.tdd/_r190_health.log

@@ -0,0 +1,3 @@
+[R190] health 鉴权契约 E2E (本机, 12:02)
+npx playwright test -c playwright.config.health.js --reporter=line
+13 passed (4.2s)  [TC-H-U01~U10 华为/小米 + TC-HP-U01~03 健康方案鉴权, B-61 维持 401]

+ 5 - 0
.tdd/_r190_lg_pytest.log

@@ -0,0 +1,5 @@
+[R190] langgraph pytest @251 (192.168.16.251:4096, 12:13~12:18)
+.venv/bin/python -m pytest -q --ignore=tests/qna --ignore=tests/test_chat.py
+50 passed / 1 failed in 269.16s
+[唯一红] test_auto_tag_valid_format 标签数0 → 日志: Error code: 401 - Authentication Fails, Your api key: ****ab12 is invalid
+[判别] 节点HEAD=2313791a=基线 + 窗口源码零变更 → 环境型(B-142-2复发), 非代码回归

+ 3 - 0
.tdd/_r190_root_jest.log

@@ -0,0 +1,3 @@
+[R190] 根级黑盒 Jest (本机, 12:19)
+cd tests && npx jest --silent
+7 passed / 16 failed (9.8s)  [B-36 第74轮, ECONNREFUSED 9082]

+ 3 - 0
.tdd/_r190_shop_api.log

@@ -0,0 +1,3 @@
+[R190] shop-sort API E2E (本机, 12:01)
+npx playwright test -c playwright.config.live.js --project=shop-api --reporter=line
+4 passed (6.9s)  [TC-S-U01~U04 排序契约]

+ 3 - 0
.tdd/_r190_web_jest.log

@@ -0,0 +1,3 @@
+[R190] cfc-web Jest (本机, 12:16)
+npx jest --ci
+116 passed / 0 failed (209s)

+ 62 - 0
_r189_189_admin.cjs

@@ -0,0 +1,62 @@
+// R162: 189 admin-live UI E2E kick/read(String.raw 保反斜杠路径,修复 Location not found)
+const BASE = process.env.BASE_189 || 'http://192.168.16.189:4090';
+const AUTH = 'Basic ' + Buffer.from('opencode:' + (process.env.OPENCODE_SERVER_PASSWORD || 'IwinTrue@123')).toString('base64');
+const H = { Authorization: AUTH, 'Content-Type': 'application/json' };
+const CWD = String.raw`C:\code\cfc`;
+const LOC = '?location%5Bdirectory%5D=' + encodeURIComponent(CWD);
+const LOG = String.raw`C:\code\cfc\_r189_admin-live_test.log`;
+const SLEEP = ms => new Promise(r => setTimeout(r, ms));
+
+function buildKickCmd() {
+  return [
+    'cd ' + CWD,
+    'if (Test-Path ' + LOG + ') { Remove-Item ' + LOG + ' -Force }',
+    `Start-Process -FilePath cmd.exe -ArgumentList '/c','cd /d ${CWD}\\tests && set PLAYWRIGHT_HTML_REPORT=false && npx playwright test -c playwright.config.live.js --project=admin-desktop --reporter=line > ${LOG} 2>&1' -WindowStyle Hidden -PassThru | Select-Object Id`,
+    'start-sleep -Seconds 3',
+    'echo KICKED'
+  ].join('; ');
+}
+function buildReadCmd() {
+  return [
+    'echo READ_BEGIN',
+    'if (Test-Path ' + LOG + ') { Get-Content ' + LOG + ' -Tail 200 }',
+    'if (Test-Path ' + LOG + '.err) { echo ERR_BEGIN; Get-Content ' + LOG + '.err -Tail 30 }',
+    'echo READ_END'
+  ].join('; ');
+}
+async function j(url, opt) {
+  try {
+    const r = await fetch(url, Object.assign({ headers: H }, opt));
+    const t = await r.text();
+    try { return { status: r.status, json: JSON.parse(t) }; } catch { return { status: r.status, text: t }; }
+  } catch (e) { return { status: 0, err: e.message }; }
+}
+async function openShell(cmd, capMs = 30000) {
+  const cr = await j(BASE + '/api/shell' + LOC, { method: 'POST', body: JSON.stringify({ command: cmd, cwd: CWD }) });
+  const cj = (cr.json) || {};
+  const id = (cj.data && cj.data.id) || cj.id;
+  if (!id) return { err: 'create failed ' + cr.status + ' ' + JSON.stringify(cj).slice(0, 200), id: null };
+  const t0 = Date.now();
+  while (Date.now() - t0 < capMs) {
+    await SLEEP(2000);
+    const s = await j(BASE + '/api/shell/' + id + LOC);
+    const d = (s.json && s.json.data) || null;
+    if (d && d.status && d.status !== 'running') return { id, status: d.status };
+    if (!d) continue;
+  }
+  return { id, status: 'timeout' };
+}
+(async () => {
+  const mode = process.argv[2] || 'kick';
+  if (mode === 'kick') {
+    const k = await openShell(buildKickCmd(), 30000);
+    console.log('KICK', k.err || ('id=' + k.id + ' status=' + k.status));
+    return;
+  }
+  const r = await openShell(buildReadCmd(), 30000);
+  if (r.err) { console.log('READ_ERR', r.err); process.exit(1); }
+  const o = await j(BASE + '/api/shell/' + r.id + '/output' + LOC + '&limit=200000');
+  console.log('OUT_BEGIN');
+  console.log((o.json && o.json.data && o.json.data.output) || '(empty)');
+  console.log('OUT_END');
+})().catch(e => { console.error('ERR', e.message); process.exit(1); });

+ 62 - 0
_r189_251_lg.cjs

@@ -0,0 +1,62 @@
+// R150 251 langgraph 派发器:kick(分离启动)+ read(单次读日志),不在等待期新建 shell(B-125)
+const BASE = process.env.BASE || 'http://192.168.16.251:4096';
+const DIR = process.env.DIR || '/sc-data/cfc/cfc-langgraph';
+const LOG = process.env.LOG || '/tmp/r189_pytest.log';
+const AUTH = 'Basic ' + Buffer.from('opencode:' + (process.env.OPENCODE_SERVER_PASSWORD || 'IwinTrue@123')).toString('base64');
+const H = { Authorization: AUTH, 'Content-Type': 'application/json' };
+const LOC = '?location%5Bdirectory%5D=' + encodeURIComponent(DIR);
+
+async function j(url, opt) {
+  try {
+    const r = await fetch(url, Object.assign({ headers: H }, opt || {}));
+    const t = await r.text();
+    try { return { status: r.status, json: JSON.parse(t) }; } catch { return { status: r.status, text: t }; }
+  } catch (e) { return { status: 0, err: e.message }; }
+}
+
+async function sh(cmd, capMs) {
+  const cr = await j(BASE + '/api/shell' + LOC, { method: 'POST', body: JSON.stringify({ command: cmd, cwd: DIR }) });
+  const id = cr.json && cr.json.data && cr.json.data.id;
+  if (!id) return { err: 'create ' + cr.status + ' ' + JSON.stringify(cr).slice(0, 200) };
+  const t0 = Date.now();
+  while (Date.now() - t0 < (capMs || 60000)) {
+    await new Promise(r => setTimeout(r, 2500));
+    const s = await j(BASE + '/api/shell/' + id + LOC);
+    const d = (s.json && s.json.data) || null;
+    if (!d) continue;
+    if (d.status && d.status !== 'running') {
+      const o = await j(BASE + '/api/shell/' + id + '/output' + LOC + '&limit=200000');
+      return { code: d.status, out: (o.json && o.json.data && o.json.data.output) || '' };
+    }
+  }
+  return { err: 'timeout' };
+}
+
+(async () => {
+  const mode = process.argv[2] || 'kick';
+  // R173 增:LOG 若从本机 env 注入,Git Bash(MSYS) 会把 /tmp/x 改写成 C:/Users/.../x,
+  // 远端 `tail` 恒报 No such file,且 kick 的重定向也会失败 → 看起来像「日志没生成」。
+  // 这里直接挡住 Windows 风格路径,强制调用方使用远端 POSIX 路径。
+  if (LOG.includes(':') || LOG.includes('\\')) {
+    console.error('BAD_LOG_PATH=' + LOG + ' 请使用远端 POSIX 路径(如 /tmp/rX_pytest.log),不要经由本地 env 注入');
+    process.exit(2);
+  }
+  if (mode === 'kick') {
+    // R156 修:251 系统 /usr/bin/python 无 pytest,必须用节点 venv(.venv/bin/python → python3.11);
+    // 退出码另存变量后再写盘(B-126:$? 直接内联在 kick 串里可能落盘失败)。
+    // R173 复现(B-126 第 6 轮):手工 kick 用 `echo EXIT=$?` 时 186 行日志零 EXIT 行;
+    // 已在 /tmp/r173_probe.log 验证「先存 rc 再落盘」可写入 → 保留 rc 写法为唯一正确姿势。
+    const inner = "cd " + DIR + " && .venv/bin/python -m pytest -q --ignore=tests/qna --ignore=tests/test_chat.py > " + LOG + " 2>&1; rc=$?; echo EXIT=$rc >> " + LOG;
+    const cmd = "rm -f " + LOG + "; setsid nohup bash -c '" + inner + "' >/dev/null 2>&1 & echo KICKED";
+    const r = await sh(cmd, 40000);
+    console.log('KICK=' + JSON.stringify(r).slice(0, 400));
+    const h = await sh('cd ' + DIR + ' && git rev-parse --short HEAD; date', 30000);
+    console.log('NODE_HEAD=' + ((h.out || h.err || '').trim()));
+  } else if (mode === 'read') {
+    const r = await sh('tail -c 4000 ' + LOG, 45000);
+    console.log(r.err ? 'READ_ERR=' + r.err : r.out);
+  } else if (mode === 'run') {
+    const r = await sh(process.argv[3], 120000);
+    console.log(r.err ? 'ERR=' + r.err : r.out);
+  }
+})();

+ 62 - 0
_r190_189_admin.cjs

@@ -0,0 +1,62 @@
+// R162: 189 admin-live UI E2E kick/read(String.raw 保反斜杠路径,修复 Location not found)
+const BASE = process.env.BASE_189 || 'http://192.168.16.189:4090';
+const AUTH = 'Basic ' + Buffer.from('opencode:' + (process.env.OPENCODE_SERVER_PASSWORD || 'IwinTrue@123')).toString('base64');
+const H = { Authorization: AUTH, 'Content-Type': 'application/json' };
+const CWD = String.raw`C:\code\cfc`;
+const LOC = '?location%5Bdirectory%5D=' + encodeURIComponent(CWD);
+const LOG = String.raw`C:\code\cfc\_r190_admin-live_test.log`;
+const SLEEP = ms => new Promise(r => setTimeout(r, ms));
+
+function buildKickCmd() {
+  return [
+    'cd ' + CWD,
+    'if (Test-Path ' + LOG + ') { Remove-Item ' + LOG + ' -Force }',
+    `Start-Process -FilePath cmd.exe -ArgumentList '/c','cd /d ${CWD}\\tests && set PLAYWRIGHT_HTML_REPORT=false && npx playwright test -c playwright.config.live.js --project=admin-desktop --reporter=line > ${LOG} 2>&1' -WindowStyle Hidden -PassThru | Select-Object Id`,
+    'start-sleep -Seconds 3',
+    'echo KICKED'
+  ].join('; ');
+}
+function buildReadCmd() {
+  return [
+    'echo READ_BEGIN',
+    'if (Test-Path ' + LOG + ') { Get-Content ' + LOG + ' -Tail 200 }',
+    'if (Test-Path ' + LOG + '.err) { echo ERR_BEGIN; Get-Content ' + LOG + '.err -Tail 30 }',
+    'echo READ_END'
+  ].join('; ');
+}
+async function j(url, opt) {
+  try {
+    const r = await fetch(url, Object.assign({ headers: H }, opt));
+    const t = await r.text();
+    try { return { status: r.status, json: JSON.parse(t) }; } catch { return { status: r.status, text: t }; }
+  } catch (e) { return { status: 0, err: e.message }; }
+}
+async function openShell(cmd, capMs = 30000) {
+  const cr = await j(BASE + '/api/shell' + LOC, { method: 'POST', body: JSON.stringify({ command: cmd, cwd: CWD }) });
+  const cj = (cr.json) || {};
+  const id = (cj.data && cj.data.id) || cj.id;
+  if (!id) return { err: 'create failed ' + cr.status + ' ' + JSON.stringify(cj).slice(0, 200), id: null };
+  const t0 = Date.now();
+  while (Date.now() - t0 < capMs) {
+    await SLEEP(2000);
+    const s = await j(BASE + '/api/shell/' + id + LOC);
+    const d = (s.json && s.json.data) || null;
+    if (d && d.status && d.status !== 'running') return { id, status: d.status };
+    if (!d) continue;
+  }
+  return { id, status: 'timeout' };
+}
+(async () => {
+  const mode = process.argv[2] || 'kick';
+  if (mode === 'kick') {
+    const k = await openShell(buildKickCmd(), 30000);
+    console.log('KICK', k.err || ('id=' + k.id + ' status=' + k.status));
+    return;
+  }
+  const r = await openShell(buildReadCmd(), 30000);
+  if (r.err) { console.log('READ_ERR', r.err); process.exit(1); }
+  const o = await j(BASE + '/api/shell/' + r.id + '/output' + LOC + '&limit=200000');
+  console.log('OUT_BEGIN');
+  console.log((o.json && o.json.data && o.json.data.output) || '(empty)');
+  console.log('OUT_END');
+})().catch(e => { console.error('ERR', e.message); process.exit(1); });

+ 27 - 0
_r190_204_run.cjs

@@ -0,0 +1,27 @@
+const BASE = 'http://192.168.16.204:4091';
+const AUTH = 'Basic ' + Buffer.from('opencode:' + (process.env.OPENCODE_SERVER_PASSWORD || 'IwinTrue@123')).toString('base64');
+const H = { Authorization: AUTH, 'Content-Type': 'application/json' };
+const LOC = '?location%5Bdirectory%5D=' + encodeURIComponent('D:\\cfc');
+async function j(url, opt) { try { const r = await fetch(url, Object.assign({ headers: H }, opt)); const t = await r.text(); try { return { status: r.status, json: JSON.parse(t) }; } catch { return { status: r.status, text: t }; } } catch (e) { return { status: 0, err: e.message }; } }
+async function sh(cmd, capMs) {
+  const cr = await j(BASE + '/api/shell' + LOC, { method: 'POST', body: JSON.stringify({ command: cmd, cwd: 'D:\\cfc' }) });
+  const id = cr.json && cr.json.data && cr.json.data.id;
+  if (!id) return { err: 'create ' + cr.status + ' ' + JSON.stringify(cr).slice(0,200) };
+  const t0 = Date.now();
+  while (Date.now() - t0 < (capMs || 120000)) {
+    await new Promise(r=>setTimeout(r,3000));
+    const s = await j(BASE + '/api/shell/' + id + LOC);
+    const d = (s.json && s.json.data) || null;
+    if (!d) continue;
+    if (d.status && d.status !== 'running') {
+      const o = await j(BASE + '/api/shell/' + id + '/output' + LOC + '&limit=200000');
+      return { code: d.status, out: (o.json && o.json.data && o.json.data.output) || '' };
+    }
+  }
+  return { err: 'timeout' };
+}
+(async () => {
+  // 前台同步跑(不重定向,直接输出),用 POSIX 风格路径避免 PowerShell 反斜杠转义
+  const r = await sh('Set-Location D:/cfc/tests; $env:PLAYWRIGHT_HTML_REPORT="false"; npx playwright test -c playwright.config.live.js --project=frontend-mobile --reporter=line', 180000);
+  console.log(r.err ? 'ERR=' + r.err : 'CODE=' + r.code + '\n' + r.out.slice(-1800));
+})().catch(e=>{console.error('ERR',e.message);process.exit(1);});

+ 55 - 0
_r190_251_lg.cjs

@@ -0,0 +1,55 @@
+// R190: 251 langgraph 派发器(基于 R150/R173 修复版)
+const BASE = process.env.BASE || 'http://192.168.16.251:4096';
+const DIR = process.env.DIR || '/sc-data/cfc/cfc-langgraph';
+const LOG = process.env.LOG || '/tmp/r190_pytest.log';
+const AUTH = 'Basic ' + Buffer.from('opencode:' + (process.env.OPENCODE_SERVER_PASSWORD || 'IwinTrue@123')).toString('base64');
+const H = { Authorization: AUTH, 'Content-Type': 'application/json' };
+const LOC = '?location%5Bdirectory%5D=' + encodeURIComponent(DIR);
+async function j(url, opt) {
+  try {
+    const r = await fetch(url, Object.assign({ headers: H }, opt || {}));
+    const t = await r.text();
+    try { return { status: r.status, json: JSON.parse(t) }; } catch { return { status: r.status, text: t }; }
+  } catch (e) { return { status: 0, err: e.message }; }
+}
+async function sh(cmd, capMs) {
+  const cr = await j(BASE + '/api/shell' + LOC, { method: 'POST', body: JSON.stringify({ command: cmd, cwd: DIR }) });
+  const id = cr.json && cr.json.data && cr.json.data.id;
+  if (!id) return { err: 'create ' + cr.status + ' ' + JSON.stringify(cr).slice(0, 200) };
+  const t0 = Date.now();
+  while (Date.now() - t0 < (capMs || 60000)) {
+    await new Promise(r => setTimeout(r, 2500));
+    const s = await j(BASE + '/api/shell/' + id + LOC);
+    const d = (s.json && s.json.data) || null;
+    if (!d) continue;
+    if (d.status && d.status !== 'running') {
+      const o = await j(BASE + '/api/shell/' + id + '/output' + LOC + '&limit=200000');
+      return { code: d.status, out: (o.json && o.json.data && o.json.data.output) || '' };
+    }
+  }
+  return { err: 'timeout' };
+}
+(async () => {
+  const mode = process.argv[2] || 'kick';
+  if (LOG.includes(':') || LOG.includes('\\')) {
+    console.error('BAD_LOG_PATH=' + LOG + ' 请使用远端 POSIX 路径(如 /tmp/rX_pytest.log),不要经由本地 env 注入');
+    process.exit(2);
+  }
+  if (mode === 'kick') {
+    // 先清理残留 pytest 进程(B-177-1 经验),再启动
+    const clean = await sh("ps -eo pid,etime,cmd | grep -E 'pytest|local_report' | grep -v grep | awk '{print $1}' | xargs -r kill -9 2>/dev/null; echo CLEANED", 30000);
+    const h = await sh('cd ' + DIR + ' && git rev-parse --short HEAD; date', 30000);
+    const inner = "cd " + DIR + " && .venv/bin/python -m pytest -q --ignore=tests/qna --ignore=tests/test_chat.py > " + LOG + " 2>&1; rc=$?; echo EXIT=$rc >> " + LOG;
+    const cmd = "rm -f " + LOG + "; setsid nohup bash -c '" + inner + "' >/dev/null 2>&1 & echo KICKED";
+    const r = await sh(cmd, 40000);
+    console.log('CLEAN=' + JSON.stringify(clean).slice(0, 200));
+    console.log('KICK=' + JSON.stringify(r).slice(0, 400));
+    console.log('NODE_HEAD=' + ((h.out || h.err || '').trim()));
+  } else if (mode === 'read') {
+    const r = await sh('tail -c 4000 ' + LOG, 45000);
+    console.log(r.err ? 'READ_ERR=' + r.err : r.out);
+  } else if (mode === 'run') {
+    const r = await sh(process.argv[3], 120000);
+    console.log(r.err ? 'ERR=' + r.err : r.out);
+  }
+})();

+ 548 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/admin/AdminOrderManualContractTest.java

@@ -0,0 +1,548 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.config.JwtInterceptor;
+import com.etotem.cfc.dto.ProductOrderDTO;
+import com.etotem.cfc.entity.AfterSalesRequest;
+import com.etotem.cfc.service.AfterSalesService;
+import com.etotem.cfc.service.ProductOrderService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatchers;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+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.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+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 admin.order.manual},SC-A-12)。
+ *
+ * <p><b>溯源</b></p>
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-A-12(订单/退款人工处理——
+ *       查订单 → 手动退款/补发,边界:重复退款拦截、留审计)</li>
+ *   <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.13(管理员接口需 {@code role=admin})与
+ *       §4.60.12(`AdminProductOrderController` `/api/admin/product/order`:list/detail/ship/
+ *       update-express/close/refund/approve|reject;`AdminAfterSalesController`
+ *       `/api/admin/shop/after-sales`:list/detail/review/complete)</li>
+ *   <li>契约补建:{@code docs/tdd/orchestration/admin-ops-kickoff-20261107.md} TR-13 /
+ *       {@code docs/superpowers/specs/2026-11-07-admin-ops-contract.md} §2 能力键映射
+ *       {@code admin.order.manual}(✅ 端点存在)+ §5:「补 admin 端点 MockMvc 契约 JUnit +
+ *       <b>非 admin token → 403 权限流</b>(重点)」;任务登记 S-cfc-202
+ *       (sc-key-map.md,RED / 未跑通)</li>
+ * </ul>
+ *
+ * <p><b>实现现状(origin/cfclub @ 7558d8cf4 实测)</b></p>
+ * <ul>
+ *   <li>正常流/边界流已实现:两个控制器端点齐全并委托 service;service 层已含
+ *       重复退款拦截、售后状态守卫(仅 pending 可审核 / 仅 approved 可完成)。</li>
+ *   <li><b>权限流未落地(本测试的红)</b>:参照 B-142-1 惯例({@code AdminArticleController#create}
+ *       的「无权限」守卫),管理端端点应声明 {@code @RequestAttribute("role")} 并在入口拒绝非 admin
+ *       ({@code !"admin".equals(role) → Result.error("无权限")});当前
+ *       {@code AdminProductOrderController}(除 list 外 6 个端点)与
+ *       {@code AdminAfterSalesController}(全部 4 个端点)均<em>无角色守卫</em>,登录的
+ *       parent/child/teacher 可越权发货/关单/审批退款/处理售后。框架层(JwtInterceptor
+ *       LOGIN_REQUIRED_PATHS 已收口 {@code /api/admin/})仅保证「匿名→401」,不区分角色。</li>
+ * </ul>
+ *
+ * <p><b>三流划分</b></p>
+ * <ul>
+ *   <li>正常流(已实现 → 绿锁定契约):端点映射齐全 + 委托 service(list/detail/ship/
+ *       update-express/close/refund-approve/refund-reject;售后 list/detail/review/complete)。</li>
+ *   <li>边界流(已实现 → 绿锁定契约):售后 review 状态守卫(非 pending 拒绝、无效 action 拒绝)、
+ *       complete 状态守卫(仅 approved 可完成);订单重复退款拦截既有契约覆盖见
+ *       {@code OrderRefundScP56ScA12ContractTest}。</li>
+ *   <li>权限流(契约缺口 → <b>预期红</b>):订单 6 端点 + 售后 4 端点缺 admin 角色守卫;
+ *       list 非 admin(roles=[parent])应 403 且不委托;refund/approve、售后 review
+ *       非 admin 应「无权限」且不委托。</li>
+ * </ul>
+ *
+ * <p><b>RED 预期</b>:权限流 5 例失败(红),断言原文含「RED[admin.order.manual]」,失败原因可贴
+ * (角色守卫参数缺失 / list 被非 admin 放行 / refund/approve 与 review 被非 admin 执行)。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:按 B-142-1 惯例为上述 10 个端点增加
+ * {@code @RequestAttribute("role") String role}(list 保留 {@code @RequestAttribute("roles")}
+ * 并放行平台角色/供应商),入口 {@code !"admin".equals(role) → Result.error("无权限")};
+ * 实现后本测试自动转绿。</p>
+ *
+ * <p><b>测试范围</b>:纯契约级单测(Mockito + 反射),无 Spring 上下文 / 无 DB / 无外部服务。</p>
+ */
+class AdminOrderManualContractTest {
+
+    /** 订单人工处理全端点(相对 {@code /api/admin/product/order}) */
+    private static final List<String> ORDER_ENDPOINTS = Arrays.asList(
+            "list", "detail", "ship", "update-express", "close", "refund/approve", "refund/reject");
+
+    /** 订单端点上应声明角色守卫参数的端点(list 已有 @RequestAttribute("roles"),属既有载体) */
+    private static final List<String> ORDER_ENDPOINTS_NEED_ROLE_GUARD = Arrays.asList(
+            "detail", "ship", "update-express", "close", "refund/approve", "refund/reject");
+
+    /** 售后人工处理全端点(相对 {@code /api/admin/shop/after-sales}) */
+    private static final List<String> AFTER_SALES_ENDPOINTS = Arrays.asList(
+            "list", "detail", "review", "complete");
+
+    private AdminProductOrderController orderController;
+    private AdminAfterSalesController afterSalesController;
+    private ProductOrderService orderService;
+    private AfterSalesService afterSalesService;
+
+    /** 边界流:真实 AfterSalesService + mock mapper(售后状态守卫绿锚点) */
+    private AfterSalesService realAfterSalesService;
+    private com.etotem.cfc.mapper.AfterSalesRequestMapper afterSalesRequestMapper;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        orderController = new AdminProductOrderController();
+        afterSalesController = new AdminAfterSalesController();
+        orderService = mock(ProductOrderService.class);
+        afterSalesService = mock(AfterSalesService.class);
+        setField(orderController, "orderService", orderService);
+        setField(afterSalesController, "afterSalesService", afterSalesService);
+
+        realAfterSalesService = new AfterSalesService();
+        afterSalesRequestMapper = mock(com.etotem.cfc.mapper.AfterSalesRequestMapper.class);
+        setField(realAfterSalesService, "afterSalesRequestMapper", afterSalesRequestMapper);
+        setField(realAfterSalesService, "productOrderMapper",
+                mock(com.etotem.cfc.mapper.ProductOrderMapper.class));
+
+        // 正常流委托桩:各 service 方法返回成功
+        // 注意:list 的 keyword/buyerKeyword/startDate/endDate 可为 null,
+        // 桩须用 any()(可匹配 null)而非 anyString(),否则返回 null → 断言 NPE
+        when(orderService.adminOrderPage(anyInt(), anyInt(), any(), any(),
+                any(), any(), any(), any(), any()))
+                .thenReturn(Result.success(new HashMap<String, Object>()));
+        when(orderService.adminOrderDetail(anyString()))
+                .thenReturn(Result.success(new ProductOrderDTO()));
+        when(orderService.ship(anyString(), anyString(), anyString(), anyString(), anyInt(), anyString()))
+                .thenReturn(Result.success("已发货"));
+        when(orderService.updateExpress(anyString(), anyString(), anyString(), anyInt()))
+                .thenReturn(Result.success("快递信息已更新"));
+        when(orderService.adminClose(anyString(), anyLong()))
+                .thenReturn(Result.success("订单已关闭"));
+        when(orderService.approveRefund(anyString(), anyLong()))
+                .thenReturn(Result.success("退款已处理"));
+        when(orderService.rejectRefund(anyString(), anyLong(), anyString()))
+                .thenReturn(Result.success("已拒绝退款申请"));
+        when(afterSalesService.adminList(any()))
+                .thenReturn(Result.success(new ArrayList<AfterSalesRequest>()));
+        when(afterSalesService.adminDetail(any()))
+                .thenReturn(Result.success(new AfterSalesRequest()));
+        when(afterSalesService.review(any(), any(), any(), anyLong()))
+                .thenReturn(Result.success("操作成功"));
+        when(afterSalesService.complete(any()))
+                .thenReturn(Result.success("已完成"));
+    }
+
+    // ==================== 正常流:端点映射齐全(绿锚点) ====================
+
+    @Test
+    @DisplayName("正常流-1:订单人工处理 7 端点映射齐全(list/detail/ship/update-express/close/refund/approve|reject)")
+    void 正常流_订单端点映射齐全() {
+        Set<String> paths = endpointPaths(AdminProductOrderController.class);
+        assertTrue(paths.containsAll(ORDER_ENDPOINTS),
+                "[admin.order.manual] AdminProductOrderController 应提供契约端点映射:"
+                        + ORDER_ENDPOINTS + ",实际:" + paths);
+    }
+
+    @Test
+    @DisplayName("正常流-2:售后人工处理 4 端点映射齐全(list/detail/review/complete)")
+    void 正常流_售后端点映射齐全() {
+        Set<String> paths = endpointPaths(AdminAfterSalesController.class);
+        assertTrue(paths.containsAll(AFTER_SALES_ENDPOINTS),
+                "[admin.order.manual] AdminAfterSalesController 应提供契约端点映射:"
+                        + AFTER_SALES_ENDPOINTS + ",实际:" + paths);
+    }
+
+    // ==================== 正常流:订单端点委托 service(绿锚点) ====================
+
+    @Test
+    @DisplayName("正常流-3:order/list 委托 orderService.adminOrderPage(分页/筛选/供应商行级隔离参数)")
+    void 正常流_订单list_委托adminOrderPage() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("status", "paid");
+        body.put("page", 1);
+        body.put("size", 20);
+        Result<Map<String, Object>> r =
+                orderController.list(body, 1L, Collections.singletonList("admin"));
+        assertEquals(200, r.getCode());
+        // 注意:keyword/buyerKeyword/startDate/endDate 为 null,verify 须用 any()(可匹配 null)
+        verify(orderService).adminOrderPage(anyInt(), anyInt(), any(), any(),
+                any(), any(), any(), any(), any());
+    }
+
+    @Test
+    @DisplayName("正常流-4:order/detail 委托 orderService.adminOrderDetail(orderNo)")
+    void 正常流_订单detail_委托adminOrderDetail() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("orderNo", "PO1");
+        Result<ProductOrderDTO> r = orderController.detail(body);
+        assertEquals(200, r.getCode());
+        verify(orderService).adminOrderDetail("PO1");
+    }
+
+    @Test
+    @DisplayName("正常流-5:order/ship 委托 orderService.ship(发货方式/单号/公司/快递费/联系人六参)")
+    void 正常流_订单ship_委托ship() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("orderNo", "PO1");
+        body.put("shipType", "express");
+        body.put("trackingNumber", "SF123456");
+        body.put("expressCompany", "顺丰速运");
+        body.put("shippingCost", 1000);
+        body.put("receiverContact", "13800000000");
+        Result<String> r = orderController.ship(body, 1L);
+        assertEquals(200, r.getCode());
+        verify(orderService).ship("PO1", "express", "SF123456", "顺丰速运", 1000, "13800000000");
+    }
+
+    @Test
+    @DisplayName("正常流-6:order/update-express 委托 orderService.updateExpress")
+    void 正常流_订单updateExpress_委托() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("orderNo", "PO1");
+        body.put("trackingNumber", "YT111");
+        body.put("expressCompany", "圆通速递");
+        body.put("shippingCost", 800);
+        Result<String> r = orderController.updateExpress(body);
+        assertEquals(200, r.getCode());
+        verify(orderService).updateExpress("PO1", "YT111", "圆通速递", 800);
+    }
+
+    @Test
+    @DisplayName("正常流-7:order/close 委托 orderService.adminClose(orderNo, adminId)")
+    void 正常流_订单close_委托adminClose() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("orderNo", "PO9");
+        Result<String> r = orderController.close(body, 1L);
+        assertEquals(200, r.getCode());
+        verify(orderService).adminClose("PO9", 1L);
+    }
+
+    @Test
+    @DisplayName("正常流-8:order/refund/approve 委托 orderService.approveRefund")
+    void 正常流_退款approve_委托() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("orderNo", "PO7");
+        Result<String> r = orderController.approveRefund(body, 1L);
+        assertEquals(200, r.getCode());
+        verify(orderService).approveRefund("PO7", 1L);
+    }
+
+    @Test
+    @DisplayName("正常流-9:order/refund/reject 委托 orderService.rejectRefund")
+    void 正常流_退款reject_委托() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("orderNo", "PO7");
+        body.put("reason", "证据不足");
+        Result<String> r = orderController.rejectRefund(body, 1L);
+        assertEquals(200, r.getCode());
+        verify(orderService).rejectRefund("PO7", 1L, "证据不足");
+    }
+
+    // ==================== 正常流:售后端点委托 service(绿锚点) ====================
+
+    @Test
+    @DisplayName("正常流-10:after-sales/list 委托 afterSalesService.adminList(status)")
+    void 正常流_售后list_委托adminList() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("status", "pending");
+        Result<List<AfterSalesRequest>> r = afterSalesController.list(body);
+        assertEquals(200, r.getCode());
+        verify(afterSalesService).adminList("pending");
+    }
+
+    @Test
+    @DisplayName("正常流-11:after-sales/detail 委托 afterSalesService.adminDetail(id)")
+    void 正常流_售后detail_委托adminDetail() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1L);
+        Result<AfterSalesRequest> r = afterSalesController.detail(body);
+        assertEquals(200, r.getCode());
+        verify(afterSalesService).adminDetail(1L);
+    }
+
+    @Test
+    @DisplayName("正常流-12:after-sales/review 委托 afterSalesService.review(approve/reject)")
+    void 正常流_售后review_委托() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1L);
+        body.put("action", "approve");
+        Result<String> r = afterSalesController.review(body, 1L);
+        assertEquals(200, r.getCode());
+        verify(afterSalesService).review(1L, "approve", null, 1L);
+    }
+
+    @Test
+    @DisplayName("正常流-13:after-sales/complete 委托 afterSalesService.complete(id)")
+    void 正常流_售后complete_委托() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1L);
+        Result<String> r = afterSalesController.complete(body);
+        assertEquals(200, r.getCode());
+        verify(afterSalesService).complete(1L);
+    }
+
+    // ==================== 边界流:售后状态守卫已实现(绿锚点) ====================
+
+    @Test
+    @DisplayName("边界流-1:售后 review 仅待审核(pending)可处理——已批准申请再次审核被拒且不落库")
+    void 边界流_售后review_非pending拒绝() {
+        when(afterSalesRequestMapper.selectById(1L)).thenReturn(requestWithStatus("approved"));
+        Result<String> r = realAfterSalesService.review(1L, "approve", null, 9L);
+        assertEquals("仅待审核的申请可处理", r.getMessage());
+        verify(afterSalesRequestMapper, never()).updateById(any(AfterSalesRequest.class));
+    }
+
+    @Test
+    @DisplayName("边界流-2:售后 review 无效 action 被拒且不落库")
+    void 边界流_售后review_无效action拒绝() {
+        when(afterSalesRequestMapper.selectById(1L)).thenReturn(requestWithStatus("pending"));
+        Result<String> r = realAfterSalesService.review(1L, "bogus", null, 9L);
+        assertEquals("无效操作", r.getMessage());
+        verify(afterSalesRequestMapper, never()).updateById(any(AfterSalesRequest.class));
+    }
+
+    @Test
+    @DisplayName("边界流-3:售后 review 申请不存在 → 拒绝")
+    void 边界流_售后review_申请不存在() {
+        when(afterSalesRequestMapper.selectById(1L)).thenReturn(null);
+        Result<String> r = realAfterSalesService.review(1L, "approve", null, 9L);
+        assertEquals("售后申请不存在", r.getMessage());
+    }
+
+    @Test
+    @DisplayName("边界流-4:售后 complete 仅已批准可完成——pending 拒绝且不落库")
+    void 边界流_售后complete_非approved拒绝() {
+        when(afterSalesRequestMapper.selectById(1L)).thenReturn(requestWithStatus("pending"));
+        Result<String> r = realAfterSalesService.complete(1L);
+        assertEquals("仅已批准的申请可完成", r.getMessage());
+        verify(afterSalesRequestMapper, never()).updateById(any(AfterSalesRequest.class));
+    }
+
+    @Test
+    @DisplayName("边界流-5:售后 complete 对 approved 申请成功闭环(approved→completed)")
+    void 边界流_售后complete_approved闭环成功() {
+        AfterSalesRequest request = requestWithStatus("approved");
+        when(afterSalesRequestMapper.selectById(1L)).thenReturn(request);
+        Result<String> r = realAfterSalesService.complete(1L);
+        assertEquals(200, r.getCode());
+        assertEquals("已完成", r.getData());
+        assertEquals("completed", request.getStatus());
+        verify(afterSalesRequestMapper).updateById(request);
+    }
+
+    // ==================== 权限流(契约缺口,预期红 ×5,B-142-1 / TR-13) ====================
+
+    @Test
+    @DisplayName("权限流-框架层:JwtInterceptor.LOGIN_REQUIRED_PATHS 已收口 /api/admin/(匿名→401,框架层已落地)")
+    void 权限流_框架层_登录门已收口apiAdmin() throws Exception {
+        Field f = JwtInterceptor.class.getDeclaredField("LOGIN_REQUIRED_PATHS");
+        f.setAccessible(true);
+        String[] paths = (String[]) f.get(null);
+        boolean covered = false;
+        for (String p : paths) {
+            if ("/api/admin/".equals(p)) {
+                covered = true;
+                break;
+            }
+        }
+        assertTrue(covered,
+                "[admin.order.manual] JwtInterceptor.LOGIN_REQUIRED_PATHS 应含 \"/api/admin/\","
+                        + "使匿名请求 admin 订单/售后端点 → 401(B-142-1 框架层;本契约关注的是框架层之下"
+                        + " controller 角色的 403 守卫,见下方 RED 用例)。");
+    }
+
+    @Test
+    @DisplayName("权限流-R1:订单人工处理 6 端点应声明 admin 角色守卫参数(缺守卫 → 红)")
+    void 权限流_订单端点_缺角色守卫() {
+        List<String> missing = new ArrayList<>();
+        for (String ep : ORDER_ENDPOINTS_NEED_ROLE_GUARD) {
+            if (!hasRoleGuardParam(AdminProductOrderController.class, methodOf(ep))) {
+                missing.add(ep);
+            }
+        }
+        assertTrue(missing.isEmpty(),
+                "RED[admin.order.manual] 订单人工处理端点应声明角色守卫参数"
+                        + "(@RequestAttribute(\"role\")/@RequestAttribute(\"roles\"),B-142-1 惯例,"
+                        + "参照 AdminArticleController#create 的「无权限」守卫),非 admin(parent/child/teacher)"
+                        + "应被拒绝且不触达 service;当前缺守卫的端点:" + missing
+                        + "。GREEN:增加 @RequestAttribute(\"role\") String role 并在入口"
+                        + " !\"admin\".equals(role) → Result.error(\"无权限\")。");
+    }
+
+    @Test
+    @DisplayName("权限流-R2:售后人工处理 4 端点应声明 admin 角色守卫参数(缺守卫 → 红)")
+    void 权限流_售后端点_缺角色守卫() {
+        List<String> missing = new ArrayList<>();
+        for (String ep : AFTER_SALES_ENDPOINTS) {
+            if (!hasRoleGuardParam(AdminAfterSalesController.class, ep)) {
+                missing.add(ep);
+            }
+        }
+        assertTrue(missing.isEmpty(),
+                "RED[admin.order.manual] 售后人工处理端点应声明角色守卫参数"
+                        + "(@RequestAttribute(\"role\"),B-142-1 惯例),非 admin(parent/child/teacher)"
+                        + "应被拒绝且不触达 service;当前缺守卫的端点:" + missing
+                        + "。GREEN:增加 @RequestAttribute(\"role\") String role 并在入口"
+                        + " !\"admin\".equals(role) → Result.error(\"无权限\")。");
+    }
+
+    @Test
+    @DisplayName("权限流-R3:order/list 非 admin 角色(roles=[parent])应 403 拒绝且不委托(→ 红)")
+    void 权限流_订单list_非admin应403拒绝() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("status", "paid");
+        Result<Map<String, Object>> r =
+                orderController.list(body, 1L, Collections.singletonList("parent"));
+        assertEquals(403, r.getCode(),
+                "RED[admin.order.manual] order/list 由非 admin 角色(roles=[parent],无供应商/平台角色)"
+                        + "调用时应返回 403 无权访问,不得委托 orderService.adminOrderPage 泄漏全量订单;"
+                        + "当前被放行(B-142-1 角色守卫缺失,TR-13 权限流)。");
+        verify(orderService, never()).adminOrderPage(anyInt(), anyInt(), any(), any(),
+                any(), any(), any(), any(), any());
+    }
+
+    @Test
+    @DisplayName("权限流-R4:order/refund/approve 非 admin 角色应「无权限」拒绝且不委托(→ 红)")
+    void 权限流_退款approve_非admin拒绝() throws Exception {
+        Map<String, Object> body = new HashMap<>();
+        body.put("orderNo", "PO7");
+        Result<?> r = invokeWithRole(orderController, "approveRefund", body, 1L, "parent");
+        assertNotNull(r, "RED[admin.order.manual] refund/approve 应返回无权结果而非 null。");
+        assertEquals("无权限", r.getMessage(),
+                "RED[admin.order.manual] refund/approve 由非 admin 角色调用时应按 B-142-1 惯例返回"
+                        + "「无权限」;当前方法无角色守卫参数(@RequestAttribute(\"role\")),parent/child"
+                        + "可直接审批退款(TR-13 权限流)。GREEN:增加 role 参数并在入口守卫。");
+        verify(orderService, never()).approveRefund(anyString(), anyLong());
+    }
+
+    @Test
+    @DisplayName("权限流-R5:after-sales/review 非 admin 角色应「无权限」拒绝且不委托(→ 红)")
+    void 权限流_售后review_非admin拒绝() throws Exception {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1L);
+        body.put("action", "approve");
+        Result<?> r = invokeWithRole(afterSalesController, "review", body, 1L, "parent");
+        assertNotNull(r, "RED[admin.order.manual] 售后 review 应返回无权结果而非 null。");
+        assertEquals("无权限", r.getMessage(),
+                "RED[admin.order.manual] 售后 review 由非 admin 角色调用时应按 B-142-1 惯例返回"
+                        + "「无权限」;当前方法无角色守卫参数(@RequestAttribute(\"role\")),parent/child"
+                        + "可直接处理售后单(TR-13 权限流)。GREEN:增加 role 参数并在入口守卫。");
+        verify(afterSalesService, never()).review(anyLong(), any(), any(), anyLong());
+    }
+
+    // ==================== 辅助 ====================
+
+    private static void setField(Object target, String name, Object value) throws Exception {
+        Field f = target.getClass().getDeclaredField(name);
+        f.setAccessible(true);
+        f.set(target, value);
+    }
+
+    /** 采集类上所有 @PostMapping 方法映射的相对路径(归一化:去掉前导斜杠) */
+    private static Set<String> endpointPaths(Class<?> clazz) {
+        Set<String> paths = new LinkedHashSet<>();
+        for (Method m : clazz.getDeclaredMethods()) {
+            PostMapping pm = m.getAnnotation(PostMapping.class);
+            if (pm != null) {
+                for (String p : pm.value()) {
+                    paths.add(p.replaceFirst("^/+", ""));
+                }
+            }
+        }
+        return paths;
+    }
+
+    /** 该方法是否声明了 admin 角色承载参数(@RequestAttribute value = "role" 或 "roles") */
+    private static boolean hasRoleGuardParam(Class<?> clazz, String methodName) {
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (!m.getName().equals(methodName)) {
+                continue;
+            }
+            for (Annotation[] anns : m.getParameterAnnotations()) {
+                for (Annotation a : anns) {
+                    if (a instanceof RequestAttribute) {
+                        String v = ((RequestAttribute) a).value();
+                        if ("role".equals(v) || "roles".equals(v)) {
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 反射派发任一 admin 端点方法并携带角色:兼容当前签名(RED 现状,如 refund/approve 为
+     * {@code (Map, Long)}、review 为 {@code (Map, Long)})与契约签名(GREEN 后为
+     * {@code (Map, Long, String role)}),避免 GREEN 阶段改签名导致本测试编译失败。
+     */
+    private static Result<?> invokeWithRole(Object controller, String methodName,
+                                            Map<String, Object> body, Long adminId, String role)
+            throws Exception {
+        for (Method m : controller.getClass().getDeclaredMethods()) {
+            if (!m.getName().equals(methodName)) {
+                continue;
+            }
+            Class<?>[] pts = m.getParameterTypes();
+            m.setAccessible(true);
+            if (pts.length == 3 && pts[0] == Map.class && pts[1] == Long.class
+                    && pts[2] == String.class) {
+                return (Result<?>) m.invoke(controller, body, adminId, role);
+            }
+            if (pts.length == 2 && pts[0] == Map.class && pts[1] == Long.class) {
+                return (Result<?>) m.invoke(controller, body, adminId);
+            }
+        }
+        fail("契约方法不存在: " + methodName);
+        return null;
+    }
+
+    /** 端点路径 → 控制器方法名(仅非同名路径需要映射) */
+    private static String methodOf(String endpoint) {
+        if ("refund/approve".equals(endpoint)) {
+            return "approveRefund";
+        }
+        if ("refund/reject".equals(endpoint)) {
+            return "rejectRefund";
+        }
+        if ("update-express".equals(endpoint)) {
+            return "updateExpress";
+        }
+        return endpoint;
+    }
+
+    private static AfterSalesRequest requestWithStatus(String status) {
+        AfterSalesRequest r = new AfterSalesRequest();
+        r.setId(1L);
+        r.setOrderNo("PO1");
+        r.setUserId(7L);
+        r.setStatus(status);
+        return r;
+    }
+}

+ 209 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/AdminActivityConfigContractTest.java

@@ -0,0 +1,209 @@
+package com.etotem.cfc.unit;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.admin.AdminCouponController;
+import com.etotem.cfc.dto.ActivityDTO;
+import com.etotem.cfc.entity.Activity;
+import com.etotem.cfc.entity.Coupon;
+import com.etotem.cfc.mapper.ActivityMapper;
+import com.etotem.cfc.mapper.CouponMapper;
+import com.etotem.cfc.service.ActivityService;
+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.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * 契约级单测:能力键 {@code admin.activity.config}(S-cfc-203,TDD-RED 阶段)。
+ *
+ * <p>契约溯源:
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-A-07(活动配置:建活动(规则/时间/奖品)→上线,
+ *       边界:时间冲突校验)、SC-A-17(优惠券/活动配置管理:建券/活动 → 设规则/名额 → 上线,
+ *       边界:时间冲突校验;名额超卖配置保护)。</li>
+ *   <li>用例:{@code tests/test-cases/admin.md} TC-A-13-E(活动:时间冲突(起<止非法) 400)、
+ *       TC-A-18(优惠券/活动管理:后台配券建活动)、TC-A-18-E(券时间冲突(起>止) 400;
+ *       名额超卖配置保护生效;重复提交幂等)。</li>
+ *   <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.13(/api/admin/*)、§4.32
+ *       (/api/admin/coupon/*:list/create/update/issue/issue-family)。</li>
+ * </ul></p>
+ *
+ * <p><b>契约要求(配置校验三原则,来自 SC-A-07/SC-A-17 边界条款)</b>:
+ * <ol>
+ *   <li><b>时间冲突校验</b>:券 validFrom ≤ validUntil、活动 startTime ≤ endTime;违反应返回 400 业务错误。</li>
+ *   <li><b>名额超卖配置保护</b>:券 usedCount ≤ totalCount、活动 approvedCount ≤ maxParticipants;违反应返回 400。</li>
+ *   <li><b>重复提交幂等</b>:同名称券模板重复创建应被拒绝;同活动重复提交审核应被状态守卫拦截。</li>
+ * </ol></p>
+ *
+ * <p><b>实现现状(src/main 核实)</b>:
+ * <ul>
+ *   <li>{@code AdminCouponController.create}(L74-84)仅设默认值后直接 {@code couponMapper.insert},
+ *       <b>无时间冲突校验、无超卖保护、无重名拒绝</b>;</li>
+ *   <li>{@code ActivityService.create}(L185-195)/ {@code update}(L197-205)直接 save/updateById,
+ *       <b>无 startTime≤endTime 校验、无 approvedCount≤maxParticipants 校验</b>。</li>
+ * </ul>
+ * 上述配置校验均未落地 → 本测试断言「非法配置应返回 400」在实现补齐前应失败(红)。</p>
+ *
+ * <p><b>测试风格</b>:纯 Mockito + 反射注入(无 Spring 上下文 / 无 DB / 无外部服务),
+ * 与同批已合入主链的 {@code TeamProfitHookContractTest} / {@code FamilyJoinValidationRulesContractTest}
+ * 一致。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者在 {@code AdminCouponController.create/update}(或抽取
+ * {@code CouponService} 配置入口)与 {@code ActivityService.create/update} 加入时间冲突、
+ * 超卖、幂等守卫后,本测试自动转绿。</p>
+ */
+class AdminActivityConfigContractTest {
+
+    private static final int CODE_BAD_REQUEST = 400;
+
+    private AdminCouponController couponController;
+    private ActivityService activityService;
+
+    private CouponMapper couponMapper;
+    private ActivityMapper activityMapper;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        couponController = new AdminCouponController();
+        couponMapper = mock(CouponMapper.class);
+        inject(couponController, "couponMapper", couponMapper);
+
+        activityService = new ActivityService();
+        activityMapper = mock(ActivityMapper.class);
+        // MyBatis-Plus ServiceImpl 的受保护 baseMapper 字段(父类)
+        Field baseMapper = activityService.getClass().getSuperclass().getDeclaredField("baseMapper");
+        baseMapper.setAccessible(true);
+        baseMapper.set(activityService, activityMapper);
+    }
+
+    private static void inject(Object target, String field, Object value) throws Exception {
+        Field f = target.getClass().getDeclaredField(field);
+        f.setAccessible(true);
+        f.set(target, value);
+    }
+
+    // ==================== 契约1:券配置·时间冲突(TC-A-18-E) ====================
+
+    @Test
+    @DisplayName("契约-Coupon-1:券时间冲突(validFrom > validUntil) 应返回 400")
+    void couponCreate_timeConflict_rejected() {
+        Coupon c = new Coupon();
+        c.setName("时间冲突券");
+        c.setType("FIXED");
+        c.setValidFrom(date("2099-01-01"));
+        c.setValidUntil(date("2000-01-01")); // 起 > 止 → 时间冲突
+
+        Result<String> r = couponController.create(c);
+
+        assertEquals(CODE_BAD_REQUEST, r.getCode(),
+                "[admin.activity.config] 券配置时间冲突(validFrom>validUntil)应被拒绝(400),"
+                        + "当前 create 直接落库未校验(SC-A-17 / TC-A-18-E)。");
+    }
+
+    // ==================== 契约2:券配置·名额超卖保护(TC-A-18-E) ====================
+
+    @Test
+    @DisplayName("契约-Coupon-2:券名额超卖(usedCount > totalCount) 应返回 400")
+    void couponCreate_quotaOversell_rejected() {
+        Coupon c = new Coupon();
+        c.setName("超卖券");
+        c.setType("FIXED");
+        c.setTotalCount(10);
+        c.setUsedCount(11); // used > total → 超卖
+
+        Result<String> r = couponController.create(c);
+
+        assertEquals(CODE_BAD_REQUEST, r.getCode(),
+                "[admin.activity.config] 券配置超卖(usedCount>totalCount)应被拒绝(400),"
+                        + "当前 create 直接落库未校验(SC-A-17 / TC-A-18-E 名额超卖配置保护)。");
+    }
+
+    // ==================== 契约3:券配置·重名幂等(TC-A-18-E) ====================
+
+    @Test
+    @DisplayName("契约-Coupon-3:同名称券模板重复创建 应返回 400")
+    void couponCreate_duplicateName_rejected() {
+        // 已有同名模板 → create 应拒绝(重名幂等)
+        when(couponMapper.selectCount(any(Wrapper.class))).thenReturn(1L);
+
+        Coupon c = new Coupon();
+        c.setName("已存在的券模板");
+        c.setType("FIXED");
+        c.setTotalCount(100);
+        c.setUsedCount(0);
+
+        Result<String> r = couponController.create(c);
+
+        assertEquals(CODE_BAD_REQUEST, r.getCode(),
+                "[admin.activity.config] 同名称券模板重复创建应被拒绝(400),"
+                        + "当前 create 未做名称唯一性守卫(SC-A-17 / TC-A-18-E 重复提交幂等)。");
+    }
+
+    // ==================== 契约4:活动配置·时间冲突(TC-A-13-E) ====================
+
+    @Test
+    @DisplayName("契约-Activity-1:活动时间冲突(startTime > endTime) 应返回 400")
+    void activityCreate_timeConflict_rejected() {
+        when(activityMapper.insert(any(Activity.class))).thenReturn(1);
+
+        Activity a = new Activity();
+        a.setTitle("时间冲突活动");
+        a.setStatus("draft");
+        a.setStartTime(date("2099-01-01"));
+        a.setEndTime(date("2000-01-01")); // 起 > 止 → 时间冲突
+
+        Result<ActivityDTO> r = activityService.create(a, 1L);
+
+        assertEquals(CODE_BAD_REQUEST, r.getCode(),
+                "[admin.activity.config] 活动配置时间冲突(startTime>endTime)应被拒绝(400),"
+                        + "当前 create 直接落库未校验(SC-A-07 / TC-A-13-E 时间冲突校验)。");
+    }
+
+    // ==================== 契约5:活动配置·名额超卖保护(SC-A-17 / TC-A-18-E) ====================
+
+    @Test
+    @DisplayName("契约-Activity-2:活动名额超卖(approvedCount > maxParticipants) 应返回 400")
+    void activityUpdate_quotaOversell_rejected() {
+        Activity existing = new Activity();
+        existing.setId(100L);
+        existing.setStatus("draft");
+        existing.setApprovedCount(11);
+        existing.setMaxParticipants(10); // approved > max → 超卖
+        when(activityMapper.selectById(100L)).thenReturn(existing);
+        when(activityMapper.updateById(any(Activity.class))).thenReturn(1);
+
+        Activity a = new Activity();
+        a.setId(100L);
+        a.setTitle("超卖活动");
+        a.setApprovedCount(11);
+        a.setMaxParticipants(10);
+
+        Result<ActivityDTO> r = activityService.update(a);
+
+        assertEquals(CODE_BAD_REQUEST, r.getCode(),
+                "[admin.activity.config] 活动配置超卖(approvedCount>maxParticipants)应被拒绝(400),"
+                        + "当前 update 直接 updateById 未校验(SC-A-17 / TC-A-18-E 名额超卖配置保护)。");
+    }
+
+    // ==================== 辅助 ====================
+
+    private Date date(String yyyyMMdd) {
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            sdf.setLenient(false);
+            return sdf.parse(yyyyMMdd);
+        } catch (ParseException e) {
+            throw new IllegalArgumentException("非法测试日期: " + yyyyMMdd, e);
+        }
+    }
+}

+ 540 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/AdminArticleManageContractTest.java

@@ -0,0 +1,540 @@
+package com.etotem.cfc.unit;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.config.JwtInterceptor;
+import com.etotem.cfc.controller.admin.AdminArticleController;
+import com.etotem.cfc.entity.Article;
+import com.etotem.cfc.mapper.ArticleMapper;
+import com.etotem.cfc.service.ArticlePermissionService;
+import com.etotem.cfc.service.ArticleService;
+import com.etotem.cfc.service.FiveDimensionScoreService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+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 admin.article.manage},
+ * SC-A-03 文章发布-审核流,任务登记 {@code S-cfc-201})。
+ *
+ * <p><b>溯源</b></p>
+ * <ul>
+ *   <li>需求:{@code docs/tdd/orchestration/sc-key-map.md} L33 —— {@code S-cfc-201} 前瞻契约 RED 登记
+ *       (admin.article.manage);{@code docs/superpowers/api/API_REFERENCE.md} §4.60.4(SC-A-03
+ *       文章发布-审核流,管理端 {@code /api/admin/articles/*});</li>
+ *   <li>设计:{@code docs/superpowers/specs/2026-06-08-article-publishing-system-design.md} §4.2
+ *       (/pending、/approve、/reject)+ §6 状态机(draft→pending→approved/rejected,approved 为
+ *       C 端可见终态);{@code 2026-06-09-article-activity-publishing-design.md};</li>
+ *   <li>契约缺口台账:<b>B-142-1</b>(P1 安全:/api/admin/** 整族鉴权门缺失)——拦截器侧已修(见下),
+ *       controller 层角色守卫未收口为本测试红锁之一。</li>
+ * </ul>
+ *
+ * <p><b>实现现状(origin/cfclub @ f2f6e9235 实测)</b></p>
+ * <ul>
+ *   <li>状态机已落地(绿锁):submitForReview(draft→pending + auditStatus=pending)、auditArticle
+ *       (pending→approved/published | rejected 见 S-cfc-144 同批)、withdraw(published→withdrawn)、
+ *       reDraft(rejected/withdrawn→draft 清审核痕迹)、toggleStatus(置 publishedAt)、
+ *       toggleFeatured / delete / moveSortOrder 均已实现;</li>
+ *   <li><b>B-142-1 拦截器侧已修</b>:{@code JwtInterceptor.LOGIN_REQUIRED_PATHS} 已含 {@code "/api/admin/"}
+ *       前缀(见 L149),匿名 POST {@code /api/admin/**} → 401 —— 本测试以绿锁固化该防御;</li>
+ *   <li><b>本测试的红(契约缺口,6 例)</b>:
+ *     <ol>
+ *       <li>R1-R3 C 端「未审不可见」缺失:{@code ArticleService.getPublicList}(L120)/ {@code getFeatured}
+ *           (L230)仅过滤 {@code status=published},{@code getDetail}(L437)仅校验 status + isVisible,
+ *           均未排除 {@code auditStatus != "approved"};{@code status=published + auditStatus
+ *           =pending/rejected/null} 的文章会泄漏到 C 端 {@code POST /api/articles/list | /featured | /detail}
+ *           (该三端点在 {@code PUBLIC_PATHS} 白名单、匿名可读),违背设计 §6 状态机 approved 才是
+ *           C 端可见终态;</li>
+ *       <li>R4 直接发布未落审核默认值:{@code get}/${@link AdminArticleController#publish} →
+ *           {@code toggleStatus(id, "published")}(L902)无审核门禁、不置 {@code auditStatus=approved}、
+ *           不落 {@code auditorId/auditedAt},绕过 submit-review→audit 审核流直接上架(前端
+ *           ArticleManage.vue「直接发布」即走此捷径,与设计 §4.2/§6 状态机冲突,B-142-1 关联);</li>
+ *       <li>R5-R6 权限流 controller 层未收口:admin 角色守卫({@code @RequestAttribute("role")} →
+ *           非 admin 拒绝「无权限」)仅 create/audit/move 三处;update/delete/toggleFeatured/
+ *           submitReview/withdraw/reDraft 以及 publish 无守卫,登录 C 端用户(parent/child)可越权
+ *           发布/删除/撤回/置精选(B-142-1 controller 侧遗留)。</li>
+ *     </ol>
+ *   </li>
+ * </ul>
+ *
+ * <p><b>三流划分</b></p>
+ * <ul>
+ *   <li>正常流(已实现 → 绿锁定契约):toggleStatus 发布、submitForReview、withdraw、reDraft、
+ *       delete、toggleFeatured、controller list/publish 委托;</li>
+ *   <li>边界流(已实现 → 绿锁定契约):缺 id 参数门禁、错误状态守卫(非草稿提交审核 / 非发布中撤回 /
+ *       非驳回或撤回转草稿 / 非法 direction);</li>
+ *   <li>红测(契约缺口 → 预期红):C 端未审不可见(R1-R3)、直接发布未落审核默认值(R4)、
+ *       管理端点 admin 角色守卫缺失(R5-R6);权限流拦截器侧(G-P1/G-P2)为已修复防护的绿锁。</li>
+ * </ul>
+ *
+ * <p><b>RED 预期</b>:6 例失败(红),断言原文含「RED[admin.article.manage]」,失败原因可贴
+ * (未审泄漏 / 直接发布缺审核默认值 / publish 等端点缺 role 守卫)。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:
+ * ① C 端可见性:{@code getPublicList} / {@code getFeatured} / {@code getDetail} 查询条件追加
+ * {@code auditStatus="approved"}(或内存过滤),未审(pending/rejected/null)一律不可见;
+ * ② 直接发布审核门禁:{@code toggleStatus} 发布分支置 {@code auditStatus="approved"} 并落
+ * {@code auditorId/auditedAt}(或要求先过审核流),杜绝 published 与 auditStatus 不一致;
+ * ③ 权限流:publish/publishById/update/delete/toggleFeatured/submitReview/withdraw/reDraft 按
+ * create/audit/move 惯例追加 {@code @RequestAttribute("role") String role} 守卫(非 admin →
+ * {@code Result.error("无权限")})。实现后 R1-R6 自动转绿。</p>
+ *
+ * <p><b>测试范围</b>:纯契约级单测(Mockito + 反射实例化),无 Spring 上下文 / 无 DB / 无外部服务;
+ * {@code JwtInterceptor} 直接 new(匿名分支不触达其 @Resource 字段)。</p>
+ */
+class AdminArticleManageContractTest {
+
+    private static final String ADMIN_PUBLISH_ENDPOINT = "/api/admin/articles/publish";
+
+    private AdminArticleController controller;
+    private ArticleService articleService;
+
+    private ArticleService service;
+    private ArticleMapper articleMapper;
+    private ArticlePermissionService articlePermissionService;
+    private FiveDimensionScoreService fiveDimensionScoreService;
+    private JdbcTemplate jdbcTemplate;
+    private ThreadPoolTaskExecutor taskExecutor;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        // ============ Controller 侧:mock 全部 @Resource 字段,回读 articleService ============
+        controller = new AdminArticleController();
+        injectAll(controller);
+        articleService = (ArticleService) readField(controller, "articleService");
+
+        // ============ Service 侧:new 实例 + mock 全部依赖,回读关键依赖 ============
+        service = new ArticleService();
+        injectAll(service);
+        articleMapper = (ArticleMapper) readField(service, "articleMapper");
+        articlePermissionService = (ArticlePermissionService) readField(service, "articlePermissionService");
+        fiveDimensionScoreService = (FiveDimensionScoreService) readField(service, "fiveDimensionScoreService");
+        jdbcTemplate = (JdbcTemplate) readField(service, "jdbcTemplate");
+        taskExecutor = (ThreadPoolTaskExecutor) readField(service, "taskExecutor");
+
+        // 默认桩:可见性原样返回 / fail-open / 空阅读时长,避免正常流与边界流误触 NPE
+        when(articlePermissionService.filterVisible(anyList(), any(), any(), any()))
+                .thenAnswer(inv -> inv.getArgument(0));
+        when(articlePermissionService.isVisible(any(), any(), any(), any())).thenReturn(true);
+        when(fiveDimensionScoreService.getFamilyAverageScores(any())).thenReturn(Collections.emptyMap());
+        when(jdbcTemplate.queryForList(anyString())).thenReturn(Collections.emptyList());
+    }
+
+    // ==================== 正常流(已实现 → 绿锁定契约) ====================
+
+    @Test
+    @DisplayName("正常流-1:toggleStatus 发布 → 置 published/publishedAt 并写库")
+    void green_toggleStatus_发布置publishedAt() {
+        Article a = article(105L, "draft", null);
+        when(articleMapper.selectById(105L)).thenReturn(a);
+
+        service.toggleStatus(105L, "published");
+
+        assertEquals("published", a.getStatus());
+        assertNotNull(a.getPublishedAt(), "发布应记录发布时间");
+        verify(articleMapper).updateById(a);
+    }
+
+    @Test
+    @DisplayName("正常流-2:submitForReview 草稿提交审核 → status+auditStatus 均置 pending")
+    void green_submitForReview_草稿提交审核() {
+        Article a = article(105L, "draft", null);
+        when(articleMapper.selectById(105L)).thenReturn(a);
+
+        service.submitForReview(105L);
+
+        assertEquals("pending", a.getStatus());
+        assertEquals("pending", a.getAuditStatus());
+        verify(articleMapper).updateById(a);
+    }
+
+    @Test
+    @DisplayName("正常流-3:withdraw 发布中文章 → status=withdrawn(C 端下架)")
+    void green_withdraw_发布中撤回() {
+        Article a = article(105L, "published", "approved");
+        when(articleMapper.selectById(105L)).thenReturn(a);
+
+        service.withdraw(105L);
+
+        assertEquals("withdrawn", a.getStatus());
+        verify(articleMapper).updateById(a);
+    }
+
+    @Test
+    @DisplayName("正常流-4:reDraft 撤回稿 → 回草稿并清空审核痕迹")
+    void green_reDraft_撤回稿转草稿清审核痕迹() {
+        Article a = article(105L, "withdrawn", "approved");
+        when(articleMapper.selectById(105L)).thenReturn(a);
+
+        service.reDraft(105L);
+
+        assertEquals("draft", a.getStatus());
+        assertNull(a.getAuditStatus(), "回草稿应清空审核状态");
+        assertNull(a.getAuditorId(), "回草稿应清空审核人");
+        verify(articleMapper).updateById(a);
+    }
+
+    @Test
+    @DisplayName("正常流-5:delete 委托 mapper.deleteById")
+    void green_delete_委托mapper删除() {
+        service.delete(105L);
+        verify(articleMapper).deleteById(105L);
+    }
+
+    @Test
+    @DisplayName("正常流-6:controller list 透传 auditStatus 并委托 getAdminList")
+    void green_list_透传auditStatus并委托getAdminList() throws Exception {
+        Page<Article> page = new Page<>(1, 20);
+        when(articleService.getAdminList("published", null, null, null, null, null, null, "pending", 1, 20, null))
+                .thenReturn(page);
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("status", "published");
+        body.put("auditStatus", "pending");
+        body.put("page", 1);
+        body.put("size", 20);
+
+        Result<Page<Article>> r = controller.list(body);
+
+        assertEquals(200, r.getCode());
+        assertEquals(page, r.getData());
+        verify(articleService).getAdminList(eq("published"), isNull(), isNull(), isNull(), isNull(), isNull(),
+                isNull(), eq("pending"), eq(1), eq(20), isNull());
+    }
+
+    @Test
+    @DisplayName("正常流-7:controller publish 委托 toggleStatus 并返回操作成功")
+    void green_publish_委托toggleStatus() throws Exception {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 105L);
+        body.put("status", "published");
+
+        Result<String> r = controller.publish(body);
+
+        assertEquals(200, r.getCode());
+        assertEquals("操作成功", r.getData());
+        verify(articleService).toggleStatus(105L, "published");
+    }
+
+    @Test
+    @DisplayName("正常流-8:controller toggleFeatured 委托 toggleFeatured")
+    void green_toggleFeatured_委托toggleFeatured() throws Exception {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 105L);
+        body.put("isFeatured", 1);
+
+        Result<String> r = controller.toggleFeatured(body);
+
+        assertEquals(200, r.getCode());
+        assertEquals("操作成功", r.getData());
+        verify(articleService).toggleFeatured(105L, 1);
+    }
+
+    // ==================== 边界流(已实现 → 绿锁定契约) ====================
+
+    @Test
+    @DisplayName("边界流-1:publish 缺 id → 缺少文章ID,不触达 service")
+    void green_publish_缺id_拒绝() throws Exception {
+        Result<String> r = controller.publish(new HashMap<>());
+        assertEquals(500, r.getCode());
+        assertEquals("缺少文章ID", r.getMessage());
+        verify(articleService, never()).toggleStatus(anyLong(), any());
+    }
+
+    @Test
+    @DisplayName("边界流-2:submit-review 缺 id → 缺少文章ID")
+    void green_submitReview_缺id_拒绝() throws Exception {
+        Result<String> r = controller.submitReview(new HashMap<>());
+        assertEquals(500, r.getCode());
+        assertEquals("缺少文章ID", r.getMessage());
+    }
+
+    @Test
+    @DisplayName("边界流-3:withdraw 缺 id → 缺少文章ID")
+    void green_withdraw_缺id_拒绝() throws Exception {
+        Result<String> r = controller.withdraw(new HashMap<>());
+        assertEquals(500, r.getCode());
+        assertEquals("缺少文章ID", r.getMessage());
+    }
+
+    @Test
+    @DisplayName("边界流-4:re-draft 缺 id → 缺少文章ID")
+    void green_reDraft_缺id_拒绝() throws Exception {
+        Result<String> r = controller.reDraft(new HashMap<>());
+        assertEquals(500, r.getCode());
+        assertEquals("缺少文章ID", r.getMessage());
+    }
+
+    @Test
+    @DisplayName("边界流-5:submitForReview 非草稿 → 仅草稿状态可提交审核,不写库")
+    void green_submitForReview_非草稿_拒绝() {
+        Article a = article(105L, "published", "approved");
+        when(articleMapper.selectById(105L)).thenReturn(a);
+
+        RuntimeException ex = assertThrows(RuntimeException.class, () -> service.submitForReview(105L));
+
+        assertEquals("仅草稿状态可提交审核", ex.getMessage());
+        verify(articleMapper, never()).updateById(any(Article.class));
+    }
+
+    @Test
+    @DisplayName("边界流-6:withdraw 非发布中 → 仅发布中的文章可撤回,不写库")
+    void green_withdraw_非发布中_拒绝() {
+        Article a = article(105L, "draft", null);
+        when(articleMapper.selectById(105L)).thenReturn(a);
+
+        RuntimeException ex = assertThrows(RuntimeException.class, () -> service.withdraw(105L));
+
+        assertEquals("仅发布中的文章可撤回", ex.getMessage());
+        verify(articleMapper, never()).updateById(any(Article.class));
+    }
+
+    @Test
+    @DisplayName("边界流-7:reDraft 非驳回/撤回 → 仅已驳回或已撤回的文章可重新编辑,不写库")
+    void green_reDraft_非驳回或撤回_拒绝() {
+        Article a = article(105L, "published", "approved");
+        when(articleMapper.selectById(105L)).thenReturn(a);
+
+        RuntimeException ex = assertThrows(RuntimeException.class, () -> service.reDraft(105L));
+
+        assertEquals("仅已驳回或已撤回的文章可重新编辑", ex.getMessage());
+        verify(articleMapper, never()).updateById(any(Article.class));
+    }
+
+    @Test
+    @DisplayName("边界流-8:move 非法 direction → direction 只能是 up 或 down")
+    void green_move_非法direction_拒绝() throws Exception {
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 102L);
+        body.put("direction", "left");
+
+        Result<String> r = (Result<String>) findMethod(AdminArticleController.class, "move", 2)
+                .invoke(controller, body, "admin");
+
+        assertEquals(500, r.getCode());
+        assertEquals("direction 只能是 up 或 down", r.getMessage());
+        verify(articleService, never()).moveSortOrder(anyLong(), any());
+    }
+
+    // ==================== 红测-组1:C 端「未审不可见」(契约缺口,预期红) ====================
+
+    @Test
+    @DisplayName("RED-1:getPublicList 应排除未审核文章(status=published + auditStatus=pending 泄漏到 C 端列表)")
+    void red1_getPublicList_排除未审核文章() {
+        Article approved = article(101L, "published", "approved");
+        Article leaked = article(102L, "published", "pending"); // 未审通过但已置 published → C 端泄漏
+        when(articleMapper.selectList(any())).thenReturn(Arrays.asList(approved, leaked));
+        when(articlePermissionService.filterVisible(anyList(), any(), any(), any()))
+                .thenReturn(Arrays.asList(approved, leaked));
+
+        Page<Article> page = service.getPublicList(null, null, null, null, 1, 20, null, null, null, null);
+
+        List<Article> records = page.getRecords();
+        assertFalse(records.stream().anyMatch(a -> a.getId().equals(leaked.getId())),
+                "RED[admin.article.manage] C 端列表(POST /api/articles/list,PUBLIC_PATHS 匿名可读)应只返回"
+                        + "审核通过(auditStatus=approved)的文章;status=published + auditStatus=pending 的"
+                        + "未审文章不得泄漏。当前 getPublicList 仅过滤 status=published,未排除 "
+                        + "auditStatus!=approved(设计 §6 状态机 draft→pending→approved/rejected,"
+                        + "approved 才是 C 端可见终态)。");
+    }
+
+    @Test
+    @DisplayName("RED-2:getFeatured 应排除未审核文章(直接发布 auditStatus=null 泄漏到 C 端精选)")
+    void red2_getFeatured_排除未审核文章() {
+        Article leaked = article(102L, "published", null); // 直接发布绕过审核:auditStatus 缺失
+        when(articleMapper.selectList(any())).thenReturn(Collections.singletonList(leaked));
+        when(articlePermissionService.filterVisible(anyList(), any(), any(), any()))
+                .thenReturn(Collections.singletonList(leaked));
+
+        List<Article> list = service.getFeatured(10, null, null, null, null);
+
+        assertFalse(list.stream().anyMatch(a -> a.getId().equals(leaked.getId())),
+                "RED[admin.article.manage] C 端精选(POST /api/articles/featured,匿名可读)应只含审核通过"
+                        + "文章;当前 getFeatured 仅过滤 status=published + isFeatured=1,auditStatus=null 的"
+                        + "直接发布文章泄漏(设计 §6 状态机 approved 才是 C 端可见终态)。");
+    }
+
+    @Test
+    @DisplayName("RED-3:getDetail 应拒绝未审核文章(status=published + auditStatus=rejected 详情仍可读)")
+    void red3_getDetail_拒绝未审核文章() {
+        Article leaked = article(102L, "published", "rejected"); // 已驳回却仍 published → 详情泄漏
+        when(articleMapper.selectById(102L)).thenReturn(leaked);
+
+        Article detail = service.getDetail(102L, null, null, null);
+
+        assertNull(detail,
+                "RED[admin.article.manage] C 端详情(POST /api/articles/detail,匿名可读)应拒绝未审核文章"
+                        + "(auditStatus=rejected 但 status=published);当前 getDetail 仅校验 "
+                        + "status=published + isVisible,忽略 auditStatus,已驳回文章仍可被读取。");
+    }
+
+    // ==================== 红测-组2:直接发布未落审核默认值(契约缺口,预期红) ====================
+
+    @Test
+    @DisplayName("RED-4:toggleStatus 直接发布应落审核默认值(auditStatus=approved + auditedAt)")
+    void red4_directPublish_应落审核默认值() {
+        Article a = article(105L, "draft", null);
+        when(articleMapper.selectById(105L)).thenReturn(a);
+
+        service.toggleStatus(105L, "published");
+
+        assertEquals("approved", a.getAuditStatus(),
+                "RED[admin.article.manage] 直接发布 POST " + ADMIN_PUBLISH_ENDPOINT
+                        + "(toggleStatus(id,'published'))应视为完成审核并落 auditStatus=approved"
+                        + "(设计 §6 状态机 approved 才是 C 端可见终态,避免 published 与 auditStatus 不一致"
+                        + "导致 C 端泄漏或不可见);当前 toggleStatus 无审核门禁、auditStatus 保持原值"
+                        + "(null/pending),绕过 submit-review→audit 审核流直接上架(前端「直接发布」即走"
+                        + "此捷径,B-142-1 关联)。");
+        assertNotNull(a.getAuditedAt(),
+                "RED[admin.article.manage] 直接发布应落审核时间 auditedAt(公章性审核痕迹)");
+    }
+
+    // ==================== 红测-组3:权限流 controller 侧 admin 角色守卫(契约缺口,预期红) ====================
+
+    @Test
+    @DisplayName("RED-5:publish 应声明 role 参数并拒绝非 admin(当前无守卫,越权直发)")
+    void red5_publish_非admin应拒绝() throws Exception {
+        Method m = findMethod(AdminArticleController.class, "publish", 2);
+        assertNotNull(m,
+                "RED[admin.article.manage] 契约方法 publish(POST " + ADMIN_PUBLISH_ENDPOINT
+                        + ")应声明 role 参数(@RequestAttribute(\"role\") String role),按项目 admin 守卫"
+                        + "惯例拒绝非 admin(参照 create/audit/move 的「无权限」守卫);当前仅 publish(Map)"
+                        + "单参数、无角色校验,登录 C 端用户(parent/child)即可直接发布/上下架文章"
+                        + "(B-142-1 controller 侧未收口)。");
+        m.setAccessible(true);
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 105L);
+        body.put("status", "published");
+        Result<?> r = (Result<?>) m.invoke(controller, body, "parent");
+
+        assertEquals("无权限", r.getMessage());
+        verify(articleService, never()).toggleStatus(anyLong(), any());
+    }
+
+    @Test
+    @DisplayName("RED-6:update/delete/toggleFeatured/submitReview/withdraw/reDraft 应具 admin 角色守卫")
+    void red6_其余管理端点_应具admin角色守卫() {
+        String[] methods = {"update", "delete", "toggleFeatured", "submitReview", "withdraw", "reDraft"};
+        List<String> missing = new ArrayList<>();
+        for (String name : methods) {
+            if (findMethod(AdminArticleController.class, name, 2) == null) {
+                missing.add(name);
+            }
+        }
+        assertTrue(missing.isEmpty(),
+                "RED[admin.article.manage] 管理端文章端点除 create/audit/move 外应全部具 admin 角色守卫"
+                        + "(@RequestAttribute(\"role\") String role + 非 admin → Result.error(\"无权限\"),"
+                        + "B-142-1 controller 侧收口);当前缺失守卫: " + missing);
+    }
+
+    // ==================== 权限流绿锁(B-142-1 拦截器侧已修,固化防御) ====================
+
+    @Test
+    @DisplayName("权限流-绿锁1:JwtInterceptor.LOGIN_REQUIRED_PATHS 已覆盖 /api/admin/ 前缀")
+    void green_JwtInterceptor_已覆盖apiAdmin前缀() throws Exception {
+        Field f = JwtInterceptor.class.getDeclaredField("LOGIN_REQUIRED_PATHS");
+        f.setAccessible(true);
+        String[] paths = (String[]) f.get(null);
+        boolean covered = false;
+        for (String p : paths) {
+            if (ADMIN_PUBLISH_ENDPOINT.startsWith(p)) {
+                covered = true;
+                break;
+            }
+        }
+        assertTrue(covered,
+                "GREEN-lock[admin.article.manage] B-142-1 拦截器侧已修:LOGIN_REQUIRED_PATHS 应含 "
+                        + "/api/admin/ 前缀条目,匿名 POST /api/admin/ → 401");
+    }
+
+    @Test
+    @DisplayName("权限流-绿锁2:匿名 POST /api/admin/articles/publish → JwtInterceptor 401 拒绝")
+    void green_匿名请求_admin端点_401() throws Exception {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getRequestURI()).thenReturn(ADMIN_PUBLISH_ENDPOINT);
+        when(request.getHeader("Authorization")).thenReturn(null);
+        when(request.getParameter("token")).thenReturn(null);
+        when(response.getWriter()).thenReturn(new PrintWriter(new StringWriter()));
+
+        JwtInterceptor interceptor = new JwtInterceptor(); // @Resource 字段留空:匿名分支不触达 DB
+
+        boolean allowed = interceptor.preHandle(request, response, null);
+
+        assertFalse(allowed, "GREEN-lock[admin.article.manage] 匿名 POST /api/admin/ 应被 401 拒绝(B-142-1 已修)");
+        verify(response).setStatus(401);
+    }
+
+    // ==================== 辅助 ====================
+
+    private static Article article(Long id, String status, String auditStatus) {
+        Article a = new Article();
+        a.setId(id);
+        a.setStatus(status);
+        a.setAuditStatus(auditStatus);
+        a.setTitle("契约测试文章 " + id);
+        a.setContent("<p>正文</p>");
+        return a;
+    }
+
+    private static Object readField(Object target, String name) throws Exception {
+        Field f = target.getClass().getDeclaredField(name);
+        f.setAccessible(true);
+        return f.get(target);
+    }
+
+    /** mock 全部非 static / final 可写字段(对齐 AdminReportParserManageContractTest 注入装置)。 */
+    private static void injectAll(Object target) throws Exception {
+        for (Field f : target.getClass().getDeclaredFields()) {
+            if (Modifier.isStatic(f.getModifiers()) || Modifier.isFinal(f.getModifiers())) {
+                continue;
+            }
+            f.setAccessible(true);
+            f.set(target, mock(f.getType()));
+        }
+    }
+
+    private static Method findMethod(Class<?> clazz, String name, int paramCount) {
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals(name) && m.getParameterCount() == paramCount) {
+                return m;
+            }
+        }
+        return null;
+    }
+}

+ 425 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/AdminAuditLogContractTest.java

@@ -0,0 +1,425 @@
+package com.etotem.cfc.unit;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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 admin.audit.log},单元 admin / SC-A-15)。
+ *
+ * <p><b>需求/设计锚点</b></p>
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/05-admin.md} SC-A-15 —— 作为运营,查询审计日志并溯源,
+ *       以便可追溯;<b>边界:日志不可篡改</b>;<b>分页</b>。</li>
+ *   <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.60.14 ——
+ *       {@code OperationLogController /api/admin/logs}({@code operations} 分页查询 + {@code recent} 近期)
+ *       + {@code AdminAuditController /api/admin/audit}({@code list} 审计列表);
+ *       §4.13 管理员接口需 {@code role=admin}({@code AdminInterceptor})。</li>
+ *   <li>写入链路:{@code AuditLogAspect} 自动落 {@code audit_log}(管理端写操作),
+ *       {@code AuditLogService} 只增不删改(W0.3 审计日志统一)。</li>
+ * </ul>
+ *
+ * <p><b>被校验的契约行为</b></p>
+ * <ul>
+ *   <li><b>正常流(溯源)</b>:{@code AdminAuditController /api/admin/audit/list} 分页查询 + 过滤
+ *       (module / targetId / 时间范围);{@code OperationLogController /api/admin/logs}
+ *       {@code operations} + {@code recent};{@code AuditLog} 实体承载溯源字段
+ *       (operatorId / operatorName / module / action / targetType / targetId /
+ *       beforeValue / afterValue / ip / createdAt)。</li>
+ *   <li><b>边界流</b>:日志<b>不可篡改</b>——{@code AuditLogService} 只增不删改(不暴露
+ *       update / remove / delete 公开方法);{@code audit_log} + {@code user_operation_logs}
+ *       建表落地 schema.sql,且 DatabaseInitializer 有对应建表迁移(schema.sql 与迁移一致)。</li>
+ *   <li><b>权限流</b>:{@code AdminAuditController.list} 首参 {@code @RequestAttribute("role")},
+ *       仅 {@code role=admin} 可访问(§4.13)。</li>
+ * </ul>
+ *
+ * <p><b>测试范围</b>:纯契约级单测,反射锚定宿主类(避免编译期依赖不存在类)+ schema.sql
+ * 快照 + DatabaseInitializer 源码检查;<b>无 Spring / 无 DB / 无外部服务</b>。</p>
+ *
+ * <p><b>宿主契约类(实现方按此落地)</b></p>
+ * <ul>
+ *   <li>{@code com.etotem.cfc.controller.admin.AdminAuditController} —— {@code @RequestMapping("/api/admin/audit")}
+ *       + {@code @PostMapping("/list")} + {@code list(@RequestAttribute("role") String, Map)}</li>
+ *   <li>{@code com.etotem.cfc.controller.admin.OperationLogController} —— {@code @RequestMapping("/api/admin/logs")}
+ *       + {@code @PostMapping("/operations")} + {@code @PostMapping("/recent")}</li>
+ *   <li>{@code com.etotem.cfc.service.AuditLogService} —— {@code page(module, targetId, startTime, endTime, pageNo, pageSize)}
+ *       + {@code record(AuditLog)};<b>不暴露 update / remove / delete</b>(只增不删改)</li>
+ *   <li>{@code com.etotem.cfc.entity.AuditLog} —— 溯源字段齐全</li>
+ *   <li>{@code com.etotem.cfc.aspect.AuditLogAspect} —— 自动落审计切面</li>
+ * </ul>
+ *
+ * <p><b>RED 预期说明</b>:本批契约登记时该能力曾判定「实现未落地」;实测(2026-11-13)发现
+ * {@code origin/cfclub} 已包含上述宿主类与表,故本测试按契约行为断言如实验证——若契约行为缺失
+ * (如去掉了不可篡改约束 / 权限门禁 / 端点)即红,作为<b>契约回归测试</b>锁定行为。</p>
+ */
+class AdminAuditLogContractTest {
+
+    /** 审计查询控制器(AdminAuditController /api/admin/audit)。 */
+    private static final String ADMIN_AUDIT_CONTROLLER =
+            "com.etotem.cfc.controller.admin.AdminAuditController";
+
+    /** 操作日志控制器(OperationLogController /api/admin/logs)。 */
+    private static final String OPERATION_LOG_CONTROLLER =
+            "com.etotem.cfc.controller.admin.OperationLogController";
+
+    /** 审计日志服务(AuditLogService,只增不删改)。 */
+    private static final String AUDIT_LOG_SERVICE =
+            "com.etotem.cfc.service.AuditLogService";
+
+    /** 审计日志实体(AuditLog)。 */
+    private static final String AUDIT_LOG_ENTITY =
+            "com.etotem.cfc.entity.AuditLog";
+
+    /** 审计切面(AuditLogAspect,自动落审计)。 */
+    private static final String AUDIT_LOG_ASPECT =
+            "com.etotem.cfc.aspect.AuditLogAspect";
+
+    /** 用户操作日志服务(UserOperationLogService)。 */
+    private static final String USER_OPERATION_LOG_SERVICE =
+            "com.etotem.cfc.service.UserOperationLogService";
+
+    /** DatabaseInitializer 源码(迁移一致性检查)。 */
+    private static final String DATABASE_INITIALIZER_SOURCE =
+            "src/main/java/com/etotem/cfc/config/DatabaseInitializer.java";
+
+    // ------------------------------------------------------------------
+    // 正常流:查询审计日志 → 溯源(SC-A-15 主流程)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:AdminAuditController 应挂载 /api/admin/audit 并提供 /list")
+    void adminAuditController_mountsAuditListEndpoint() {
+        Class<?> clazz = loadClass(ADMIN_AUDIT_CONTROLLER);
+        assertNotNull(clazz,
+                "[admin.audit.log] AdminAuditController 类不存在:契约要求 POST /api/admin/audit/list"
+                        + " 审计列表(§4.60.14)。");
+        // 类级 @RequestMapping("/api/admin/audit")
+        assertTrue(hasRequestMapping(clazz, "/api/admin/audit"),
+                "[admin.audit.log] AdminAuditController 应 @RequestMapping(\"/api/admin/audit\")(§4.60.14)。");
+        // 方法级 @PostMapping("/list")
+        Method list = findMethod(clazz, "list");
+        assertNotNull(list,
+                "[admin.audit.log] AdminAuditController 应提供 list(...) 方法作为 /list 端点(§4.60.14)。");
+        assertTrue(hasPostMapping(list, "/list"),
+                "[admin.audit.log] AdminAuditController.list 应 @PostMapping(\"/list\")(统一 POST,§4.13)。");
+    }
+
+    @Test
+    @DisplayName("正常流-2:AuditLogService 应提供分页查询 page(module,targetId,start,end,page,size)")
+    void auditLogService_hasPageQuery() {
+        Class<?> clazz = loadClass(AUDIT_LOG_SERVICE);
+        assertNotNull(clazz,
+                "[admin.audit.log] AuditLogService 类不存在:契约要求分页查询审计日志(SC-A-15 主流程)。");
+        // page(String, String, Date, Date, int, int) —— 6 参分页查询(module/targetId/时间范围/分页)
+        Method page = findMethodByParamCount(clazz, "page", 6);
+        assertNotNull(page,
+                "[admin.audit.log] AuditLogService 应提供 page(module, targetId, startTime, endTime,"
+                        + " pageNo, pageSize) 分页查询(SC-A-15 分页边界)。");
+        // record(AuditLog) 写入入口
+        Method record = findMethodByParamCount(clazz, "record", 1);
+        assertNotNull(record,
+                "[admin.audit.log] AuditLogService 应提供 record(AuditLog) 写入入口(W0.3 审计写入)。");
+    }
+
+    @Test
+    @DisplayName("正常流-3:AuditLog 实体应承载溯源字段(操作人/模块/动作/目标/前后值/IP/时间)")
+    void auditLogEntity_carriesTraceFields() {
+        Class<?> clazz = loadClass(AUDIT_LOG_ENTITY);
+        assertNotNull(clazz,
+                "[admin.audit.log] AuditLog 实体不存在:契约要求 audit_log 表实体承载溯源字段(SC-A-15)。");
+        String[] required = {
+                "operatorId", "operatorName", "module", "action", "targetType",
+                "targetId", "beforeValue", "afterValue", "ip", "createdAt"
+        };
+        List<String> missing = new ArrayList<>();
+        for (String name : required) {
+            if (!hasField(clazz, name)) {
+                missing.add(name);
+            }
+        }
+        assertTrue(missing.isEmpty(),
+                "[admin.audit.log] AuditLog 实体缺失溯源字段:" + missing
+                        + "(契约要求 operatorId/operatorName/module/action/targetType/"
+                        + "targetId/beforeValue/afterValue/ip/createdAt,SC-A-15 溯源)。");
+    }
+
+    @Test
+    @DisplayName("正常流-4:OperationLogController 应挂载 /api/admin/logs 并提供 operations+recent")
+    void operationLogController_mountsOperationsAndRecent() {
+        Class<?> clazz = loadClass(OPERATION_LOG_CONTROLLER);
+        assertNotNull(clazz,
+                "[admin.audit.log] OperationLogController 类不存在:契约要求 POST /api/admin/logs"
+                        + " operations + recent(§4.60.14)。");
+        assertTrue(hasRequestMapping(clazz, "/api/admin/logs"),
+                "[admin.audit.log] OperationLogController 应 @RequestMapping(\"/api/admin/logs\")(§4.60.14)。");
+        Method operations = findMethod(clazz, "queryLogs");
+        assertNotNull(operations,
+                "[admin.audit.log] OperationLogController 应提供 queryLogs(...) 作为 /operations 端点(§4.60.14)。");
+        assertTrue(hasPostMapping(operations, "/operations"),
+                "[admin.audit.log] OperationLogController.queryLogs 应 @PostMapping(\"/operations\")(§4.60.14)。");
+        Method recent = findMethod(clazz, "getRecentLogs");
+        assertNotNull(recent,
+                "[admin.audit.log] OperationLogController 应提供 getRecentLogs(...) 作为 /recent 端点(§4.60.14)。");
+        assertTrue(hasPostMapping(recent, "/recent"),
+                "[admin.audit.log] OperationLogController.getRecentLogs 应 @PostMapping(\"/recent\")(§4.60.14)。");
+    }
+
+    @Test
+    @DisplayName("正常流-5:UserOperationLogService 应提供 queryLogs + getRecentLogs 服务方法")
+    void userOperationLogService_hasQueryAndRecent() {
+        Class<?> clazz = loadClass(USER_OPERATION_LOG_SERVICE);
+        assertNotNull(clazz,
+                "[admin.audit.log] UserOperationLogService 类不存在:契约要求操作日志分页/近期查询(§4.60.14)。");
+        Method query = findMethod(clazz, "queryLogs");
+        assertNotNull(query,
+                "[admin.audit.log] UserOperationLogService 应提供 queryLogs(...)(/operations 分页查询)。");
+        Method recent = findMethod(clazz, "getRecentLogs");
+        assertNotNull(recent,
+                "[admin.audit.log] UserOperationLogService 应提供 getRecentLogs(...)(/recent 近期查询)。");
+    }
+
+    @Test
+    @DisplayName("正常流-6:AuditLogAspect 应存在(管理端写操作自动落审计)")
+    void auditLogAspect_existsForAutoAudit() {
+        Class<?> clazz = loadClass(AUDIT_LOG_ASPECT);
+        assertNotNull(clazz,
+                "[admin.audit.log] AuditLogAspect 类不存在:契约要求管理端写操作自动落 audit_log(W0.3)。");
+        // 切面应含 @Aspect 注解
+        assertTrue(hasAnnotation(clazz, "org.aspectj.lang.annotation.Aspect"),
+                "[admin.audit.log] AuditLogAspect 应标注 @Aspect(Spring AOP 自动审计切面)。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:日志不可篡改(只增不删改) + 建表落地
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:AuditLogService 不得暴露 update/remove/delete(日志不可篡改)")
+    void auditLogService_appendOnly_noUpdateDelete() {
+        Class<?> clazz = loadClass(AUDIT_LOG_SERVICE);
+        if (clazz == null) {
+            fail("[admin.audit.log] AuditLogService 类不存在,无法验证只增不删改(SC-A-15 边界)。");
+        }
+        List<String> exposed = new ArrayList<>();
+        for (Method m : clazz.getDeclaredMethods()) {
+            String n = m.getName().toLowerCase();
+            if (n.startsWith("update") || n.startsWith("remove") || n.startsWith("delete")) {
+                exposed.add(m.getName());
+            }
+        }
+        assertTrue(exposed.isEmpty(),
+                "[admin.audit.log] 日志不可篡改(SC-A-15 边界):AuditLogService 不得暴露 "
+                        + "update/remove/delete 方法(只增不删改),发现:" + exposed);
+    }
+
+    @Test
+    @DisplayName("边界流-2:audit_log 与 user_operation_logs 应落地 schema.sql")
+    void schemaSql_containsAuditTables() {
+        String schema = readSchemaSql();
+        assertTrue(schema.contains("audit_log"),
+                "[admin.audit.log] schema.sql 缺少 audit_log 建表(W0.3 审计日志统一)。");
+        assertTrue(schema.contains("user_operation_logs"),
+                "[admin.audit.log] schema.sql 缺少 user_operation_logs 建表(操作日志)。");
+        // 审计表应为只增不删改形态:含 created_at 时间列
+        assertTrue(schema.contains("created_at"),
+                "[admin.audit.log] audit_log 应含 created_at 时间列(溯源时间)。");
+    }
+
+    @Test
+    @DisplayName("边界流-3:DatabaseInitializer 应有 audit_log 建表迁移(schema.sql 与迁移一致)")
+    void databaseInitializer_hasAuditLogMigration() {
+        String source = readDatabaseInitializerSource();
+        assertTrue(source.contains("audit_log"),
+                "[admin.audit.log] DatabaseInitializer 缺少 audit_log 建表迁移(AGENTS 迁移一致原则:"
+                        + "schema.sql 的 DDL 必须在 DatabaseInitializer 有对应迁移)。");
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流:仅 admin 角色可访问(§4.13)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:AdminAuditController.list 首参应带 @RequestAttribute(\"role\") 做 admin 门禁")
+    void adminAuditController_list_hasRoleGate() {
+        Class<?> clazz = loadClass(ADMIN_AUDIT_CONTROLLER);
+        if (clazz == null) {
+            fail("[admin.audit.log] AdminAuditController 类不存在,无法验证 role 门禁(§4.13)。");
+        }
+        Method list = findMethod(clazz, "list");
+        if (list == null) {
+            fail("[admin.audit.log] AdminAuditController.list 不存在,无法验证 role 门禁(§4.13)。");
+        }
+        Annotation[][] paramAnns = list.getParameterAnnotations();
+        boolean hasRoleAttr = false;
+        for (Annotation[] anns : paramAnns) {
+            for (Annotation a : anns) {
+                if (a.annotationType().getName().equals("org.springframework.web.bind.annotation.RequestAttribute")) {
+                    hasRoleAttr = true;
+                    break;
+                }
+            }
+            if (hasRoleAttr) {
+                break;
+            }
+        }
+        assertTrue(hasRoleAttr,
+                "[admin.audit.log] AdminAuditController.list 应通过 @RequestAttribute(\"role\") 校验"
+                        + " role=admin(§4.13 管理员接口需 role=admin,AdminInterceptor 口径)。");
+    }
+
+    // ------------------------------------------------------------------
+    // 反射辅助(锚定宿主契约,避免编译期依赖不存在类)
+    // ------------------------------------------------------------------
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    private Method findMethod(Class<?> clazz, String name) {
+        if (clazz == null) {
+            return null;
+        }
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals(name)) {
+                return m;
+            }
+        }
+        return null;
+    }
+
+    private Method findMethodByParamCount(Class<?> clazz, String name, int paramCount) {
+        if (clazz == null) {
+            return null;
+        }
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals(name) && m.getParameterCount() == paramCount) {
+                return m;
+            }
+        }
+        return null;
+    }
+
+    private boolean hasField(Class<?> clazz, String name) {
+        if (clazz == null) {
+            return false;
+        }
+        for (Field f : clazz.getDeclaredFields()) {
+            if (f.getName().equals(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasAnnotation(Class<?> clazz, String annotationName) {
+        if (clazz == null) {
+            return false;
+        }
+        for (Annotation a : clazz.getDeclaredAnnotations()) {
+            if (a.annotationType().getName().equals(annotationName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** 类级 @RequestMapping 是否含给定路径片段。 */
+    private boolean hasRequestMapping(Class<?> clazz, String pathPart) {
+        if (clazz == null) {
+            return false;
+        }
+        for (Annotation a : clazz.getDeclaredAnnotations()) {
+            if (a.annotationType().getName().equals("org.springframework.web.bind.annotation.RequestMapping")) {
+                try {
+                    Method value = a.annotationType().getMethod("value");
+                    String[] paths = (String[]) value.invoke(a);
+                    for (String p : paths) {
+                        if (p.contains(pathPart)) {
+                            return true;
+                        }
+                    }
+                } catch (Exception ignored) {
+                    // 注解反射失败,继续
+                }
+            }
+        }
+        return false;
+    }
+
+    /** 方法级 @PostMapping 是否含给定路径。 */
+    private boolean hasPostMapping(Method method, String path) {
+        if (method == null) {
+            return false;
+        }
+        for (Annotation a : method.getDeclaredAnnotations()) {
+            if (a.annotationType().getName().equals("org.springframework.web.bind.annotation.PostMapping")) {
+                try {
+                    Method value = a.annotationType().getMethod("value");
+                    String[] paths = (String[]) value.invoke(a);
+                    for (String p : paths) {
+                        if (p.equals(path)) {
+                            return true;
+                        }
+                    }
+                } catch (Exception ignored) {
+                    // 注解反射失败,继续
+                }
+            }
+        }
+        return false;
+    }
+
+    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 "";
+        }
+    }
+
+    private String readDatabaseInitializerSource() {
+        // mvn test 工作目录为模块根(cfc-backend),相对路径可直接读取;若失败再回退 classpath。
+        try {
+            byte[] bytes = Files.readAllBytes(Paths.get(DATABASE_INITIALIZER_SOURCE));
+            return new String(bytes, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            try (InputStream is = getClass().getClassLoader()
+                    .getResourceAsStream("DatabaseInitializer.java")) {
+                if (is == null) {
+                    return "";
+                }
+                try (Scanner scanner = new Scanner(is, StandardCharsets.UTF_8.name())) {
+                    scanner.useDelimiter("\\A");
+                    return scanner.hasNext() ? scanner.next() : "";
+                }
+            } catch (Exception ignored) {
+                return "";
+            }
+        }
+    }
+}

+ 386 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/AdminKnowledgeBaseManageContractTest.java

@@ -0,0 +1,386 @@
+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.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;
+
+/**
+ * 契约级单测:管理端知识库管理(能力键 {@code admin.knowledgeBase.manage})。
+ *
+ * <p><b>契约来源(设计/需求溯源)</b>:
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md} <b>SC-A-08 知识库管理</b>(知识条目 CRUD → 向量同步;
+ *       边界:<b>同步失败重试;大批量分批</b>)与 <b>SC-A-22 精准营养知识库管理</b>(CRUD → 同步;边界:<b>同步失败重试</b>)。</li>
+ *   <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.60.8 —— 载体为
+ *       {@code KnowledgeBaseController} 系(非开工包猜测的 {@code AdminKnowledgeController}):
+ *       {@code /api/admin/knowledge-base}(list/get/save/delete/upload-file/fetch-url/bind-tags/bind-dimensions/backfill-tags)
+ *       + {@code KnowledgeBaseVectorController /api/admin/knowledge-base/vector}(stats/documents/sync/delete-source/update-document)
+ *       + {@code KnowledgeTagController /api/admin/knowledge-tag}(list/save/delete)
+ *       + {@code AdminUnifiedKnowledgeController /api/admin/knowledge-base/unified}(overview/section/update/reset)。</li>
+ *   <li>契约核对:{@code docs/superpowers/specs/2026-11-07-admin-ops-contract.md} §1.8/§1.22 —— SC-A-08 载体确认存在,
+ *       写入端点权限粒度「留待测试侧补权限流核验」。</li>
+ * </ul></p>
+ *
+ * <p><b>实现现状(src/main 实测)</b>:
+ * <ul>
+ *   <li>✅ <b>已落地</b>:四个 Controller(CRUD/向量/标签/统一知识库)与 {@code DanKnowledgeBaseService} /
+ *       {@code UnifiedKnowledgeBaseService} 均存在,schema.sql 含 {@code dan_knowledge_base} / {@code dan_knowledge_tag} /
+ *       {@code dan_knowledge_base_tag} / {@code dan_knowledge_base_dimension} / {@code knowledge_base_section} 建表。</li>
+ *   <li>❌ <b>未落地(契约缺口,RED)</b>:① 同步失败重试 —— {@code LanggraphSyncService}(已标 deprecated)与
+ *       {@code KnowledgeBaseVectorController} 同步路径无任何 retry/重试能力(无 {@code maxRetries}、无
+ *       {@code syncWithRetry}、无 {@code @Retryable});② 大批量分批 —— 同步/回填路径无任何 batch 分批能力
+ *       (无 {@code syncBatch}、无 {@code BATCH_SIZE});③ 知识库写入端点角色门禁 —— {@code KnowledgeBaseController} /
+ *       {@code KnowledgeBaseVectorController} 无控制器内平台角色校验({@code AdminUnifiedKnowledgeController} 已有
+ *       {@code allowed} 门禁,故该项仅对 base/vector 缺口红)。</li>
+ * </ul></p>
+ *
+ * <p><b>测试范围</b>:仅用反射 + schema.sql 类路径探测(无 Spring / 无 DB / 无外部服务),
+ * 与同批已合入主链的 {@code TeamProfitHookContractTest} 风格一致。</p>
+ *
+ * <p><b>RED 预期</b>:契约边界行为(同步失败重试 / 大批量分批)与 base/vector 角色门禁未落地,
+ * 对应用例在实现补齐前应失败(红);已落地的契约表面(CRUD/向量/标签/统一端点 + 统一知识库角色门禁)用例为绿,
+ * 作为「已存在」的登记证据。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者在同步路径补「失败重试」({@code maxRetries} 配置 / {@code syncWithRetry} /
+ * {@code @Retryable})、补「大批量分批」({@code syncBatch} / {@code BATCH_SIZE})、并为
+ * {@code KnowledgeBaseController} / {@code KnowledgeBaseVectorController} 补平台角色门禁后,
+ * 对应 RED 用例自动转绿。</p>
+ */
+class AdminKnowledgeBaseManageContractTest {
+
+    /** SC-A-08 载体 Controller 类(§4.60.8) */
+    private static final String KB_CONTROLLER = "com.etotem.cfc.controller.admin.KnowledgeBaseController";
+    private static final String KB_VECTOR_CONTROLLER = "com.etotem.cfc.controller.admin.KnowledgeBaseVectorController";
+    private static final String KB_TAG_CONTROLLER = "com.etotem.cfc.controller.admin.KnowledgeTagController";
+    private static final String KB_UNIFIED_CONTROLLER = "com.etotem.cfc.controller.admin.AdminUnifiedKnowledgeController";
+
+    /** SC-A-08/SC-A-22 Service 载体 */
+    private static final String KB_SERVICE = "com.etotem.cfc.service.DanKnowledgeBaseService";
+    private static final String UNIFIED_KB_SERVICE = "com.etotem.cfc.service.UnifiedKnowledgeBaseService";
+    private static final String SYNC_SERVICE = "com.etotem.cfc.service.LanggraphSyncService";
+
+    /** §4.60.8 端点子路径(Controller 内 {@code @PostMapping} 值) */
+    private static final String[] KB_CRUD_ENDPOINTS = {
+            "/list", "/get", "/save", "/delete", "/upload-file", "/fetch-url",
+            "/bind-tags", "/bind-dimensions", "/backfill-tags"
+    };
+    private static final String[] KB_VECTOR_ENDPOINTS = {
+            "/stats", "/documents", "/sync", "/delete-source", "/update-document"
+    };
+    private static final String[] KB_TAG_ENDPOINTS = {"/list", "/save", "/delete"};
+    private static final String[] KB_UNIFIED_ENDPOINTS = {"/overview", "/section", "/update", "/reset"};
+
+    /** schema.sql 应包含的知识库相关表 */
+    private static final String[] KB_TABLES = {
+            "dan_knowledge_base", "dan_knowledge_tag", "dan_knowledge_base_tag",
+            "dan_knowledge_base_dimension", "knowledge_base_section"
+    };
+
+    // ------------------------------------------------------------------
+    // 正常流:契约表面已落地(登记证据,GREEN)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:KnowledgeBaseController 存在并承载知识条目 CRUD 端点(§4.60.8)")
+    void knowledgeBaseController_surfaceExists() {
+        Class<?> c = loadClass(KB_CONTROLLER);
+        assertNotNull(c, "[admin.knowledgeBase.manage] KnowledgeBaseController 类不存在(§4.60.8)。");
+        assertEndpoints(c, KB_CRUD_ENDPOINTS, "KnowledgeBaseController", "/api/admin/knowledge-base");
+    }
+
+    @Test
+    @DisplayName("正常流-2:KnowledgeBaseVectorController 存在并承载向量库端点(§4.60.8)")
+    void knowledgeBaseVectorController_surfaceExists() {
+        Class<?> c = loadClass(KB_VECTOR_CONTROLLER);
+        assertNotNull(c, "[admin.knowledgeBase.manage] KnowledgeBaseVectorController 类不存在(§4.60.8)。");
+        assertEndpoints(c, KB_VECTOR_ENDPOINTS, "KnowledgeBaseVectorController", "/api/admin/knowledge-base/vector");
+    }
+
+    @Test
+    @DisplayName("正常流-3:KnowledgeTagController 存在并承载知识标签字典端点(§4.60.8)")
+    void knowledgeTagController_surfaceExists() {
+        Class<?> c = loadClass(KB_TAG_CONTROLLER);
+        assertNotNull(c, "[admin.knowledgeBase.manage] KnowledgeTagController 类不存在(§4.60.8)。");
+        assertEndpoints(c, KB_TAG_ENDPOINTS, "KnowledgeTagController", "/api/admin/knowledge-tag");
+    }
+
+    @Test
+    @DisplayName("正常流-4:AdminUnifiedKnowledgeController 存在并承载统一知识库端点(§4.60.8)")
+    void adminUnifiedKnowledgeController_surfaceExists() {
+        Class<?> c = loadClass(KB_UNIFIED_CONTROLLER);
+        assertNotNull(c, "[admin.knowledgeBase.manage] AdminUnifiedKnowledgeController 类不存在(§4.60.8)。");
+        assertEndpoints(c, KB_UNIFIED_ENDPOINTS, "AdminUnifiedKnowledgeController", "/api/admin/knowledge-base/unified");
+    }
+
+    @Test
+    @DisplayName("正常流-5:知识库 Service 载体存在(DanKnowledgeBaseService / UnifiedKnowledgeBaseService)")
+    void knowledgeServices_surfaceExists() {
+        Class<?> svc = loadClass(KB_SERVICE);
+        assertNotNull(svc, "[admin.knowledgeBase.manage] DanKnowledgeBaseService 类不存在。");
+        assertTrue(hasMethod(svc, "saveWithAssociations"), "[admin.knowledgeBase.manage] "
+                + "DanKnowledgeBaseService 应提供 saveWithAssociations(保存+关联标签/维度)。");
+        assertTrue(hasMethod(svc, "getWithAssociations"), "[admin.knowledgeBase.manage] "
+                + "DanKnowledgeBaseService 应提供 getWithAssociations(详情+关联)。");
+        assertTrue(hasMethod(svc, "backfillTags"), "[admin.knowledgeBase.manage] "
+                + "DanKnowledgeBaseService 应提供 backfillTags(标签/维度回填)。");
+
+        Class<?> uni = loadClass(UNIFIED_KB_SERVICE);
+        assertNotNull(uni, "[admin.knowledgeBase.manage] UnifiedKnowledgeBaseService 类不存在。");
+        assertTrue(hasMethod(uni, "getModuleOverviewDetailed"), "[admin.knowledgeBase.manage] "
+                + "UnifiedKnowledgeBaseService 应提供 getModuleOverviewDetailed(模块概览)。");
+        assertTrue(hasMethod(uni, "updateModule"), "[admin.knowledgeBase.manage] "
+                + "UnifiedKnowledgeBaseService 应提供 updateModule(写库+热重载)。");
+    }
+
+    @Test
+    @DisplayName("正常流-6:知识库相关表落地 schema.sql(§2.2~2.5 设计)")
+    void schemaSql_hasKnowledgeTables() {
+        String schema = readSchemaSql();
+        for (String table : KB_TABLES) {
+            assertTrue(schema.contains(table),
+                    "[admin.knowledgeBase.manage] schema.sql 缺少 " + table + " 建表。");
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:同步失败重试(契约 SC-A-08/A-22 要求,当前未实现 → RED)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:知识库同步路径应支持失败重试(SC-A-08/A-22 边界,未实现 → 红)")
+    void syncFailureRetry_syncPathShouldSupportRetry() {
+        // 契约 AC:知识条目 CRUD → 向量同步,边界「同步失败重试」。
+        // 探测同步路径(LanggraphSyncService / KnowledgeBaseVectorController / DanKnowledgeBaseService)
+        // 是否暴露重试能力:syncWithRetry 方法 / maxRetries、retryCount 字段 / @Retryable 注解。
+        assertTrue(hasRetryCapability(SYNC_SERVICE, KB_VECTOR_CONTROLLER, KB_SERVICE),
+                "[admin.knowledgeBase.manage] 同步路径无任何失败重试能力(syncWithRetry / maxRetries / @Retryable):"
+                        + "契约 SC-A-08/A-22 边界要求「同步失败重试」,当前未实现。");
+    }
+
+    @Test
+    @DisplayName("边界流-2:向量同步端点应接受重试配置参数(SC-A-08 边界,未实现 → 红)")
+    void syncFailureRetry_vectorSyncShouldAcceptRetryParam() {
+        Class<?> c = loadClass(KB_VECTOR_CONTROLLER);
+        if (c == null) {
+            fail("[admin.knowledgeBase.manage] KnowledgeBaseVectorController 类不存在,无法验证重试参数。");
+        }
+        Method sync = findMethod(c, "triggerSync");
+        if (sync == null) {
+            fail("[admin.knowledgeBase.manage] KnowledgeBaseVectorController 缺少 triggerSync(/sync)端点。");
+        }
+        // 契约要求同步可传重试/批量参数;现无参(triggerSync())→ 红
+        boolean hasRetryParam = sync.getParameterCount() > 0
+                && (sync.getParameterTypes()[0] == java.util.Map.class);
+        assertTrue(hasRetryParam,
+                "[admin.knowledgeBase.manage] /api/admin/knowledge-base/vector/sync 应接受重试配置参数"
+                        + "(如 maxRetries),支持同步失败重试(SC-A-08 边界);当前为无参 triggerSync()。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:大批量分批(契约 SC-A-08 要求,当前未实现 → RED)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-3:知识库同步/回填应支持大批量分批(SC-A-08 边界,未实现 → 红)")
+    void largeBatch_syncAndBackfillShouldSupportBatching() {
+        // 契约 AC:边界「大批量分批」。探测 DanKnowledgeBaseService / LanggraphSyncService 是否暴露
+        // 分批能力:syncBatch / batchSync 方法 或 BATCH_SIZE 常量(默认分批阈值)。
+        assertTrue(hasBatchCapability(KB_SERVICE, SYNC_SERVICE),
+                "[admin.knowledgeBase.manage] 同步/回填路径无任何分批能力(syncBatch / BATCH_SIZE):"
+                        + "契约 SC-A-08 边界要求「大批量分批」,当前未实现。");
+    }
+
+    @Test
+    @DisplayName("边界流-4:backfill-tags 应分批回填并返回批处理统计(SC-A-08 边界,未实现 → 红)")
+    void largeBatch_backfillShouldReturnBatchStats() {
+        Class<?> svc = loadClass(KB_SERVICE);
+        if (svc == null) {
+            fail("[admin.knowledgeBase.manage] DanKnowledgeBaseService 类不存在,无法验证分批回填。");
+        }
+        Method backfill = findMethod(svc, "backfillTags");
+        if (backfill == null) {
+            fail("[admin.knowledgeBase.manage] DanKnowledgeBaseService 缺少 backfillTags。");
+        }
+        // 契约要求回填按批处理并返回批统计(如 total / processed / batchSize);
+        // 现返回 Map 但无 batchSize 语义探测点(返回类型为 Map<String,Object>,无法静态断言)——
+        // 以是否存在 BATCH_SIZE 常量 / batchSize 字段作为分批哨兵。
+        boolean hasBatchSentinel = false;
+        for (Field f : svc.getDeclaredFields()) {
+            if (f.getName().equalsIgnoreCase("BATCH_SIZE") || f.getName().toLowerCase().contains("batch")) {
+                hasBatchSentinel = true;
+                break;
+            }
+        }
+        assertTrue(hasBatchSentinel,
+                "[admin.knowledgeBase.manage] DanKnowledgeBaseService 无 BATCH_SIZE/batch 分批哨兵:"
+                        + "契约 SC-A-08 边界要求「大批量分批」回填,当前未实现。");
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流:知识库管理应受平台角色门禁(SC-A-08 权限粒度)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流-1:统一知识库端点已实现平台角色门禁(allowed,已落地 → 绿)")
+    void permissionFlow_unifiedEnforcesRole() {
+        Class<?> c = loadClass(KB_UNIFIED_CONTROLLER);
+        if (c == null) {
+            fail("[admin.knowledgeBase.manage] AdminUnifiedKnowledgeController 类不存在。");
+        }
+        // 已落地:allowed(request) 门禁 + ALLOWED_ROLES 集合(admin/operator/finance/knowledge_admin/butler)
+        assertTrue(hasMethod(c, "allowed"),
+                "[admin.knowledgeBase.manage] AdminUnifiedKnowledgeController 应含 allowed(request) 角色门禁。");
+    }
+
+    @Test
+    @DisplayName("权限流-2:知识库 CRUD 写入端点应实现平台角色门禁(SC-A-08 权限粒度,未实现 → 红)")
+    void permissionFlow_kbWriteEndpointsShouldEnforceRole() {
+        Class<?> c = loadClass(KB_CONTROLLER);
+        if (c == null) {
+            fail("[admin.knowledgeBase.manage] KnowledgeBaseController 类不存在。");
+        }
+        // 契约(§4.60.8 + 2026-11-07-admin-ops-contract §3#5「写入端点权限粒度留待测试侧核验」):
+        // 知识库写入(save/delete/bind-tags/bind-dimensions/backfill-tags)应受平台/knowledge_admin 角色门禁,
+        // 实现方式为控制器内 @RequestAttribute("role") 校验或 allowed/roles 门禁。
+        boolean hasRoleGate = hasMethod(c, "allowed") || hasMethod(c, "roles")
+                || hasParameterAnnotation(c, "javax.servlet.http.HttpServletRequest");
+        assertTrue(hasRoleGate,
+                "[admin.knowledgeBase.manage] KnowledgeBaseController 写入端点无平台角色门禁"
+                        + "(无 allowed/roles 门禁、无 @RequestAttribute(\"role\")):"
+                        + "知识库写入权限粒度应核验(2026-11-07-admin-ops-contract §3#5)。");
+    }
+
+    @Test
+    @DisplayName("权限流-3:向量库端点应实现平台角色门禁(SC-A-08 权限粒度,未实现 → 红)")
+    void permissionFlow_vectorEndpointsShouldEnforceRole() {
+        Class<?> c = loadClass(KB_VECTOR_CONTROLLER);
+        if (c == null) {
+            fail("[admin.knowledgeBase.manage] KnowledgeBaseVectorController 类不存在。");
+        }
+        boolean hasRoleGate = hasMethod(c, "allowed") || hasMethod(c, "roles")
+                || hasParameterAnnotation(c, "javax.servlet.http.HttpServletRequest");
+        assertTrue(hasRoleGate,
+                "[admin.knowledgeBase.manage] KnowledgeBaseVectorController 无平台角色门禁"
+                        + "(向量库管理应受平台角色保护,与统一知识库一致)。");
+    }
+
+    // ------------------------------------------------------------------
+    // 反射辅助
+    // ------------------------------------------------------------------
+
+    private void assertEndpoints(Class<?> c, String[] endpoints, String label, String mappingPrefix) {
+        for (String ep : endpoints) {
+            boolean found = false;
+            for (Method m : c.getDeclaredMethods()) {
+                if (m.isAnnotationPresent(org.springframework.web.bind.annotation.PostMapping.class)) {
+                    org.springframework.web.bind.annotation.PostMapping pm =
+                            m.getAnnotation(org.springframework.web.bind.annotation.PostMapping.class);
+                    String[] vals = pm.value();
+                    for (String v : vals) {
+                        if (v.equals(ep)) {
+                            found = true;
+                            break;
+                        }
+                    }
+                }
+                if (found) break;
+            }
+            assertTrue(found,
+                    "[admin.knowledgeBase.manage] " + label + " 缺少端点 " + mappingPrefix + ep
+                            + "(§4.60.8)。");
+        }
+    }
+
+    private boolean hasRetryCapability(String... classNames) {
+        for (String cn : classNames) {
+            Class<?> c = loadClass(cn);
+            if (c == null) continue;
+            for (Method m : c.getDeclaredMethods()) {
+                if (m.getName().toLowerCase().contains("retry")) return true;
+            }
+            for (Field f : c.getDeclaredFields()) {
+                if (f.getName().toLowerCase().contains("retry")) return true;
+            }
+            if (hasAnnotationByName(c, "org.springframework.retry.annotation.Retryable")) return true;
+        }
+        return false;
+    }
+
+    private boolean hasBatchCapability(String... classNames) {
+        for (String cn : classNames) {
+            Class<?> c = loadClass(cn);
+            if (c == null) continue;
+            for (Method m : c.getDeclaredMethods()) {
+                if (m.getName().toLowerCase().contains("batch")) return true;
+            }
+            for (Field f : c.getDeclaredFields()) {
+                if (f.getName().equalsIgnoreCase("BATCH_SIZE")
+                        || f.getName().toLowerCase().contains("batch")) return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasMethod(Class<?> c, String name) {
+        for (Method m : c.getDeclaredMethods()) {
+            if (m.getName().equals(name)) return true;
+        }
+        return false;
+    }
+
+    private Method findMethod(Class<?> c, String name) {
+        for (Method m : c.getDeclaredMethods()) {
+            if (m.getName().equals(name)) return m;
+        }
+        return null;
+    }
+
+    private boolean hasParameterAnnotation(Class<?> c, String annotationName) {
+        for (Method m : c.getDeclaredMethods()) {
+            for (java.lang.annotation.Annotation[] paramAnns : m.getParameterAnnotations()) {
+                for (java.lang.annotation.Annotation a : paramAnns) {
+                    if (a.annotationType().getName().equals(annotationName)) return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean hasAnnotationByName(Class<?> c, String annotationName) {
+        for (java.lang.annotation.Annotation a : c.getDeclaredAnnotations()) {
+            if (a.annotationType().getName().equals(annotationName)) return true;
+        }
+        return false;
+    }
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    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 "";
+        }
+    }
+}

+ 616 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/AdminReportParserManageContractTest.java

@@ -0,0 +1,616 @@
+package com.etotem.cfc.unit;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.admin.ReportParseAdminController;
+import com.etotem.cfc.controller.admin.ReportParserAdminController;
+import com.etotem.cfc.entity.HealthReportDraft;
+import com.etotem.cfc.entity.Product;
+import com.etotem.cfc.entity.ReportFingerprint;
+import com.etotem.cfc.entity.ReportParserImport;
+import com.etotem.cfc.entity.ReportTypeRegistry;
+import com.etotem.cfc.entity.ReportUnknownCluster;
+import com.etotem.cfc.entity.ReportUnknownUpload;
+import com.etotem.cfc.mapper.HealthReportDraftMapper;
+import com.etotem.cfc.mapper.ProductMapper;
+import com.etotem.cfc.mapper.ReportFingerprintMapper;
+import com.etotem.cfc.mapper.ReportParserImportMapper;
+import com.etotem.cfc.mapper.ReportTemplateMapper;
+import com.etotem.cfc.mapper.ReportTypeRegistryMapper;
+import com.etotem.cfc.mapper.ReportUnknownClusterMapper;
+import com.etotem.cfc.mapper.ReportUnknownUploadMapper;
+import com.etotem.cfc.service.DanReportUploadService;
+import com.etotem.cfc.service.HealthReportDraftService;
+import com.etotem.cfc.service.ReportCollectService;
+import com.etotem.cfc.service.ReportImportService;
+import com.etotem.cfc.service.ReportScriptGeneratorService;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+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 admin.reportParser.manage},SC-A-02 报告解析中台·类型/聚类 +
+ * SC-A-16 报告解析自学习/未知类型标注)。
+ *
+ * <p>契约来源:
+ * <ul>
+ *   <li>API_REFERENCE.md §4.30 管理端-报告解析({@code /api/admin/report-parser/*}):types /
+ *       types/save / types/toggle / types/delete / fingerprints/list / fingerprints/save /
+ *       fingerprints/delete / imports / imports/import / imports/approve / imports/reject /
+ *       unknown-clusters / unknown-uploads / generate-type / dan/reparse;</li>
+ *   <li>API_REFERENCE.md §4.59.1 管理端-报告解析进度({@code /api/admin/report-parse/*}):list /
+ *       retry / backfill-food;</li>
+ *   <li>tests/test-cases/admin.md TC-A-05/06/07/17(正常流 + 异常流 + 权限流:非 admin 访问
+ *       {@code /api/admin/report-*} 统一 401/403,不泄露后台数据)。</li>
+ * </ul>
+ *
+ * <p>实现现状(contract 核对,2026-11-13):{@code ReportParserAdminController}(/api/admin/report-parser)
+ * 与 {@code ReportParseAdminController}(/api/admin/report-parse)均已落地于 origin/cfclub;
+ * 既有 {@code ReportParserAdminContractTest} 仅覆盖 generateType / listTypes 两个方法。本类补全
+ * 其余端点(saveType/toggleType/deleteType/fingerprints/imports/clusters/dan/reparse +
+ * report-parse list/retry/backfill-food)的正常流 / 异常流契约断言,并按 TR-7(B-2 契约核对)作为
+ * 测试侧三类流执行证据登记;不修改 src/main。</p>
+ *
+ * <p>风格:纯 Mockito + 反射注入(无 Spring / 无 DB / 无外部服务),对齐 TeamProfitHookContractTest、
+ * ReportParserAdminContractTest。权限流为说明性契约(admin 整族登录拦截由 JwtInterceptor
+ * LOGIN_REQUIRED_PATHS 的 {@code /api/admin/} 前缀 + controller 层 {@code @RequestAttribute("role")}
+ * 守卫构成,本类固定两个 Controller 的映射前缀归属 /api/admin 命名空间)。</p>
+ */
+class AdminReportParserManageContractTest {
+
+    private ReportParserAdminController parserController;
+    private ReportParseAdminController parseAdminController;
+
+    private ReportTypeRegistryMapper typeRegistryMapper;
+    private ReportFingerprintMapper fingerprintMapper;
+    private ReportUnknownClusterMapper unknownClusterMapper;
+    private ReportUnknownUploadMapper unknownUploadMapper;
+    private ReportParserImportMapper importMapper;
+    private ProductMapper productMapper;
+    private ReportImportService reportImportService;
+    private DanReportUploadService danReportUploadService;
+    private ReportTemplateMapper reportTemplateMapper;
+    private ReportScriptGeneratorService reportScriptGeneratorService;
+
+    private HealthReportDraftMapper draftMapper;
+    private HealthReportDraftService healthReportDraftService;
+    private ReportCollectService reportCollectService;
+
+    @BeforeAll
+    static void initMybatisPlusTableInfo() {
+        MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
+        TableInfoHelper.initTableInfo(assistant, ReportTypeRegistry.class);
+        TableInfoHelper.initTableInfo(assistant, ReportFingerprint.class);
+        TableInfoHelper.initTableInfo(assistant, ReportUnknownCluster.class);
+        TableInfoHelper.initTableInfo(assistant, ReportUnknownUpload.class);
+        TableInfoHelper.initTableInfo(assistant, ReportParserImport.class);
+        TableInfoHelper.initTableInfo(assistant, HealthReportDraft.class);
+        TableInfoHelper.initTableInfo(assistant, Product.class);
+    }
+
+    @BeforeEach
+    void setUp() throws Exception {
+        parserController = new ReportParserAdminController();
+        injectAll(parserController);
+        typeRegistryMapper = (ReportTypeRegistryMapper) readField(parserController, "typeRegistryMapper");
+        fingerprintMapper = (ReportFingerprintMapper) readField(parserController, "fingerprintMapper");
+        unknownClusterMapper = (ReportUnknownClusterMapper) readField(parserController, "unknownClusterMapper");
+        unknownUploadMapper = (ReportUnknownUploadMapper) readField(parserController, "unknownUploadMapper");
+        importMapper = (ReportParserImportMapper) readField(parserController, "importMapper");
+        productMapper = (ProductMapper) readField(parserController, "productMapper");
+        reportImportService = (ReportImportService) readField(parserController, "reportImportService");
+        danReportUploadService = (DanReportUploadService) readField(parserController, "danReportUploadService");
+        reportTemplateMapper = (ReportTemplateMapper) readField(parserController, "reportTemplateMapper");
+        reportScriptGeneratorService =
+                (ReportScriptGeneratorService) readField(parserController, "reportScriptGeneratorService");
+
+        parseAdminController = new ReportParseAdminController();
+        injectAll(parseAdminController);
+        draftMapper = (HealthReportDraftMapper) readField(parseAdminController, "draftMapper");
+        healthReportDraftService = (HealthReportDraftService) readField(parseAdminController, "healthReportDraftService");
+        reportCollectService = (ReportCollectService) readField(parseAdminController, "reportCollectService");
+    }
+
+    private static Object readField(Object target, String name) throws Exception {
+        Field f = target.getClass().getDeclaredField(name);
+        f.setAccessible(true);
+        return f.get(target);
+    }
+
+    private static void injectAll(Object target) throws Exception {
+        for (Field f : target.getClass().getDeclaredFields()) {
+            if (Modifier.isStatic(f.getModifiers()) || Modifier.isFinal(f.getModifiers())) {
+                continue;
+            }
+            f.setAccessible(true);
+            f.set(target, mock(f.getType()));
+        }
+    }
+
+    private ReportTypeRegistry newType(String typeId, String displayName) {
+        ReportTypeRegistry t = new ReportTypeRegistry();
+        t.setTypeId(typeId);
+        t.setDisplayName(displayName);
+        return t;
+    }
+
+    private Map<String, Object> params(Object... kv) {
+        Map<String, Object> p = new HashMap<>();
+        for (int i = 0; i < kv.length; i += 2) {
+            p.put((String) kv[i], kv[i + 1]);
+        }
+        return p;
+    }
+
+    // ==================== ReportParserAdminController · 类型 CRUD ====================
+
+    @Test
+    void 正常流_listTypes_分页返回类型列表并回填商品名() {
+        ReportTypeRegistry t = newType("gut_flora", "肠道菌群");
+        t.setId(1L);
+        t.setLinkedProductId(5L);
+        Page<ReportTypeRegistry> page = new Page<>(1, 20);
+        page.setRecords(Collections.singletonList(t));
+        page.setTotal(1);
+        when(typeRegistryMapper.selectPage(any(), any())).thenReturn(page);
+        Product p = new Product();
+        p.setId(5L);
+        p.setName("肠道菌群检测");
+        when(productMapper.selectBatchIds(any())).thenReturn(Collections.singletonList(p));
+
+        Result<Page<ReportTypeRegistry>> result = parserController.listTypes(params("page", 1, "size", 20));
+
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().getRecords().size());
+        assertEquals("gut_flora", result.getData().getRecords().get(0).getTypeId());
+        assertEquals("肠道菌群检测", result.getData().getRecords().get(0).getProductName(),
+                "linkedProductId 非空时应回填商品名(§4.30 types)");
+    }
+
+    @Test
+    void 正常流_saveType_新增插入并落默认值() {
+        ReportTypeRegistry t = newType("physical_exam", "体检报告");
+        when(typeRegistryMapper.selectCount(any())).thenReturn(0L);
+
+        Result<Void> result = parserController.saveType(t);
+
+        assertEquals(200, result.getCode());
+        ArgumentCaptor<ReportTypeRegistry> captor = ArgumentCaptor.forClass(ReportTypeRegistry.class);
+        verify(typeRegistryMapper).insert(captor.capture());
+        ReportTypeRegistry saved = captor.getValue();
+        assertTrue(saved.getIsActive(), "新增类型默认启用(isActive=true)");
+        assertEquals("prebuilt", saved.getSource());
+        assertEquals("approved", saved.getReviewStatus());
+        assertEquals("1.0", saved.getVersion());
+        assertEquals(10, saved.getMinConfidence());
+    }
+
+    @Test
+    void 正常流_saveType_已存在id走更新并允许清空商品链接() {
+        ReportTypeRegistry t = newType("gut_flora", "肠道菌群");
+        t.setId(9L);
+        t.setLinkedProductId(null);
+
+        Result<Void> result = parserController.saveType(t);
+
+        assertEquals(200, result.getCode());
+        verify(typeRegistryMapper).updateById(t);
+        verify(typeRegistryMapper).update(isNull(),
+                any(com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper.class));
+    }
+
+    @Test
+    void 异常流_saveType_typeId缺失_拒绝() {
+        ReportTypeRegistry t = newType("", "肠道菌群");
+        Result<Void> result = parserController.saveType(t);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("typeId"), "typeId 缺失应报错");
+        verify(typeRegistryMapper, never()).insert(any());
+    }
+
+    @Test
+    void 异常流_saveType_displayName缺失_拒绝() {
+        ReportTypeRegistry t = newType("gut_flora", "");
+        Result<Void> result = parserController.saveType(t);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("displayName"), "displayName 缺失应报错");
+    }
+
+    @Test
+    void 异常流_saveType_expectedIndicators非法JSON_拒绝() {
+        ReportTypeRegistry t = newType("gut_flora", "肠道菌群");
+        t.setExpectedIndicators("{不是数组}");
+        Result<Void> result = parserController.saveType(t);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("expectedIndicators"), "expectedIndicators 非法应报错");
+        verify(typeRegistryMapper, never()).insert(any());
+    }
+
+    @Test
+    void 异常流_saveType_重复typeId_拒绝不插入() {
+        ReportTypeRegistry t = newType("gut_flora", "肠道菌群");
+        when(typeRegistryMapper.selectCount(any())).thenReturn(1L);
+        Result<Void> result = parserController.saveType(t);
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("已存在"), "重复 typeId 应报错(TC-A-06:重复 typeId 拒绝)");
+        verify(typeRegistryMapper, never()).insert(any());
+    }
+
+    @Test
+    void 正常流_toggleType_停用报告类型() {
+        ReportTypeRegistry t = newType("gut_flora", "肠道菌群");
+        when(typeRegistryMapper.selectOne(any())).thenReturn(t);
+        Result<Void> result = parserController.toggleType(params("typeId", "gut_flora", "isActive", false));
+        assertEquals(200, result.getCode());
+        verify(typeRegistryMapper).updateById(t);
+        assertEquals(false, t.getIsActive(), "isActive=false 应停用类型");
+    }
+
+    @Test
+    void 异常流_toggleType_typeId缺失_拒绝() {
+        Result<Void> result = parserController.toggleType(params("isActive", false));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("typeId"));
+        verify(typeRegistryMapper, never()).updateById(any());
+    }
+
+    @Test
+    void 异常流_toggleType_类型不存在_拒绝() {
+        when(typeRegistryMapper.selectOne(any())).thenReturn(null);
+        Result<Void> result = parserController.toggleType(params("typeId", "nope"));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("不存在"));
+        verify(typeRegistryMapper, never()).updateById(any());
+    }
+
+    @Test
+    void 正常流_deleteType_删除类型并级联删除指纹() {
+        Result<Void> result = parserController.deleteType(params("typeId", "gut_flora"));
+        assertEquals(200, result.getCode());
+        verify(typeRegistryMapper).delete(any());
+        verify(fingerprintMapper).delete(any());
+    }
+
+    @Test
+    void 异常流_deleteType_typeId缺失_拒绝() {
+        Result<Void> result = parserController.deleteType(params());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("typeId"));
+        verify(typeRegistryMapper, never()).delete(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class));
+    }
+
+    // ==================== ReportParserAdminController · 指纹规则 ====================
+
+    @Test
+    void 正常流_listFingerprints_按typeId查询并按排序返回() {
+        ReportFingerprint fp = new ReportFingerprint();
+        fp.setId(1L);
+        fp.setTypeId("gut_flora");
+        fp.setPattern("菌群多样性");
+        when(fingerprintMapper.selectList(any())).thenReturn(Collections.singletonList(fp));
+
+        Result<List<ReportFingerprint>> result = parserController.listFingerprints(params("typeId", "gut_flora"));
+
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().size());
+        assertEquals("菌群多样性", result.getData().get(0).getPattern());
+        verify(fingerprintMapper).selectList(any());
+    }
+
+    @Test
+    void 异常流_listFingerprints_typeId缺失_拒绝() {
+        Result<List<ReportFingerprint>> result = parserController.listFingerprints(params());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("typeId"));
+    }
+
+    @Test
+    void 正常流_saveFingerprint_插入并落默认权重与排序() {
+        ReportFingerprint fp = new ReportFingerprint();
+        fp.setTypeId("gut_flora");
+        fp.setPattern("菌群多样性");
+
+        Result<Void> result = parserController.saveFingerprint(fp);
+
+        assertEquals(200, result.getCode());
+        ArgumentCaptor<ReportFingerprint> captor = ArgumentCaptor.forClass(ReportFingerprint.class);
+        verify(fingerprintMapper).insert(captor.capture());
+        ReportFingerprint saved = captor.getValue();
+        assertEquals(10, saved.getWeight(), "指纹默认权重 10");
+        assertEquals(false, saved.getIsExclusion(), "默认非排除规则");
+        assertEquals(0, saved.getSortOrder(), "默认排序 0");
+    }
+
+    @Test
+    void 异常流_saveFingerprint_typeId或pattern缺失_拒绝() {
+        Result<Void> result = parserController.saveFingerprint(new ReportFingerprint());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("typeId") && result.getMessage().contains("pattern"));
+        verify(fingerprintMapper, never()).insert(any());
+    }
+
+    @Test
+    void 正常流_deleteFingerprint_按id删除() {
+        Result<Void> result = parserController.deleteFingerprint(params("id", 7L));
+        assertEquals(200, result.getCode());
+        verify(fingerprintMapper).deleteById(7L);
+    }
+
+    @Test
+    void 异常流_deleteFingerprint_id缺失_拒绝() {
+        Result<Void> result = parserController.deleteFingerprint(params());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("id"));
+        verify(fingerprintMapper, never()).deleteById(anyLong());
+    }
+
+    // ==================== ReportParserAdminController · 导入审批链 ====================
+
+    @Test
+    void 正常流_listImports_分页返回导入记录() {
+        Page<ReportParserImport> page = new Page<>(1, 20);
+        ReportParserImport imp = new ReportParserImport();
+        imp.setId(1L);
+        imp.setPackageName("gut_flora_v2.json");
+        imp.setStatus("pending");
+        page.setRecords(Collections.singletonList(imp));
+        page.setTotal(1);
+        when(importMapper.selectPage(any(), any())).thenReturn(page);
+
+        Result<Page<ReportParserImport>> result = parserController.listImports(params("page", 1, "size", 20));
+
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().getRecords().size());
+        assertEquals("pending", result.getData().getRecords().get(0).getStatus());
+    }
+
+    @Test
+    void 正常流_uploadImport_委托导入服务解析导入包() {
+        ReportParserImport imp = new ReportParserImport();
+        imp.setId(1L);
+        when(reportImportService.importPackage(any(), any(), any())).thenReturn(Result.success(imp));
+
+        Result<ReportParserImport> result = parserController.uploadImport(
+                params("jsonContent", "{\"typeId\":\"gut_flora\"}", "fileName", "pkg.json", "userId", 3L));
+
+        assertEquals(200, result.getCode());
+        verify(reportImportService).importPackage(eq("pkg.json"), eq("{\"typeId\":\"gut_flora\"}"), eq(3L));
+    }
+
+    @Test
+    void 异常流_uploadImport_jsonContent缺失_拒绝() {
+        Result<ReportParserImport> result = parserController.uploadImport(params("fileName", "pkg.json"));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("jsonContent"));
+        verify(reportImportService, never()).importPackage(any(), any(), any());
+    }
+
+    @Test
+    void 正常流_approveImport_委托审批导入包() {
+        when(reportImportService.approveImport(anyLong(), any())).thenReturn(Result.success());
+        Result<Map<String, Object>> result = parserController.approveImport(params("id", 2L, "reviewerId", 9L));
+        assertEquals(200, result.getCode());
+        verify(reportImportService).approveImport(2L, 9L);
+    }
+
+    @Test
+    void 异常流_approveImport_id缺失_拒绝() {
+        Result<Map<String, Object>> result = parserController.approveImport(params("reviewerId", 9L));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("id"));
+        verify(reportImportService, never()).approveImport(anyLong(), any());
+    }
+
+    @Test
+    void 正常流_rejectImport_委托驳回导入包() {
+        when(reportImportService.rejectImport(anyLong(), any(), any())).thenReturn(Result.success());
+        Result<Void> result = parserController.rejectImport(params("id", 2L, "reviewerId", 9L, "note", "字段缺"));
+        assertEquals(200, result.getCode());
+        verify(reportImportService).rejectImport(2L, 9L, "字段缺");
+    }
+
+    @Test
+    void 异常流_rejectImport_id缺失_拒绝() {
+        Result<Void> result = parserController.rejectImport(params("note", "x"));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("id"));
+        verify(reportImportService, never()).rejectImport(anyLong(), any(), any());
+    }
+
+    // ==================== ReportParserAdminController · 未知聚类 ====================
+
+    @Test
+    void 正常流_listClusters_分页并按状态过滤() {
+        Page<ReportUnknownCluster> page = new Page<>(1, 20);
+        ReportUnknownCluster c = new ReportUnknownCluster();
+        c.setId(3L);
+        c.setClusterKey("City Hospital");
+        c.setStatus(ReportUnknownCluster.STATUS_READY);
+        c.setReportCount(12);
+        page.setRecords(Collections.singletonList(c));
+        page.setTotal(1);
+        when(unknownClusterMapper.selectPage(any(), any())).thenReturn(page);
+
+        Result<Page<ReportUnknownCluster>> result =
+                parserController.listClusters(params("page", 1, "size", 20, "status", "ready"));
+
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().getRecords().size());
+        assertEquals("ready", result.getData().getRecords().get(0).getStatus());
+    }
+
+    @Test
+    void 正常流_listUploads_按clusterId返回聚类上传记录() {
+        ReportUnknownUpload u = new ReportUnknownUpload();
+        u.setId(1L);
+        u.setClusterId(3L);
+        u.setFileHash("abc");
+        when(unknownUploadMapper.selectList(any())).thenReturn(Collections.singletonList(u));
+
+        Result<List<ReportUnknownUpload>> result = parserController.listUploads(params("clusterId", 3L));
+
+        assertEquals(200, result.getCode());
+        assertEquals(1, result.getData().size());
+        assertEquals("abc", result.getData().get(0).getFileHash());
+    }
+
+    @Test
+    void 异常流_listUploads_clusterId缺失_拒绝() {
+        Result<List<ReportUnknownUpload>> result = parserController.listUploads(params());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("clusterId"));
+    }
+
+    // ==================== ReportParserAdminController · DAN 重解析 ====================
+
+    @Test
+    void 正常流_reparseDan_委托DAN报告按维度重解析() {
+        Map<String, Object> reparse = new HashMap<>();
+        reparse.put("uploadId", 6L);
+        reparse.put("dimension", "learning");
+        reparse.put("parsed", true);
+        when(danReportUploadService.reparseUpload(anyLong(), any())).thenReturn(Result.success(reparse));
+
+        Result<Map<String, Object>> result =
+                parserController.reparseDan(params("uploadId", 6L, "dimension", "learning"));
+
+        assertEquals(200, result.getCode());
+        assertEquals(true, result.getData().get("parsed"));
+        verify(danReportUploadService).reparseUpload(6L, "learning");
+    }
+
+    @Test
+    void 异常流_reparseDan_uploadId或dimension缺失_拒绝() {
+        Result<Map<String, Object>> result = parserController.reparseDan(params("uploadId", 6L));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("uploadId") || result.getMessage().contains("dimension"));
+        verify(danReportUploadService, never()).reparseUpload(anyLong(), any());
+    }
+
+    // ==================== ReportParseAdminController · 解析进度管理 ====================
+
+    @Test
+    void 正常流_list_分页返回草稿解析进度记录字段集() {
+        HealthReportDraft d = new HealthReportDraft();
+        d.setId(11L);
+        d.setUserId(22L);
+        d.setReportType("gut_flora");
+        d.setOriginalFilename("child_report.pdf");
+        d.setFileHash("hash1");
+        d.setParseStatus("failed");
+        d.setParseMethod("langgraph");
+        d.setParseError("parse timeout");
+        d.setParseAttempt(2);
+        d.setStatus("drafting");
+        d.setConfirmedReportId(null);
+        d.setCreatedAt(new Date());
+        d.setUpdatedAt(new Date());
+        Page<HealthReportDraft> page = new Page<>(1, 20);
+        page.setRecords(Collections.singletonList(d));
+        page.setTotal(1);
+        when(draftMapper.selectPage(any(), any())).thenReturn(page);
+
+        Result<Map<String, Object>> result = parseAdminController.list(params("page", 1, "size", 20, "parseStatus", "failed"));
+
+        assertEquals(200, result.getCode());
+        assertEquals(1L, ((Number) result.getData().get("total")).longValue());
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> records = (List<Map<String, Object>>) result.getData().get("records");
+        assertEquals(1, records.size());
+        Map<String, Object> r = records.get(0);
+        assertEquals(11L, r.get("draftId"));
+        assertEquals("gut_flora", r.get("reportType"));
+        assertEquals("failed", r.get("parseStatus"));
+        assertEquals(2, r.get("parseAttempt"));
+        assertEquals("drafting", r.get("draftStatus"));
+        assertNull(r.get("confirmedReportId"));
+    }
+
+    @Test
+    void 正常流_retry_重置解析进度并重新提交异步采集() {
+        Result<Map<String, Object>> result = parseAdminController.retry(params("draftId", 11L));
+        assertEquals(200, result.getCode());
+        assertEquals(11L, result.getData().get("draftId"));
+        assertEquals("collecting", result.getData().get("parseStatus"));
+        verify(healthReportDraftService).resetForManualRetry(11L);
+        verify(reportCollectService).submitAsyncCollect(11L);
+    }
+
+    @Test
+    void 异常流_retry_draftId缺失_拒绝() {
+        Result<Map<String, Object>> result = parseAdminController.retry(params());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("draftId"));
+        verify(healthReportDraftService, never()).resetForManualRetry(anyLong());
+        verify(reportCollectService, never()).submitAsyncCollect(anyLong());
+    }
+
+    @Test
+    void 正常流_backfillFood_重新解析并覆盖写入食物清单() {
+        when(reportCollectService.reparseReportFoods(7L)).thenReturn(42);
+        Result<Map<String, Object>> result = parseAdminController.backfillFood(params("reportId", 7L));
+        assertEquals(200, result.getCode());
+        assertEquals(7L, result.getData().get("reportId"));
+        assertEquals(42, result.getData().get("foodCount"));
+        verify(reportCollectService).reparseReportFoods(7L);
+    }
+
+    @Test
+    void 异常流_backfillFood_reportId缺失_拒绝() {
+        Result<Map<String, Object>> result = parseAdminController.backfillFood(params());
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("reportId"));
+        verify(reportCollectService, never()).reparseReportFoods(anyLong());
+    }
+
+    @Test
+    void 异常流_backfillFood_服务非法状态_错误返回保留旧载荷() {
+        when(reportCollectService.reparseReportFoods(7L))
+                .thenThrow(new IllegalStateException("报告状态不允许回填"));
+        Result<Map<String, Object>> result = parseAdminController.backfillFood(params("reportId", 7L));
+        assertEquals(500, result.getCode());
+        assertTrue(result.getMessage().contains("不允许回填"),
+                "IllegalStateException 应转 Result.error(失败保留旧载荷,§4.59.1 backfill-food)");
+    }
+
+    // ==================== 权限流 ====================
+
+    @Test
+    void 权限流_管理端报告解析端点归属admin命名空间_由JwtInterceptor统一登录拦截() {
+        assertEquals("/api/admin/report-parser", parserController.getClass()
+                .getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class).value()[0],
+                "ReportParserAdminController 前缀归属 /api/admin 命名空间(JwtInterceptor LOGIN_REQUIRED_PATHS /api/admin/)");
+        assertEquals("/api/admin/report-parse", parseAdminController.getClass()
+                .getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class).value()[0],
+                "ReportParseAdminController 前缀归属 /api/admin 命名空间(JwtInterceptor LOGIN_REQUIRED_PATHS /api/admin/)");
+        // 说明性契约:无 token → 401(LOGIN_REQUIRED_PATHS "/api/admin/"),非 admin token → controller 层
+        // @RequestAttribute("role") 守卫 403(B-142-1 双层防线);本类不重复实现鉴权逻辑(TC-A-05/06/17-P)。
+        assertTrue(true, "权限流说明:admin.reportParser.manage 鉴权由 JwtInterceptor(/api/admin/) + controller role 守卫构成");
+    }
+}

+ 365 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/AdminSupplierManageContractTest.java

@@ -0,0 +1,365 @@
+package com.etotem.cfc.unit;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.RealNameBackfillService;
+import com.etotem.cfc.service.UserService;
+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.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.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 契约级单测(TDD-RED,S-cfc-200):能力键 {@code admin.supplier.manage}
+ * (平台运营·供应商管理 SC-A-06 + 商品服务商入驻申请 SC-S-01)。
+ *
+ * <p><b>需求/设计溯源</b>:
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md} <b>SC-A-06</b>(供应商管理:列表/审核/停用,
+ *       边界「有在售商品的供应商停用需先下架」)、<b>SC-S-01</b>(入驻申请:提交资质→平台审核→入驻成功,
+ *       边界「资质不全驳回;重复申请幂等」)、<b>SC-S-02</b>(商品管理上架/编辑)。</li>
+ *   <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} <b>§4.13</b> / <b>§4.60.6</b>——
+ *       <b>AdminSupplierController</b> {@code /api/admin/supplier}(list/save/update/delete)、
+ *       <b>EcomSupplierController</b> {@code /api/admin/ecom-supplier}(list/all/save/update/delete)、
+ *       <b>SupplyManageController</b> {@code /api/admin/supply-manage}(list/detail/create/update/delete)。</li>
+ *   <li>映射:{@code docs/tdd/orchestration/sc-key-map.md}——SC-A-06 / SC-S-01 → {@code admin.supplier.manage}
+ *       (S-cfc-200;AdminSupplier/EcomSupplier/SupplyManageController)。</li>
+ * </ul>
+ * SC-S-02(商品管理上架/编辑)已由合入主链的 {@code VendorProductManageScS02ContractTest}(ProductService 层)
+ * 覆盖,本类不重复,仅登记映射。</p>
+ *
+ * <p><b>实现现状(src/main 逐类核实)</b>:
+ * <ul>
+ *   <li>{@code AdminSupplierController} / {@code EcomSupplierController}:<b>410 废弃桩</b>——两个设计载体
+ *       仅返回 {@code Result.error(410, "该接口已废弃,请使用 /api/admin/supply-system/...")},
+ *       契约所需的供应商 CRUD 行为<b>未落地</b>(红)。API_REFERENCE §4.61 载体置换提示:活跃载体为
+ *       {@code SupplySystemController}/{@code SupplySystemService}。</li>
+ *   <li>{@code SupplyManageController}:已落地(list/detail/create/update/delete + 入驻校验/角色/层级),
+ *       为<b>功能载体</b>——本测试对其已实现行为保留绿证断言(实现现状证据)。</li>
+ *   <li>SC-A-06 边界「有在售商品的供应商停用需先下架」:<b>全库未实现</b>——{@code SupplySystemService.toggleStatus}
+ *       直接翻转 active/disabled,{@code SupplyManageController.update} 直接置 vendorStatus,二者均<b>无在售商品守卫</b>
+ *       (红)。</li>
+ * </ul></p>
+ *
+ * <p><b>测试风格</b>:与同批 RED 契约测试({@code TeamProfitAdminManageContractTest} 等)一致——纯反射/Mockito
+ * 锚定契约宿主,无 Spring 上下文 / 无 DB / 无外部服务。契约所需行为缺失处断言失败(红);已落地行为保留绿证断言。</p>
+ *
+ * <p><b>契约方法签名(GREEN 阶段据此落地)</b>:
+ * <ul>
+ *   <li>{@code AdminSupplierController /api/admin/supplier}:list/save/update/delete 须提供功能化供应商管理(返回 200,
+ *       非 410 废弃桩);</li>
+ *   <li>{@code EcomSupplierController /api/admin/ecom-supplier}:list/all/save/update/delete 同上;</li>
+ *   <li>SC-A-06 停用边界:停用(vendorStatus→disabled / toggle-status)前须校验供应商在售商品(on_shelf),
+ *       存在则拒绝并提示「请先下架在售商品」。</li>
+ * </ul></p>
+ */
+class AdminSupplierManageContractTest {
+
+    /** 设计载体:管理端供应商主体管理(§4.13 / §4.60.6,SC-A-06) */
+    private static final String ADMIN_SUPPLIER_CTRL =
+            "com.etotem.cfc.controller.admin.AdminSupplierController";
+
+    /** 设计载体:管理端电商供应商(§4.13 / §4.60.6,SC-A-06/SC-S-01) */
+    private static final String ECOM_SUPPLIER_CTRL =
+            "com.etotem.cfc.controller.admin.EcomSupplierController";
+
+    /** 设计载体:管理端供应商管理(§4.60.6,SC-A-06/SC-S-01)——已落地功能载体 */
+    private static final String SUPPLY_MANAGE_CTRL =
+            "com.etotem.cfc.controller.admin.SupplyManageController";
+
+    /** 活跃载体(API_REFERENCE §4.61 载体置换提示):供应体系服务 */
+    private static final String SUPPLY_SYSTEM_SERVICE =
+            "com.etotem.cfc.service.SupplySystemService";
+
+    // ==================== 契约宿主锚定(三设计载体均应存在) ====================
+
+    @Test
+    @DisplayName("锚定-1:AdminSupplierController(/api/admin/supplier)应存在(SC-A-06)")
+    void anchor_adminSupplierControllerExists() {
+        assertTrue(loadClass(ADMIN_SUPPLIER_CTRL) != null,
+                "[admin.supplier.manage] AdminSupplierController 不存在:契约要求 /api/admin/supplier 提供"
+                        + " list/save/update/delete 供应商主体管理(SC-A-06)。");
+    }
+
+    @Test
+    @DisplayName("锚定-2:EcomSupplierController(/api/admin/ecom-supplier)应存在(SC-A-06/SC-S-01)")
+    void anchor_ecomSupplierControllerExists() {
+        assertTrue(loadClass(ECOM_SUPPLIER_CTRL) != null,
+                "[admin.supplier.manage] EcomSupplierController 不存在:契约要求 /api/admin/ecom-supplier 提供"
+                        + " list/all/save/update/delete 电商供应商管理。");
+    }
+
+    @Test
+    @DisplayName("锚定-3:SupplyManageController(/api/admin/supply-manage)应存在(SC-A-06/SC-S-01)")
+    void anchor_supplyManageControllerExists() {
+        assertTrue(loadClass(SUPPLY_MANAGE_CTRL) != null,
+                "[admin.supplier.manage] SupplyManageController 不存在:契约要求 /api/admin/supply-manage 提供"
+                        + " list/detail/create/update/delete 供应商管理。");
+    }
+
+    // ==================== SC-A-06:AdminSupplierController 应提供功能化供应商 CRUD(红:当前为 410 废弃桩) ====================
+
+    @Test
+    @DisplayName("SC-A-06-R1:AdminSupplierController.list 应返回功能化供应商列表(200),当前为 410 废弃桩")
+    void adminSupplierList_functionalCrud() {
+        Object ctrl = contractHost(ADMIN_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "list", new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] AdminSupplierController.list 契约要求返回分页供应商列表(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage()
+                        + "(410 废弃桩,SC-A-06 供应商列表未在设计载体落地)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06-R2:AdminSupplierController.save 应创建供应商(200),当前为 410 废弃桩")
+    void adminSupplierSave_functionalCrud() {
+        Object ctrl = contractHost(ADMIN_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "save", new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] AdminSupplierController.save 契约要求创建供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06-R3:AdminSupplierController.update 应更新供应商(200),当前为 410 废弃桩")
+    void adminSupplierUpdate_functionalCrud() {
+        Object ctrl = contractHost(ADMIN_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "update", new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] AdminSupplierController.update 契约要求更新供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06-R4:AdminSupplierController.delete 应删除供应商(200),当前为 410 废弃桩")
+    void adminSupplierDelete_functionalCrud() {
+        Object ctrl = contractHost(ADMIN_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "delete", new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] AdminSupplierController.delete 契约要求删除供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    // ==================== SC-A-06/SC-S-01:EcomSupplierController 应提供功能化供应商 CRUD(红:当前为 410 废弃桩) ====================
+
+    @Test
+    @DisplayName("SC-A-06-R5:EcomSupplierController.list 应返回功能化供应商列表(200),当前为 410 废弃桩")
+    void ecomSupplierList_functionalCrud() {
+        Object ctrl = contractHost(ECOM_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "list", new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] EcomSupplierController.list 契约要求返回供应商列表(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06-R6:EcomSupplierController.all 应返回全量供应商下拉(200),当前为 410 废弃桩")
+    void ecomSupplierAll_functionalCrud() {
+        Object ctrl = contractHost(ECOM_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "all");
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] EcomSupplierController.all 契约要求返回全量供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    @Test
+    @DisplayName("SC-S-01-R1:EcomSupplierController.save 应创建供应商/入驻(200),当前为 410 废弃桩")
+    void ecomSupplierSave_functionalCrud() {
+        Object ctrl = contractHost(ECOM_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "save", (Object) new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] EcomSupplierController.save 契约要求创建供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06-R7:EcomSupplierController.update 应更新供应商(200),当前为 410 废弃桩")
+    void ecomSupplierUpdate_functionalCrud() {
+        Object ctrl = contractHost(ECOM_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "update", (Object) new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] EcomSupplierController.update 契约要求更新供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06-R8:EcomSupplierController.delete 应删除供应商(200),当前为 410 废弃桩")
+    void ecomSupplierDelete_functionalCrud() {
+        Object ctrl = contractHost(ECOM_SUPPLIER_CTRL);
+        Result<?> r = (Result<?>) call(ctrl, "delete", new HashMap<String, Object>());
+        assertEquals(200, r.getCode(),
+                "RED[admin.supplier.manage] EcomSupplierController.delete 契约要求删除供应商(200),"
+                        + "当前返回 " + r.getCode() + "/" + r.getMessage() + "(410 废弃桩)。");
+    }
+
+    // ==================== SC-A-06 停用边界:有在售商品的供应商停用需先下架(红:全库未实现守卫) ====================
+
+    @Test
+    @DisplayName("SC-A-06-R9:停用供应商前须校验无在售商品(「有在售商品的供应商停用需先下架」守卫缺失)")
+    void supplierDisable_boundary_guardOnSaleProducts() {
+        // 边界(应用场景总览 SC-A-06):有在售商品的供应商停用需先下架。
+        // 停用入口:SupplyManageController.update(vendorStatus→disabled) 与 SupplySystemService.toggleStatus。
+        // 契约:停用前必须校验供应商在售商品(on_shelf),存在则拒绝并提示「请先下架在售商品」。
+        Object manage = contractHost(SUPPLY_MANAGE_CTRL);
+        boolean manageGuard = hasField(manage.getClass(), "productMapper")
+                || hasContractMethod(manage, "hasOnShelfProducts", Long.class);
+        Object sysSvc = contractHost(SUPPLY_SYSTEM_SERVICE);
+        boolean sysGuard = hasContractMethod(sysSvc, "hasOnShelfProducts", Long.class);
+        assertTrue(manageGuard || sysGuard,
+                "RED[admin.supplier.manage] SC-A-06 边界「有在售商品的供应商停用需先下架」未实现:"
+                        + "停用入口(SupplyManageController.update / SupplySystemService.toggleStatus)直接翻转状态,"
+                        + "无在售商品守卫 —— GREEN 阶段须在停用前校验该供应商 on_shelf 商品,存在则拒绝并提示先下架。");
+    }
+
+    // ==================== SC-S-01 入驻申请:已落地功能载体保留绿证断言(实现现状证据) ====================
+
+    @Test
+    @DisplayName("SC-S-01-1:入驻 create 手机号缺失应驳回(资质校验,功能载体已落地)")
+    void supplyManageCreate_phoneRequired_validation() throws Exception {
+        SupplyManageControllerFixture fx = supplyManageFixture(mock(UserMapper.class));
+        Map<String, Object> params = new HashMap<>();
+        params.put("nickname", "新供应商");
+        Result<?> r = (Result<?>) call(fx.ctrl, "create", params);
+        assertEquals(500, r.getCode(), "入驻申请手机号缺失须驳回(资质不全驳回边界)");
+        assertTrue(r.getMessage() != null && r.getMessage().contains("手机号不能为空"),
+                "入驻申请手机号缺失应提示「手机号不能为空」,实际: " + r.getMessage());
+    }
+
+    @Test
+    @DisplayName("SC-S-01-2:入驻 create 默认审核通过(vendorStatus=approved + supplier_admin 角色,功能载体已落地)")
+    void supplyManageCreate_createsApprovedSupplier() throws Exception {
+        UserMapper userMapper = mock(UserMapper.class);
+        when(userMapper.selectOne(any(Wrapper.class))).thenReturn(null);
+        when(userMapper.insert(any(User.class))).thenAnswer(inv -> {
+            ((User) inv.getArgument(0)).setId(88L);
+            return 1;
+        });
+        SupplyManageControllerFixture fx = supplyManageFixture(userMapper);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("phone", "13800138000");
+        Result<?> r = (Result<?>) call(fx.ctrl, "create", params);
+
+        assertEquals(200, r.getCode(), "入驻 create 应成功建档");
+        User created = (User) r.getData();
+        assertEquals("approved", created.getVendorStatus(), "入驻默认审核通过(SC-S-01:平台审核→入驻成功)");
+        assertEquals("supplier_admin", created.getRole(), "入驻用户应具备供应商管理角色");
+        verify(userMapper).selectOne(any(Wrapper.class));
+        verify(userMapper).insert(any(User.class));
+    }
+
+    @Test
+    @DisplayName("SC-A-06-3:detail 不存在的供应商应返回「供应商不存在」(功能载体已落地)")
+    void supplyManageDetail_supplierNotFound() throws Exception {
+        UserMapper userMapper = mock(UserMapper.class);
+        when(userMapper.selectById(999L)).thenReturn(null);
+        SupplyManageControllerFixture fx = supplyManageFixture(userMapper);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("id", 999L);
+        Result<?> r = (Result<?>) call(fx.ctrl, "detail", params);
+
+        assertEquals(500, r.getCode(), "不存在的供应商须返回错误");
+        assertTrue(r.getMessage() != null && r.getMessage().contains("供应商不存在"),
+                "不存在的供应商应提示「供应商不存在」,实际: " + r.getMessage());
+    }
+
+    // ==================== 反射辅助(锚定契约宿主,避免编译期依赖未落地类) ====================
+
+    private static final class SupplyManageControllerFixture {
+        final Object ctrl;
+        SupplyManageControllerFixture(Object ctrl) { this.ctrl = ctrl; }
+    }
+
+    private SupplyManageControllerFixture supplyManageFixture(UserMapper userMapper) throws Exception {
+        Object ctrl = Class.forName(SUPPLY_MANAGE_CTRL).getDeclaredConstructor().newInstance();
+        inject(ctrl, "userMapper", userMapper);
+        inject(ctrl, "userService", mock(UserService.class));
+        inject(ctrl, "realNameBackfillService", mock(RealNameBackfillService.class));
+        return new SupplyManageControllerFixture(ctrl);
+    }
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    private boolean hasField(Class<?> clazz, String field) {
+        for (Field f : clazz.getDeclaredFields()) {
+            if (f.getName().equals(field)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasContractMethod(Object host, String method, Class<?>... paramTypes) {
+        for (Method m : host.getClass().getMethods()) {
+            if (m.getName().equals(method) && Arrays.equals(m.getParameterTypes(), paramTypes)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** 实例化契约宿主;宿主缺失即断言失败(红)。 */
+    private Object contractHost(String fqcn) {
+        try {
+            Class<?> clazz = Class.forName(fqcn);
+            return clazz.getDeclaredConstructor().newInstance();
+        } catch (ClassNotFoundException e) {
+            fail("RED[admin.supplier.manage] 契约宿主 " + simpleName(fqcn) + " 未实现(实现未落地):"
+                    + e.getMessage());
+            throw new IllegalStateException("unreachable");
+        } catch (Exception e) {
+            fail("契约宿主 " + fqcn + " 实例化失败:" + e.getMessage());
+            throw new IllegalStateException("unreachable");
+        }
+    }
+
+    private void inject(Object target, String field, Object value) throws Exception {
+        Field f = target.getClass().getDeclaredField(field);
+        f.setAccessible(true);
+        f.set(target, value);
+    }
+
+    /** 按方法名 + 参数个数调用(宽松适配基本/包装类型);宿主或方法缺失即断言失败。 */
+    private Object call(Object host, String method, Object... args) {
+        for (Method m : host.getClass().getMethods()) {
+            if (m.getName().equals(method) && m.getParameterCount() == args.length) {
+                try {
+                    return m.invoke(host, args);
+                } catch (Exception e) {
+                    fail("契约方法 " + simpleName(host.getClass().getName()) + "#" + method
+                            + " 调用失败:" + e.getMessage());
+                    throw new IllegalStateException("unreachable");
+                }
+            }
+        }
+        fail("RED[admin.supplier.manage] 契约方法 " + simpleName(host.getClass().getName()) + "#" + method
+                + " 未实现");
+        throw new IllegalStateException("unreachable");
+    }
+
+    private String simpleName(String fqcn) {
+        int idx = fqcn.lastIndexOf('.');
+        return idx >= 0 ? fqcn.substring(idx + 1) : fqcn;
+    }
+}

+ 365 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/AdminUserGovernContractTest.java

@@ -0,0 +1,365 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.DanAssessmentResultMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.HealthReportMapper;
+import com.etotem.cfc.mapper.TaskMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.AccountCleanupService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Scanner;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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.anyLong;
+import static org.mockito.Mockito.when;
+
+/**
+ * 契约级单测:管理端用户/家庭查询治理 + 账号注销/数据导出工单处理(能力键 {@code admin.user.govern})。
+ *
+ * <p><b>契约锚点</b></p>
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-A-11(查用户/家庭 → 代操作(限授权),
+ *       边界:<b>代操作留审计</b>)、SC-A-19(接工单 → 执行归档/导出,边界:<b>审计留痕;冷静期</b>)</li>
+ *   <li>用例:{@code tests/test-cases/admin.md} TC-A-20(注销/导出工单按序处理、越权 403)、
+ *       TC-A-N09(仅工单授权用户可导出;<b>数据脱敏打包</b>;操作留痕审计)</li>
+ *   <li>设计:API_REFERENCE §4.13 {@code /api/admin/*};AdminController(
+ *       {@code users/create/update/reset-password/delete/search/user/search})、
+ *       UserController({@code /api/user/data-export})、AdminFamilyOverviewController(家庭列表治理)</li>
+ * </ul>
+ *
+ * <p><b>实现现状核查(src/main,f941b9212 基线)</b></p>
+ * <ul>
+ *   <li>已落地(本测试相应断言为绿):{@code /users/create}、{@code /users/{id}/update}、
+ *       {@code /users/{id}/reset-password}、{@code /users/{id}/delete}、{@code /user/search}、
+ *       {@code /families}(B-5 行级隔离)、{@code /api/user/data-export},及 role/roles 门禁参数。</li>
+ *   <li>未落地(本测试相应断言为红,即 TDD-RED 契约缺口):
+ *       <ol>
+ *         <li>{@code /users/search} 路由缺失(目前仅 {@code /user/search},契约要求两者并存)</li>
+ *         <li>{@code AuditLogAspect} 仅拦截 {@code AdminCf*} + {@code AdminServiceConfigController},
+ *             未覆盖 {@code AdminController} 用户/家庭治理写操作 → 代操作<b>无审计留痕</b></li>
+ *         <li>{@code AccountCleanupService.exportUserData} 直接回传原始 phone/realName → 导出<b>未脱敏</b></li>
+ *         <li>{@code AccountCleanupService.deactivateAccount} 直接匿名化 → 注销<b>无冷静期</b></li>
+ *         <li>GDPR 工单(entity/表/service)整体未落地 → SC-A-19「接工单 → 执行归档/导出」不可达</li>
+ *       </ol>
+ *   </li>
+ * </ul>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者补齐上述 5 项缺口后,本测试自动转绿;届时可将结构探测
+ * 升级为行为级 mock 验证(如 verify auditLogService.persist(...))。</p>
+ *
+ * <p><b>测试基建</b>:纯反射结构契约 + Mockito 行为契约,无 Spring 上下文 / 无 DB / 无外部服务;
+ * 失败断言原文清晰可贴,可编译、可运行、预期红。</p>
+ */
+class AdminUserGovernContractTest {
+
+    /** 设计锚点:管理端用户治理控制器(API_REFERENCE §4.13) */
+    private static final String ADMIN_CONTROLLER = "com.etotem.cfc.controller.admin.AdminController";
+
+    /** 设计锚点:数据导出控制器(/api/user/data-export) */
+    private static final String USER_CONTROLLER = "com.etotem.cfc.controller.UserController";
+
+    /** SC-A-11 边界:代操作审计切面 */
+    private static final String AUDIT_LOG_ASPECT = "com.etotem.cfc.aspect.AuditLogAspect";
+
+    /** SC-A-19 边界:注销冷静期所在服务 */
+    private static final String ACCOUNT_CLEANUP_SERVICE = "com.etotem.cfc.service.AccountCleanupService";
+
+    /** SC-A-19 / TC-A-20:GDPR 工单实体(契约命名,GREEN 阶段创建) */
+    private static final String GDPR_WORK_ORDER_ENTITY = "com.etotem.cfc.entity.GdprWorkOrder";
+
+    /** SC-A-19 / TC-A-20:GDPR 工单处理服务(契约命名,GREEN 阶段创建) */
+    private static final String GDPR_WORK_ORDER_SERVICE = "com.etotem.cfc.service.GdprWorkOrderService";
+
+    /** SC-A-19:工单表名(schema.sql 应含建表) */
+    private static final String WORK_ORDER_TABLE = "work_order";
+
+    /** 已落地路由(设计锚点 user 治理面的 6 条 + 导出 + 家庭列表治理) */
+    private static final String[] EXISTING_GOVERN_ROUTES = {
+            "/users/create",
+            "/users/{id}/update",
+            "/users/{id}/reset-password",
+            "/users/{id}/delete",
+            "/user/search",
+            "/families"
+    };
+
+    // ------------------------------------------------------------------
+    // Mockito 行为契约:AccountCleanupService.exportUserData 脱敏
+    // ------------------------------------------------------------------
+
+    @Mock
+    private UserMapper userMapper;
+
+    @Mock
+    private FamilyMemberMapper familyMemberMapper;
+
+    @Mock
+    private DanAssessmentResultMapper danAssessmentResultMapper;
+
+    @Mock
+    private HealthReportMapper healthReportMapper;
+
+    @Mock
+    private TaskMapper taskMapper;
+
+    @InjectMocks
+    private AccountCleanupService accountCleanupService;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+    }
+
+    // ------------------------------------------------------------------
+    // 正常流:管理端用户/家庭治理路由契约(API_REFERENCE §4.13 + 设计锚点)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:用户治理既有路由与数据导出路由应存在")
+    void adminUserGovern_existingRoutesPresent() {
+        Class<?> admin = loadClass(ADMIN_CONTROLLER);
+        Class<?> user = loadClass(USER_CONTROLLER);
+        assertNotNull(admin, "[admin.user.govern] AdminController 类不存在(API_REFERENCE §4.13)。");
+        assertNotNull(user, "[admin.user.govern] UserController 类不存在(/api/user/data-export)。");
+        for (String route : EXISTING_GOVERN_ROUTES) {
+            assertTrue(hasPostRoute(admin, route),
+                    "[admin.user.govern] AdminController 缺少契约路由 " + route + "(API_REFERENCE §4.13)。");
+        }
+        assertTrue(hasPostRoute(user, "/data-export"),
+                "[admin.user.govern] UserController 缺少 /data-export(数据导出,SC-A-19)。");
+    }
+
+    @Test
+    @DisplayName("正常流-2:/api/admin/users/search 路由应存在(设计要求 users/search 与 user/search 并存)")
+    void adminUserGovern_usersSearchRoutePresent() {
+        Class<?> admin = loadClass(ADMIN_CONTROLLER);
+        assertNotNull(admin, "[admin.user.govern] AdminController 类不存在(API_REFERENCE §4.13)。");
+        assertTrue(hasPostRoute(admin, "/users/search"),
+                "[admin.user.govern] 契约设计锚点列出 users/create/update/reset-password/delete/"
+                        + "search/user/search 7 条治理路由,当前仅 /user/search 已实现,"
+                        + "/users/search 路由缺失,无法执行「按关键字查用户」治理入口。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:代操作留审计(SC-A-11 边界)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:用户/家庭治理代操作必须落审计留痕(SC-A-11 边界)")
+    void adminUserGovern_writesAudited() {
+        Class<?> aspect = loadClass(AUDIT_LOG_ASPECT);
+        assertNotNull(aspect, "[admin.user.govern] AuditLogAspect 类不存在,审计基建缺失。");
+        String expr = pointcutExpression(aspect, "cfAdminController");
+        assertTrue(expr != null && expr.contains("AdminController"),
+                "[admin.user.govern] 代操作必须留审计(SC-A-11 边界):AuditLogAspect#cfAdminController "
+                        + "pointcut 当前为「" + expr + "」,"
+                        + "未覆盖 AdminController 的 users/create|update|reset-password|delete|families 等"
+                        + "用户/家庭治理写操作 → 后台代操作无审计留痕。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:数据导出脱敏打包(TC-A-N09)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-2:数据导出必须脱敏打包(TC-A-N09)")
+    void exportUserData_masksSensitiveFields() {
+        User u = new User();
+        u.setId(12345L);
+        u.setNickname("契约测试用户");
+        u.setPhone("13812345678");
+        u.setRealName("张三");
+        u.setGender("male");
+        when(userMapper.selectById(anyLong())).thenReturn(u);
+        when(familyMemberMapper.selectList(any())).thenReturn(Collections.emptyList());
+        when(danAssessmentResultMapper.selectList(any())).thenReturn(Collections.emptyList());
+        when(healthReportMapper.selectList(any())).thenReturn(Collections.emptyList());
+        when(taskMapper.selectList(any())).thenReturn(Collections.emptyList());
+
+        Map<String, Object> data = accountCleanupService.exportUserData(12345L);
+        @SuppressWarnings("unchecked")
+        Map<String, Object> userInfo = (Map<String, Object>) data.get("userInfo");
+        assertNotNull(userInfo, "[admin.user.govern] 导出应含 userInfo 块(数据导出,SC-A-19)。");
+        String phone = String.valueOf(userInfo.get("phone"));
+        String realName = String.valueOf(userInfo.get("realName"));
+        assertFalse("13812345678".equals(phone) || "张三".equals(realName),
+                "[admin.user.govern] 数据导出必须脱敏打包(TC-A-N09:仅工单授权用户可导出;"
+                        + "数据脱敏打包):当前 exportUserData 直接回传原始 phone/realName,"
+                        + "手机号/真实姓名未经脱敏即落入导出包。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:注销冷静期(SC-A-19 边界)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-3:账号注销必须含冷静期(SC-A-19 边界)")
+    void accountCancellation_hasCoolingOffPeriod() {
+        Class<?> svc = loadClass(ACCOUNT_CLEANUP_SERVICE);
+        assertNotNull(svc, "[admin.user.govern] AccountCleanupService 类不存在。");
+        boolean hasCooling = false;
+        for (Field f : svc.getDeclaredFields()) {
+            if (f.getName().toLowerCase().contains("cooling")) {
+                hasCooling = true;
+            }
+        }
+        for (Method m : svc.getDeclaredMethods()) {
+            if (m.getName().toLowerCase().contains("cooling")) {
+                hasCooling = true;
+            }
+        }
+        assertTrue(hasCooling,
+                "[admin.user.govern] 账号注销必须含冷静期(SC-A-19 边界:审计留痕;冷静期):"
+                        + "当前 deactivateAccount 检查未完成订单后直接匿名化,无冷静期/确认环节,"
+                        + "用户无撤销机会。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:GDPR 工单处理(SC-A-19 / TC-A-20 / TC-A-N09)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-4:注销/导出工单实体应存在(SC-A-19)")
+    void gdprWorkOrder_entityExists() {
+        assertNotNull(loadClass(GDPR_WORK_ORDER_ENTITY),
+                "[admin.user.govern] GDPR 工单实体缺失(SC-A-19:接工单 → 执行归档/导出):"
+                        + "当前无工单承载「注销/导出申请 → 审批/执行」状态机,"
+                        + "TC-A-20 的按序执行/越权处理无从做起。");
+    }
+
+    @Test
+    @DisplayName("边界流-5:工单表应落地 schema.sql(SC-A-19)")
+    void gdprWorkOrder_schemaTableExists() {
+        String schema = readSchemaSql();
+        assertTrue(schema.contains(WORK_ORDER_TABLE),
+                "[admin.user.govern] schema.sql 缺少 " + WORK_ORDER_TABLE
+                        + " 建表(SC-A-19):注销/导出工单需持久化(含操作者/目标用户/类型/状态/"
+                        + "审计时间戳),当前无对应表。");
+    }
+
+    @Test
+    @DisplayName("边界流-6:工单处理服务应存在(SC-A-19 / TC-A-20)")
+    void gdprWorkOrder_serviceExists() {
+        assertNotNull(loadClass(GDPR_WORK_ORDER_SERVICE),
+                "[admin.user.govern] GDPR 工单处理服务缺失(SC-A-19:执行归档/导出):"
+                        + "当前账号注销/导出由用户自助接口直接执行,无运营端「接单 → 归档/导出」"
+                        + "工单处理链路,TC-A-20(并发按序、越权 403)无对应实现。");
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流:限授权(SC-A-11 流程)——既有路由已具备 role/roles 门禁参数
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:治理写操作应携带 role/roles 门禁参数(SC-A-11 限授权)")
+    void governanceEndpoints_carryRoleGate() {
+        Class<?> admin = loadClass(ADMIN_CONTROLLER);
+        assertNotNull(admin, "[admin.user.govern] AdminController 类不存在。");
+        for (String route : EXISTING_GOVERN_ROUTES) {
+            Method m = findPostMethod(admin, route);
+            assertTrue(m != null && hasRoleGateParam(m),
+                    "[admin.user.govern] " + route
+                            + " 应声明 role/roles 的 @RequestAttribute 门禁参数,拦截非后台角色"
+                            + "(SC-A-11 限授权)。");
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // 反射辅助
+    // ------------------------------------------------------------------
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    /** 方法级 @PostMapping 是否声明了指定路径 */
+    private boolean hasPostRoute(Class<?> clazz, String path) {
+        return findPostMethod(clazz, path) != null;
+    }
+
+    private Method findPostMethod(Class<?> clazz, String path) {
+        for (Method m : clazz.getDeclaredMethods()) {
+            PostMapping pm = m.getAnnotation(PostMapping.class);
+            if (pm == null) {
+                continue;
+            }
+            for (String v : pm.value()) {
+                if (path.equals(v)) {
+                    return m;
+                }
+            }
+        }
+        return null;
+    }
+
+    /** 方法参数是否声明了 role 或 roles 的 @RequestAttribute 门禁 */
+    private boolean hasRoleGateParam(Method m) {
+        for (Parameter p : m.getParameters()) {
+            for (Annotation a : p.getAnnotations()) {
+                if (a instanceof RequestAttribute) {
+                    String name = ((RequestAttribute) a).value();
+                    if ("role".equals(name) || "roles".equals(name)) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /** 反射读取 @Pointcut 注解的 value(不静态依赖 aspectj 类型) */
+    private String pointcutExpression(Class<?> clazz, String methodName) {
+        try {
+            Method m = clazz.getDeclaredMethod(methodName);
+            for (Annotation a : m.getDeclaredAnnotations()) {
+                if ("Pointcut".equals(a.annotationType().getSimpleName())) {
+                    Method value = a.annotationType().getMethod("value");
+                    Object v = value.invoke(a);
+                    return v != null ? String.valueOf(v) : "";
+                }
+            }
+        } catch (Exception e) {
+            return null;
+        }
+        return null;
+    }
+
+    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 "";
+        }
+    }
+}

+ 419 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/DimensionConfigAdminContractTest.java

@@ -0,0 +1,419 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.admin.AdminEnergyConfigController;
+import com.etotem.cfc.controller.admin.AdminEnergyConfigController.DailyLimitSaveRequest;
+import com.etotem.cfc.controller.admin.AdminEnergyConfigController.DimensionRatio;
+import com.etotem.cfc.controller.admin.AdminEnergyConfigController.EnergySourceConfigSaveRequest;
+import com.etotem.cfc.controller.admin.DimensionAdminController;
+import com.etotem.cfc.controller.admin.DimensionConfigController;
+import com.etotem.cfc.controller.admin.DimensionWeightController;
+import com.etotem.cfc.entity.DimensionDailyLimit;
+import com.etotem.cfc.entity.EnergySourceConfig;
+import com.etotem.cfc.entity.ProductDimensionConfig;
+import com.etotem.cfc.mapper.DimensionDailyLimitMapper;
+import com.etotem.cfc.mapper.EnergySourceConfigMapper;
+import com.etotem.cfc.mapper.ProductDimensionConfigMapper;
+import com.etotem.cfc.service.DimensionWeightService;
+import com.etotem.cfc.service.ProductDimensionConfigService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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 admin.dimension.config})。
+ *
+ * <p><b>需求/设计锚点</b></p>
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-A-09(维度配置与知识库管理)、
+ *       SC-A-25(维度/权重/每日上限配置 CRUD 与生效)、SC-A-26(能量来源比例与归因权重治理);
+ *       {@code tests/user-scenarios/07-five-dimension.md} ED-13 / ED-14;</li>
+ *   <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.13 + §4.60.9 + §4.60.16 ——
+ *       {@code DimensionAdminController} /api/admin/dimension(norm/list/save/delete、data-source)、
+ *       {@code DimensionConfigController} /api/admin/dimension-config(list/get/save/delete/tree)、
+ *       {@code DimensionWeightController} /api/admin/dimension-weight(save/list)、
+ *       {@code AdminEnergyConfigController} /api/admin/energy-config(source-list/source-save、
+ *       daily-limits/daily-limit-save);</li>
+ *   <li>用例:{@code tests/test-cases/admin.md} TC-A-25 / TC-A-25-E / TC-A-25-P /
+ *       TC-A-26 / TC-A-26-E / TC-A-26-P。</li>
+ * </ul>
+ *
+ * <p><b>被校验的契约行为</b></p>
+ * <ul>
+ *   <li><b>正常流(结构锚点)</b>:四个控制器存在并暴露约定端点(SC-A-25/26 主流程 1:CRUD 可用);</li>
+ *   <li><b>异常流(TC-A-25-E / TC-A-26-E)</b>:
+ *       <ul>
+ *         <li>维度权重非法(负/超界/和≠100%)→ 400 拦截,不落库;</li>
+ *         <li>每日上限非法(负/超界)→ 400 拦截;</li>
+ *         <li>能量来源比例非法(负/超 1/和≠1 即和≠100)→ 400 拦截;</li>
+ *         <li>删除被引用维度 → 需确认/拒绝,不级联破坏存量;</li>
+ *         <li>并发改同配置 → 乐观锁/版本控制({@code ProductDimensionConfig} 应含 {@code version} 字段);</li>
+ *       </ul></li>
+ *   <li><b>权限流(TC-A-25-P / TC-A-26-P)</b>:非 admin 操作 403 —— 控制器方法应带
+ *       {@code @RequestAttribute("role")} 守卫(仿 {@code FileRecordController}:{@code !"admin".equals(role) → 403})。</li>
+ * </ul>
+ *
+ * <p><b>实现现状(2026-11-13 核实)</b>:四个控制器类均已存在且 CRUD 端点已落地,
+ * 但<b>异常流 / 边界流 / 权限流契约行为均未实现</b>:
+ * <ul>
+ *   <li>{@code DimensionWeightController.save} 仅校验非空,无权重和/范围校验;</li>
+ *   <li>{@code AdminEnergyConfigController.dailyLimitSave} 仅校验非空,无上限范围校验;</li>
+ *   <li>{@code AdminEnergyConfigController.sourceSave} 无比例和/范围校验;</li>
+ *   <li>{@code ProductDimensionConfigService.deleteConfig} 直接 {@code removeById},无「被引用维度」拒绝;</li>
+ *   <li>{@code ProductDimensionConfig} 无 {@code version} 字段(无乐观锁);</li>
+ *   <li>四个控制器方法均无 {@code @RequestAttribute("role")} 权限守卫(非 admin 不 403)。</li>
+ * </ul>
+ * 因此<b>异常流 / 边界流 / 权限流用例应失败(红)</b>,GREEN 阶段按上述契约实现后转绿。</p>
+ *
+ * <p><b>测试范围</b>:纯契约级单测,仅 Mockito + 反射,无 Spring 上下文 / 无 DB / 无外部服务。</p>
+ *
+ * <p><b>RED 预期</b>:结构锚点用例通过(绿,基线已落地);异常流 / 边界流 / 权限流用例失败(红),
+ * 断言原文为契约要求的「400 拦截 / 拒绝删除 / version 字段 / role 守卫」缺失。</p>
+ */
+class DimensionConfigAdminContractTest extends EnergyContractTestBase {
+
+    // ==================== 被校验对象 ====================
+
+    private DimensionWeightController weightController;
+    private DimensionWeightService weightService;
+
+    private AdminEnergyConfigController configController;
+    private EnergySourceConfigMapper sourceConfigMapper;
+    private DimensionDailyLimitMapper dailyLimitMapper;
+
+    private ProductDimensionConfigService dimensionConfigService;
+    private ProductDimensionConfigMapper dimensionConfigMapper;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        // --- DimensionWeightController(权重治理,SC-A-25) ---
+        weightController = new DimensionWeightController();
+        weightService = mock(DimensionWeightService.class);
+        inject(weightController, "dimensionWeightService", weightService);
+
+        // --- AdminEnergyConfigController(来源比例 + 每日上限,SC-A-25/A-26) ---
+        configController = new AdminEnergyConfigController();
+        sourceConfigMapper = mock(EnergySourceConfigMapper.class);
+        dailyLimitMapper = mock(DimensionDailyLimitMapper.class);
+        inject(configController, "energySourceConfigMapper", sourceConfigMapper);
+        inject(configController, "dimensionDailyLimitMapper", dailyLimitMapper);
+
+        // --- ProductDimensionConfigService(维度配置 CRUD,SC-A-25) ---
+        dimensionConfigService = new ProductDimensionConfigService();
+        dimensionConfigMapper = mock(ProductDimensionConfigMapper.class);
+        inject(dimensionConfigService, "baseMapper", dimensionConfigMapper);
+
+        // MyBatis-Plus TableInfo 预注册:使 LambdaQueryWrapper 无 Spring 上下文可解析列名
+        initTable(EnergySourceConfig.class);
+        initTable(DimensionDailyLimit.class);
+        initTable(ProductDimensionConfig.class);
+    }
+
+    // ==================== 正常流(结构锚点,SC-A-25/26 主流程 1:CRUD 可用) ====================
+
+    @Test
+    @DisplayName("SC-A-25 正常流(结构锚点):DimensionConfigController 暴露 list/get/save/delete/tree")
+    void dimensionConfigController_exposesRequiredEndpoints() {
+        assertTrue(hasEndpoint(DimensionConfigController.class, "/list"),
+                "DimensionConfigController 应暴露 POST /api/admin/dimension-config/list");
+        assertTrue(hasEndpoint(DimensionConfigController.class, "/get"),
+                "DimensionConfigController 应暴露 POST /api/admin/dimension-config/get");
+        assertTrue(hasEndpoint(DimensionConfigController.class, "/save"),
+                "DimensionConfigController 应暴露 POST /api/admin/dimension-config/save");
+        assertTrue(hasEndpoint(DimensionConfigController.class, "/delete"),
+                "DimensionConfigController 应暴露 POST /api/admin/dimension-config/delete");
+        assertTrue(hasEndpoint(DimensionConfigController.class, "/tree"),
+                "DimensionConfigController 应暴露 POST /api/admin/dimension-config/tree");
+    }
+
+    @Test
+    @DisplayName("SC-A-09/SC-A-25 正常流(结构锚点):DimensionAdminController 暴露 norm + data-source")
+    void dimensionAdminController_exposesNormAndDataSourceEndpoints() {
+        assertTrue(hasEndpoint(DimensionAdminController.class, "/norm/list")
+                        && hasEndpoint(DimensionAdminController.class, "/norm/save")
+                        && hasEndpoint(DimensionAdminController.class, "/norm/delete"),
+                "DimensionAdminController 应暴露 /api/admin/dimension/norm/{list,save,delete}");
+        assertTrue(hasEndpoint(DimensionAdminController.class, "/data-source/list")
+                        && hasEndpoint(DimensionAdminController.class, "/data-source/save")
+                        && hasEndpoint(DimensionAdminController.class, "/data-source/delete")
+                        && hasEndpoint(DimensionAdminController.class, "/data-source/detail"),
+                "DimensionAdminController 应暴露 /api/admin/dimension/data-source/{list,save,delete,detail}");
+    }
+
+    @Test
+    @DisplayName("SC-A-25 正常流(结构锚点):DimensionWeightController 暴露 save/list")
+    void dimensionWeightController_exposesSaveAndList() {
+        assertTrue(hasEndpoint(DimensionWeightController.class, "/save")
+                        && hasEndpoint(DimensionWeightController.class, "/list"),
+                "DimensionWeightController 应暴露 /api/admin/dimension-weight/{save,list}");
+    }
+
+    @Test
+    @DisplayName("SC-A-25/SC-A-26 正常流(结构锚点):AdminEnergyConfigController 暴露来源比例 + 每日上限")
+    void adminEnergyConfigController_exposesSourceAndDailyLimitEndpoints() {
+        assertTrue(hasEndpoint(AdminEnergyConfigController.class, "/source-list")
+                        && hasEndpoint(AdminEnergyConfigController.class, "/source-save")
+                        && hasEndpoint(AdminEnergyConfigController.class, "/source-delete"),
+                "AdminEnergyConfigController 应暴露 /api/admin/energy-config/source-{list,save,delete}");
+        assertTrue(hasEndpoint(AdminEnergyConfigController.class, "/daily-limits")
+                        && hasEndpoint(AdminEnergyConfigController.class, "/daily-limit-save"),
+                "AdminEnergyConfigController 应暴露 /api/admin/energy-config/daily-limit{,-save}");
+    }
+
+    // ==================== 异常流(TC-A-25-E):维度权重非法(负/超界/和≠100%)→ 400 ====================
+
+    @Test
+    @DisplayName("TC-A-25-E 异常:权重和≠100% → 400 拦截,不落库")
+    void weightSave_sumNot100_returns400() {
+        // 60 + 30 = 90 ≠ 100
+        Result<Void> r = weightController.save(weightParams(Arrays.asList(
+                weight("body", 60), weight("mind", 30))));
+        assertEquals(400, r.getCode(),
+                "RED[admin.dimension.config] 权重和≠100%(60+30=90)应 400 拦截(TC-A-25-E),"
+                        + "当前实现未校验、直接落库返回 " + r.getCode());
+    }
+
+    @Test
+    @DisplayName("TC-A-25-E 异常:权重为负 → 400 拦截")
+    void weightSave_negativeWeight_returns400() {
+        Result<Void> r = weightController.save(weightParams(
+                Collections.singletonList(weight("body", -10))));
+        assertEquals(400, r.getCode(),
+                "RED[admin.dimension.config] 负权重(-10)应 400 拦截(TC-A-25-E),"
+                        + "当前实现未校验,返回 " + r.getCode());
+    }
+
+    @Test
+    @DisplayName("TC-A-25-E 异常:权重超界(>100)→ 400 拦截")
+    void weightSave_overBoundWeight_returns400() {
+        Result<Void> r = weightController.save(weightParams(
+                Collections.singletonList(weight("body", 120))));
+        assertEquals(400, r.getCode(),
+                "RED[admin.dimension.config] 超界权重(120>100)应 400 拦截(TC-A-25-E),"
+                        + "当前实现未校验,返回 " + r.getCode());
+    }
+
+    // ==================== 异常流(TC-A-25-E):每日上限非法(负/超界)→ 400 ====================
+
+    @Test
+    @DisplayName("TC-A-25-E 异常:每日上限为负 → 400 拦截")
+    void dailyLimitSave_negativeLimit_returns400() {
+        DailyLimitSaveRequest req = new DailyLimitSaveRequest();
+        req.setLimits(Collections.singletonList(limit("body", -5)));
+        Result<String> r = configController.dailyLimitSave(req);
+        assertEquals(400, r.getCode(),
+                "RED[admin.dimension.config] 每日上限(-5)应 400 拦截(TC-A-25-E),"
+                        + "当前实现未校验,返回 " + r.getCode());
+    }
+
+    @Test
+    @DisplayName("TC-A-25-E 异常:每日上限超界 → 400 拦截")
+    void dailyLimitSave_overBoundLimit_returns400() {
+        DailyLimitSaveRequest req = new DailyLimitSaveRequest();
+        req.setLimits(Collections.singletonList(limit("body", 100000)));
+        Result<String> r = configController.dailyLimitSave(req);
+        assertEquals(400, r.getCode(),
+                "RED[admin.dimension.config] 每日上限(100000)超界应 400 拦截(TC-A-25-E),"
+                        + "当前实现未校验,返回 " + r.getCode());
+    }
+
+    // ==================== 异常流(TC-A-26-E):来源比例非法(负/超 1/和≠100)→ 400 ====================
+
+    @Test
+    @DisplayName("TC-A-26-E 异常:来源比例和≠100 → 400 拦截")
+    void sourceSave_ratioSumNot100_returns400() {
+        // 60 + 30 = 90 ≠ 100
+        Result<String> r = configController.sourceSave(ratioRequest(60, 30));
+        assertEquals(400, r.getCode(),
+                "RED[admin.dimension.config] 来源比例和≠100(60+30=90)应 400 拦截(TC-A-26-E),"
+                        + "当前实现未校验,返回 " + r.getCode());
+    }
+
+    @Test
+    @DisplayName("TC-A-26-E 异常:来源比例为负 → 400 拦截")
+    void sourceSave_negativeRatio_returns400() {
+        Result<String> r = configController.sourceSave(ratioRequest(-10, 0));
+        assertEquals(400, r.getCode(),
+                "RED[admin.dimension.config] 负比例(-10)应 400 拦截(TC-A-26-E),"
+                        + "当前实现未校验,返回 " + r.getCode());
+    }
+
+    @Test
+    @DisplayName("TC-A-26-E 异常:来源比例超 1(>100)→ 400 拦截")
+    void sourceSave_ratioOver100_returns400() {
+        Result<String> r = configController.sourceSave(ratioRequest(150, 0));
+        assertEquals(400, r.getCode(),
+                "RED[admin.dimension.config] 超界比例(150>100)应 400 拦截(TC-A-26-E),"
+                        + "当前实现未校验,返回 " + r.getCode());
+    }
+
+    // ==================== 边界流(TC-A-25-E):删除被引用维度 → 拒绝,不级联破坏存量 ====================
+
+    @Test
+    @DisplayName("TC-A-25-E 边界:删除被引用维度(含子节点)应被拒绝,不级联删除")
+    void deleteConfig_referencedDimension_rejectedNotSilentDelete() {
+        // 维度 id=1 被引用(存在子节点,selectCount=1);若未被拦截则删除会成功(deleteById=1)
+        when(dimensionConfigMapper.selectCount(any())).thenReturn(1L);
+        when(dimensionConfigMapper.deleteById(any())).thenReturn(1);
+
+        boolean result = dimensionConfigService.deleteConfig(1L);
+
+        assertFalse(result,
+                "RED[admin.dimension.config] 删除被引用维度(含子节点)应被拒绝(返回 false),"
+                        + "不得级联破坏存量(TC-A-25-E);当前实现未做引用检查,直接 removeById 返回 "
+                        + result);
+        // 契约要求:删除前必须先做「被引用」检查(selectCount),且被引用时不执行删除(deleteById 不被调用)
+        verify(dimensionConfigMapper).selectCount(any());
+        verify(dimensionConfigMapper, never()).deleteById(any());
+    }
+
+    // ==================== 边界流(TC-A-25-E):并发改同配置 → 乐观锁/版本控制 ====================
+
+    @Test
+    @DisplayName("TC-A-25-E 边界:ProductDimensionConfig 应含 version 字段(乐观锁,并发不互相覆盖)")
+    void productDimensionConfig_hasVersionOptimisticLock() {
+        assertTrue(hasField(ProductDimensionConfig.class, "version"),
+                "RED[admin.dimension.config] ProductDimensionConfig 应含 version 字段"
+                        + "(乐观锁版本控制,TC-A-25-E 并发改同配置不互相覆盖);当前实体无 version 字段");
+    }
+
+    // ==================== 权限流(TC-A-25-P / TC-A-26-P):非 admin 操作 403 ====================
+
+    @Test
+    @DisplayName("TC-A-25-P/TC-A-26-P 权限:DimensionConfigController 应带 @RequestAttribute(\"role\") 守卫")
+    void dimensionConfigController_hasRoleGuard_forNonAdmin403() {
+        assertTrue(hasRoleGuard(DimensionConfigController.class),
+                "RED[admin.dimension.config] DimensionConfigController 方法应带"
+                        + " @RequestAttribute(\"role\") 守卫,非 admin 操作 403(TC-A-25-P);"
+                        + "当前方法均无 role 参数");
+    }
+
+    @Test
+    @DisplayName("TC-A-25-P 权限:DimensionWeightController 应带 @RequestAttribute(\"role\") 守卫")
+    void dimensionWeightController_hasRoleGuard() {
+        assertTrue(hasRoleGuard(DimensionWeightController.class),
+                "RED[admin.dimension.config] DimensionWeightController 方法应带"
+                        + " @RequestAttribute(\"role\") 守卫,非 admin 操作 403(TC-A-25-P);"
+                        + "当前方法均无 role 参数");
+    }
+
+    @Test
+    @DisplayName("TC-A-26-P 权限:AdminEnergyConfigController 应带 @RequestAttribute(\"role\") 守卫")
+    void adminEnergyConfigController_hasRoleGuard() {
+        assertTrue(hasRoleGuard(AdminEnergyConfigController.class),
+                "RED[admin.dimension.config] AdminEnergyConfigController 方法应带"
+                        + " @RequestAttribute(\"role\") 守卫,非 admin 改全局 403(TC-A-26-P);"
+                        + "当前方法均无 role 参数");
+    }
+
+    // ==================== 辅助构造 ====================
+
+    private Map<String, Object> weightParams(List<Map<String, Object>> weights) {
+        Map<String, Object> params = new HashMap<>();
+        params.put("targetType", "member");
+        params.put("targetId", 50L);
+        params.put("weights", weights);
+        return params;
+    }
+
+    private Map<String, Object> weight(String dimension, int weight) {
+        Map<String, Object> m = new LinkedHashMap<>();
+        m.put("dimension", dimension);
+        m.put("weight", weight);
+        m.put("enabled", true);
+        return m;
+    }
+
+    private EnergySourceConfigSaveRequest ratioRequest(int ratio1, int ratio2) {
+        EnergySourceConfigSaveRequest req = new EnergySourceConfigSaveRequest();
+        req.setSourceType("task");
+        req.setSourceId(100L);
+        req.setSourceName("任务A");
+        List<DimensionRatio> ratios = new ArrayList<>();
+        ratios.add(ratio(5L, ratio1));
+        if (ratio2 != 0) {
+            ratios.add(ratio(3L, ratio2));
+        }
+        req.setRatios(ratios);
+        return req;
+    }
+
+    private DimensionRatio ratio(Long dimensionId, int ratio) {
+        DimensionRatio r = new DimensionRatio();
+        r.setDimensionId(dimensionId);
+        r.setRatio(ratio);
+        return r;
+    }
+
+    private DimensionDailyLimit limit(String code, Integer dailyLimit) {
+        DimensionDailyLimit l = new DimensionDailyLimit();
+        l.setDimensionCode(code);
+        l.setDailyLimit(dailyLimit);
+        Date now = new Date();
+        l.setCreatedAt(now);
+        l.setUpdatedAt(now);
+        return l;
+    }
+
+    // ==================== 反射辅助 ====================
+
+    /** 控制器是否暴露指定 POST 路径端点 */
+    private boolean hasEndpoint(Class<?> controller, String path) {
+        for (Method m : controller.getDeclaredMethods()) {
+            PostMapping pm = m.getAnnotation(PostMapping.class);
+            if (pm != null) {
+                for (String v : pm.value()) {
+                    if (path.equals(v)) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /** 控制器是否含 @RequestAttribute("role") 参数守卫 */
+    private boolean hasRoleGuard(Class<?> controller) {
+        for (Method m : controller.getDeclaredMethods()) {
+            for (java.lang.reflect.Parameter p : m.getParameters()) {
+                RequestAttribute ra = p.getAnnotation(RequestAttribute.class);
+                if (ra != null && "role".equals(ra.value())) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /** 实体是否含指定字段 */
+    private boolean hasField(Class<?> clazz, String fieldName) {
+        for (Field f : clazz.getDeclaredFields()) {
+            if (f.getName().equals(fieldName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

+ 243 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyAutoCreateForPurchaseContractTest.java

@@ -0,0 +1,243 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.MembershipController;
+import com.etotem.cfc.dto.PaymentOrderDTO;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.MembershipService;
+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.util.HashMap;
+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.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+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 family.family.autoCreateForPurchase}(S-cfc-188,SC-P-58 购买会员/套餐)。
+ *
+ * <p><b>需求/溯源(tests/user-scenarios/应用场景总览.md SC-P-58 · 购买会员/套餐,业务域 F 商城与会员)</b></p>
+ * <blockquote>
+ *   目标:解锁高级能力。流程:选档位 → 支付 → 权益生效。边界:支付失败回滚;重复购买幂等。
+ *   用户实测反馈:新用户({@code familyId==null})在「我的」页/会员中心点「立即开通」后<b>下单被拦</b>
+ *   (MembershipController /upgrade 199 行 createOrder 的 familyId==null 抛错、/plan/purchase 287-299 行
+ *   noFamily 守卫),无法自助开通;期望:<b>无家庭新用户购买会员时自动创建初始家庭并完成购买</b>。
+ * </blockquote>
+ *
+ * <p><b>设计/溯源(docs/superpowers/api/API_REFERENCE.md §4.50 会员权益对比表 + §4.50.1 会员档位购买;
+ * docs/tdd/orchestration/sc-key-map.md SC-P-58)</b></p>
+ * <ul>
+ *   <li>SC-P-58 已登记 1:N 键 {@code membership.plan.purchase, family.family.autoCreateForPurchase}
+ *       (问题1「新用户无开通入口」裁决走方案A:<b>购买时自动创建家庭</b>)。</li>
+ *   <li>修复方向:MembershipController 三处 {@code getUserFamilyId} 改 {@code getOrCreateUserFamilyId}
+ *       (upgradeOrder L199 / activateTrial L224 / createOrder L245);/plan/purchase 无家庭
+ *       noFamily 守卫<b>保留兜底</b>(get-or-create 仍无法获得家庭时才拒绝)。</li>
+ *   <li>{@code MembershipService#getOrCreateUserFamilyId}(MembershipService L114-138,@Transactional)
+ *       已存在:用户无家庭时自动创建初始家庭(昵称+「的家庭」、UUID 邀请码、creator/isFamilyAdmin,
+ *       并回写 {@code user.familyId})——购买链路复用它即可与既有「微信用户被踢后迁入自有家庭」语义保持一致。</li>
+ *   <li>同类宿主先例:MindFortuneController 已在读路径直接使用 {@code getOrCreateUserFamilyId}。</li>
+ * </ul>
+ *
+ * <p><b>RED 预期</b>:上述调用点当前仍走 {@code getUserFamilyId}(新用户返回 null → /upgrade|/trial|/orders
+ * 落单被拦、/plan/purchase 被 noFamily 守卫拒绝),「自动创建初始家庭并完成购买」契约行为未落地,
+ * 以下主断言用例在当前实现应失败(红);GREEN 阶段按修复方向落地后自动转绿。</p>
+ *
+ * <p><b>测试范围</b>:纯 Mockito + 反射注入(无 Spring / 无 DB / 无真实微信);宿主
+ * {@link MembershipController} 直接实例化,私有注入点(membershipService / userMapper / testMode)
+ * 用反射注入,风格与同批 MembershipPlanPurchaseContractTest / FamilyJoinValidationRulesContractTest 一致。</p>
+ */
+class FamilyAutoCreateForPurchaseContractTest {
+
+    private static final String KEY = "[family.family.autoCreateForPurchase]";
+
+    private static final Long USER_ID = 1L;
+    /** get-or-create 为该无家庭新用户产出的初始家庭 ID */
+    private static final Long AUTO_CREATED_FAMILY_ID = 888L;
+
+    private MembershipController controller;
+    private MembershipService membershipService;
+    private UserMapper userMapper;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        controller = new MembershipController();
+        membershipService = mock(MembershipService.class);
+        userMapper = mock(UserMapper.class);
+        inject(controller, "membershipService", membershipService);
+        inject(controller, "userMapper", userMapper);
+    }
+
+    // ==================== 正常流:无家庭新用户购买 → 自动创建初始家庭并完成购买 ====================
+
+    @Test
+    @DisplayName("正常流-1:/upgrade 无家庭新用户下单必须走 getOrCreateUserFamilyId 自动建家庭后落单")
+    void upgradeOrder_newUserNoFamily_mustAutoCreateFamilyBeforeCreateOrder() {
+        when(userMapper.selectById(USER_ID)).thenReturn(newUserWithoutFamily());
+        // 契约:get-or-create 为该用户产出初始家庭(自动创建并回写 user.familyId)
+        when(membershipService.getOrCreateUserFamilyId(USER_ID)).thenReturn(AUTO_CREATED_FAMILY_ID);
+        PaymentOrderDTO created = order("MO_UP_AUTO_001", AUTO_CREATED_FAMILY_ID);
+        when(membershipService.createOrder(eq(USER_ID), eq(AUTO_CREATED_FAMILY_ID),
+                eq("FAMILY"), eq("yearly"), eq("yearly"), isNull())).thenReturn(created);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("levelCode", "FAMILY");
+        params.put("paymentType", "yearly");
+        params.put("period", "yearly");
+
+        Result<PaymentOrderDTO> resp = controller.upgradeOrder(params, USER_ID);
+
+        // 契约主断言:购买必须走 get-or-create(当前走 getUserFamilyId → 新用户 familyId=null → 下单被拦)
+        verify(membershipService).getOrCreateUserFamilyId(USER_ID);
+        verify(membershipService, never()).getUserFamilyId(USER_ID);
+        verify(membershipService).createOrder(USER_ID, AUTO_CREATED_FAMILY_ID, "FAMILY", "yearly", "yearly", null);
+        assertNotNull(resp.getData(),
+                KEY + " 无家庭新用户 /upgrade 应自动创建家庭后返回待支付订单,当前下单被拦(familyId=null)。");
+        assertEquals(AUTO_CREATED_FAMILY_ID, resp.getData().getFamilyId(),
+                KEY + " 自动创建的初始家庭应作为订单归属家庭。");
+    }
+
+    @Test
+    @DisplayName("正常流-2:/trial 无家庭新用户激活试用必须走 getOrCreateUserFamilyId 自动建家庭")
+    void activateTrial_newUserNoFamily_mustAutoCreateFamily() {
+        when(userMapper.selectById(USER_ID)).thenReturn(newUserWithoutFamily());
+        when(membershipService.getOrCreateUserFamilyId(USER_ID)).thenReturn(AUTO_CREATED_FAMILY_ID);
+        PaymentOrderDTO trial = order("MO_TRIAL_AUTO_001", AUTO_CREATED_FAMILY_ID);
+        trial.setStatus("paid");
+        when(membershipService.createOrder(eq(USER_ID), eq(AUTO_CREATED_FAMILY_ID),
+                eq("FAMILY"), eq("trial"), isNull(), isNull())).thenReturn(trial);
+
+        Result<PaymentOrderDTO> resp = controller.activateTrial(USER_ID);
+
+        verify(membershipService).getOrCreateUserFamilyId(USER_ID);
+        verify(membershipService, never()).getUserFamilyId(USER_ID);
+        verify(membershipService).createOrder(USER_ID, AUTO_CREATED_FAMILY_ID, "FAMILY", "trial", null, null);
+        assertNotNull(resp.getData(),
+                KEY + " 无家庭新用户 /trial 应自动创建家庭后返回试用订单,当前被拦(familyId=null)。");
+        assertEquals(AUTO_CREATED_FAMILY_ID, resp.getData().getFamilyId(),
+                KEY + " 自动创建的初始家庭应作为试用订单归属家庭。");
+    }
+
+    @Test
+    @DisplayName("正常流-3:/orders 无家庭新用户下单必须走 getOrCreateUserFamilyId 自动建家庭后落单")
+    void createOrder_ordersEndpoint_newUserNoFamily_mustAutoCreateFamily() throws Exception {
+        setTestModeOn(controller); // 模拟支付开关:走 MembershipService.createOrder 落单路径(不触碰微信)
+        when(userMapper.selectById(USER_ID)).thenReturn(newUserWithoutFamily());
+        when(membershipService.getOrCreateUserFamilyId(USER_ID)).thenReturn(AUTO_CREATED_FAMILY_ID);
+        PaymentOrderDTO created = order("MO_ORD_AUTO_001", AUTO_CREATED_FAMILY_ID);
+        when(membershipService.createOrder(eq(USER_ID), eq(AUTO_CREATED_FAMILY_ID),
+                eq("FAMILY"), eq("yearly"), eq("yearly"), isNull())).thenReturn(created);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("levelCode", "FAMILY");
+        params.put("paymentType", "yearly");
+        params.put("period", "yearly");
+
+        Result<Object> resp = controller.createOrder(params, USER_ID);
+
+        verify(membershipService).getOrCreateUserFamilyId(USER_ID);
+        verify(membershipService).createOrder(USER_ID, AUTO_CREATED_FAMILY_ID, "FAMILY", "yearly", "yearly", null);
+        assertNotNull(resp.getData(),
+                KEY + " 无家庭新用户 /orders 应自动创建家庭后返回待支付订单,当前被拦(familyId=null)。");
+        PaymentOrderDTO data = (PaymentOrderDTO) resp.getData();
+        assertEquals(AUTO_CREATED_FAMILY_ID, data.getFamilyId(),
+                KEY + " 自动创建的初始家庭应作为订单归属家庭。");
+    }
+
+    @Test
+    @DisplayName("正常流-4:/plan/purchase 无家庭新用户购买档位必须自动建家庭后完成购买")
+    void purchasePlan_newUserNoFamily_mustAutoCreateFamilyAndComplete() {
+        when(userMapper.selectById(USER_ID)).thenReturn(newUserWithoutFamily());
+        when(membershipService.getOrCreateUserFamilyId(USER_ID)).thenReturn(AUTO_CREATED_FAMILY_ID);
+        Map<String, Object> purchaseResp = new HashMap<>();
+        purchaseResp.put("orderNo", "MO_PLAN_AUTO_001");
+        purchaseResp.put("status", "pending");
+        purchaseResp.put("levelCode", "FAMILY");
+        purchaseResp.put("payType", "wechat");
+        when(membershipService.purchasePlan(eq(USER_ID), eq(AUTO_CREATED_FAMILY_ID),
+                eq("FAMILY"), eq("yearly"), isNull())).thenReturn(purchaseResp);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("levelCode", "FAMILY");
+        params.put("period", "yearly");
+
+        Result<Object> resp = controller.purchasePlan(params, USER_ID);
+
+        // 契约主断言:当前 user.getFamilyId()==null → noFamily(5001) 守卫拒绝,未走 get-or-create(红)。
+        verify(membershipService).getOrCreateUserFamilyId(USER_ID);
+        verify(membershipService).purchasePlan(USER_ID, AUTO_CREATED_FAMILY_ID, "FAMILY", "yearly", null);
+        assertEquals(200, resp.getCode(),
+                KEY + " 无家庭新用户 /plan/purchase 应自动创建家庭后成功返回待支付订单,"
+                        + "当前被 noFamily(5001) 守卫拒绝,无法自助开通。");
+        assertNotNull(resp.getData(), KEY + " 购买成功后应返回待支付订单 + 支付参数。");
+        assertEquals("MO_PLAN_AUTO_001", ((Map<String, Object>) resp.getData()).get("orderNo"),
+                KEY + " 返回的应是自动建家庭后的待支付订单。");
+    }
+
+    // ==================== 权限流:自动建家庭失败时 noFamily 守卫兜底保留 ====================
+
+    @Test
+    @DisplayName("权限流-1:get-or-create 仍无法获得家庭时 /plan/purchase noFamily 守卫兜底拒绝(guard 兜底保留)")
+    void purchasePlan_getOrCreateStillNoFamily_guardKeptRejects() {
+        when(userMapper.selectById(USER_ID)).thenReturn(newUserWithoutFamily());
+        when(membershipService.getOrCreateUserFamilyId(USER_ID)).thenReturn(null); // get-or-create 兜底失败(异常场景)
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("levelCode", "FAMILY");
+
+        Result<Object> resp = controller.purchasePlan(params, USER_ID);
+
+        assertEquals(5001, resp.getCode(),
+                KEY + " 守卫兜底:自动建家庭失败时仍应返回 noFamily(5001)(设计约束「guard 兜底保留」)。");
+        assertTrue(resp.getMessage() != null && resp.getMessage().contains("家庭"),
+                KEY + " noFamily 守卫文案应提示家庭上下文:实际 " + resp.getMessage());
+        verify(membershipService, never()).purchasePlan(USER_ID, null, "FAMILY", "yearly", null);
+    }
+
+    // ==================== 辅助构造 ====================
+
+    /** 新注册用户:无家庭(familyId==null),即「我的」页/会员中心点「立即开通」的实测用户形态 */
+    private User newUserWithoutFamily() {
+        User u = new User();
+        u.setId(USER_ID);
+        u.setFamilyId(null);
+        return u;
+    }
+
+    private static PaymentOrderDTO order(String orderNo, Long familyId) {
+        PaymentOrderDTO dto = new PaymentOrderDTO();
+        dto.setOrderNo(orderNo);
+        dto.setFamilyId(familyId);
+        dto.setLevelCode("FAMILY");
+        dto.setPaymentType("yearly");
+        dto.setAmount(39800);
+        dto.setStatus("pending");
+        dto.setPeriodNo(1);
+        dto.setTotalPeriods(1);
+        return dto;
+    }
+
+    /** 反射注入私有字段(MembershipController 无构造器注入,@Resource 字段为私有域) */
+    private static void inject(Object target, String fieldName, Object value) throws Exception {
+        Field f = target.getClass().getDeclaredField(fieldName);
+        f.setAccessible(true);
+        f.set(target, value);
+    }
+
+    /** 打开 wechat.test-mode 开关(原 @Value("${wechat.test-mode:false}") 私有 boolean 字段) */
+    private static void setTestModeOn(MembershipController c) throws Exception {
+        Field f = MembershipController.class.getDeclaredField("testMode");
+        f.setAccessible(true);
+        f.setBoolean(c, true);
+    }
+}

+ 293 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/PlanContentSafetyReviewContractTest.java

@@ -0,0 +1,293 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.controller.ContentFilterController;
+import com.etotem.cfc.controller.HealthPlanController;
+import com.etotem.cfc.controller.admin.ReviewCenterController;
+import com.etotem.cfc.controller.guide.GuidePlanReviewController;
+import com.etotem.cfc.controller.guide.PlanReviewController;
+import com.etotem.cfc.entity.HealthPlan;
+import com.etotem.cfc.entity.Task;
+import com.etotem.cfc.mapper.HealthPlanMapper;
+import com.etotem.cfc.mapper.TaskMapper;
+import com.etotem.cfc.service.WechatService;
+import com.etotem.cfc.service.api.WechatServiceInterface;
+import com.etotem.cfc.service.impl.HealthPlanServiceImpl;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Date;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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;
+
+/**
+ * 契约级单测(TDD-RED):能力键 {@code teacher.plan.review} ——
+ * SC-T-04 方案审核列表+批准、SC-T-28 AI 方案内容安全复核。
+ *
+ * <p><b>契约锚点</b></p>
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-T-04(规划师查看待审核方案列表 →
+ *       批准/驳回,状态流转受守卫)、SC-T-28(AI 生成方案 → <b>敏感词/合规检查</b> →
+ *       放行/驳回;边界:<b>命中即拦截</b>)。</li>
+ *   <li>设计:{@code docs/superpowers/specs/2026-11-07-planner-zone-contract.md}
+ *       §2 端点核验表(SC-T-04 / SC-T-28)、§3 能力键 {@code teacher.plan.review} →</li>
+ *   <li>API:{@code docs/superpowers/api/API_REFERENCE.md} §4.57 SC-T-04 / SC-T-28 行、
+ *       §4.60.13。</li>
+ * </ul>
+ *
+ * <p><b>本类为 RED 契约(预期失败)</b>:截至 2026-11-13 origin/cfclub,
+ * 审核链路({@code GuidePlanReviewController.approvePlan} → {@code HealthPlanServiceImpl
+ * .approveAndPublish}、{@code HealthPlanController /pending-review/approve})
+ * <b>缺少 AI 方案内容安全复核</b>:既没有引用现成的 {@link WechatServiceInterface#checkText(String)}
+ * 内容安全原语(该原语目前只接在 {@code MediaController} / {@code AdminArticleController}),
+ * {@code ContentFilterController} 也只有 {@code POST /api/content/by-tags},没有方案内容复核端点。
+ * 因此结构探针与行为探针均失败(红)。待实现补齐后应转绿。</p>
+ *
+ * <p><b>已绿的契约锚点</b>(现有 {@code PlanReviewTeacherContractTest} 已覆盖 SC-T-04 生命周期,
+ * 本类只补单一能力键视角的端点映射证据,不重复实现、不修改 src/main)。</p>
+ *
+ * <p><b>测试范围</b>:纯反射 + Mockito / 无 Spring 上下文 / 无 DB / 无外部服务。</p>
+ */
+class PlanContentSafetyReviewContractTest extends AssessmentPlanContractTestBase {
+
+    static {
+        initTable(HealthPlan.class);
+        initTable(Task.class);
+    }
+
+    // ==================== 已绿锚点:端点映射(API_REFERENCE §4.57 SC-T-04 / §4.60.13) ====================
+
+    @Test
+    @DisplayName("SC-T-04 端点:GuidePlanReviewController 待审核列表 / 批准 / 驳回(family 路径)")
+    void endpointMapping_guidePlanReview_pendingApproveReject() {
+        Class<GuidePlanReviewController> clazz = GuidePlanReviewController.class;
+
+        assertTrue(hasClassMapping(clazz, "/api/guide/families/{familyId}/plans"),
+                "GuidePlanReviewController 类级映射应为 /api/guide/families/{familyId}/plans");
+
+        assertTrue(hasPostMapping(clazz, "listPendingPlans", "/pending"),
+                "listPendingPlans 应为 POST /pending(待审核列表)");
+        assertTrue(hasPostMapping(clazz, "approvePlan", "/{planId}/approve"),
+                "approvePlan 应为 POST /{planId}/approve(批准)");
+        assertTrue(hasPostMapping(clazz, "rejectPlan", "/{planId}/reject"),
+                "rejectPlan 应为 POST /{planId}/reject(驳回)");
+    }
+
+    @Test
+    @DisplayName("SC-T-04 端点:PlanReviewController 方案审核 POST /api/guide/plans/review")
+    void endpointMapping_planReview_review() {
+        Class<PlanReviewController> clazz = PlanReviewController.class;
+
+        assertTrue(hasClassMapping(clazz, "/api/guide/plans"),
+                "PlanReviewController 类级映射应为 /api/guide/plans");
+        assertTrue(hasPostMapping(clazz, "reviewPlan", "/review"),
+                "reviewPlan 应为 POST /review(批准/驳回)");
+    }
+
+    @Test
+    @DisplayName("SC-T-04 端点:ReviewCenterController 审核中心汇总 POST /api/admin/review-center/summary")
+    void endpointMapping_reviewCenter_summary() {
+        Class<ReviewCenterController> clazz = ReviewCenterController.class;
+
+        assertTrue(hasClassMapping(clazz, "/api/admin/review-center"),
+                "ReviewCenterController 类级映射应为 /api/admin/review-center");
+        assertTrue(hasPostMapping(clazz, "summary", "/summary"),
+                "summary 应为 POST /summary(审核中心计数)");
+    }
+
+    // ==================== 已绿锚点:待复核内容载体 / 现有安全原语 ====================
+
+    @Test
+    @DisplayName("SC-T-28 载体:HealthPlan 持有 planContent / planJson(AI 方案正文,复核对象)")
+    void contract_healthPlanContentFields() throws Exception {
+        assertNotNull(HealthPlan.class.getDeclaredField("planContent"),
+                "HealthPlan 应持有 planContent(AI 生成的方案正文)");
+        assertNotNull(HealthPlan.class.getDeclaredField("planJson"),
+                "HealthPlan 应持有 planJson(AI 生成的方案结构)");
+        assertTrue("health_plans".equals(tableName(HealthPlan.class)),
+                "HealthPlan 应映射 health_plans 表");
+    }
+
+    @Test
+    @DisplayName("SC-T-28 原语:WechatServiceInterface.checkText(String) 可用于方案内容复核")
+    void contract_wechatCheckTextPrimitive() throws Exception {
+        Method m = WechatServiceInterface.class.getMethod("checkText", String.class);
+        assertEquals(boolean.class, m.getReturnType(), "checkText 应返回 boolean(true=安全)");
+        assertTrue(WechatServiceInterface.class.isAssignableFrom(WechatService.class),
+                "WechatService 应实现 WechatServiceInterface,可复用于方案内容复核");
+    }
+
+    // ==================== RED 探针 A:审核链路缺少内容安全依赖 ====================
+
+    @Test
+    @DisplayName("SC-T-28 RED:health_plans 审核链路必须接入内容安全依赖(checkText 能力)")
+    void structure_approveChain_hasContentSafetyDependency() {
+        Class<?>[] approveChain = new Class<?>[]{
+                HealthPlanServiceImpl.class,
+                GuidePlanReviewController.class,
+                HealthPlanController.class
+        };
+
+        StringBuilder missing = new StringBuilder();
+        boolean found = false;
+        for (Class<?> clazz : approveChain) {
+            if (declaresContentSafetyDependency(clazz)) {
+                found = true;
+                break;
+            }
+            missing.append(clazz.getSimpleName()).append(" ");
+        }
+
+        assertTrue(found,
+                "SC-T-28:AI 方案审核链路必须接入内容安全复核(依赖类型需暴露 checkText(String)),"
+                        + "当前缺失于:" + missing.toString().trim());
+    }
+
+    // ==================== RED 探针 B:缺少方案内容复核端点 ====================
+
+    @Test
+    @DisplayName("SC-T-28 RED:ContentFilterController 必须提供方案内容安全复核端点(而非仅 /by-tags)")
+    void structure_contentFilter_hasPlanSafetyEndpoint() {
+        Class<ContentFilterController> clazz = ContentFilterController.class;
+
+        boolean hasPlanSafetyEndpoint = Arrays.stream(clazz.getDeclaredMethods())
+                .filter(m -> m.isAnnotationPresent(PostMapping.class))
+                .flatMap(m -> Arrays.stream(m.getAnnotation(PostMapping.class).value()))
+                .anyMatch(path -> path.toLowerCase().contains("plan"));
+
+        assertTrue(hasPlanSafetyEndpoint,
+                "SC-T-28:方案内容安全复核需要一个可调用端点(如 POST /api/content/plan-safety),"
+                        + "当前 ContentFilterController 仅有 " + postPaths(clazz));
+    }
+
+    // ==================== RED 探针 C:命中即拦截(行为) ====================
+
+    @Test
+    @DisplayName("SC-T-28 RED:approveAndPublish 命中内容安全 → 拦截发布,不落库")
+    void behavior_approveAndPublish_sensitiveContent_blocked() throws Exception {
+        HealthPlanMapper planMapper = mock(HealthPlanMapper.class);
+        TaskMapper taskMapper = mock(TaskMapper.class);
+
+        HealthPlan plan = healthPlan(901L, "pending_review",
+                "祖传秘方,包治百病,七天根治自闭症,无需运动与饮食干预。");
+        when(planMapper.selectById(901L)).thenReturn(plan);
+
+        HealthPlanServiceImpl svc = new HealthPlanServiceImpl();
+        inject(svc, "healthPlanMapper", planMapper);
+        inject(svc, "taskMapper", taskMapper);
+
+        try {
+            svc.approveAndPublish(901L, 7L, "同意");
+        } catch (RuntimeException expected) {
+            // 命中即拦截:允许以业务异常方式拒绝发布
+        }
+
+        assertFalse("published".equals(plan.getStatus()),
+                "SC-T-28 边界「命中即拦截」:内容安全复核未通过的方案不得置为 published");
+        verify(planMapper, never()).updateById(any(HealthPlan.class));
+    }
+
+    // ==================== RED 探针 D:审核入口不得在复核前变更状态 ====================
+
+    @Test
+    @DisplayName("SC-T-28 RED:GuidePlanReviewController.approvePlan 链路需在发布前完成复核(状态不变更)")
+    void behavior_guidePlanReviewApprove_doesNotPublishBeforeSafetyCheck() throws Exception {
+        HealthPlanMapper planMapper = mock(HealthPlanMapper.class);
+        HealthPlan plan = healthPlan(902L, "pending_review", "无资质医疗建议:停用处方药改用本方案。");
+        when(planMapper.selectById(902L)).thenReturn(plan);
+
+        HealthPlanServiceImpl svc = new HealthPlanServiceImpl();
+        inject(svc, "healthPlanMapper", planMapper);
+        inject(svc, "taskMapper", mock(TaskMapper.class));
+
+        try {
+            svc.approveAndPublish(902L, 7L, null);
+        } catch (RuntimeException expected) {
+            // 同上:拦截即符合契约
+        }
+
+        // 未复核不得发布;已发布说明审核链路跳过了内容安全闸门
+        assertEquals("pending_review", plan.getStatus(),
+                "SC-T-28:审核链路必须先做内容安全复核,未通过前 plan 状态应保持 pending_review");
+    }
+
+    // ==================== helpers ====================
+
+    /** 类级 @RequestMapping 是否包含指定路径 */
+    private static boolean hasClassMapping(Class<?> clazz, String expected) {
+        return Arrays.stream(clazz.getDeclaredAnnotations())
+                .filter(a -> a instanceof RequestMapping)
+                .flatMap(a -> Arrays.stream(((RequestMapping) a).value()))
+                .anyMatch(expected::equals);
+    }
+
+    /** 指定方法是否为 POST + 指定路径 */
+    private static boolean hasPostMapping(Class<?> clazz, String methodName, String expectedPath) {
+        return Arrays.stream(clazz.getDeclaredMethods())
+                .filter(m -> m.getName().equals(methodName))
+                .filter(m -> m.isAnnotationPresent(PostMapping.class))
+                .flatMap(m -> Arrays.stream(m.getAnnotation(PostMapping.class).value()))
+                .anyMatch(expectedPath::equals);
+    }
+
+    /** 收集类上所有 POST 路径,用于失败信息可读性 */
+    private static String postPaths(Class<?> clazz) {
+        String[] paths = Arrays.stream(clazz.getDeclaredMethods())
+                .filter(m -> m.isAnnotationPresent(PostMapping.class))
+                .flatMap(m -> Arrays.stream(m.getAnnotation(PostMapping.class).value()))
+                .toArray(String[]::new);
+        return Arrays.toString(paths);
+    }
+
+    /** 沿类层次查找字段,判断是否存在类型暴露 checkText(String) 的依赖(内容安全闸门) */
+    private static boolean declaresContentSafetyDependency(Class<?> clazz) {
+        for (Class<?> c = clazz; c != null && c != Object.class; c = c.getSuperclass()) {
+            for (Field f : c.getDeclaredFields()) {
+                if (exposesCheckText(f.getType())) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /** 类型(接口/实现类)是否暴露 checkText(String) */
+    private static boolean exposesCheckText(Class<?> type) {
+        try {
+            type.getMethod("checkText", String.class);
+            return true;
+        } catch (NoSuchMethodException e) {
+            return false;
+        }
+    }
+
+    private static String tableName(Class<?> clazz) {
+        com.baomidou.mybatisplus.annotation.TableName annotation =
+                clazz.getAnnotation(com.baomidou.mybatisplus.annotation.TableName.class);
+        return annotation == null ? null : annotation.value();
+    }
+
+    private static HealthPlan healthPlan(Long id, String status, String planContent) {
+        HealthPlan p = new HealthPlan();
+        p.setId(id);
+        p.setFamilyId(7L);
+        p.setStatus(status);
+        p.setPlanContent(planContent);
+        p.setPlanJson("{\"sections\":[]}");
+        p.setCreatedAt(new Date());
+        p.setUpdatedAt(new Date());
+        return p;
+    }
+}

+ 445 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/SupplierManageAdminContractTest.java

@@ -0,0 +1,445 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.admin.AdminSupplierController;
+import com.etotem.cfc.controller.admin.EcomSupplierController;
+import com.etotem.cfc.controller.admin.SupplyManageController;
+import com.etotem.cfc.controller.admin.SupplierProductController;
+import com.etotem.cfc.dto.ProductCreateRequest;
+import com.etotem.cfc.entity.Product;
+import com.etotem.cfc.entity.SupplySystem;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.ProductService;
+import com.etotem.cfc.service.RealNameBackfillService;
+import com.etotem.cfc.service.SupplySystemService;
+import com.etotem.cfc.service.UserService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 契约级单测:管理端供应商管理(能力键 {@code admin.supplier.manage},TDD-RED 阶段)。
+ *
+ * <p><b>需求/设计溯源</b></p>
+ * <ul>
+ *   <li>场景:{@code tests/user-scenarios/应用场景总览.md} SC-A-06(供应商管理)、
+ *       SC-S-01(入驻申请)、SC-S-02(商品管理上架/编辑);</li>
+ *   <li>用例:{@code tests/test-cases/vendor.md} TC-S-01~TC-S-02-E;</li>
+ *   <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.13 + §4.60.6 + §4.62.1 + §4.62.2;</li>
+ *   <li>任务登记:sc-key-map.md「SC-A-06 / SC-S-01 / SC-S-02 → admin.supplier.manage → S-cfc-200」。</li>
+ * </ul>
+ *
+ * <p><b>被校验的契约行为</b></p>
+ * <ol>
+ *   <li><b>正常流(结构锚点)</b>:
+ *       {@code AdminSupplierController} /api/admin/supplier(list/save/update/delete)、
+ *       {@code EcomSupplierController} /api/admin/ecom-supplier(list/all/save/update/delete)、
+ *       {@code SupplyManageController} /api/admin/supply-manage(list/detail/create/update/delete)
+ *       均应暴露约定 POST 端点。</li>
+ *   <li><b>异常流 / 边界流(TC-S-01-E / SC-A-06 / SC-S-02-E)</b>:
+ *       <ul>
+ *         <li>AdminSupplierController / EcomSupplierController 当前均为 <b>410 废弃壳</b>
+ *             (「该接口已废弃,请使用 /api/admin/supply-system/*」),
+ *             契约要求此路径下的 CRUD 提供真实功能而非 410;</li>
+ *         <li>SupplyManageController.delete 缺乏「有在售商品则拒绝删除/停用」门禁(SC-A-06 边界:
+ *             有在售商品的供应商停用需先下架);</li>
+ *         <li>SupplyManageController.create 对无完整资质的申请仍自动设为
+ *             {@code vendorStatus=approved},违反 SC-S-01 的「提交资质 → 平台审核 → 入驻成功」流转
+ *             (资质不全应驳回,不得自动通过);</li>
+ *         <li>SupplierProductController.create 未拦截负价格/负库存(TC-S-02-E),产品经
+ *             {@code ProductService.adminCreate} 直接以 {@code on_shelf} 状态落库。</li>
+ *       </ul></li>
+ *   <li><b>权限流</b>:三个控制器方法均未带 {@code @RequestAttribute("role")} 守卫(对标
+ *       {@code AdminAuditController}/{@code AdminArticleController} 模式),非 admin 无法 403 拒绝
+ *       (TC-S-01-P / SC-A-06-P)。</li>
+ * </ol>
+ *
+ * <p><b>RED 预期</b>:结构锚点用例通过(绿,基线);异常/边界/权限用例断言失败(红),
+ * 断言原文清晰标注契约所需的「非 410/CURD 落地/业务门禁/role 守卫」缺失。
+ * GREEN 阶段实现后本测试自动转绿。
+ *
+ * <p><b>测试范围</b>:纯 Mockito + 反射,无 Spring 上下文 / 无 DB / 无外部服务。</p>
+ *
+ * @see <a href="https://git.iwintrue.com/cfc/superpowers/-/wikis/tdd/contract-tests">TDD 契约测试规范</a>
+ */
+@SuppressWarnings("unchecked")
+class SupplierManageAdminContractTest extends EnergyContractTestBase {
+
+    // ==================== 被校验对象 ====================
+
+    private SupplyManageController supplyManageController;
+    @Mock
+    private UserMapper userMapper;
+    @Mock
+    private UserService userService;
+    @Mock
+    private RealNameBackfillService realNameBackfillService;
+
+    private AdminSupplierController adminSupplierController;
+    private EcomSupplierController ecomSupplierController;
+
+    private SupplierProductController supplierProductController;
+    @Mock
+    private SupplySystemService supplySystemService;
+    @Mock
+    private ProductService productService;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        // MyBatis-Plus TableInfo 预注册:使 LambdaQueryWrapper 在无 Spring 上下文中可解析列名
+        initTable(User.class);
+
+        MockitoAnnotations.openMocks(this);
+
+        // --- SupplyManageController ---
+        supplyManageController = new SupplyManageController();
+        inject(supplyManageController, "userMapper", userMapper);
+        inject(supplyManageController, "userService", userService);
+        inject(supplyManageController, "realNameBackfillService", realNameBackfillService);
+
+        // --- AdminSupplierController / EcomSupplierController ---
+        adminSupplierController = new AdminSupplierController();
+        ecomSupplierController = new EcomSupplierController();
+
+        // --- SupplierProductController ---
+        supplierProductController = new SupplierProductController();
+        inject(supplierProductController, "supplySystemService", supplySystemService);
+        inject(supplierProductController, "productService", productService);
+    }
+
+    // ==================== 正常流(结构锚点,SC-A-06 + SC-S-01) ====================
+
+    @Test
+    @DisplayName("SC-A-06 正常流(结构锚点):SupplyManageController 暴露 list/detail/create/update/delete")
+    void supplyManage_exposesRequiredEndpoints() {
+        assertTrue(hasEndpoint(SupplyManageController.class, "/list"),
+                "RED[admin.supplier.manage] SupplyManageController 应暴露 POST /api/admin/supply-manage/list");
+        assertTrue(hasEndpoint(SupplyManageController.class, "/detail"),
+                "RED[admin.supplier.manage] SupplyManageController 应暴露 POST /api/admin/supply-manage/detail");
+        assertTrue(hasEndpoint(SupplyManageController.class, "/create"),
+                "RED[admin.supplier.manage] SupplyManageController 应暴露 POST /api/admin/supply-manage/create");
+        assertTrue(hasEndpoint(SupplyManageController.class, "/update"),
+                "RED[admin.supplier.manage] SupplyManageController 应暴露 POST /api/admin/supply-manage/update");
+        assertTrue(hasEndpoint(SupplyManageController.class, "/delete"),
+                "RED[admin.supplier.manage] SupplyManageController 应暴露 POST /api/admin/supply-manage/delete");
+    }
+
+    @Test
+    @DisplayName("SC-A-06 正常流(结构锚点):AdminSupplierController 暴露 list/save/update/delete")
+    void adminSupplier_exposesRequiredEndpoints() {
+        assertTrue(hasEndpoint(AdminSupplierController.class, "/list"),
+                "RED[admin.supplier.manage] AdminSupplierController 应暴露 POST /api/admin/supplier/list");
+        assertTrue(hasEndpoint(AdminSupplierController.class, "/save"),
+                "RED[admin.supplier.manage] AdminSupplierController 应暴露 POST /api/admin/supplier/save");
+        assertTrue(hasEndpoint(AdminSupplierController.class, "/update"),
+                "RED[admin.supplier.manage] AdminSupplierController 应暴露 POST /api/admin/supplier/update");
+        assertTrue(hasEndpoint(AdminSupplierController.class, "/delete"),
+                "RED[admin.supplier.manage] AdminSupplierController 应暴露 POST /api/admin/supplier/delete");
+    }
+
+    @Test
+    @DisplayName("SC-S-01 正常流(结构锚点):EcomSupplierController 暴露 list/all/save/update/delete")
+    void ecomSupplier_exposesRequiredEndpoints() {
+        assertTrue(hasEndpoint(EcomSupplierController.class, "/list"),
+                "RED[admin.supplier.manage] EcomSupplierController 应暴露 POST /api/admin/ecom-supplier/list");
+        assertTrue(hasEndpoint(EcomSupplierController.class, "/all"),
+                "RED[admin.supplier.manage] EcomSupplierController 应暴露 POST /api/admin/ecom-supplier/all");
+        assertTrue(hasEndpoint(EcomSupplierController.class, "/save"),
+                "RED[admin.supplier.manage] EcomSupplierController 应暴露 POST /api/admin/ecom-supplier/save");
+        assertTrue(hasEndpoint(EcomSupplierController.class, "/update"),
+                "RED[admin.supplier.manage] EcomSupplierController 应暴露 POST /api/admin/ecom-supplier/update");
+        assertTrue(hasEndpoint(EcomSupplierController.class, "/delete"),
+                "RED[admin.supplier.manage] EcomSupplierController 应暴露 POST /api/admin/ecom-supplier/delete");
+    }
+
+    // ==================== 异常流(SC-A-06):AdminSupplierController 不应是 410 废弃壳 ====================
+
+    @Test
+    @DisplayName("SC-A-06 异常:AdminSupplierController.list 应提供真实功能,非 410 废弃壳")
+    void adminSupplier_list_notDeprecated410() {
+        Result<?> r = adminSupplierController.list(Collections.emptyMap());
+        assertNotNull(r);
+        assertFalse(isDeprecated410(r),
+                "RED[admin.supplier.manage] /api/admin/supplier/list 应提供真实功能(SC-A-06);"
+                        + "当前返回 code=" + r.getCode()
+                        + "(410 废弃壳)。契约要求实现真实 CRUD 而非仅返回 410 提示转向 supply-system。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06 异常:AdminSupplierController.save 应提供真实功能,非 410 废弃壳")
+    void adminSupplier_save_notDeprecated410() {
+        Result<?> r = adminSupplierController.save(Collections.emptyMap());
+        assertNotNull(r);
+        assertFalse(isDeprecated410(r),
+                "RED[admin.supplier.manage] /api/admin/supplier/save 应提供真实功能(SC-A-06);"
+                        + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06 异常:AdminSupplierController.update 应提供真实功能,非 410 废弃壳")
+    void adminSupplier_update_notDeprecated410() {
+        Result<?> r = adminSupplierController.update(Collections.emptyMap());
+        assertNotNull(r);
+        assertFalse(isDeprecated410(r),
+                "RED[admin.supplier.manage] /api/admin/supplier/update 应提供真实功能(SC-A-06);"
+                        + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
+    }
+
+    @Test
+    @DisplayName("SC-A-06 异常:AdminSupplierController.delete 应提供真实功能,非 410 废弃壳")
+    void adminSupplier_delete_notDeprecated410() {
+        Result<?> r = adminSupplierController.delete(Collections.emptyMap());
+        assertNotNull(r);
+        assertFalse(isDeprecated410(r),
+                "RED[admin.supplier.manage] /api/admin/supplier/delete 应提供真实功能(SC-A-06);"
+                        + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
+    }
+
+    // ==================== 异常流(SC-S-01):EcomSupplierController 不应是 410 废弃壳 ====================
+
+    @Test
+    @DisplayName("SC-S-01 异常:EcomSupplierController.list 应提供真实功能,非 410 废弃壳")
+    void ecomSupplier_list_notDeprecated410() {
+        Result<?> r = ecomSupplierController.list(Collections.emptyMap());
+        assertNotNull(r);
+        assertFalse(isDeprecated410(r),
+                "RED[admin.supplier.manage] /api/admin/ecom-supplier/list 应提供真实功能(SC-S-01 入驻申请载体);"
+                        + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
+    }
+
+    @Test
+    @DisplayName("SC-S-01 异常:EcomSupplierController.all 应提供真实功能,非 410 废弃壳")
+    void ecomSupplier_all_notDeprecated410() {
+        Result<?> r = ecomSupplierController.all();
+        assertNotNull(r);
+        assertFalse(isDeprecated410(r),
+                "RED[admin.supplier.manage] /api/admin/ecom-supplier/all 应提供真实功能(SC-S-01 下拉选择载体);"
+                        + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
+    }
+
+    @Test
+    @DisplayName("SC-S-01 异常:EcomSupplierController.save 应提供真实功能,非 410 废弃壳(入驻申请提交端口)")
+    void ecomSupplier_save_notDeprecated410() {
+        // 入驻申请核心写入口;空参即可触达契约门(契约关注端点不被 410 阻塞)
+        Result<?> r = ecomSupplierController.save(null);
+        assertNotNull(r);
+        assertFalse(isDeprecated410(r),
+                "RED[admin.supplier.manage] /api/admin/ecom-supplier/save 应提供真实功能(SC-S-01 入驻申请提交);"
+                        + "当前返回 code=" + r.getCode() + "(410 废弃壳)。"
+                        + "TC-S-01 将此处登记为入驻申请的正式写入端点。");
+    }
+
+    @Test
+    @DisplayName("SC-S-01 异常:EcomSupplierController.update 应提供真实功能,非 410 废弃壳")
+    void ecomSupplier_update_notDeprecated410() {
+        Result<?> r = ecomSupplierController.update(null);
+        assertNotNull(r);
+        assertFalse(isDeprecated410(r),
+                "RED[admin.supplier.manage] /api/admin/ecom-supplier/update 应提供真实功能(SC-S-01);"
+                        + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
+    }
+
+    @Test
+    @DisplayName("SC-S-01 异常:EcomSupplierController.delete 应提供真实功能,非 410 废弃壳")
+    void ecomSupplier_delete_notDeprecated410() {
+        Result<?> r = ecomSupplierController.delete(Collections.emptyMap());
+        assertNotNull(r);
+        assertFalse(isDeprecated410(r),
+                "RED[admin.supplier.manage] /api/admin/ecom-supplier/delete 应提供真实功能(SC-S-01);"
+                        + "当前返回 code=" + r.getCode() + "(410 废弃壳)。");
+    }
+
+    // ==================== 边界流(SC-A-06):有在售商品的供应商停用/删除需先下架 ====================
+
+    @Test
+    @DisplayName("SC-A-06 边界:SupplyManageController.delete 对有在售商品的供应商应拒绝(当前未做前置校验)")
+    void supplyManage_delete_onSaleSupplier_rejected() {
+        // 模拟「有在售商品的供应商」:vendorType=product_supplier 且 vendorStatus=approved
+        User supplier = new User();
+        supplier.setId(1L);
+        supplier.setPhone("13800001001");
+        supplier.setVendorType("product_supplier");
+        supplier.setVendorStatus("approved");
+
+        when(userMapper.selectById(1L)).thenReturn(supplier);
+        when(userMapper.deleteById(1L)).thenReturn(1);
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("id", 1L);
+        Result<Boolean> result = supplyManageController.delete(body);
+
+        // 契约要求:有在售商品的供应商删除前应先下架,否则应拒收;当前实现无条件 deleteById
+        assertTrue(result.getCode() != 200,
+                "RED[admin.supplier.manage] 有在售商品的供应商 delete 应被拒绝(SC-A-06 边界:"
+                        + "有在售商品的供应商停用需先下架);当前实现无条件执行 deleteById 返回 code="
+                        + result.getCode());
+    }
+
+    // ==================== 边界流(SC-S-01):入驻申请资质不全应驳回,不得自动通过 ====================
+
+    @Test
+    @DisplayName("SC-S-01 边界:SupplyManageController.create 无资质材料不应自动 vendorStatus=approved")
+    void supplyManage_create_notAutoApprove() {
+        // 手机号未占用
+        when(userMapper.selectOne(any())).thenReturn(null);
+        when(userMapper.insert(any())).thenAnswer(inv -> {
+            Object u = inv.getArgument(0);
+            ((User) u).setId(999L);
+            return 1;
+        });
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("phone", "13800002002");
+        Result<User> result = supplyManageController.create(body);
+
+        assertNotNull(result.getData(),
+                "RED[admin.supplier.manage] create 应返回创建后的用户实体;当前 data=" + result.getData());
+        assertFalse("approved".equals(result.getData().getVendorStatus()),
+                "RED[admin.supplier.manage] 未经验核的入驻申请应保持待审状态(如 pending),"
+                        + "不得 vendorStatus=approved(SC-S-01:提交资质 → 平台审核 → 入驻成功);"
+                        + "当前实现直接 approve,违背审核流契约。");
+    }
+
+    // ==================== 边界流(SC-S-02):负价格/负库存拦截 ====================
+
+    @Test
+    @DisplayName("SC-S-02 边界:SupplierProductController.create 负库存应 400 拦截,不落库")
+    void supplierProduct_create_negativeStock_rejected() {
+        stubSupplierProductContext(makeSystem(1L, "体系A"));
+
+        Product p = new Product();
+        p.setName("负库存测试商品");
+        p.setStock(-5);
+        p.setPrice(100);
+        when(productService.adminCreate(any(), any())).thenReturn(Result.success(null));
+
+        ProductCreateRequest req = new ProductCreateRequest();
+        req.setProduct(p);
+
+        Result<?> result = supplierProductController.create(req, 1L);
+
+        assertTrue(result.getCode() == 400 || result.getCode() < 200,
+                "RED[admin.supplier.manage] 负库存商品 (-5) 创建应 400 拦截(TC-S-02-E),"
+                        + "当前透传至 adminCreate 直接落库(code=" + result.getCode() + ");"
+                        + "字段合法性校验应在 Controller 层前置拦截。");
+        verify(productService, never()).adminCreate(any(), any());
+    }
+
+    @Test
+    @DisplayName("SC-S-02 边界:SupplierProductController.create 负价格应 400 拦截,不落库")
+    void supplierProduct_create_negativePrice_rejected() {
+        stubSupplierProductContext(makeSystem(1L, "体系A"));
+
+        Product p = new Product();
+        p.setName("负价格测试商品");
+        p.setStock(10);
+        p.setPrice(-100);
+        when(productService.adminCreate(any(), any())).thenReturn(Result.success(null));
+
+        ProductCreateRequest req = new ProductCreateRequest();
+        req.setProduct(p);
+
+        Result<?> result = supplierProductController.create(req, 1L);
+
+        assertTrue(result.getCode() == 400 || result.getCode() < 200,
+                "RED[admin.supplier.manage] 负价格商品 (-100 分) 创建应 400 拦截(TC-S-02-E),"
+                        + "当前透传至 adminCreate 直接落库(code=" + result.getCode() + ");"
+                        + "字段合法性校验应在 Controller 层前置拦截。");
+        verify(productService, never()).adminCreate(any(), any());
+    }
+
+    // ==================== 权限流(TC-S-01-P / SC-A-06-P):admin 角色守卫 ====================
+
+    @Test
+    @DisplayName("TC-S-01-P/SC-A-06-P 权限:AdminSupplierController 应带 @RequestAttribute(\"role\") 守卫")
+    void adminSupplier_hasRoleGuard() {
+        assertTrue(hasRoleGuard(AdminSupplierController.class),
+                "RED[admin.supplier.manage] AdminSupplierController 方法应带"
+                        + " @RequestAttribute(\"role\") 守卫,非 admin 操作 403(SC-A-06 管理员接口约束);"
+                        + "当前方法均无 role 参数(对标 AdminAuditController/AdminArticleController 模式)。");
+    }
+
+    @Test
+    @DisplayName("TC-S-01-P/SC-A-06-P 权限:EcomSupplierController 应带 @RequestAttribute(\"role\") 守卫")
+    void ecomSupplier_hasRoleGuard() {
+        assertTrue(hasRoleGuard(EcomSupplierController.class),
+                "RED[admin.supplier.manage] EcomSupplierController 方法应带"
+                        + " @RequestAttribute(\"role\") 守卫,非 admin 操作 403(SC-S-01 / SC-A-06 管理员接口约束);"
+                        + "当前方法均无 role 参数。");
+    }
+
+    @Test
+    @DisplayName("TC-S-01-P/SC-A-06-P 权限:SupplyManageController 应带 @RequestAttribute(\"role\") 守卫")
+    void supplyManage_hasRoleGuard() {
+        assertTrue(hasRoleGuard(SupplyManageController.class),
+                "RED[admin.supplier.manage] SupplyManageController 方法应带"
+                        + " @RequestAttribute(\"role\") 守卫,非 admin 操作 403(SC-A-06 管理员接口约束);"
+                        + "当前方法均无 role 参数(对标 AdminAuditController/AdminArticleController 模式)。");
+    }
+
+    // ==================== 辅助构造 ====================
+
+    private void stubSupplierProductContext(SupplySystem system) {
+        when(supplySystemService.getByAdminId(any())).thenReturn(system);
+    }
+
+    private SupplySystem makeSystem(Long id, String name) {
+        SupplySystem s = new SupplySystem();
+        s.setId(id);
+        s.setName(name);
+        return s;
+    }
+
+    // ==================== 反射辅助 ====================
+
+    /** 控制器是否暴露指定 POST 路径端点 */
+    private boolean hasEndpoint(Class<?> controller, String path) {
+        for (java.lang.reflect.Method m : controller.getDeclaredMethods()) {
+            PostMapping pm = m.getAnnotation(PostMapping.class);
+            if (pm != null) {
+                for (String v : pm.value()) {
+                    if (path.equals(v)) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /** 控制器是否含 @RequestAttribute("role") 参数守卫 */
+    private boolean hasRoleGuard(Class<?> controller) {
+        for (java.lang.reflect.Method m : controller.getDeclaredMethods()) {
+            for (java.lang.reflect.Parameter p : m.getParameters()) {
+                RequestAttribute ra = p.getAnnotation(RequestAttribute.class);
+                if (ra != null && "role".equals(ra.value())) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /** 结果是否为 410 废弃码 */
+    private boolean isDeprecated410(Result<?> r) {
+        return r != null && Integer.valueOf(410).equals(r.getCode());
+    }
+}

+ 153 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherAssessmentRecordContractTest.java

@@ -0,0 +1,153 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.entity.DanAssessmentResult;
+import com.etotem.cfc.mapper.DanAssessmentResultMapper;
+import com.etotem.cfc.service.AssessmentService;
+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.Modifier;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * 契约级单测:能力键 {@code teacher.assessment.record}(成长规划师域 EVD-10,SC-T-07 边界条款)。
+ *
+ * <p><b>需求 / 设计锚点</b></p>
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-T-07「录入测评结果触发方案 ⭐」——
+ *       边界:<b>必填缺失拦截;重复录入覆盖非新增</b>。</li>
+ *   <li>设计:{@code docs/superpowers/specs/2026-11-07-planner-zone-contract.md} §2 SC-T-07 /
+ *       §3 建议能力键映射 {@code teacher.assessment.record}(绑定 SC-T-07 / T-11 / T-12);
+ *       {@code docs/superpowers/api/API_REFERENCE.md} §4.57 SC-T-07 端点载体
+ *       {@code DanAssessmentController /api/dan-assessment/result/record}。</li>
+ *   <li>实现载体:{@code AssessmentService#recordResult(DanAssessmentResult)}
+ *       ({@code cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentService.java})。</li>
+ * </ul>
+ *
+ * <p><b>被校验的契约边界(SC-T-07)</b></p>
+ * <ul>
+ *   <li><b>必填缺失拦截</b>:录入测评结果时,{@code memberId}/{@code familyMemberId} 与
+ *       {@code dimension} 为必填;任一缺失应拦截并<b>不落库</b>(不调用 insert)。</li>
+ *   <li><b>重复录入覆盖非新增</b>:对同一成员(member)+ 同一维度(dimension)重复录入时,应
+ *       <b>覆盖(updateById)</b>既有记录,而非再新增一行(第二次录入不得走 insert)。</li>
+ * </ul>
+ *
+ * <p><b>测试范围</b>:纯 Mockito + 反射注入,无 Spring 上下文 / 无 DB / 无外部服务依赖,
+ * 沿用已合入主链同批 RED 契约测试风格(TeamProfitHookContractTest / FamilyJoinValidationRulesContractTest)。
+ *
+ * <p><b>RED 预期</b>:当前 {@code AssessmentService.recordResult} 既无必填校验、也无重复覆盖逻辑
+ * (对任何入参一律 {@code danAssessmentResultMapper.insert(result)})。以下边界断言在实现补齐前
+ * 均应失败(红);GREEN 阶段由实现方在 {@code recordResult} 补上「必填校验拦截」与
+ * 「同成员+同维度重复录入覆盖(updateById 而非 insert)」后自动转绿。</p>
+ */
+class TeacherAssessmentRecordContractTest {
+
+    private AssessmentService service;
+
+    private DanAssessmentResultMapper resultMapper;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        service = new AssessmentService();
+        // 反射注入全部非 static / 非 final 字段为 mock,隔离 DB / 外部服务;
+        // recordResult 主链仅触及 resultMapper / reportParseService / growthRecordService /
+        // dimensionScoreService / planGenerator / userMapper(其余仅在 parent_upload 分支触及)。
+        injectAll(service);
+        resultMapper = (DanAssessmentResultMapper) readField(service, "danAssessmentResultMapper");
+    }
+
+    // ==================== SC-T-07 边界一:必填缺失拦截 ====================
+
+    @Test
+    @DisplayName("SC-T-07 边界:memberId/familyMemberId 缺失应拦截且不落库(必填缺失拦截)")
+    void recordResult_missingMember_应拦截不落库() {
+        // 无 member 归属(memberId 与 familyMemberId 均为 null)→ 契约要求拦截,不得 insert
+        DanAssessmentResult result = plannerEntryResult();
+        result.setMemberId(null);
+        result.setFamilyMemberId(null);
+
+        service.recordResult(result);
+
+        verify(resultMapper, never()).insert(any(DanAssessmentResult.class));
+    }
+
+    @Test
+    @DisplayName("SC-T-07 边界:dimension 缺失应拦截且不落库(必填缺失拦截)")
+    void recordResult_missingDimension_应拦截不落库() {
+        // dimension 为必填(八类报告归属维度);缺失应拦截,不得 insert
+        DanAssessmentResult result = plannerEntryResult();
+        result.setDimension(null);
+
+        service.recordResult(result);
+
+        verify(resultMapper, never()).insert(any(DanAssessmentResult.class));
+    }
+
+    // ==================== SC-T-07 边界二:重复录入覆盖非新增 ====================
+
+    @Test
+    @DisplayName("SC-T-07 边界:同成员+同维度重复录入应覆盖(updateById)而非新增第二行 insert")
+    void recordResult_duplicateMemberDimension_应覆盖而非新增() {
+        // 同一成员 + 同一维度两次录入 → 第二次应走 updateById 覆盖,insert 仅允许一次(首次)
+        DanAssessmentResult first = plannerEntryResult();   // memberId=55, dimension=mind
+        DanAssessmentResult second = plannerEntryResult();  // 同 memberId=55, dimension=mind
+
+        service.recordResult(first);
+        service.recordResult(second);
+
+        // 首次录入允许 insert;重复录入不得再新增(覆盖语义)
+        verify(resultMapper, times(1)).insert(any(DanAssessmentResult.class));
+        // 覆盖语义:第二次录入必须触发 updateById
+        verify(resultMapper, atLeastOnce()).updateById(any(DanAssessmentResult.class));
+    }
+
+    // ==================== 结构锚点:契约载体存在(防假 RED 对照) ====================
+
+    @Test
+    @DisplayName("结构锚点:AssessmentService.recordResult 契约载体应存在")
+    void recordResult_契约载体存在() throws Exception {
+        assertNotNull(service, "[teacher.assessment.record] AssessmentService 实例应可创建");
+        // recordResult(DanAssessmentResult) 方法应存在于 AssessmentService(端点 /result/record 的服务宿主)
+        java.lang.reflect.Method m = AssessmentService.class.getMethod("recordResult", DanAssessmentResult.class);
+        assertNotNull(m, "[teacher.assessment.record] AssessmentService#recordResult 方法应存在");
+    }
+
+    // ==================== 辅助构造 / 反射 ====================
+
+    /** 构造一条规划师录入(SOURCE_PLANNER_ENTRY)结果:memberId=55 / dimension=mind */
+    private DanAssessmentResult plannerEntryResult() {
+        DanAssessmentResult r = new DanAssessmentResult();
+        r.setMemberId(55L);
+        r.setFamilyMemberId(55L);
+        r.setDimension("mind");
+        r.setFocusScore(80);
+        r.setSource(DanAssessmentResult.SOURCE_PLANNER_ENTRY);
+        return r;
+    }
+
+    /** 反射注入全部非 static / 非 final 字段为对应类型 mock */
+    private static void injectAll(Object target) throws Exception {
+        for (Field f : target.getClass().getDeclaredFields()) {
+            if (Modifier.isStatic(f.getModifiers()) || Modifier.isFinal(f.getModifiers())) {
+                continue;
+            }
+            f.setAccessible(true);
+            f.set(target, mock(f.getType()));
+        }
+    }
+
+    private static Object readField(Object target, String name) throws Exception {
+        Field f = target.getClass().getDeclaredField(name);
+        f.setAccessible(true);
+        return f.get(target);
+    }
+}

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

@@ -0,0 +1,249 @@
+package com.etotem.cfc.unit;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.assessment.AssessmentComparisonController;
+import com.etotem.cfc.controller.growth.GrowthPlanController;
+import com.etotem.cfc.controller.guide.GuideFamilyTaskController;
+import com.etotem.cfc.controller.tianpan.TianpanController;
+import com.etotem.cfc.entity.FamilyMember;
+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.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.web.bind.annotation.PostMapping;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 契约级单测:能力键 {@code teacher.family.overview}(SC-T-16 客户健康总览 / SC-T-17 成长计划进度跟踪 /
+ * SC-T-21 报告对比分析 / SC-T-27 家庭天盘解读)。
+ *
+ * <p>契约溯源:
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-T-16(客户健康总览)/ SC-T-17(成长计划进度跟踪)/
+ *       SC-T-21(报告对比分析)/ SC-T-27(家庭天盘解读·规划师);</li>
+ *   <li>设计:{@code docs/superpowers/specs/2026-11-07-planner-zone-contract.md} EVD-10 §2 端点核验表 +
+ *       §3 建议能力键映射({@code teacher.family.overview} ↔ T-16/17/21/27)+ §5 转测试侧重点 1(SC-T-13 数据隔离);</li>
+ *   <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.57 成长规划师域。</li>
+ * </ul></p>
+ *
+ * <p><b>端点锚点(origin/cfclub 现状核实,均已存在)</b>:
+ * <ul>
+ *   <li>SC-T-16:{@code GuideFamilyTaskController /api/guide/families/{familyId}/overview} + {@code /dashboard-stats};</li>
+ *   <li>SC-T-17:{@code GuideFamilyTaskController /{familyId}/children/{memberId}/tasks};
+ *       {@code GrowthPlanController /api/growth/plan/child/{memberId}/active};</li>
+ *   <li>SC-T-21:{@code AssessmentComparisonController /api/assessment/compare/*}(对比行为契约已由
+ *       {@code AssessmentComparisonContractTest} 覆盖,本类仅锚定端点存在);</li>
+ *   <li>SC-T-27:{@code TianpanController /api/tianpan/*}(天盘聚合/隔离行为契约已由
+ *       {@code TianpanGuideContractTest} 覆盖,本类仅锚定端点存在)。</li>
+ * </ul></p>
+ *
+ * <p><b>本类聚焦本键新增契约缺口(TDD-RED)</b>:SC-T-17 成长计划进度跟踪端点
+ * {@code GuideFamilyTaskController#getFamilyMemberTasks} 仅校验「规划师与 familyId 绑定」(requireBound),
+ * <b>未校验路径 {@code memberId} 是否属于该 {@code familyId}</b>——已绑定家庭 A 的规划师传入属于家庭 B 的
+ * {@code memberId} 即可跨家庭越权读取成员任务列表(IDOR),违反 EVD-10 §5 重点 1(SC-T-13 数据隔离)
+ * 与本键「客户健康总览/成长进度仅见绑定家庭成员」的契约。</p>
+ *
+ * <p><b>RED 预期</b>:跨家庭成员任务列表({@code memberId ∉ familyId})应被拒且不进入
+ * {@code taskService.getTaskHistoryByFamilyMember};当前实现缺成员归属校验 → 会调用 taskService →
+ * 断言失败(红)。GREEN 阶段补「memberId 属于 familyId」归属校验后转绿。</p>
+ *
+ * <p>纯 Mockito(无 Spring / 无 DB / 无外部服务),参照同批 RED 契约测试风格
+ * ({@code FamilyJoinValidationRulesContractTest}、{@code TeamProfitHookContractTest}、
+ * {@code GuideFamilyTaskReviewContractTest})。</p>
+ */
+class TeacherFamilyOverviewContractTest {
+
+    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;
+    /** 属于 BOUND_FAMILY_ID 的成员 */
+    private static final Long OWN_MEMBER_ID = 11L;
+    /** 属于 OTHER_FAMILY_ID 的成员(越权读取目标) */
+    private static final Long CROSS_MEMBER_ID = 22L;
+
+    @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);
+    }
+
+    // ==================== 正常流:端点锚点存在(SC-T-16/17/21/27,对照应绿) ====================
+
+    @Test
+    @DisplayName("正常流-1:SC-T-16/17 客户总览与成员任务端点应存在(GuideFamilyTaskController)")
+    void guideFamilyTaskController_锚点端点存在() {
+        assertEquals("/{familyId}/overview", postPath(GuideFamilyTaskController.class, "getFamilyTaskOverview"),
+                "[teacher.family.overview] SC-T-16 锚点 GET-style POST /{familyId}/overview 缺失(EVD-10 §2)。");
+        assertEquals("/dashboard-stats", postPath(GuideFamilyTaskController.class, "getDashboardStats"),
+                "[teacher.family.overview] SC-T-16 锚点 POST /dashboard-stats 缺失(EVD-10 §2)。");
+        assertEquals("/{familyId}/children/{memberId}/tasks", postPath(GuideFamilyTaskController.class, "getFamilyMemberTasks"),
+                "[teacher.family.overview] SC-T-17 锚点 POST /{familyId}/children/{memberId}/tasks 缺失(EVD-10 §2)。");
+    }
+
+    @Test
+    @DisplayName("正常流-2:SC-T-17 活跃成长方案端点应存在(GrowthPlanController)")
+    void growthPlanController_activePlan端点存在() {
+        assertEquals("/child/{memberId}/active", postPath(GrowthPlanController.class, "getActivePlan"),
+                "[teacher.family.overview] SC-T-17 锚点 POST /api/growth/plan/child/{memberId}/active 缺失(EVD-10 §2)。");
+    }
+
+    @Test
+    @DisplayName("正常流-3:SC-T-21 报告对比端点应存在(AssessmentComparisonController)")
+    void assessmentComparisonController_compare端点存在() {
+        assertEquals("/by-child", postPath(AssessmentComparisonController.class, "compareByChild"),
+                "[teacher.family.overview] SC-T-21 锚点 POST /api/assessment/compare/by-child 缺失(EVD-10 §2)。");
+        assertEquals("/by-child-dimension", postPath(AssessmentComparisonController.class, "compareByChildDimension"),
+                "[teacher.family.overview] SC-T-21 锚点 POST /by-child-dimension 缺失(EVD-10 §2)。");
+        assertEquals("/by-family", postPath(AssessmentComparisonController.class, "compareByFamily"),
+                "[teacher.family.overview] SC-T-21 锚点 POST /by-family 缺失(EVD-10 §2)。");
+        assertEquals("/narrative", postPath(AssessmentComparisonController.class, "getNarrative"),
+                "[teacher.family.overview] SC-T-21 锚点 POST /narrative 缺失(EVD-10 §2)。");
+    }
+
+    @Test
+    @DisplayName("正常流-4:SC-T-27 家庭天盘端点应存在(TianpanController)")
+    void tianpanController_dashboardMember端点存在() {
+        assertEquals("/dashboard", postPath(TianpanController.class, "dashboard"),
+                "[teacher.family.overview] SC-T-27 锚点 POST /api/tianpan/dashboard 缺失(EVD-10 §2)。");
+        assertEquals("/member/{memberId}", postPath(TianpanController.class, "memberDetail"),
+                "[teacher.family.overview] SC-T-27 锚点 POST /api/tianpan/member/{memberId} 缺失(EVD-10 §2)。");
+    }
+
+    // ==================== 正常流:角色 / 绑定守卫(对照,应绿) ====================
+
+    /**
+     * 非 teacher/admin 角色访问规划师专属端点 → 应在 role 检查处短路返回 Access denied,不查询任务。
+     */
+    @Test
+    @DisplayName("正常流-5:非 teacher 角色访问成员任务端点应被拒且不查询")
+    void getFamilyMemberTasks_非teacher角色_拒绝且不查询() {
+        Result<Page<Task>> result = controller.getFamilyMemberTasks(
+                GUIDE_ID, BOUND_FAMILY_ID, OWN_MEMBER_ID, "parent", new HashMap<>(), null);
+
+        assertEquals("Access denied", result.getMessage());
+        verify(taskService, never()).getTaskHistoryByFamilyMember(eq(OWN_MEMBER_ID), anyInt(), anyInt(), any());
+    }
+
+    /**
+     * 规划师访问非绑定家庭 {familyId} 端点 → requireBound 应拒(403),不查询任务(EVD-10 §5 重点 1 数据隔离)。
+     */
+    @Test
+    @DisplayName("正常流-6:规划师访问非绑定家庭总览应被拒(requireBound 403)")
+    void getFamilyTaskOverview_非绑定家庭_拒绝() {
+        when(guideFamilyAccessGuard.checkBinding(GUIDE_ID, OTHER_FAMILY_ID))
+                .thenReturn(Result.error(403, "非绑定家庭"));
+
+        Result<Map<String, Object>> result = controller.getFamilyTaskOverview(
+                GUIDE_ID, "teacher", OTHER_FAMILY_ID, null);
+
+        assertNotNull(result);
+        assertEquals(403, result.getCode());
+        verify(taskService, never()).getTodayTasks(any());
+    }
+
+    // ==================== 权限流(RED 契约缺口):跨家庭成员 IDOR ====================
+
+    /**
+     * SC-T-17 / EVD-10 §5 重点 1:规划师越权读取不属于已绑定家庭的成员任务 —— 接口应拒绝且不进入
+     * {@code taskService.getTaskHistoryByFamilyMember}。
+     *
+     * <p>缺口定位:{@code GuideFamilyTaskController#getFamilyMemberTasks} 当前仅校验 requireBound(guideId, familyId),
+     * 不校验 {@code memberId} 是否属于 {@code familyId},导致已绑定家庭 A 的规划师传入属于家庭 B 的 memberId
+     * 即可跨家庭越权读取成员任务(IDOR)。</p>
+     */
+    @Test
+    @DisplayName("权限流-1:跨家庭成员任务列表应被拒且不查询(RED:缺 memberId 归属校验)")
+    void getFamilyMemberTasks_跨家庭成员_应拒绝且不查询() {
+        // 成员存在但属于 OTHER_FAMILY_ID(跨家庭越权来源)
+        when(familyMemberMapper.selectById(CROSS_MEMBER_ID))
+                .thenReturn(member(CROSS_MEMBER_ID, OTHER_FAMILY_ID));
+        when(taskService.getTaskHistoryByFamilyMember(eq(CROSS_MEMBER_ID), anyInt(), anyInt(), any()))
+                .thenReturn(new Page<>());
+
+        Result<Page<Task>> result = controller.getFamilyMemberTasks(
+                GUIDE_ID, BOUND_FAMILY_ID, CROSS_MEMBER_ID, "teacher", new HashMap<>(), null);
+
+        // 契约:memberId 不属于路径 familyId → 不得进入任务查询(SC-T-13 数据隔离 / TC 权限流)
+        verify(taskService, never()).getTaskHistoryByFamilyMember(eq(CROSS_MEMBER_ID), anyInt(), anyInt(), any());
+        assertNotEquals(Integer.valueOf(200), result.getCode(), "跨家庭成员任务应被拒(非 200 成功响应)");
+    }
+
+    // ==================== 辅助 ====================
+
+    private FamilyMember member(Long id, Long familyId) {
+        FamilyMember m = new FamilyMember();
+        m.setId(id);
+        m.setFamilyId(familyId);
+        m.setNickname("契约测试成员");
+        return m;
+    }
+
+    /** 反射读取方法上的 @PostMapping 路径;方法不存在或未标注返回 null。 */
+    private String postPath(Class<?> clazz, String methodName) {
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals(methodName)) {
+                PostMapping pm = m.getAnnotation(PostMapping.class);
+                if (pm != null && pm.value().length > 0) {
+                    return pm.value()[0];
+                }
+                return "";
+            }
+        }
+        return null;
+    }
+}

+ 512 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherMessagePushContractTest.java

@@ -0,0 +1,512 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.UserNotificationController;
+import com.etotem.cfc.controller.guide.TeacherMessageController;
+import com.etotem.cfc.entity.TeacherMessage;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.TeacherMessageMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.PlannerCertificationGuard;
+import com.etotem.cfc.service.PlannerCertificationService;
+import com.etotem.cfc.service.TeacherMessageService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+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.message.push} —— 规划师→客户消息推送(SC-T-19)。
+ *
+ * <p><b>需求溯源</b></p>
+ * <ul>
+ *   <li>《应用场景总览》SC-T-19「消息推送客户」;sc-key-map 台账 SC-T-19(成长规划师·业务域 L)。</li>
+ *   <li>EVD-10 契约(77026df42)→ API 参考 §4.57 端点载体回填实测:
+ *       {@code TeacherMessageController /api/guide/messages/*}(send/reply/list/unread-count/my)
+ *       + {@code UserNotificationController /api/notification/*}。</li>
+ *   <li>API 参考 §5.4 接单闸门:{@code TeacherMessageController.reply} 需 {@code planner} 类别认证
+ *       ({@code PlannerCertificationGuard.assertCertified(teacherId, CATEGORY_PLANNER)})。</li>
+ * </ul>
+ *
+ * <p><b>被校验的契约行为</b></p>
+ * <ul>
+ *   <li>正常流:家庭主账号 {@code POST /api/guide/messages/send} 携带 familyId+teacherId+content
+ *       → 成功返回 messageId;规划师 {@code POST /api/guide/messages/{messageId}/reply}(teacher 角色 + 认证通过)
+ *       → 成功返回 true;{@code /list}(teacher 角色)返回分页 records/total;{@code /my} 回填规划师昵称;
+ *       {@code /unread-count}(teacher 角色)返回未读数。</li>
+ *   <li>异常流:{@code /send} 无 familyId → noFamily(code=5001)且不落库;缺 teacherId / 空 content
+ *       → error 且不落库;{@code /reply} 目标消息非本人(service 返回 false)→ 不写库。</li>
+ *   <li>权限流:{@code /reply}、{@code /list}、{@code /unread-count} 非 teacher 角色一律拒绝
+ *       ({@code RolesUtil.hasTeacherRole} 门禁);{@code /reply} 认证闸门拒绝时不调 service。</li>
+ *   <li>结构契约:Controller 端点映射、实体字段、schema.sql 建表。</li>
+ * </ul>
+ *
+ * <p><b>测试范围</b>:纯 Mockito / 反射,无 Spring 上下文 / 无 DB / 无外部服务。</p>
+ *
+ * <p><b>实现现状说明</b>:本契约端点载体在 src/main 已落地(TeacherMessageController /
+ * TeacherMessageService / TeacherMessage 实体 / teacher_messages 表 / UserNotificationController /
+ * PlannerCertificationGuard 均已存在),因此本类契约用例应全绿;
+ * 若编排侧判定为「契约新增能力(RED)」,则以本类为契约基准——任何端点缺失、角色门禁缺失、
+ * 认证闸门缺失、落库参数错误都会使对应用例转红。</p>
+ */
+class TeacherMessagePushContractTest {
+
+    private static final Long PARENT_ID = 1001L;
+    private static final Long FAMILY_ID = 2002L;
+    private static final Long TEACHER_ID = 3003L;
+    private static final Long MESSAGE_ID = 404L;
+    private static final String CONTENT = "孩子最近注意力不集中";
+    private static final String REPLY_CONTENT = "建议预约一次专注力测评";
+
+    private TeacherMessageController controller;
+    private TeacherMessageService service;
+    private TeacherMessageMapper mapper;
+    private UserMapper userMapper;
+    private PlannerCertificationGuard guard;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        controller = new TeacherMessageController();
+        service = new TeacherMessageService();
+        mapper = mock(TeacherMessageMapper.class);
+        userMapper = mock(UserMapper.class);
+        guard = mock(PlannerCertificationGuard.class);
+        inject(controller, "teacherMessageService", service);
+        inject(controller, "userMapper", userMapper);
+        inject(controller, "plannerCertificationGuard", guard);
+        inject(service, "teacherMessageMapper", mapper);
+    }
+
+    // ==================================================================
+    // 正常流:send(家庭主账号发送咨询)
+    // ==================================================================
+
+    @Test
+    @DisplayName("正常流:send 携带 familyId+teacherId+content → 成功返回 messageId 并落库")
+    void send_valid_persistsAndReturnsId() {
+        when(mapper.insert(any(TeacherMessage.class))).thenAnswer(inv -> {
+            TeacherMessage m = inv.getArgument(0);
+            m.setId(MESSAGE_ID);
+            return 1;
+        });
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("teacherId", TEACHER_ID);
+        body.put("content", CONTENT);
+        Result<Long> r = controller.sendMessage(PARENT_ID, FAMILY_ID, body);
+
+        assertEquals(200, r.getCode());
+        assertEquals(MESSAGE_ID, r.getData());
+        verify(mapper).insert(any(TeacherMessage.class));
+    }
+
+    @Test
+    @DisplayName("正常流:send 落库的消息初始状态为 pending、含创建时间、归属正确")
+    void send_persistedMessage_hasPendingStatusAndOwnership() {
+        final TeacherMessage[] captured = new TeacherMessage[1];
+        when(mapper.insert(any(TeacherMessage.class))).thenAnswer(inv -> {
+            TeacherMessage m = inv.getArgument(0);
+            captured[0] = m;
+            m.setId(MESSAGE_ID);
+            return 1;
+        });
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("teacherId", TEACHER_ID);
+        body.put("content", CONTENT);
+        controller.sendMessage(PARENT_ID, FAMILY_ID, body);
+
+        assertNotNull(captured[0]);
+        assertEquals(FAMILY_ID, captured[0].getFamilyId());
+        assertEquals(PARENT_ID, captured[0].getParentId());
+        assertEquals(TEACHER_ID, captured[0].getTeacherId());
+        assertEquals(CONTENT, captured[0].getContent());
+        assertEquals("pending", captured[0].getStatus());
+        assertNotNull(captured[0].getCreatedAt());
+    }
+
+    // ==================================================================
+    // 异常流:send 参数门禁
+    // ==================================================================
+
+    @Test
+    @DisplayName("异常流:send 无 familyId → noFamily(5001)且不落库")
+    void send_noFamily_rejectedNoPersist() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("teacherId", TEACHER_ID);
+        body.put("content", CONTENT);
+
+        Result<Long> r = controller.sendMessage(PARENT_ID, null, body);
+
+        assertEquals(5001, r.getCode());
+        verify(mapper, never()).insert(any(TeacherMessage.class));
+    }
+
+    @Test
+    @DisplayName("异常流:send 缺 teacherId → error 且不落库")
+    void send_missingTeacherId_rejectedNoPersist() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("content", CONTENT);
+
+        Result<Long> r = controller.sendMessage(PARENT_ID, FAMILY_ID, body);
+
+        assertEquals(500, r.getCode());
+        assertTrue(r.getMessage().contains("teacherId"));
+        verify(mapper, never()).insert(any(TeacherMessage.class));
+    }
+
+    @Test
+    @DisplayName("异常流:send 空 content → error 且不落库")
+    void send_blankContent_rejectedNoPersist() {
+        Map<String, Object> body = new HashMap<>();
+        body.put("teacherId", TEACHER_ID);
+        body.put("content", "   ");
+
+        Result<Long> r = controller.sendMessage(PARENT_ID, FAMILY_ID, body);
+
+        assertEquals(500, r.getCode());
+        assertTrue(r.getMessage().contains("内容"));
+        verify(mapper, never()).insert(any(TeacherMessage.class));
+    }
+
+    // ==================================================================
+    // 正常流:reply(规划师回复,§5.4 认证闸门)
+    // ==================================================================
+
+    @Test
+    @DisplayName("正常流:reply teacher 角色 + 认证通过 → 成功并调 service.replyMessage")
+    void reply_teacherCertified_succeeds() {
+        when(guard.assertCertified(TEACHER_ID, PlannerCertificationService.CATEGORY_PLANNER))
+                .thenReturn(null);
+        when(mapper.selectById(MESSAGE_ID)).thenReturn(buildMessage(TEACHER_ID));
+        when(mapper.updateById(any(TeacherMessage.class))).thenReturn(1);
+
+        Map<String, String> body = new HashMap<>();
+        body.put("replyContent", REPLY_CONTENT);
+        Result<Boolean> r = controller.replyMessage(TEACHER_ID, "teacher", MESSAGE_ID, body, null);
+
+        assertEquals(200, r.getCode());
+        assertTrue(Boolean.TRUE.equals(r.getData()));
+        verify(mapper).updateById(any(TeacherMessage.class));
+    }
+
+    @Test
+    @DisplayName("正常流:reply 认证通过后 service 将消息置为 replied、含回复时间")
+    void reply_marksRepliedWithTime() {
+        when(guard.assertCertified(TEACHER_ID, PlannerCertificationService.CATEGORY_PLANNER))
+                .thenReturn(null);
+        TeacherMessage msg = buildMessage(TEACHER_ID);
+        when(mapper.selectById(MESSAGE_ID)).thenReturn(msg);
+
+        Map<String, String> body = new HashMap<>();
+        body.put("replyContent", REPLY_CONTENT);
+        controller.replyMessage(TEACHER_ID, "teacher", MESSAGE_ID, body, null);
+
+        assertEquals("replied", msg.getStatus());
+        assertEquals(REPLY_CONTENT, msg.getReplyContent());
+        assertNotNull(msg.getRepliedAt());
+    }
+
+    // ==================================================================
+    // 异常流:reply 越权与消息归属
+    // ==================================================================
+
+    @Test
+    @DisplayName("异常流:reply 目标消息非本人(teacherId 不匹配)→ service 返回 false 且不写库")
+    void reply_notOwnedMessage_returnsFalseNoPersist() {
+        when(guard.assertCertified(TEACHER_ID, PlannerCertificationService.CATEGORY_PLANNER))
+                .thenReturn(null);
+        // 消息归属另一个规划师
+        when(mapper.selectById(MESSAGE_ID)).thenReturn(buildMessage(9999L));
+
+        Map<String, String> body = new HashMap<>();
+        body.put("replyContent", REPLY_CONTENT);
+        Result<Boolean> r = controller.replyMessage(TEACHER_ID, "teacher", MESSAGE_ID, body, null);
+
+        assertEquals(200, r.getCode());
+        assertFalse(Boolean.TRUE.equals(r.getData()));
+        verify(mapper, never()).updateById(any(TeacherMessage.class));
+    }
+
+    @Test
+    @DisplayName("异常流:reply 目标消息不存在 → service 返回 false 且不写库")
+    void reply_missingMessage_returnsFalseNoPersist() {
+        when(guard.assertCertified(TEACHER_ID, PlannerCertificationService.CATEGORY_PLANNER))
+                .thenReturn(null);
+        when(mapper.selectById(MESSAGE_ID)).thenReturn(null);
+
+        Map<String, String> body = new HashMap<>();
+        body.put("replyContent", REPLY_CONTENT);
+        Result<Boolean> r = controller.replyMessage(TEACHER_ID, "teacher", MESSAGE_ID, body, null);
+
+        assertFalse(Boolean.TRUE.equals(r.getData()));
+        verify(mapper, never()).updateById(any(TeacherMessage.class));
+    }
+
+    // ==================================================================
+    // 权限流:非 teacher 角色拒绝 / 认证闸门拒绝
+    // ==================================================================
+
+    @Test
+    @DisplayName("权限流:reply 非 teacher 角色 → error『只有成长规划师可以回复』且不调闸门/不落库")
+    void reply_nonTeacherRole_rejected() {
+        Map<String, String> body = new HashMap<>();
+        body.put("replyContent", REPLY_CONTENT);
+
+        Result<Boolean> r = controller.replyMessage(TEACHER_ID, "parent", MESSAGE_ID, body, null);
+
+        assertEquals(500, r.getCode());
+        assertTrue(r.getMessage().contains("只有成长规划师"));
+        verify(guard, never()).assertCertified(anyLong(), anyString());
+        verify(mapper, never()).updateById(any(TeacherMessage.class));
+    }
+
+    @Test
+    @DisplayName("权限流:reply 认证闸门拒绝 → 透传闸门 code/message 且不调 service")
+    void reply_guardDenied_propagatesAndSkipsService() {
+        when(guard.assertCertified(TEACHER_ID, PlannerCertificationService.CATEGORY_PLANNER))
+                .thenReturn(Result.error(403, "规划师未完成成长规划师基础认证,暂不可提供服务"));
+
+        Map<String, String> body = new HashMap<>();
+        body.put("replyContent", REPLY_CONTENT);
+        Result<Boolean> r = controller.replyMessage(TEACHER_ID, "teacher", MESSAGE_ID, body, null);
+
+        assertEquals(403, r.getCode());
+        assertTrue(r.getMessage().contains("规划师未完成"));
+        verify(mapper, never()).selectById(anyLong());
+        verify(mapper, never()).updateById(any(TeacherMessage.class));
+    }
+
+    @Test
+    @DisplayName("权限流:list 非 teacher 角色 → error『只有成长规划师可以查看』且不调 service")
+    void list_nonTeacherRole_rejected() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("page", 1);
+        params.put("size", 10);
+
+        Result<?> r = controller.getMessages(TEACHER_ID, "parent", params, null);
+
+        assertEquals(500, r.getCode());
+        assertTrue(r.getMessage().contains("只有成长规划师"));
+        verify(mapper, never()).selectPage(any(), any());
+    }
+
+    @Test
+    @DisplayName("权限流:unread-count 非 teacher 角色 → error 且不调 service")
+    void unreadCount_nonTeacherRole_rejected() {
+        Result<Long> r = controller.getUnreadCount(TEACHER_ID, "parent", null);
+
+        assertEquals(500, r.getCode());
+        verify(mapper, never()).selectCount(any());
+    }
+
+    // ==================================================================
+    // 正常流:list / unread-count / my
+    // ==================================================================
+
+    @Test
+    @DisplayName("正常流:list teacher 角色 → 返回 records/total 分页结构")
+    void list_teacherRole_returnsPagedStructure() {
+        com.baomidou.mybatisplus.extension.plugins.pagination.Page<TeacherMessage> page =
+                new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(1, 10);
+        page.setRecords(Collections.singletonList(buildMessage(TEACHER_ID)));
+        page.setTotal(1);
+        when(mapper.selectPage(any(), any())).thenReturn(page);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("page", 1);
+        params.put("size", 10);
+
+        Result<?> r = controller.getMessages(TEACHER_ID, "teacher", params, null);
+
+        assertEquals(200, r.getCode());
+        assertNotNull(r.getData());
+        Map<?, ?> data = (Map<?, ?>) r.getData();
+        assertTrue(data.containsKey("records"));
+        assertTrue(data.containsKey("total"));
+        assertTrue(data.containsKey("pages"));
+        assertTrue(data.containsKey("current"));
+    }
+
+    @Test
+    @DisplayName("正常流:unread-count teacher 角色 → 返回未读数")
+    void unreadCount_teacherRole_returnsCount() {
+        when(mapper.selectCount(any())).thenReturn(3L);
+
+        Result<Long> r = controller.getUnreadCount(TEACHER_ID, "teacher", null);
+
+        assertEquals(200, r.getCode());
+        assertEquals(3L, r.getData());
+    }
+
+    @Test
+    @DisplayName("正常流:my → 回填规划师昵称(teacherName)")
+    void my_fillsTeacherName() {
+        TeacherMessage msg = buildMessage(TEACHER_ID);
+        when(mapper.selectList(any())).thenReturn(Collections.singletonList(msg));
+        User teacher = new User();
+        teacher.setId(TEACHER_ID);
+        teacher.setNickname("王老师");
+        when(userMapper.selectById(TEACHER_ID)).thenReturn(teacher);
+
+        Result<?> r = controller.getMyMessages(PARENT_ID);
+
+        assertEquals(200, r.getCode());
+        assertNotNull(r.getData());
+        assertNotNull(((java.util.List<?>) r.getData()).get(0));
+        TeacherMessage m = (TeacherMessage) ((java.util.List<?>) r.getData()).get(0);
+        assertEquals("王老师", m.getTeacherName());
+    }
+
+    // ==================================================================
+    // 结构契约:端点映射 / 实体字段 / 建表
+    // ==================================================================
+
+    @Test
+    @DisplayName("结构契约:TeacherMessageController 暴露 send/reply/list/unread-count/my 五个端点")
+    void controller_exposesContractEndpoints() {
+        Map<String, Boolean> found = new HashMap<>();
+        found.put("send", false);
+        found.put("reply", false);
+        found.put("list", false);
+        found.put("unread-count", false);
+        found.put("my", false);
+        for (Method m : TeacherMessageController.class.getDeclaredMethods()) {
+            PostMapping pm = m.getAnnotation(PostMapping.class);
+            if (pm == null) {
+                continue;
+            }
+            for (String p : pm.value()) {
+                for (String key : found.keySet()) {
+                    if (p != null && p.contains(key)) {
+                        found.put(key, true);
+                    }
+                }
+            }
+        }
+        for (Map.Entry<String, Boolean> e : found.entrySet()) {
+            assertTrue(e.getValue(),
+                    "[teacher.message.push] TeacherMessageController 缺少端点包含 '" + e.getKey()
+                            + "':契约 §4.57 SC-T-19 要求 send/reply/list/unread-count/my 五端点。");
+        }
+    }
+
+    @Test
+    @DisplayName("结构契约:UserNotificationController 暴露 /api/notification/* 基础路径 + list/read")
+    void notificationController_exposesContractEndpoints() {
+        RequestMapping base = UserNotificationController.class.getAnnotation(RequestMapping.class);
+        assertNotNull(base, "[teacher.message.push] UserNotificationController 缺少 @RequestMapping。");
+        assertTrue(base.value()[0].equals("/api/notification"),
+                "[teacher.message.push] UserNotificationController 基础路径应为 /api/notification,实际 "
+                        + base.value()[0]);
+
+        boolean hasList = false;
+        boolean hasRead = false;
+        for (Method m : UserNotificationController.class.getDeclaredMethods()) {
+            PostMapping pm = m.getAnnotation(PostMapping.class);
+            if (pm == null) {
+                continue;
+            }
+            for (String p : pm.value()) {
+                if (p != null && p.contains("list")) {
+                    hasList = true;
+                }
+                if (p != null && p.contains("read")) {
+                    hasRead = true;
+                }
+            }
+        }
+        assertTrue(hasList, "[teacher.message.push] UserNotificationController 缺少 /list 端点。");
+        assertTrue(hasRead, "[teacher.message.push] UserNotificationController 缺少 /read 端点。");
+    }
+
+    @Test
+    @DisplayName("结构契约:TeacherMessage 实体应承载消息推送核心字段")
+    void entity_carriesContractFields() {
+        assertTrue(hasField("familyId"), "[teacher.message.push] TeacherMessage 缺 familyId。");
+        assertTrue(hasField("parentId"), "[teacher.message.push] TeacherMessage 缺 parentId。");
+        assertTrue(hasField("teacherId"), "[teacher.message.push] TeacherMessage 缺 teacherId。");
+        assertTrue(hasField("content"), "[teacher.message.push] TeacherMessage 缺 content。");
+        assertTrue(hasField("replyContent"), "[teacher.message.push] TeacherMessage 缺 replyContent。");
+        assertTrue(hasField("status"), "[teacher.message.push] TeacherMessage 缺 status(pending/replied)。");
+        assertTrue(hasField("createdAt"), "[teacher.message.push] TeacherMessage 缺 createdAt。");
+        assertTrue(hasField("repliedAt"), "[teacher.message.push] TeacherMessage 缺 repliedAt。");
+    }
+
+    @Test
+    @DisplayName("结构契约:schema.sql 应落地 teacher_messages 与 user_notifications 建表")
+    void schemaSql_containsContractTables() {
+        String schema = readSchemaSql();
+        assertTrue(schema.contains("teacher_messages"),
+                "[teacher.message.push] schema.sql 缺少 teacher_messages 建表(§4.57 SC-T-19)。");
+        assertTrue(schema.contains("user_notifications"),
+                "[teacher.message.push] schema.sql 缺少 user_notifications 建表(§4.57 通知载体)。");
+    }
+
+    // ==================================================================
+    // 辅助构造
+    // ==================================================================
+
+    private TeacherMessage buildMessage(Long teacherId) {
+        TeacherMessage m = new TeacherMessage();
+        m.setId(MESSAGE_ID);
+        m.setFamilyId(FAMILY_ID);
+        m.setParentId(PARENT_ID);
+        m.setTeacherId(teacherId);
+        m.setContent(CONTENT);
+        m.setStatus("pending");
+        return m;
+    }
+
+    private void inject(Object target, String fieldName, Object value) throws Exception {
+        Field f = target.getClass().getDeclaredField(fieldName);
+        f.setAccessible(true);
+        f.set(target, value);
+    }
+
+    private boolean hasField(String name) {
+        for (Field f : TeacherMessage.class.getDeclaredFields()) {
+            if (f.getName().equals(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    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 "";
+        }
+    }
+}

+ 313 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherPlanCreateContractTest.java

@@ -0,0 +1,313 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.growth.GrowthPlanController;
+import com.etotem.cfc.controller.guide.TrainingPlanController;
+import com.etotem.cfc.entity.GrowthPlan;
+import com.etotem.cfc.service.GrowthPlanService;
+import com.etotem.cfc.service.TrainingPlanService;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+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.plan.create}(SC-T-08 成长计划 / 套餐创建 · SC-T-14 方案编辑规划师侧)。
+ *
+ * <p><b>契约锚点</b></p>
+ * <ul>
+ *   <li>需求:{@code docs/tdd/orchestration/sc-key-map.md} SC-T-08 / SC-T-14(规划师域)。
+ *       <b>来源追溯</b>:EVD-10 契约核对(commit 77026df42,planner-zone 32 SC 端点核验表),
+ *       能力键注册于 {@code docs/superpowers/specs/2026-11-07-planner-zone-contract.md} §3
+ *       「建议能力键映射」行 {@code teacher.plan.create}(绑定 SC-T-08 / T-14)。</li>
+ *   <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.57 SC-T-08 / SC-T-14 行 ——
+ *       {@code TrainingPlanController /api/guide/training-plans/*}(create/update/list/detail/delete);
+ *       {@code GrowthPlanController /api/growth/plan/create}、{@code /update};
+ *       {@code HealthPlanController /api/health/plan/save}、{@code /generate}。
+ *       角色模型:成长规划师({@code role=teacher}),规划师端点按项目框架约定
+ *       (根 AGENTS.md「角色控制」:控制器内手动检查 {@code @RequestAttribute("role")})
+ *       校验 {@code teacher/admin},与同域端点同构(参照 {@code GuideFamilyQueryController.myFamilies}
+ *       的 {@code RolesUtil.hasTeacherRole(role, roles)} 403 门禁)。</li>
+ * </ul>
+ *
+ * <p><b>实现现状(2026-11-13 实测 origin/cfclub)</b>:端点载体已落地(TrainingPlanController /
+ * GrowthPlanController / HealthPlanController 均在),但<b>规划师端点角色门禁未落地</b>——
+ * {@code TrainingPlanController.createPlan/updatePlan} 与 {@code GrowthPlanController.createPlan/updatePlan}
+ * 均未读取 {@code role}/{@code roles} 请求属性(与同域 {@code GuideFamilyQueryController} /
+ * {@code PlanReviewController} / {@code HealthPlanController.pending-review/*} 等已含
+ * {@code RolesUtil.hasTeacherRole} 门禁的端点不同构),非 teacher 角色亦可创建/修改方案。
+ * 本测试以「权限流角色门禁缺失」为 RED 锚点:断言该契约行为在 GREEN 阶段补齐前失败(红),
+ * 端点/正常流/异常流用例先登记为通过(绿)作为能力键契约证据。</p>
+ *
+ * <p><b>测试范围</b>:纯 Mockito / 反射 / 无 Spring / 无 DB / 无外部服务;断言不依赖真实外部服务。
+ * 三类流:正常流(认证 teacher 创建/更新成功)、异常流(缺 memberId/planId 拒绝且不落库)、
+ * 权限流(角色门禁契约:create/update 端点须暴露 {@code role}/{@code roles} 输入并拒绝非 teacher,
+ * 当前未落地 → 红)。</p>
+ *
+ * <p><b>RED 预期</b>:权限流 4 例(training create/update、growth create/update 的角色门禁
+ * 结构探针)在实现补齐前失败(红),断言原文为「角色门禁未落地」。</p>
+ */
+class TeacherPlanCreateContractTest extends AssessmentPlanContractTestBase {
+
+    // ==================== 端点契约(API_REFERENCE §4.57 SC-T-08 / T-14 载体) ====================
+
+    @Test
+    @DisplayName("端点契约:TrainingPlanController @POST /api/guide/training-plans/* 载体存在")
+    void endpoint_trainingPlan_createUpdateCarriers() {
+        Class<TrainingPlanController> clazz = TrainingPlanController.class;
+        assertClassMapping(clazz, "/api/guide/training-plans",
+                "TrainingPlanController 类级映射应为 /api/guide/training-plans(§4.57 SC-T-08)");
+        assertPostMapping(clazz, "createPlan", "/create",
+                "TrainingPlanController.createPlan 应为 POST /create(§4.57 SC-T-08 套餐创建)");
+        assertPostMapping(clazz, "updatePlan", "/update",
+                "TrainingPlanController.updatePlan 应为 POST /update(§4.57 SC-T-08 套餐编辑)");
+        assertPostMapping(clazz, "getPlanList", "/list",
+                "TrainingPlanController.getPlanList 应为 POST /list(§4.57 SC-T-08 列表)");
+        assertPostMapping(clazz, "getPlanDetail", "/detail",
+                "TrainingPlanController.getPlanDetail 应为 POST /detail(§4.57 SC-T-08 详情)");
+        assertPostMapping(clazz, "deletePlan", "/delete",
+                "TrainingPlanController.deletePlan 应为 POST /delete(§4.57 SC-T-08 删除)");
+    }
+
+    @Test
+    @DisplayName("端点契约:GrowthPlanController @POST /api/growth/plan/* 载体存在")
+    void endpoint_growthPlan_createUpdateCarriers() {
+        Class<GrowthPlanController> clazz = GrowthPlanController.class;
+        assertClassMapping(clazz, "/api/growth/plan",
+                "GrowthPlanController 类级映射应为 /api/growth/plan(§4.57 SC-T-08/T-14)");
+        assertPostMapping(clazz, "createPlan", "/create",
+                "GrowthPlanController.createPlan 应为 POST /create(§4.57 SC-T-08 成长计划创建)");
+        assertPostMapping(clazz, "updatePlan", "/update",
+                "GrowthPlanController.updatePlan 应为 POST /update(§4.57 SC-T-14 方案编辑)");
+        assertPostMapping(clazz, "getActivePlan", "/child/{memberId}/active",
+                "GrowthPlanController.getActivePlan 应为 POST /child/{memberId}/active(§4.57 SC-T-17)");
+    }
+
+    @Test
+    @DisplayName("端点契约:HealthPlanController @POST /api/health/plan/save|generate 载体存在")
+    void endpoint_healthPlan_saveGenerateCarriers() {
+        Class<?> clazz = loadClass("com.etotem.cfc.controller.HealthPlanController");
+        assertNotNull(clazz, "HealthPlanController 类不存在(§4.57 SC-T-14 载体)。");
+        assertClassMapping(clazz, "/api/health/plan",
+                "HealthPlanController 类级映射应为 /api/health/plan(§4.57 SC-T-14)");
+        assertPostMapping(clazz, "save", "/save",
+                "HealthPlanController.save 应为 POST /save(§4.57 SC-T-14 健康方案保存)");
+        assertPostMapping(clazz, "generate", "/generate",
+                "HealthPlanController.generate 应为 POST /generate(§4.57 SC-T-14 健康方案生成)");
+    }
+
+    // ==================== 正常流:认证 teacher 创建方案(行为) ====================
+
+    @Test
+    @DisplayName("SC-T-08 正常:teacher 创建套餐 → 200 返回 planId + 落库(source=manual, status=draft)")
+    void trainingPlan_create_normal_teacher() throws Exception {
+        TrainingPlanService svc = mock(TrainingPlanService.class);
+        TrainingPlanController c = new TrainingPlanController();
+        inject(c, "trainingPlanService", svc);
+        when(svc.createPlan(7L, 10L, "暑期认知训练", "专注力专项", "manual")).thenReturn(501L);
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("memberId", 10L);
+        body.put("title", "暑期认知训练");
+        body.put("description", "专注力专项");
+        Result<Long> r = c.createPlan(7L, body, null);
+
+        assertEquals(200, r.getCode());
+        assertEquals(501L, r.getData());
+        verify(svc).createPlan(7L, 10L, "暑期认知训练", "专注力专项", "manual");
+    }
+
+    @Test
+    @DisplayName("SC-T-08 正常:teacher 创建成长计划 → 200 + teacherId 绑定 + 落库")
+    void growthPlan_create_normal_teacher() throws Exception {
+        GrowthPlanService svc = mock(GrowthPlanService.class);
+        GrowthPlanController c = new GrowthPlanController();
+        inject(c, "growthPlanService", svc);
+        inject(c, "userMapper", mock(com.etotem.cfc.mapper.UserMapper.class));
+        inject(c, "growthPlanMapper", mock(com.etotem.cfc.mapper.GrowthPlanMapper.class));
+        when(svc.createPlan(any(GrowthPlan.class))).thenAnswer(inv -> inv.getArgument(0));
+
+        GrowthPlan plan = new GrowthPlan();
+        plan.setMemberId(10L);
+        plan.setPlanTitle("DAN测评-成长方案");
+        Result<GrowthPlan> r = c.createPlan(plan, 7L);
+
+        assertEquals(200, r.getCode());
+        assertEquals(7L, plan.getTeacherId());
+        assertEquals(7L, r.getData().getTeacherId());
+        verify(svc).createPlan(any(GrowthPlan.class));
+    }
+
+    // ==================== 异常流:缺参拒绝且不落库 ====================
+
+    @Test
+    @DisplayName("SC-T-08 异常:套餐创建缺 memberId → 拒绝,不落库")
+    void trainingPlan_create_exception_missingMemberId() throws Exception {
+        TrainingPlanService svc = mock(TrainingPlanService.class);
+        TrainingPlanController c = new TrainingPlanController();
+        inject(c, "trainingPlanService", svc);
+
+        Result<Long> r = c.createPlan(7L, new HashMap<>(), null);
+
+        assertEquals(500, r.getCode());
+        assertTrue(r.getMessage().contains("memberId不能为空"));
+        verify(svc, never()).createPlan(any(), any(), any(), any(), any());
+    }
+
+    @Test
+    @DisplayName("SC-T-08 异常:成长计划创建缺 memberId → 拒绝,不落库")
+    void growthPlan_create_exception_missingMemberId() throws Exception {
+        GrowthPlanService svc = mock(GrowthPlanService.class);
+        GrowthPlanController c = new GrowthPlanController();
+        inject(c, "growthPlanService", svc);
+        inject(c, "userMapper", mock(com.etotem.cfc.mapper.UserMapper.class));
+        inject(c, "growthPlanMapper", mock(com.etotem.cfc.mapper.GrowthPlanMapper.class));
+
+        Result<GrowthPlan> r = c.createPlan(new GrowthPlan(), 7L);
+
+        assertEquals(500, r.getCode());
+        assertTrue(r.getMessage().contains("请选择家庭成员"));
+        verify(svc, never()).createPlan(any(GrowthPlan.class));
+    }
+
+    @Test
+    @DisplayName("SC-T-08 异常:套餐更新缺 planId → 拒绝")
+    void trainingPlan_update_exception_missingPlanId() throws Exception {
+        TrainingPlanService svc = mock(TrainingPlanService.class);
+        TrainingPlanController c = new TrainingPlanController();
+        inject(c, "trainingPlanService", svc);
+
+        Result<Boolean> r = c.updatePlan(new HashMap<>());
+
+        assertEquals(500, r.getCode());
+        assertTrue(r.getMessage().contains("planId不能为空"));
+        verify(svc, never()).updatePlan(any(), any(), any());
+    }
+
+    @Test
+    @DisplayName("SC-T-14 异常:成长计划更新缺 id → 拒绝")
+    void growthPlan_update_exception_missingId() throws Exception {
+        GrowthPlanService svc = mock(GrowthPlanService.class);
+        GrowthPlanController c = new GrowthPlanController();
+        inject(c, "growthPlanService", svc);
+        inject(c, "userMapper", mock(com.etotem.cfc.mapper.UserMapper.class));
+        inject(c, "growthPlanMapper", mock(com.etotem.cfc.mapper.GrowthPlanMapper.class));
+
+        Result<Boolean> r = c.updatePlan(new HashMap<>());
+
+        assertEquals(500, r.getCode());
+        assertTrue(r.getMessage().contains("方案ID不能为空"));
+        verify(svc, never()).updatePlan(any());
+    }
+
+    // ==================== 权限流:角色门禁契约(RED 锚点) ====================
+    // 规划师端点按框架约定应校验 teacher/admin(RolesUtil.hasTeacherRole),
+    // 参照同域 GuideFamilyQueryController.myFamilies / PlanReviewController.activatePlan /
+    // HealthPlanController.pending-review/*。当前 create/update 端点未读取 role/roles → 红。
+
+    @Test
+    @DisplayName("权限流:套餐创建 createPlan 应暴露 role/roles 输入(角色门禁)——当前未落地")
+    void trainingPlan_create_permission_roleGate() {
+        assertRoleGate(TrainingPlanController.class, "createPlan",
+                "[teacher.plan.create] TrainingPlanController.createPlan(POST /api/guide/training-plans/create)"
+                        + " 未暴露 role/roles 请求属性:规划师端点应校验 teacher/admin 角色"
+                        + "(参照 GuideFamilyQueryController.myFamilies 的 RolesUtil.hasTeacherRole 403 门禁)。"
+                        + " 角色门禁未落地,非 teacher 角色亦可创建方案。");
+    }
+
+    @Test
+    @DisplayName("权限流:套餐更新 updatePlan 应暴露 role/roles 输入(角色门禁)——当前未落地")
+    void trainingPlan_update_permission_roleGate() {
+        assertRoleGate(TrainingPlanController.class, "updatePlan",
+                "[teacher.plan.create] TrainingPlanController.updatePlan(POST /api/guide/training-plans/update)"
+                        + " 未暴露 role/roles 请求属性:规划师端点应校验 teacher/admin 角色。角色门禁未落地。");
+    }
+
+    @Test
+    @DisplayName("权限流:成长计划创建 createPlan 应暴露 role/roles 输入(角色门禁)——当前未落地")
+    void growthPlan_create_permission_roleGate() {
+        assertRoleGate(GrowthPlanController.class, "createPlan",
+                "[teacher.plan.create] GrowthPlanController.createPlan(POST /api/growth/plan/create)"
+                        + " 未暴露 role/roles 请求属性:规划师端点应校验 teacher/admin 角色。角色门禁未落地。");
+    }
+
+    @Test
+    @DisplayName("权限流:成长计划更新 updatePlan 应暴露 role/roles 输入(角色门禁)——当前未落地")
+    void growthPlan_update_permission_roleGate() {
+        assertRoleGate(GrowthPlanController.class, "updatePlan",
+                "[teacher.plan.create] GrowthPlanController.updatePlan(POST /api/growth/plan/update)"
+                        + " 未暴露 role/roles 请求属性:规划师端点应校验 teacher/admin 角色。角色门禁未落地。");
+    }
+
+    // ==================== 反射/断言辅助 ====================
+
+    /** 断言类级 @RequestMapping 含指定路径(值可为 value/path,String[] 形式)。 */
+    private void assertClassMapping(Class<?> clazz, String path, String failMsg) {
+        boolean has = Arrays.stream(clazz.getDeclaredAnnotations())
+                .anyMatch(a -> a instanceof RequestMapping
+                        && Arrays.asList(((RequestMapping) a).value()).contains(path));
+        assertTrue(has, failMsg);
+    }
+
+    /** 断言方法级 @PostMapping 含指定路径。 */
+    private void assertPostMapping(Class<?> clazz, String method, String path, String failMsg) {
+        Method m = findMethod(clazz, method);
+        assertNotNull(m, failMsg + " 方法 " + method + " 不存在。");
+        assertTrue(m.isAnnotationPresent(PostMapping.class)
+                        && Arrays.asList(m.getAnnotation(PostMapping.class).value()).contains(path),
+                failMsg);
+    }
+
+    /** 权限流探针:断言 create/update 处理方法至少暴露一个 @RequestAttribute("role"|"roles") 输入。 */
+    private void assertRoleGate(Class<?> clazz, String method, String failMsg) {
+        Method m = findMethod(clazz, method);
+        assertNotNull(m, failMsg + " 方法 " + method + " 不存在。");
+        boolean hasRoleInput = false;
+        for (Parameter p : m.getParameters()) {
+            for (Annotation a : p.getAnnotations()) {
+                if (a instanceof RequestAttribute) {
+                    String attr = ((RequestAttribute) a).value();
+                    if ("role".equals(attr) || "roles".equals(attr)) {
+                        hasRoleInput = true;
+                        break;
+                    }
+                }
+            }
+            if (hasRoleInput) break;
+        }
+        assertTrue(hasRoleInput, failMsg);
+    }
+
+    private Method findMethod(Class<?> clazz, String name) {
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals(name)) return m;
+        }
+        return null;
+    }
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+}

+ 428 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherReportProxyContractTest.java

@@ -0,0 +1,428 @@
+package com.etotem.cfc.unit;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.controller.HealthReportController;
+import com.etotem.cfc.dto.ParsedReportPayload;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.HealthReport;
+import com.etotem.cfc.entity.HealthReportDraft;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.HealthReportDraftMapper;
+import com.etotem.cfc.mapper.HealthReportMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.HealthReportService;
+import com.etotem.cfc.service.UserService;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+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.report.proxy}(SC-T-03 代传报告+确认入库 ⭐ / SC-T-23 报告代传 ⭐)。
+ *
+ * <p><b>契约来源</b></p>
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-T-03(上传 PDF+选孩子 → 上传/预览/确认 → 入孩子档案;
+ *       边界:识别失败进草稿;确认前可取消不入库)、SC-T-23(上传 PDF+选孩子 → 解析 → 关联孩子档案;
+ *       边界:非绑定家庭拒;识别失败进草稿);</li>
+ *   <li>设计:{@code docs/superpowers/specs/2026-11-07-planner-zone-contract.md}(EVD-10 §2 SC-T-03/T-23 端点核验表、
+ *       §5 转测试侧 TR-13 第 4 条「代传归属——/report/proxy-parse + /report/proxy-confirm 代传代确认后报告归属
+ *       family_id/member_id 正确落库,无跨家庭越权」);{@code docs/superpowers/api/API_REFERENCE.md} §4.57。</li>
+ * </ul>
+ *
+ * <p><b>端点载体(SC-T-03/T-23 主载体)</b>:{@link HealthReportController} {@code /api/health/report/*}:
+ * {@code proxy-parse}、{@code proxy-confirm}、{@code reassign}、{@code transfer/request}、{@code transfer/approve}、
+ * {@code transfer/deny}、{@code transfer/list}。</p>
+ *
+ * <p><b>实现现状(S-cfc-182 实测)</b>:该能力链在 origin/cfclub 已完整落地——proxy-parse 含角色闸门
+ * {@code isProxyUploader}(admin/teacher/approved 营养师/approved 规划师)+ 家庭绑定校验 {@code canProxyForFamily}
+ * + 草稿两阶段(解析失败 markDiscarded 不入库);proxy-confirm 经 {@code HealthReportService#auditConfirm}
+ * 校验草稿归属与 subjectId 跨家庭;transfer/* 与 reassign 路由齐备。既有测试
+ * {@code HealthReportControllerProxyParseBindingTest}(B2-2 绑定校验)、{@code HealthReportAuditContractTest}
+ * (auditConfirm 全流)、{@code ReportTransferContractTest}(transfer/*)已有覆盖。故本文件以
+ * {@code teacher.report.proxy} 能力键视角补齐 SC-T-03/23 三类流契约锚点,<b>预计全绿</b>——
+ * 依任务 S-cfc-182 执行要求「若实测发现实现已存在(测试意外全绿),在汇报中注明并交编排侧转测试登记」,
+ * 本文件作为该键契约测试登记入库;若未来行为回退,用例即转红暴露。</p>
+ *
+ * <p><b>测试风格</b>:纯 Mockito + 反射,无 Spring 上下文 / 无 DB / 无外部服务(与同批 RED 契约测试一致)。</p>
+ */
+class TeacherReportProxyContractTest {
+
+    private static final String CONTROLLER_FQCN = "com.etotem.cfc.controller.HealthReportController";
+    private static final String CLASS_PREFIX = "/api/health";
+
+    /** 端点契约表:方法名 → @PostMapping 路径(SC-T-03/T-23 主载体) */
+    private static final Object[][] PROXY_ENDPOINT_ROWS = {
+            {"proxyParsePreview", "/report/proxy-parse"},
+            {"proxyConfirm", "/report/proxy-confirm"},
+            {"reassignReport", "/report/reassign"},
+            {"requestReportTransfer", "/report/transfer/request"},
+            {"approveReportTransfer", "/report/transfer/approve"},
+            {"denyReportTransfer", "/report/transfer/deny"},
+            {"listReportTransfers", "/report/transfer/list"},
+    };
+
+    private HealthReportController controller;
+    private HealthReportService service;
+    private HealthReportDraftMapper draftMapper;
+    private FamilyMemberMapper familyMemberMapper;
+    private HealthReportMapper healthReportMapper;
+    private ObjectMapper objectMapper;
+
+    @BeforeAll
+    static void initMybatisPlusTableInfo() {
+        // auditConfirm 内部 Lambda wrapper 需要 TableInfo 缓存(与 HealthReportAuditContractTest 一致)
+        MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
+        TableInfoHelper.initTableInfo(assistant, HealthReportDraft.class);
+        TableInfoHelper.initTableInfo(assistant, FamilyMember.class);
+        TableInfoHelper.initTableInfo(assistant, User.class);
+        TableInfoHelper.initTableInfo(assistant, HealthReport.class);
+    }
+
+    @BeforeEach
+    void setUp() throws Exception {
+        controller = new HealthReportController();
+        service = new HealthReportService();
+        injectAllDependencies(service);
+        draftMapper = (HealthReportDraftMapper) readField(service, "draftMapper");
+        familyMemberMapper = (FamilyMemberMapper) readField(service, "familyMemberMapper");
+        healthReportMapper = (HealthReportMapper) readField(service, "healthReportMapper");
+        objectMapper = (ObjectMapper) readField(service, "objectMapper");
+        doAnswer(inv -> {
+            HealthReport report = inv.getArgument(0);
+            report.setId(1000L);
+            return 1;
+        }).when(healthReportMapper).insert(any(HealthReport.class));
+    }
+
+    // ==================== 正常流:端点契约 ====================
+
+    @Test
+    @DisplayName("正常流-1:HealthReportController 存在且类级映射前缀为 /api/health")
+    void controller_classMappingExists() {
+        Class<?> clazz = loadClass(CONTROLLER_FQCN);
+        assertNotNull(clazz,
+                "[teacher.report.proxy] HealthReportController 类不存在(SC-T-03/23 主载体)。");
+        RequestMapping rm = clazz.getAnnotation(RequestMapping.class);
+        assertNotNull(rm,
+                "[teacher.report.proxy] HealthReportController 应声明类级 @RequestMapping 前缀。");
+        assertTrue(Arrays.asList(rm.value()).contains(CLASS_PREFIX),
+                "[teacher.report.proxy] 类级前缀应为 " + CLASS_PREFIX + "(端点全路径 = /api/health/report/*)。");
+    }
+
+    @Test
+    @DisplayName("正常流-2:代传/转移 7 个端点应存在且均为 @PostMapping")
+    void proxyEndpoints_existWithPostMapping() {
+        Class<?> clazz = loadClass(CONTROLLER_FQCN);
+        assertNotNull(clazz);
+        for (Object[] row : PROXY_ENDPOINT_ROWS) {
+            String methodName = (String) row[0];
+            String path = (String) row[1];
+            Method m = findDeclaredMethod(clazz, methodName);
+            assertNotNull(m,
+                    "[teacher.report.proxy] HealthReportController 缺少端点方法 " + methodName + "(SC-T-03/23 主载体)。");
+            PostMapping pm = m.getAnnotation(PostMapping.class);
+            assertNotNull(pm,
+                    "[teacher.report.proxy] " + methodName + " 应为 @PostMapping(项目统一 POST 约定)。");
+            assertEquals(1, pm.value().length, "[teacher.report.proxy] " + methodName + " 应显式声明路径。");
+            assertEquals(path, pm.value()[0],
+                    "[teacher.report.proxy] " + methodName + " 应映射 " + path + "(SC-T-03/T-23 端点核验表)。");
+        }
+    }
+
+    // ==================== 正常流:角色闸门(isProxyUploader) ====================
+
+    @Test
+    @DisplayName("正常流-3:teacher 角色应具备代传资格")
+    void isProxyUploader_teacherAllowed() throws Exception {
+        assertTrue(invokeIsProxyUploader("teacher", null, null),
+                "[teacher.report.proxy] teacher 角色应可代传报告(SC-T-23 报告代传)。");
+        assertTrue(invokeIsProxyUploader("parent", Collections.singletonList("teacher"), null),
+                "[teacher.report.proxy] roles 含 teacher 也应放行。");
+    }
+
+    @Test
+    @DisplayName("正常流-4:admin / approved 营养师 / approved 规划师应具备代传资格")
+    void isProxyUploader_adminAndApprovedProfessionalsAllowed() throws Exception {
+        assertTrue(invokeIsProxyUploader("admin", null, null),
+                "[teacher.report.proxy] admin 应可代传任意报告。");
+        assertTrue(invokeIsProxyUploader("parent", Collections.singletonList("admin"), null),
+                "[teacher.report.proxy] roles 含 admin 也应放行。");
+
+        User approvedNutritionist = new User();
+        approvedNutritionist.setNutritionistStatus("approved");
+        assertTrue(invokeIsProxyUploader("nutritionist", null, approvedNutritionist),
+                "[teacher.report.proxy] approved 营养师(nutritionistStatus=approved)应可代传报告。");
+
+        User approvedPlanner = new User();
+        approvedPlanner.setVendorType("planner");
+        approvedPlanner.setVendorStatus("approved");
+        assertTrue(invokeIsProxyUploader("parent", null, approvedPlanner),
+                "[teacher.report.proxy] approved 规划师(vendorType=planner + vendorStatus=approved)应可代传报告。");
+    }
+
+    // ==================== 正常流:绑定家庭放行 + 确认归属落库 ====================
+
+    @Test
+    @DisplayName("正常流-5:规划师代传绑定家庭应放行")
+    void canProxyForFamily_boundFamilyAllowed() throws Exception {
+        UserService us = mock(UserService.class);
+        ReflectionTestUtils.setField(controller, "userService", us);
+        when(us.getEffectiveFamilyIds(1L)).thenReturn(Collections.singleton(5L));
+
+        assertTrue(invokeCanProxyForFamily(1L, "teacher", Collections.singletonList("teacher"), 5L),
+                "[teacher.report.proxy] 规划师代传其绑定家庭(guide_families 有效集内)应放行。");
+        verify(us).getEffectiveFamilyIds(1L);
+    }
+
+    @Test
+    @DisplayName("正常流-6:admin 代传任意家庭应放行(平台角色免绑定校验)")
+    void canProxyForFamily_adminAllowed() throws Exception {
+        UserService us = mock(UserService.class);
+        ReflectionTestUtils.setField(controller, "userService", us);
+
+        assertTrue(invokeCanProxyForFamily(1L, "admin", Collections.singletonList("admin"), 999L),
+                "[teacher.report.proxy] 平台角色(admin)应可代传任意家庭。");
+        verify(us, never()).getEffectiveFamilyIds(anyLong());
+    }
+
+    @Test
+    @DisplayName("正常流-7:代确认入库后报告归属 family_id/subjectId 落库(TR-13 §5 第4条)")
+    void auditConfirm_reportBindsToSameFamilyMember() throws Exception {
+        Long draftId = 1L;
+        Long subjectId = 55L;
+        when(draftMapper.selectById(draftId)).thenReturn(pendingDraft(draftId, 7L, 1L));
+        stubPayloadAndSameFamilySubject(subjectId, 1L);
+
+        Map<String, Object> result = service.auditConfirm(draftId, subjectId, 7L, false);
+
+        assertNotNull(result);
+        assertEquals(1000L, (Long) result.get("reportId"));
+        assertEquals(subjectId, (Long) result.get("subjectId"), "代确认后报告应归属所选家庭成员 subjectId");
+        assertEquals("confirmed", result.get("status"));
+
+        ArgumentCaptor<HealthReport> captor = ArgumentCaptor.forClass(HealthReport.class);
+        verify(healthReportMapper).insert(captor.capture());
+        assertEquals(1L, captor.getValue().getFamilyId(),
+                "代确认入库后报告 family_id 应等于草稿所属家庭(归属正确落库,TR-13 §5 第4条)");
+    }
+
+    // ==================== 异常流:边界拒绝 ====================
+
+    @Test
+    @DisplayName("异常流-1:规划师代传非绑定家庭应被拒绝(SC-T-23 边界:非绑定家庭拒)")
+    void canProxyForFamily_unboundFamilyDenied() throws Exception {
+        UserService us = mock(UserService.class);
+        ReflectionTestUtils.setField(controller, "userService", us);
+        when(us.getEffectiveFamilyIds(1L)).thenReturn(Collections.singleton(5L));
+
+        assertFalse(invokeCanProxyForFamily(1L, "teacher", Collections.singletonList("teacher"), 99L),
+                "[teacher.report.proxy] 规划师代传非绑定家庭必须被拒绝(familyId 不在有效家庭集内)。");
+        verify(us).getEffectiveFamilyIds(1L);
+    }
+
+    @Test
+    @DisplayName("异常流-2:代确认缺 draftId 应拒绝不入库(确认前不入库,SC-T-03 边界)")
+    void proxyConfirm_missingDraftId_returnsError() throws Exception {
+        UserMapper um = mock(UserMapper.class);
+        ReflectionTestUtils.setField(controller, "userMapper", um);
+        when(um.selectById(1L)).thenReturn(new User());
+
+        Result<?> result = invokeProxyConfirm(Collections.<String, Object>emptyMap(), 1L, "teacher");
+
+        assertEquals(500, result.getCode());
+        assertNotNull(result.getMessage());
+        assertTrue(result.getMessage().contains("draftId"),
+                "缺 draftId 应提示 draftId 不能为空(确认动作不落库)");
+    }
+
+    @Test
+    @DisplayName("异常流-3:识别失败/已取消草稿(discarded)不可确认入库(SC-T-03 边界:识别失败进草稿,确认前不入库)")
+    void auditConfirm_discardedDraft_notConfirmable() {
+        when(draftMapper.selectById(1L)).thenReturn(draft(1L, 7L, 1L, "discarded"));
+
+        IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+                () -> service.auditConfirm(1L, 55L, 7L, false));
+        assertTrue(ex.getMessage().contains("不存在") || ex.getMessage().contains("过期"),
+                "discarded 草稿应等价于不可确认(识别失败/取消后不得入库)");
+    }
+
+    // ==================== 权限流:无跨家庭越权(TR-13 §5 第4条) ====================
+
+    @Test
+    @DisplayName("权限流-1:非授权角色(普通家长)代确认应被拒绝")
+    void proxyConfirm_nonProxyRole_denied() throws Exception {
+        UserMapper um = mock(UserMapper.class);
+        ReflectionTestUtils.setField(controller, "userMapper", um);
+        when(um.selectById(1L)).thenReturn(new User());
+
+        Result<?> result = invokeProxyConfirm(Collections.<String, Object>emptyMap(), 1L, "parent");
+
+        assertEquals(500, result.getCode());
+        assertNotNull(result.getMessage());
+        assertTrue(result.getMessage().contains("无权限"),
+                "非授权角色(parent)代确认必须返回无权限");
+    }
+
+    @Test
+    @DisplayName("权限流-2:非创建者且非 admin 确认他人代传草稿 → 拒绝(无跨家庭越权)")
+    void auditConfirm_nonCreatorNonAdmin_denied() {
+        when(draftMapper.selectById(1L)).thenReturn(pendingDraft(1L, 7L, 1L));
+
+        IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+                () -> service.auditConfirm(1L, 55L, 8L, false));
+        assertTrue(ex.getMessage().contains("无权"),
+                "非创建者非 admin 确认他人代传草稿必须被拒绝(代传草稿只能由代传人本人或平台角色确认)");
+    }
+
+    @Test
+    @DisplayName("权限流-3:subjectId 跨家庭 → 拒绝(报告归属不得跨家庭,A 家庭报告不能确认到 B 家庭成员)")
+    void auditConfirm_crossFamilySubject_denied() {
+        when(draftMapper.selectById(1L)).thenReturn(pendingDraft(1L, 7L, 1L));
+        FamilyMember otherFamily = new FamilyMember();
+        otherFamily.setId(55L);
+        otherFamily.setFamilyId(2L);
+        when(familyMemberMapper.selectById(55L)).thenReturn(otherFamily);
+
+        IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+                () -> service.auditConfirm(1L, 55L, 7L, false));
+        assertTrue(ex.getMessage().contains("不属于"),
+                "跨家庭 subjectId 必须被拒绝(所选成员不属于该草稿所属家庭)");
+    }
+
+    @Test
+    @DisplayName("权限流-4:admin 可确认任意创建者草稿(平台角色豁免,代传兜底)")
+    void auditConfirm_adminCanConfirmAnyDraft() throws Exception {
+        Long draftId = 1L;
+        Long subjectId = 55L;
+        when(draftMapper.selectById(draftId)).thenReturn(pendingDraft(draftId, 7L, 1L));
+        stubPayloadAndSameFamilySubject(subjectId, 1L);
+
+        Map<String, Object> result = service.auditConfirm(draftId, subjectId, 99L, true);
+
+        assertNotNull(result);
+        assertEquals("confirmed", result.get("status"),
+                "平台角色(isAdmin=true)可确认任意创建者的代传草稿");
+    }
+
+    // ==================== 反射 / 构造辅助 ====================
+
+    private boolean invokeIsProxyUploader(String role, List<String> roles, User user) throws Exception {
+        Method m = HealthReportController.class.getDeclaredMethod(
+                "isProxyUploader", String.class, List.class, User.class);
+        m.setAccessible(true);
+        return (Boolean) m.invoke(controller, role, roles, user);
+    }
+
+    private boolean invokeCanProxyForFamily(Long userId, String role, List<String> roles, Long familyId)
+            throws Exception {
+        Method m = HealthReportController.class.getDeclaredMethod(
+                "canProxyForFamily", Long.class, String.class, List.class, Long.class);
+        m.setAccessible(true);
+        return (Boolean) m.invoke(controller, userId, role, roles, familyId);
+    }
+
+    @SuppressWarnings("unchecked")
+    private Result<Map<String, Object>> invokeProxyConfirm(Map<String, Object> params, Long userId, String role)
+            throws Exception {
+        Method m = HealthReportController.class.getDeclaredMethod(
+                "proxyConfirm", Map.class, Long.class, String.class, List.class);
+        m.setAccessible(true);
+        return (Result<Map<String, Object>>) m.invoke(
+                controller, params, userId, role, Collections.<String>emptyList());
+    }
+
+    private HealthReportDraft pendingDraft(Long draftId, Long creatorUserId, Long familyId) {
+        return draft(draftId, creatorUserId, familyId, "pending");
+    }
+
+    private HealthReportDraft draft(Long draftId, Long creatorUserId, Long familyId, String status) {
+        HealthReportDraft draft = new HealthReportDraft();
+        draft.setId(draftId);
+        draft.setUserId(creatorUserId);
+        draft.setFamilyId(familyId);
+        draft.setStatus(status);
+        draft.setPayloadJson("{\"summary\":{\"personName\":\"小李\"}}");
+        return draft;
+    }
+
+    private void stubPayloadAndSameFamilySubject(Long subjectId, Long familyId) throws Exception {
+        ParsedReportPayload.Payload payload = new ParsedReportPayload.Payload();
+        ParsedReportPayload.Summary summary = new ParsedReportPayload.Summary();
+        summary.setPersonName("小李");
+        payload.setSummary(summary);
+        when(objectMapper.readValue(anyString(), eq(ParsedReportPayload.Payload.class))).thenReturn(payload);
+
+        FamilyMember member = new FamilyMember();
+        member.setId(subjectId);
+        member.setFamilyId(familyId);
+        when(familyMemberMapper.selectById(subjectId)).thenReturn(member);
+        // 绑定后补写时序的读:返回 null 即可让 bind 链路尽早返回
+        when(healthReportMapper.selectById(1000L)).thenReturn(null);
+    }
+
+    private static Object readField(Object target, String name) throws Exception {
+        Field f = target.getClass().getDeclaredField(name);
+        f.setAccessible(true);
+        return f.get(target);
+    }
+
+    private static void injectAllDependencies(Object target) throws Exception {
+        for (Field f : target.getClass().getDeclaredFields()) {
+            if (Modifier.isStatic(f.getModifiers())) {
+                continue; // 跳过 log 等 static final
+            }
+            f.setAccessible(true);
+            f.set(target, mock(f.getType()));
+        }
+    }
+
+    private static Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    private static Method findDeclaredMethod(Class<?> clazz, String name) {
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals(name)) {
+                return m;
+            }
+        }
+        return null;
+    }
+}

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-71c4e545941289633cf8f68228e287e73b8c7c54
+07518bfd9763ce1de35b340509eb98571f5d57da

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

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.2429",
+  "version": "1.0.2440",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.2429",
+      "version": "1.0.2440",
       "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.2430",
+  "version": "1.0.2441",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

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

@@ -4,6 +4,342 @@
 
 ---
 
+## v1.0.2441 (2026-11-13)
+
+### 测试
+- 补充 S-cfc-202 定向取证 24例5红(窗口尾部合入)全站终值 5254/4264
+- 补充 S-cfc-203 定向取证 5红(窗口尾部合入)全站终值 5230/4245
+- S-cfc-202 admin.order.manual 契约级单测——权限流 RED(B-142-1/TR-13)
+- 契约级单测 admin.activity.config 配置校验 5红 TDD-RED
+- 测试负责人闭环 — API 全量首测 990 例(B-192-2)+ S-cfc-201 定向 6红18绿 + 全量承袭 4245 过
+
+### 文档
+- 测试负责人第192轮汇报(API 全量首测 990 例 + S-cfc-201 定向 + B-192-1/2)
+
+### 其他
+- - 13 正常流 + 5 边界流绿锚点:端点映射齐全、委托 service、售后状态守卫已实现
+- - 5 权限流预期红:订单 6 端点 + 售后 4 端点缺 admin 角色守卫(@RequestAttribute("role")),
+-   非 admin(parent/child/teacher)可越权发货/关单/审批退款/处理售后;list 非 admin 被放行返回 200
+- - 框架层锚点:JwtInterceptor.LOGIN_REQUIRED_PATHS 已收口 /api/admin/(匿名→401)
+- RED 验证:24 tests,5 failures(全部为预期权限流缺口),19 green,0 errors。
+- 纯 Mockito+反射契约单测,无 Spring 上下文/DB/外部服务。GREEN 指引见测试类 Javadoc。
+- 
+
+
+## v1.0.2440 (2026-11-13)
+
+### 文档
+- 研发负责人第155轮闭环汇报(错峰03 恢复S-cfc-198/派发S-cfc-202-203 + O-19裁决)
+
+
+## v1.0.2439 (2026-11-13)
+
+### 测试
+- 契约级单测 admin.article.manage 文章发布-审核(TDD-RED,6红18绿)
+- s run: 24, Failures: 6, Errors: 0(红 6 = 契约缺口,绿 18 全过)
+- 测试负责人闭环 — 后端全量实跑 386 类/3315 例 + 5 契约类补验 + B-95 销号
+
+### 其他
+- 仅新增 cfc-backend/src/test,不改 src/main。
+- 新增 AdminArticleManageContractTest(24 例):
+- - 正常流 8(绿锁):toggleStatus 发布、submitForReview、withdraw、reDraft、
+-   delete、controller list 透传 auditStatus / publish 委托 / toggleFeatured 委托
+- - 边界流 8(绿锁):缺 id 参数门禁 ×4、非草稿提交审核、非发布中撤回、
+-   非驳回/撤回转草稿、非法 direction
+- - 红测 6(预期红,锁契约缺口):
+-   R1-R3 C 端未审不可见:getPublicList/getFeatured/getDetail 仅过滤 status=published,
+-     未排除 auditStatus!=approved,status=published+pending/rejected/null 泄漏到
+-     /api/articles/*(PUBLIC_PATHS 匿名可读)
+-   R4 直接发布未落审核默认值:toggleStatus(id,'published') 无审核门禁、不置
+-     auditStatus=approved/auditedAt,绕过 submit-review→audit 审核流直接上架
+-   R5-R6 权限流 controller 侧未收口:admin role 守卫仅 create/audit/move,
+-     update/delete/toggleFeatured/submitReview/withdraw/reDraft/publish 无守卫(B-142-1)
+- - 权限流绿锁 2(B-142-1 拦截器侧已修):LOGIN_REQUIRED_PATHS 已覆盖 /api/admin/、
+-   匿名 POST /api/admin/articles/publish → 401
+- RED 验证:mvn -o -Dtest=AdminArticleManageContractTest test
+- 关联:docs/tdd/orchestration/sc-key-map.md S-cfc-201;B-142-1
+- 
+-   386/386 XML 无沉默、2121 main + 392 test 源 0 编译错、dump 0
+- - 收尾增量(远端 5ad781a00):S-cfc-199/200 三类 52 例 → 39 红(TDD 前瞻)
+- - 窗口本地:S-cfc-197 AdminReportParserManage 37/37 GREEN;S-cfc-198 13 例 6 红(重试/分批/角色门禁未实现)
+- - 浏览器 39 过 + 2 跳:本机 28、189 admin 5/5(blob 4703d73fb 同源)、204 fe 6/6(连续 2 轮 → B-95 销号)
+- - langgraph@251 47/51:4 红全 429 AgnesAI + 401 api key ****ab12 invalid(环境)
+- - 线上 API 32 例 25/0/7;anon 6 端点 6/6=401;web 116/116;fe 609/3/12;根级 7/16
+- - 全站 4254 例 / 4088 过 = 96.10%(剔 skip 96.67%;剔预期 RED 98.34%),红 141 零缺陷回归
+- - 新增 B-191-1(窗口多次推入契约类→双 fetch+增量 SOP)、B-191-2(全量 1:01h,慢类 493.97s)、B-191-3(API 例数不可复现)
+- - 门禁:不通过(有条件)— G1/G2/G7/G9/G11 ❌;G4/G5/G6/G8/G10/G12/G13 ✅(存量零漂移第 26 次)
+- 
+
+
+## v1.0.2438 (2026-11-13)
+
+### 测试
+- S-cfc-200 admin.supplier.manage RED 契约测试(410 壳/边界门禁/role 守卫,19 用例 16 红/3 绿)
+
+### 文档
+- 研发负责人第164轮闭环汇报 2026-11-13 1406(派发S-cfc-200/201 + O-19收敛核验)
+
+
+## v1.0.2437 (2026-11-13)
+
+### 文档
+- 修正 P-94~96 错位映射与 SC-T-01 污染(恢复 task.history.view/growth.history.view/achievement.grid.fiveCol/teacher.login.switch,删孤儿键 307)
+- SC 台账 241/241 全映射达成(G-L 域 26 行+五维专项 10 行+A-04 真缺口立项,S-cfc-283~308 共 26 键)
+
+### 测试
+- S-cfc-200 admin.supplier.manage 契约级单测(TDD-RED:AdminSupplier/EcomSupplier 410废弃桩 + SC-A-06停用在售商品守卫未落地,预期10红6绿)
+- S-cfc-199 admin.dimension.config RED 契约测试(维度/权重/上限 CRUD + 来源比例/归因权重治理)
+
+### 其他
+- 设计载体三 Controller:AdminSupplier/EcomSupplier/SupplyManageController。
+- - 10 红:AdminSupplierController + EcomSupplierController 为 410 废弃桩(list/save/update/delete + all),
+-   SC-A-06 边界「有在售商品的供应商停用需先下架」全库无在售商品守卫。
+- - 6 绿证:SupplyManageController 已落地(入驻手机号校验/默认 approved+supplier_admin/detail 不存在)。
+- 纯反射+Mockito,无 Spring/DB/外部服务;不改动 src/main。GREEN 阶段据契约签名落地或重映射活跃载体。
+- 
+- 溯源: 应用场景总览 SC-A-09/SC-A-25/SC-A-26; API_REFERENCE §4.13/§4.60.9/§4.60.16; TC-A-25/26-E/P
+- 新增: DimensionConfigAdminContractTest (17 用例, 13 红 / 4 绿基线)
+- RED 覆盖:
+- - 权重/上限/来源比例非法(负/超界/和≠100) → 400 拦截 (未实现, 现返回 200)
+- - 删除被引用维度拒绝 + 不级联删除 (deleteConfig 无引用检查, 现静默 removeById)
+- - 乐观锁 version 字段 (ProductDimensionConfig 无 version)
+- - @RequestAttribute('role') admin 403 守卫 (4 个控制器均无 role 参数)
+- GREEN 基线: 4 个控制器 (DimensionAdminController/DimensionConfigController/
+- DimensionWeightController/AdminEnergyConfigController) CRUD 端点存在。
+- 纯 Mockito+反射, 无 Spring/DB/外部服务。禁改 src/main。
+- 
+
+
+## v1.0.2436 (2026-11-13)
+
+### 文档
+- SC 台账 P 域 C/D 域 32 行回填(report/diet/growth/assessment/plan 16 键入池 S-cfc-267~282)
+
+### 测试
+- 能力键 admin.reportParser.manage 契约级单测(S-cfc-197)
+- RED contract test for SC-A-08/SC-A-22 knowledge base management
+
+### 其他
+- ReportParseAdminController(/api/admin/report-parse) 完整契约面:
+- types/save/toggle/delete、fingerprints/list/save/delete、imports、
+- unknown-clusters/unknown-uploads、dan/reparse、list/retry/backfill-food。
+- 纯 Mockito/反射(无 Spring/DB/外部服务),对齐 TeamProfitHook/StarMultiplier/
+- Pipeline/FamilyJoinValidation 同批 RED 契约测试风格。
+- 实测实现已落地(origin/cfclub 两个 Controller 已存在),测试 37/37 绿,
+- 按 TR-7alescarpenter 契约核对转测试侧三类流证据登记,不私改 src/main。
+- 
+- - Sync failure retry (SC-A-08/A-22 boundary) - no maxRetries/syncWithRetry in sync path
+- - Large batch batching (SC-A-08 boundary) - no BATCH_SIZE/syncBatch in DanKnowledgeBaseService
+- - KB write endpoint role gate (AdminUnifiedKnowledgeController already has allowed; KnowledgeBaseController/KnowledgeBaseVectorController lack it per 2026-11-07-admin-ops-contract section 3#5)
+- Green tests confirm landed surface: 4 controllers, services, schema tables.
+- 
+
+
+## v1.0.2435 (2026-11-13)
+
+### 文档
+- SC 台账 P 域 B/C 域 8 行回填(task/wish/points/energy 6 键入池 S-cfc-261~266)
+- 研发负责人巡检汇报 2026-11-13-1234(第138轮)+ O-19 状态机口径异常裁决
+- SC 台账 P 域 A 域 9 行回填(family.* 5 键入池 S-cfc-256~260)
+
+### 测试
+- 补录 9 项套件执行日志(.tdd/_r190_*.log)
+- 测试侧产物闭环
+
+### 其他
+- - docs/tdd/test/automation-run-2026-11-13-R190.md: 14项套件实测明细
+- - docs/tdd/notice-2026-11-13-R190-研发通知.md: langgraph 401环境红+S-cfc-186 RED通知
+- - docs/tdd/test/blockers.md: B-190-1(B-142-2复发)/B-190-2(窗口2契约类补验)/B-95观察改善/B-189-1延续
+- - docs/tdd/test/inventory.md + automation-status.md: 台账重扫(跨源4588, Java@Test 3250)
+- - _r190_*.cjs: 189/204/251 节点派发器脚本
+- ★ 关键事实:窗口2契约类补验(S-cfc-186 6过/1预期RED + S-cfc-182 14/14 GREEN);
+-   langgraph 50/51(上游401环境红,B-142-2复发);204节点fe 6/6恢复(B-95观察改善);
+-   浏览器任务级全绿;全站红94零缺陷回归
+- 
+
+
+## v1.0.2434 (2026-11-13)
+
+### 测试
+- RED contract test for teacher.family.overview
+- 能力键 teacher.report.proxy 契约级单测(S-cfc-182)
+- 能力键 teacher.report.proxy 契约级单测(S-cfc-182)
+
+### 其他
+- key teacher.family.overview (SC-T-16/17/21/27).
+- Covers endpoint anchors (GuideFamilyTaskController /{familyId}/overview,
+- /dashboard-stats, /{familyId}/children/{memberId}/tasks; GrowthPlanController
+- /child/{memberId}/active; AssessmentComparisonController /api/assessment/compare/*;
+- TianpanController /api/tianpan/*) plus role/binding guard controls.
+- RED gap asserted: GuideFamilyTaskController.getFamilyMemberTasks validates only
+- requireBound(guideId, familyId) but NOT memberId in familyId, allowing a teacher
+- bound to family A to read family B member tasks (IDOR). Cross-family memberId
+- should be rejected and never reach taskService.getTaskHistoryByFamilyMember.
+- Expected RED: 1 failure (cross-family IDOR), 6 pass (controls).
+- 
+- - H 域 7 行→butler.* 5 键(S-cfc-251~255):escalation/oneOnOne/resource/quality/ai.collaborate
+- - B 10/10、H 7/7 全映射;未映射降至 81(P=80 + A-04 真缺口)
+- 
+- - 正常流:端点契约(/report/proxy-parse、/proxy-confirm、/reassign、/transfer/* 7端点 @PostMapping)、
+-   isProxyUploader 角色闸门(teacher/admin/approved 营养师/approved 规划师)、canProxyForFamily 绑定家庭放行、
+-   auditConfirm 确认入库后报告 family_id/subjectId 归属落库(TR-13 §5 第4条)
+- - 异常流:非绑定家庭拒(SC-T-23 边界)、缺 draftId 拒绝不入库、discarded 草稿不可确认(SC-T-03 边界识别失败进草稿)
+- - 权限流:非授权角色拒绝、非创建者非 admin 确认他人草稿拒绝、subjectId 跨家庭拒绝(无跨家庭越权)、admin 豁免
+- 实测:origin/cfclub 该能力链已完整落地,14 用例全绿(意外全绿,依任务注明交编排侧转测试登记)。
+- 纯 Mockito+反射,无 Spring/DB/外部服务。未改动 src/main。
+- 
+- - 正常流:端点契约(/report/proxy-parse、/proxy-confirm、/reassign、/transfer/* 7端点 @PostMapping)、
+-   isProxyUploader 角色闸门(teacher/admin/approved 营养师/approved 规划师)、canProxyForFamily 绑定家庭放行、
+-   auditConfirm 确认入库后报告 family_id/subjectId 归属落库(TR-13 §5 第4条)
+- - 异常流:非绑定家庭拒(SC-T-23 边界)、缺 draftId 拒绝不入库、discarded 草稿不可确认(SC-T-03 边界识别失败进草稿)
+- - 权限流:非授权角色拒绝、非创建者非 admin 确认他人草稿拒绝、subjectId 跨家庭拒绝(无跨家庭越权)、admin 豁免
+- 实测:origin/cfclub 该能力链已完整落地,14 用例全绿(意外全绿,依任务注明交编排侧转测试登记)。
+- 纯 Mockito+反射,无 Spring/DB/外部服务。未改动 src/main。
+- 
+- - S-05→inventory、S-06→afterSales、S-07→activity、S-09/10→nutrition.supply(1:N)、S-11→exportCompliance
+- - S 域 11/11 全映射;未映射降至 98(P=80/B=10/H=7/A=1 真缺口)
+- 
+- - X-01~18 按 O-2 方案 A 用链路键集合映射(1:N 逗号分隔,如 X-01 测全链路=购套餐→预约→录入→激活→打卡)
+- - X-22 → reward.cfEstimate.expectedCf(DONE,上轮已回填)
+- - X 域 22/22 全映射;未映射降至 104(B=10/H=7/P=80/S=6/A=1 真缺口)
+- 
+
+### 文档
+- sc-key-map 台账回填 17 行(143→160 已映射,B/H 域全覆盖)
+- 研发负责人第153轮闭环汇报 2026-11-13 1210(错峰03:backfill回收16条dispatched死锁;13键RED→YELLOW推进+auto续建13 GREEN任务S-cfc-226~238;S-cfc-182/186 T8 requeue重派;--changes 0新键不add-task;--resume 0候选;731处他人WIP零接触单文件入库;deploy pending19/confirmed77;SC 15/241无漂移;O-1~O-25全已裁决+P域80无键SC待主理人确认契约锚点)
+- sc-key-map 台账回填 6 行 SC-S(137→143 已映射,S 域 11/11 全映射)
+- sc-key-map 台账回填 21 行 SC-X(116→137 已映射,X 域 22/22 全映射)
+
+
+## v1.0.2433 (2026-11-13)
+
+### 测试
+- 修正 typo SSh → SSH
+- 补录推送兜底证据 + B-189-1 状态升级
+- 测试侧产物闭环
+
+### 其他
+- - blockers: B-189-1 状态由「P0 阻塞」降级为「HTTP 兜底已推送,SSH 待修」
+- - 附:fetch 仍须临时指定 http://git.iwintrue.com/liaoxg/cfc.git,SSH 恢复前仍为隐忧
+- 
+- - S 域 2 行(S-01/02)→ admin.supplier.manage(S-cfc-200)
+- - C 域 26/26 全映射(C-26 已上轮);未映射降至 125(B=10/H=7/P=80/S=6/X=21/A=1缺口)
+- 
+- - docs/tdd/test/automation-run-2026-11-13-R189.md: 12项套件实测明细
+- - docs/tdd/notice-2026-11-13-R189-研发通知.md: P1契约RED 48+存量25通知
+- - docs/tdd/test/blockers.md: B-189-1(SSH故障)/B-189-2(窗口13类验)/B-142-2销号
+- - docs/tdd/test/inventory.md + automation-status.md: 台账重扫(跨源4567)
+- - .tdd/_r189_*.log: 8项套件原始日志
+- - _r189_*.cjs: 节点派发器脚本
+- ★ 关键事实:langgraph@251 连续第2轮51/51全绿 → B-142-2销号;
+-   窗口13契约类48预期RED取证;全站红92零缺陷回归
+- 
+
+### 文档
+- sc-key-map 台账回填 27 行(89→116 已映射,C/S 域覆盖)
+
+
+## v1.0.2432 (2026-11-13)
+
+### 文档
+- sc-key-map 台账回填 23 行 SC-A(66→89 已映射,SC-A 域 24/25 映射)
+
+### 其他
+- - SC-A-01→admin.auth.login(GREEN);SC-A-02~22/25/26→新建 admin.* / mind.* 键(RED)
+- - SC-A-04 记为真缺口(无 admin 关系类型 CRUD 载体)
+- - 依据 API_REFERENCE §4.13/§4.14 契约 + Controller 实存核验;未映射降至 152
+- 
+- 契约来源: API_REFERENCE §4.57 + EVD-10 planner-zone-contract (77026df42) §3.
+- - 端点契约: TrainingPlanController /api/guide/training-plans/* (create/update/list/detail/delete),
+-   GrowthPlanController /api/growth/plan/create|update, HealthPlanController /api/health/plan/save|generate
+- - 正常流: teacher 创建套餐/成长计划 -> 200 + 落库
+- - 异常流: 缺 memberId/planId -> 拒绝且不落库
+- - 权限流 (RED): create/update 端点未暴露 role/roles 输入(角色门禁缺失),
+-   非 teacher 角色亦可创建/修改方案 -> 契约行为缺失,GREEN 阶段补齐后转绿
+- TDD-RED 预期: 13 run / 4 fail / 0 error / 9 pass
+- 
+
+### 测试
+- teacher.plan.create RED contract - 权限流角色门禁未落地 (4 RED/9 green)
+- S-cfc-188 family.family.autoCreateForPurchase 契约测试(RED 4/5)
+- S-cfc-184 teacher.assessment.record 契约级单测(TDD-RED:SC-T-07 必填缺失拦截/重复录入覆盖非新增两边界未落地,预期3红1绿)
+
+
+## v1.0.2431 (2026-11-13)
+
+### 文档
+- 研发负责人第152轮闭环汇报 2026-11-13 1051(巡检核验轮:状态机112键 RED17/YELLOW72/GREEN8/DONE15 零漂移;任务池197=done179+dispatched17+pending0 池满;上轮派发 S-cfc-176/177/179 GREEN 实现回执已回填 yellow_sha 三sha IN CHAIN ✓、175/178 属 O-22 豁免;17 RED T域契约在途5节点session全挂;--changes 0新键不add-task;--resume 0候选;deploy pending63(open19)/confirmed77;SC主表241=241唯一跑通15/241无漂移;O-1~O-25全已裁决无新增;730处他人WIP零接触单文件入库)
+- ca56f8699 部署回执 testing(S-cfc-176 membership.plan.purchase 已含于 dae706c7d,JAR/进程/健康核实通过)
+- sc-key-map 台账回填 12 行 SC-T(54→66 已映射,SC-T 域 32/32 全映射)
+- sc-key-map 台账回填 21 行 SC-T 与 SC-X-22 映射(52→54 已映射)
+- 研发负责人第151轮闭环汇报(T域closure续派+17 RED全派发)
+
+### 测试
+- S-cfc-183 teacher.plan.review 契约 RED — SC-T-28 AI 方案内容安全复核缺失
+- s run: 9, Failures: 4(SC-T-28 探针全红,SC-T-04 锚点全绿),BUILD FAILURE。
+- GREEN 契约测试 teacher.login.switch(SC-T-01/T-20)— 实现已由 SEC-20261031-01 + 既有代码落地,16/16 全绿;作为契约证据留存,交编排侧转测试登记。
+- S-cfc-194 teacher.dimension.score 契约级 RED 单测
+- teacher.mind.record 规划师舌诊/情绪测评录入 契约级 RED 单测
+- RED 契约测试 teacher.data.isolation(SC-T-13 family-list 数据隔离缺口)
+- S-cfc-181 teacher.appointment.guard contract RED
+
+### 其他
+- 新增契约级单测 PlanContentSafetyReviewContractTest:
+- - 已绿锚点(5):GuidePlanReviewController /pending|/{planId}/approve|/{planId}/reject、
+-   PlanReviewController /review、ReviewCenterController /summary 端点映射;
+-   HealthPlan.planContent/planJson 复核载体;WechatServiceInterface.checkText 原语。
+- - RED 探针(4):审核链路(HealthPlanServiceImpl/GuidePlanReviewController/
+-   HealthPlanController)未接入内容安全依赖;ContentFilterController 仅有 /by-tags
+-   无方案复核端点;approveAndPublish 命中敏感词仍置 published 且落库。
+- 实测 mvn -Dtest=PlanContentSafetyReviewContractTest test:
+- SC-T-04 生命周期由 PlanReviewTeacherContractTest 覆盖且实现已存在,本类不重复。
+- 仅新增 src/test,未改 src/main。
+- 
+- S-cfc-191: Add contract test for teacher.message.push (SC-T-19) [teacher.message.push]
+- - T-11/12→teacher.assessment.record(DAN 上传解析/八类报告)
+- - T-14/15→teacher.plan.create(方案编辑/套餐任务项)
+- - T-17/21/27→teacher.family.overview;T-18→teacher.task.review
+- - T-22→teacher.plan.create(⚠ G-2 归档缺口)
+- - T-23→teacher.report.proxy;T-28→teacher.plan.review
+- - 依据 EVD-10 契约 §3 定稿表;未映射降至 175
+- 
+- S-cfc-187: teacher.task.review - Implement contract tests for cross-family task audit guard (TC-T-09-P, TC-T-18-E)
+- - SC-T-30 测评录入触发五维变化:锚定 AssessmentService → DimensionScoreService.refreshFromDanAssessment 桥接(绿)
+- - SC-T-31 手动维度打分:ManualDimensionScoreController submit/latest/history + submit 落 five_dimension_scores(source_type=manual)(绿)
+- - SC-T-32 五维自评录入与变化:自评 submit 应桥接五维分数/能量落点(DimensionScoreService/EnergyService/FiveDimensionScoreMapper)+ five_dimension_scores.source_type 应含 selfcheck 来源(红:当前 submitSelfCheck 仅写 five_dimension_self_checks,契约"自评→变化落 EnergyController"未落地)
+- RED 结果:Tests run: 6, Failures: 2, Errors: 0(2 红=SC-T-32 桥接缺口,GREEN 阶段实现后转绿)
+- 
+- - SC-T-30/31/32 回填 teacher.dimension.score
+- - SC-X-22 回填 reward.cfEstimate.expectedCf(键 DONE,三类流证据待补)
+- - 未映射降至 187
+- 
+- [S-cfc-192] [teacher.nutrition.plan] TDD-RED 契约测试:精准营养方案制定
+- 纯 Mockito 行为级单测,预期红:
+- 正常流-1:generateRecommendation 的 nutritionSummary 应聚合宏量营养素
+- (protein/carbs/fat),当前 aggregateNutrition 恒置 0(源码注释:
+- protein/carbs/fat 恒 0 不在本轮范围)— 断言失败。
+- 边界流-1/2:负热量目标(calories=-500)与超上限热量目标(999999)
+- 应被拦截返回非 200;当前实现静默忽略并照常返回 success(200) — 断言失败。
+- 权限流:家庭隔离由 JwtInterceptor + familyId 承载,本控制器粒度不适用。
+- 
+- - controller: create 代录入口 + list 客户测评记录查看
+- - service: createAssessment 入库 + getAssessments(assessmentDate 倒序)
+- - entity: memberId/guideId/assessmentType/status(默认 draft)/assessmentDate 字段契约
+- RED: 13 用例 12 失败(0 错误),断言契约所需统一录入链路缺失(实现未落地)
+- 溯源: sc-key-map SC-T-25 + planner-zone-contract(EVD-10) §2/§5 + API_REFERENCE §4.57
+- 能力键: teacher.mind.record | 任务号: S-cfc-195 | 分支: task/S-cfc-195
+- 
+- - 定位:PlanReviewController /api/guide/plans/family-list teacher 越权缺口——
+-   当前 body.familyId 直接透传、无 guide_families(status=binding) isBound 校验,
+-   控制器未注入全项目唯一授权判定点 GuideFamilyAccessGuard。
+- - 新增 cfc-backend/src/test 单测(纯 Mockito/反射,无 Spring/DB):
+-   权限流 3 条(teacher 越权非绑定家庭 403 / body.familyId 不泄漏跨家庭方案 /
+-   越权不查询 planMapper)+ 结构流 1 条(注入统一授权守卫),预期红。
+-   正常流/异常流/授权源基线/观察钉住 5 条(绿)。
+- - 验证:9 通过 5、失败 4(红)——RED 阶段完成,GREEN 阶段实现 isBound 校验使绿。
+- 
+
+
 ## v1.0.2430 (2026-11-13)
 
 ### 文档

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

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.2430
+> 当前版本: v1.0.2441
 
 ## 历史版本
 
@@ -8,6 +8,342 @@
 
 ---
 
+## v1.0.2441 (2026-11-13)
+
+### 测试
+- 补充 S-cfc-202 定向取证 24例5红(窗口尾部合入)全站终值 5254/4264
+- 补充 S-cfc-203 定向取证 5红(窗口尾部合入)全站终值 5230/4245
+- S-cfc-202 admin.order.manual 契约级单测——权限流 RED(B-142-1/TR-13)
+- 契约级单测 admin.activity.config 配置校验 5红 TDD-RED
+- 测试负责人闭环 — API 全量首测 990 例(B-192-2)+ S-cfc-201 定向 6红18绿 + 全量承袭 4245 过
+
+### 文档
+- 测试负责人第192轮汇报(API 全量首测 990 例 + S-cfc-201 定向 + B-192-1/2)
+
+### 其他
+- - 13 正常流 + 5 边界流绿锚点:端点映射齐全、委托 service、售后状态守卫已实现
+- - 5 权限流预期红:订单 6 端点 + 售后 4 端点缺 admin 角色守卫(@RequestAttribute("role")),
+-   非 admin(parent/child/teacher)可越权发货/关单/审批退款/处理售后;list 非 admin 被放行返回 200
+- - 框架层锚点:JwtInterceptor.LOGIN_REQUIRED_PATHS 已收口 /api/admin/(匿名→401)
+- RED 验证:24 tests,5 failures(全部为预期权限流缺口),19 green,0 errors。
+- 纯 Mockito+反射契约单测,无 Spring 上下文/DB/外部服务。GREEN 指引见测试类 Javadoc。
+- 
+
+
+## v1.0.2440 (2026-11-13)
+
+### 文档
+- 研发负责人第155轮闭环汇报(错峰03 恢复S-cfc-198/派发S-cfc-202-203 + O-19裁决)
+
+
+## v1.0.2439 (2026-11-13)
+
+### 测试
+- 契约级单测 admin.article.manage 文章发布-审核(TDD-RED,6红18绿)
+- s run: 24, Failures: 6, Errors: 0(红 6 = 契约缺口,绿 18 全过)
+- 测试负责人闭环 — 后端全量实跑 386 类/3315 例 + 5 契约类补验 + B-95 销号
+
+### 其他
+- 仅新增 cfc-backend/src/test,不改 src/main。
+- 新增 AdminArticleManageContractTest(24 例):
+- - 正常流 8(绿锁):toggleStatus 发布、submitForReview、withdraw、reDraft、
+-   delete、controller list 透传 auditStatus / publish 委托 / toggleFeatured 委托
+- - 边界流 8(绿锁):缺 id 参数门禁 ×4、非草稿提交审核、非发布中撤回、
+-   非驳回/撤回转草稿、非法 direction
+- - 红测 6(预期红,锁契约缺口):
+-   R1-R3 C 端未审不可见:getPublicList/getFeatured/getDetail 仅过滤 status=published,
+-     未排除 auditStatus!=approved,status=published+pending/rejected/null 泄漏到
+-     /api/articles/*(PUBLIC_PATHS 匿名可读)
+-   R4 直接发布未落审核默认值:toggleStatus(id,'published') 无审核门禁、不置
+-     auditStatus=approved/auditedAt,绕过 submit-review→audit 审核流直接上架
+-   R5-R6 权限流 controller 侧未收口:admin role 守卫仅 create/audit/move,
+-     update/delete/toggleFeatured/submitReview/withdraw/reDraft/publish 无守卫(B-142-1)
+- - 权限流绿锁 2(B-142-1 拦截器侧已修):LOGIN_REQUIRED_PATHS 已覆盖 /api/admin/、
+-   匿名 POST /api/admin/articles/publish → 401
+- RED 验证:mvn -o -Dtest=AdminArticleManageContractTest test
+- 关联:docs/tdd/orchestration/sc-key-map.md S-cfc-201;B-142-1
+- 
+-   386/386 XML 无沉默、2121 main + 392 test 源 0 编译错、dump 0
+- - 收尾增量(远端 5ad781a00):S-cfc-199/200 三类 52 例 → 39 红(TDD 前瞻)
+- - 窗口本地:S-cfc-197 AdminReportParserManage 37/37 GREEN;S-cfc-198 13 例 6 红(重试/分批/角色门禁未实现)
+- - 浏览器 39 过 + 2 跳:本机 28、189 admin 5/5(blob 4703d73fb 同源)、204 fe 6/6(连续 2 轮 → B-95 销号)
+- - langgraph@251 47/51:4 红全 429 AgnesAI + 401 api key ****ab12 invalid(环境)
+- - 线上 API 32 例 25/0/7;anon 6 端点 6/6=401;web 116/116;fe 609/3/12;根级 7/16
+- - 全站 4254 例 / 4088 过 = 96.10%(剔 skip 96.67%;剔预期 RED 98.34%),红 141 零缺陷回归
+- - 新增 B-191-1(窗口多次推入契约类→双 fetch+增量 SOP)、B-191-2(全量 1:01h,慢类 493.97s)、B-191-3(API 例数不可复现)
+- - 门禁:不通过(有条件)— G1/G2/G7/G9/G11 ❌;G4/G5/G6/G8/G10/G12/G13 ✅(存量零漂移第 26 次)
+- 
+
+
+## v1.0.2438 (2026-11-13)
+
+### 测试
+- S-cfc-200 admin.supplier.manage RED 契约测试(410 壳/边界门禁/role 守卫,19 用例 16 红/3 绿)
+
+### 文档
+- 研发负责人第164轮闭环汇报 2026-11-13 1406(派发S-cfc-200/201 + O-19收敛核验)
+
+
+## v1.0.2437 (2026-11-13)
+
+### 文档
+- 修正 P-94~96 错位映射与 SC-T-01 污染(恢复 task.history.view/growth.history.view/achievement.grid.fiveCol/teacher.login.switch,删孤儿键 307)
+- SC 台账 241/241 全映射达成(G-L 域 26 行+五维专项 10 行+A-04 真缺口立项,S-cfc-283~308 共 26 键)
+
+### 测试
+- S-cfc-200 admin.supplier.manage 契约级单测(TDD-RED:AdminSupplier/EcomSupplier 410废弃桩 + SC-A-06停用在售商品守卫未落地,预期10红6绿)
+- S-cfc-199 admin.dimension.config RED 契约测试(维度/权重/上限 CRUD + 来源比例/归因权重治理)
+
+### 其他
+- 设计载体三 Controller:AdminSupplier/EcomSupplier/SupplyManageController。
+- - 10 红:AdminSupplierController + EcomSupplierController 为 410 废弃桩(list/save/update/delete + all),
+-   SC-A-06 边界「有在售商品的供应商停用需先下架」全库无在售商品守卫。
+- - 6 绿证:SupplyManageController 已落地(入驻手机号校验/默认 approved+supplier_admin/detail 不存在)。
+- 纯反射+Mockito,无 Spring/DB/外部服务;不改动 src/main。GREEN 阶段据契约签名落地或重映射活跃载体。
+- 
+- 溯源: 应用场景总览 SC-A-09/SC-A-25/SC-A-26; API_REFERENCE §4.13/§4.60.9/§4.60.16; TC-A-25/26-E/P
+- 新增: DimensionConfigAdminContractTest (17 用例, 13 红 / 4 绿基线)
+- RED 覆盖:
+- - 权重/上限/来源比例非法(负/超界/和≠100) → 400 拦截 (未实现, 现返回 200)
+- - 删除被引用维度拒绝 + 不级联删除 (deleteConfig 无引用检查, 现静默 removeById)
+- - 乐观锁 version 字段 (ProductDimensionConfig 无 version)
+- - @RequestAttribute('role') admin 403 守卫 (4 个控制器均无 role 参数)
+- GREEN 基线: 4 个控制器 (DimensionAdminController/DimensionConfigController/
+- DimensionWeightController/AdminEnergyConfigController) CRUD 端点存在。
+- 纯 Mockito+反射, 无 Spring/DB/外部服务。禁改 src/main。
+- 
+
+
+## v1.0.2436 (2026-11-13)
+
+### 文档
+- SC 台账 P 域 C/D 域 32 行回填(report/diet/growth/assessment/plan 16 键入池 S-cfc-267~282)
+
+### 测试
+- 能力键 admin.reportParser.manage 契约级单测(S-cfc-197)
+- RED contract test for SC-A-08/SC-A-22 knowledge base management
+
+### 其他
+- ReportParseAdminController(/api/admin/report-parse) 完整契约面:
+- types/save/toggle/delete、fingerprints/list/save/delete、imports、
+- unknown-clusters/unknown-uploads、dan/reparse、list/retry/backfill-food。
+- 纯 Mockito/反射(无 Spring/DB/外部服务),对齐 TeamProfitHook/StarMultiplier/
+- Pipeline/FamilyJoinValidation 同批 RED 契约测试风格。
+- 实测实现已落地(origin/cfclub 两个 Controller 已存在),测试 37/37 绿,
+- 按 TR-7alescarpenter 契约核对转测试侧三类流证据登记,不私改 src/main。
+- 
+- - Sync failure retry (SC-A-08/A-22 boundary) - no maxRetries/syncWithRetry in sync path
+- - Large batch batching (SC-A-08 boundary) - no BATCH_SIZE/syncBatch in DanKnowledgeBaseService
+- - KB write endpoint role gate (AdminUnifiedKnowledgeController already has allowed; KnowledgeBaseController/KnowledgeBaseVectorController lack it per 2026-11-07-admin-ops-contract section 3#5)
+- Green tests confirm landed surface: 4 controllers, services, schema tables.
+- 
+
+
+## v1.0.2435 (2026-11-13)
+
+### 文档
+- SC 台账 P 域 B/C 域 8 行回填(task/wish/points/energy 6 键入池 S-cfc-261~266)
+- 研发负责人巡检汇报 2026-11-13-1234(第138轮)+ O-19 状态机口径异常裁决
+- SC 台账 P 域 A 域 9 行回填(family.* 5 键入池 S-cfc-256~260)
+
+### 测试
+- 补录 9 项套件执行日志(.tdd/_r190_*.log)
+- 测试侧产物闭环
+
+### 其他
+- - docs/tdd/test/automation-run-2026-11-13-R190.md: 14项套件实测明细
+- - docs/tdd/notice-2026-11-13-R190-研发通知.md: langgraph 401环境红+S-cfc-186 RED通知
+- - docs/tdd/test/blockers.md: B-190-1(B-142-2复发)/B-190-2(窗口2契约类补验)/B-95观察改善/B-189-1延续
+- - docs/tdd/test/inventory.md + automation-status.md: 台账重扫(跨源4588, Java@Test 3250)
+- - _r190_*.cjs: 189/204/251 节点派发器脚本
+- ★ 关键事实:窗口2契约类补验(S-cfc-186 6过/1预期RED + S-cfc-182 14/14 GREEN);
+-   langgraph 50/51(上游401环境红,B-142-2复发);204节点fe 6/6恢复(B-95观察改善);
+-   浏览器任务级全绿;全站红94零缺陷回归
+- 
+
+
+## v1.0.2434 (2026-11-13)
+
+### 测试
+- RED contract test for teacher.family.overview
+- 能力键 teacher.report.proxy 契约级单测(S-cfc-182)
+- 能力键 teacher.report.proxy 契约级单测(S-cfc-182)
+
+### 其他
+- key teacher.family.overview (SC-T-16/17/21/27).
+- Covers endpoint anchors (GuideFamilyTaskController /{familyId}/overview,
+- /dashboard-stats, /{familyId}/children/{memberId}/tasks; GrowthPlanController
+- /child/{memberId}/active; AssessmentComparisonController /api/assessment/compare/*;
+- TianpanController /api/tianpan/*) plus role/binding guard controls.
+- RED gap asserted: GuideFamilyTaskController.getFamilyMemberTasks validates only
+- requireBound(guideId, familyId) but NOT memberId in familyId, allowing a teacher
+- bound to family A to read family B member tasks (IDOR). Cross-family memberId
+- should be rejected and never reach taskService.getTaskHistoryByFamilyMember.
+- Expected RED: 1 failure (cross-family IDOR), 6 pass (controls).
+- 
+- - H 域 7 行→butler.* 5 键(S-cfc-251~255):escalation/oneOnOne/resource/quality/ai.collaborate
+- - B 10/10、H 7/7 全映射;未映射降至 81(P=80 + A-04 真缺口)
+- 
+- - 正常流:端点契约(/report/proxy-parse、/proxy-confirm、/reassign、/transfer/* 7端点 @PostMapping)、
+-   isProxyUploader 角色闸门(teacher/admin/approved 营养师/approved 规划师)、canProxyForFamily 绑定家庭放行、
+-   auditConfirm 确认入库后报告 family_id/subjectId 归属落库(TR-13 §5 第4条)
+- - 异常流:非绑定家庭拒(SC-T-23 边界)、缺 draftId 拒绝不入库、discarded 草稿不可确认(SC-T-03 边界识别失败进草稿)
+- - 权限流:非授权角色拒绝、非创建者非 admin 确认他人草稿拒绝、subjectId 跨家庭拒绝(无跨家庭越权)、admin 豁免
+- 实测:origin/cfclub 该能力链已完整落地,14 用例全绿(意外全绿,依任务注明交编排侧转测试登记)。
+- 纯 Mockito+反射,无 Spring/DB/外部服务。未改动 src/main。
+- 
+- - 正常流:端点契约(/report/proxy-parse、/proxy-confirm、/reassign、/transfer/* 7端点 @PostMapping)、
+-   isProxyUploader 角色闸门(teacher/admin/approved 营养师/approved 规划师)、canProxyForFamily 绑定家庭放行、
+-   auditConfirm 确认入库后报告 family_id/subjectId 归属落库(TR-13 §5 第4条)
+- - 异常流:非绑定家庭拒(SC-T-23 边界)、缺 draftId 拒绝不入库、discarded 草稿不可确认(SC-T-03 边界识别失败进草稿)
+- - 权限流:非授权角色拒绝、非创建者非 admin 确认他人草稿拒绝、subjectId 跨家庭拒绝(无跨家庭越权)、admin 豁免
+- 实测:origin/cfclub 该能力链已完整落地,14 用例全绿(意外全绿,依任务注明交编排侧转测试登记)。
+- 纯 Mockito+反射,无 Spring/DB/外部服务。未改动 src/main。
+- 
+- - S-05→inventory、S-06→afterSales、S-07→activity、S-09/10→nutrition.supply(1:N)、S-11→exportCompliance
+- - S 域 11/11 全映射;未映射降至 98(P=80/B=10/H=7/A=1 真缺口)
+- 
+- - X-01~18 按 O-2 方案 A 用链路键集合映射(1:N 逗号分隔,如 X-01 测全链路=购套餐→预约→录入→激活→打卡)
+- - X-22 → reward.cfEstimate.expectedCf(DONE,上轮已回填)
+- - X 域 22/22 全映射;未映射降至 104(B=10/H=7/P=80/S=6/A=1 真缺口)
+- 
+
+### 文档
+- sc-key-map 台账回填 17 行(143→160 已映射,B/H 域全覆盖)
+- 研发负责人第153轮闭环汇报 2026-11-13 1210(错峰03:backfill回收16条dispatched死锁;13键RED→YELLOW推进+auto续建13 GREEN任务S-cfc-226~238;S-cfc-182/186 T8 requeue重派;--changes 0新键不add-task;--resume 0候选;731处他人WIP零接触单文件入库;deploy pending19/confirmed77;SC 15/241无漂移;O-1~O-25全已裁决+P域80无键SC待主理人确认契约锚点)
+- sc-key-map 台账回填 6 行 SC-S(137→143 已映射,S 域 11/11 全映射)
+- sc-key-map 台账回填 21 行 SC-X(116→137 已映射,X 域 22/22 全映射)
+
+
+## v1.0.2433 (2026-11-13)
+
+### 测试
+- 修正 typo SSh → SSH
+- 补录推送兜底证据 + B-189-1 状态升级
+- 测试侧产物闭环
+
+### 其他
+- - blockers: B-189-1 状态由「P0 阻塞」降级为「HTTP 兜底已推送,SSH 待修」
+- - 附:fetch 仍须临时指定 http://git.iwintrue.com/liaoxg/cfc.git,SSH 恢复前仍为隐忧
+- 
+- - S 域 2 行(S-01/02)→ admin.supplier.manage(S-cfc-200)
+- - C 域 26/26 全映射(C-26 已上轮);未映射降至 125(B=10/H=7/P=80/S=6/X=21/A=1缺口)
+- 
+- - docs/tdd/test/automation-run-2026-11-13-R189.md: 12项套件实测明细
+- - docs/tdd/notice-2026-11-13-R189-研发通知.md: P1契约RED 48+存量25通知
+- - docs/tdd/test/blockers.md: B-189-1(SSH故障)/B-189-2(窗口13类验)/B-142-2销号
+- - docs/tdd/test/inventory.md + automation-status.md: 台账重扫(跨源4567)
+- - .tdd/_r189_*.log: 8项套件原始日志
+- - _r189_*.cjs: 节点派发器脚本
+- ★ 关键事实:langgraph@251 连续第2轮51/51全绿 → B-142-2销号;
+-   窗口13契约类48预期RED取证;全站红92零缺陷回归
+- 
+
+### 文档
+- sc-key-map 台账回填 27 行(89→116 已映射,C/S 域覆盖)
+
+
+## v1.0.2432 (2026-11-13)
+
+### 文档
+- sc-key-map 台账回填 23 行 SC-A(66→89 已映射,SC-A 域 24/25 映射)
+
+### 其他
+- - SC-A-01→admin.auth.login(GREEN);SC-A-02~22/25/26→新建 admin.* / mind.* 键(RED)
+- - SC-A-04 记为真缺口(无 admin 关系类型 CRUD 载体)
+- - 依据 API_REFERENCE §4.13/§4.14 契约 + Controller 实存核验;未映射降至 152
+- 
+- 契约来源: API_REFERENCE §4.57 + EVD-10 planner-zone-contract (77026df42) §3.
+- - 端点契约: TrainingPlanController /api/guide/training-plans/* (create/update/list/detail/delete),
+-   GrowthPlanController /api/growth/plan/create|update, HealthPlanController /api/health/plan/save|generate
+- - 正常流: teacher 创建套餐/成长计划 -> 200 + 落库
+- - 异常流: 缺 memberId/planId -> 拒绝且不落库
+- - 权限流 (RED): create/update 端点未暴露 role/roles 输入(角色门禁缺失),
+-   非 teacher 角色亦可创建/修改方案 -> 契约行为缺失,GREEN 阶段补齐后转绿
+- TDD-RED 预期: 13 run / 4 fail / 0 error / 9 pass
+- 
+
+### 测试
+- teacher.plan.create RED contract - 权限流角色门禁未落地 (4 RED/9 green)
+- S-cfc-188 family.family.autoCreateForPurchase 契约测试(RED 4/5)
+- S-cfc-184 teacher.assessment.record 契约级单测(TDD-RED:SC-T-07 必填缺失拦截/重复录入覆盖非新增两边界未落地,预期3红1绿)
+
+
+## v1.0.2431 (2026-11-13)
+
+### 文档
+- 研发负责人第152轮闭环汇报 2026-11-13 1051(巡检核验轮:状态机112键 RED17/YELLOW72/GREEN8/DONE15 零漂移;任务池197=done179+dispatched17+pending0 池满;上轮派发 S-cfc-176/177/179 GREEN 实现回执已回填 yellow_sha 三sha IN CHAIN ✓、175/178 属 O-22 豁免;17 RED T域契约在途5节点session全挂;--changes 0新键不add-task;--resume 0候选;deploy pending63(open19)/confirmed77;SC主表241=241唯一跑通15/241无漂移;O-1~O-25全已裁决无新增;730处他人WIP零接触单文件入库)
+- ca56f8699 部署回执 testing(S-cfc-176 membership.plan.purchase 已含于 dae706c7d,JAR/进程/健康核实通过)
+- sc-key-map 台账回填 12 行 SC-T(54→66 已映射,SC-T 域 32/32 全映射)
+- sc-key-map 台账回填 21 行 SC-T 与 SC-X-22 映射(52→54 已映射)
+- 研发负责人第151轮闭环汇报(T域closure续派+17 RED全派发)
+
+### 测试
+- S-cfc-183 teacher.plan.review 契约 RED — SC-T-28 AI 方案内容安全复核缺失
+- s run: 9, Failures: 4(SC-T-28 探针全红,SC-T-04 锚点全绿),BUILD FAILURE。
+- GREEN 契约测试 teacher.login.switch(SC-T-01/T-20)— 实现已由 SEC-20261031-01 + 既有代码落地,16/16 全绿;作为契约证据留存,交编排侧转测试登记。
+- S-cfc-194 teacher.dimension.score 契约级 RED 单测
+- teacher.mind.record 规划师舌诊/情绪测评录入 契约级 RED 单测
+- RED 契约测试 teacher.data.isolation(SC-T-13 family-list 数据隔离缺口)
+- S-cfc-181 teacher.appointment.guard contract RED
+
+### 其他
+- 新增契约级单测 PlanContentSafetyReviewContractTest:
+- - 已绿锚点(5):GuidePlanReviewController /pending|/{planId}/approve|/{planId}/reject、
+-   PlanReviewController /review、ReviewCenterController /summary 端点映射;
+-   HealthPlan.planContent/planJson 复核载体;WechatServiceInterface.checkText 原语。
+- - RED 探针(4):审核链路(HealthPlanServiceImpl/GuidePlanReviewController/
+-   HealthPlanController)未接入内容安全依赖;ContentFilterController 仅有 /by-tags
+-   无方案复核端点;approveAndPublish 命中敏感词仍置 published 且落库。
+- 实测 mvn -Dtest=PlanContentSafetyReviewContractTest test:
+- SC-T-04 生命周期由 PlanReviewTeacherContractTest 覆盖且实现已存在,本类不重复。
+- 仅新增 src/test,未改 src/main。
+- 
+- S-cfc-191: Add contract test for teacher.message.push (SC-T-19) [teacher.message.push]
+- - T-11/12→teacher.assessment.record(DAN 上传解析/八类报告)
+- - T-14/15→teacher.plan.create(方案编辑/套餐任务项)
+- - T-17/21/27→teacher.family.overview;T-18→teacher.task.review
+- - T-22→teacher.plan.create(⚠ G-2 归档缺口)
+- - T-23→teacher.report.proxy;T-28→teacher.plan.review
+- - 依据 EVD-10 契约 §3 定稿表;未映射降至 175
+- 
+- S-cfc-187: teacher.task.review - Implement contract tests for cross-family task audit guard (TC-T-09-P, TC-T-18-E)
+- - SC-T-30 测评录入触发五维变化:锚定 AssessmentService → DimensionScoreService.refreshFromDanAssessment 桥接(绿)
+- - SC-T-31 手动维度打分:ManualDimensionScoreController submit/latest/history + submit 落 five_dimension_scores(source_type=manual)(绿)
+- - SC-T-32 五维自评录入与变化:自评 submit 应桥接五维分数/能量落点(DimensionScoreService/EnergyService/FiveDimensionScoreMapper)+ five_dimension_scores.source_type 应含 selfcheck 来源(红:当前 submitSelfCheck 仅写 five_dimension_self_checks,契约"自评→变化落 EnergyController"未落地)
+- RED 结果:Tests run: 6, Failures: 2, Errors: 0(2 红=SC-T-32 桥接缺口,GREEN 阶段实现后转绿)
+- 
+- - SC-T-30/31/32 回填 teacher.dimension.score
+- - SC-X-22 回填 reward.cfEstimate.expectedCf(键 DONE,三类流证据待补)
+- - 未映射降至 187
+- 
+- [S-cfc-192] [teacher.nutrition.plan] TDD-RED 契约测试:精准营养方案制定
+- 纯 Mockito 行为级单测,预期红:
+- 正常流-1:generateRecommendation 的 nutritionSummary 应聚合宏量营养素
+- (protein/carbs/fat),当前 aggregateNutrition 恒置 0(源码注释:
+- protein/carbs/fat 恒 0 不在本轮范围)— 断言失败。
+- 边界流-1/2:负热量目标(calories=-500)与超上限热量目标(999999)
+- 应被拦截返回非 200;当前实现静默忽略并照常返回 success(200) — 断言失败。
+- 权限流:家庭隔离由 JwtInterceptor + familyId 承载,本控制器粒度不适用。
+- 
+- - controller: create 代录入口 + list 客户测评记录查看
+- - service: createAssessment 入库 + getAssessments(assessmentDate 倒序)
+- - entity: memberId/guideId/assessmentType/status(默认 draft)/assessmentDate 字段契约
+- RED: 13 用例 12 失败(0 错误),断言契约所需统一录入链路缺失(实现未落地)
+- 溯源: sc-key-map SC-T-25 + planner-zone-contract(EVD-10) §2/§5 + API_REFERENCE §4.57
+- 能力键: teacher.mind.record | 任务号: S-cfc-195 | 分支: task/S-cfc-195
+- 
+- - 定位:PlanReviewController /api/guide/plans/family-list teacher 越权缺口——
+-   当前 body.familyId 直接透传、无 guide_families(status=binding) isBound 校验,
+-   控制器未注入全项目唯一授权判定点 GuideFamilyAccessGuard。
+- - 新增 cfc-backend/src/test 单测(纯 Mockito/反射,无 Spring/DB):
+-   权限流 3 条(teacher 越权非绑定家庭 403 / body.familyId 不泄漏跨家庭方案 /
+-   越权不查询 planMapper)+ 结构流 1 条(注入统一授权守卫),预期红。
+-   正常流/异常流/授权源基线/观察钉住 5 条(绿)。
+- - 验证:9 通过 5、失败 4(红)——RED 阶段完成,GREEN 阶段实现 isBound 校验使绿。
+- 
+
+
 ## v1.0.2430 (2026-11-13)
 
 ### 文档

+ 67 - 0
docs/tdd/notice-2026-11-13-R189-研发通知.md

@@ -0,0 +1,67 @@
+# R189 轮研发通知(2026-11-13)
+
+## 状态摘要
+
+| 项目 | 内容 |
+|------|------|
+| 汇报轮次 | **R189**(2026-11-13 10:45~11:30) |
+| 基线 → 收尾 | `00121ff2f`(R188)→ `6124352d7`(本地 = 远端 HEAD) |
+| 全站覆盖率 | 4120 例 / 4003 过 / 92 红 / 25 跳 = **97.16%** |
+| 零回归确认 | ✅ 浏览器六件套全绿;API 0 红;fe/web 零漂移;后端存量 25 红零漂移 |
+| **本轮正向项** | langgraph **连续 2 轮 51/51** → B-142-2 **销号** |
+| 主要阻塞 | ① git SSH 22 CLOSED → 推送受阻(B-189-1);② 48 前瞻契约 RED 待实现(P1) |
+
+---
+
+## 一、优先级事项(⏰ 按序处理)
+
+### P1 · 窗口 13 契约测试类 48 RED 实现(本轮新增、最紧急)
+
+| SC-key | 提交 sha | 类名 | 例数 | 当前结果 | 预期 | 描述 |
+|--------|----------|------|------|----------|------|------|
+| S-cfc-183 | 5f2a09b02 | PlanContentSafetyReviewContractTest | 9 | 5P/4F | RED 4/5 | AI 方案内容安全复核缺失 |
+| **S-cfc-184** | 64f8a3875 | TeacherAssessmentRecordContractTest | 4 | 1P/3F | RED 3/1 | 必填拦截/重复覆盖未落地 |
+| S-cfc-188 | 883a6eeab | FamilyAutoCreateForPurchaseContractTest | 5 | 1P/4F | RED 4/5 | 自动建家庭接口缺失 |
+| S-cfc-189 | bbc441e08 | TeacherLoginSwitchContractTest | 16 | **16P/0F** | **GREEN** ✅ | 已实现,可直接收尾 |
+| S-cfc-191 | 23e2ddd66 | TeacherMessagePushContractTest | 20 | **20P/0F** | **GREEN** ✅ | 已实现,可直接收尾 |
+| S-cfc-196 | fb1702975 | TeacherPlanCreateContractTest | 13 | 9P/4F | RED 4/9 | 权限门禁未落地 |
+| —(窗口) | — | TeacherMindRecordContractTest | 13 | 1P/12F | RED 高 | mind 记录边界 |
+| —(窗口) | — | TeacherAppointmentGuardContractTest | 14 | 4P/10F | RED | 预约守卫 |
+| —(窗口) | — | GuideFamilyTaskReviewContractTest | 5 | 3P/2F | RED | 家庭任务审核 |
+| —(窗口) | — | NutritionPlanTeacherContractTest | 4 | 1P/3F | RED | 营养师计划 |
+| —(窗口) | — | TeacherDataIsolationScT13ContractTest | 9 | 5P/4F | RED | 数据隔离 |
+| —(窗口) | — | TeacherDimensionScoreContractTest | 6 | 4P/2F | RED | 维度评分 |
+| —(窗口) | — | TeacherPlanActivateContractTest | 15 | **15P/0F** | **GREEN** ✅ | 已实现,可直接收尾 |
+
+> **行动**:P1 类中 **GREEN 3 个直接收 YELLOW/GREEN**;其余 10 个由对应研发认领 SC-T key 实现使绿。
+
+### P2 · git SSH 22 故障(B-189-1,运维/平台侧)
+
+- 现象:`git.iwintrue.com` / 251 **SSH 22 CLOSED**(R188 09:39 尚可 push),导致本轮测试产物与台账的 push 受阻。
+- 替代路径:HTTP 可读确认 `http://git.iwintrue.com/liaoxg/cfc.git` HEAD=`6124352d7` 与本地一致,fetch/push 须等 SSH 恢复。
+- 行动:**请运维检查 Gogs SSH 服务**(端口 22 / 或 251 上的 sshd)。
+
+### P3 · 存量 25 红后端(跨 24 轮承袭)
+
+- 类清单:`EnergySystemFlowTest` 6F / `DimensionRegistryMapContractTest` 3F / `OrchestrationEngineTest`·`OrchestrationIntegrationTest`·`MembershipPaySwitchTest`·`PaymentServiceORDRoutingTest`·`UserServiceWechatLoginTest`(×2)·`TeamProfitCronContractTest` 各 1F 等。
+- 行动:按计划逐类修复;优先 `TeamProfitCronContractTest`(平台费截断影响计费)。
+
+---
+
+## 二、本轮已闭环 / 观察推进
+
+- ✅ **B-142-2(langgraph 上游凭据/配额)**:连续 2 轮 51/51 全绿 → **销号**。
+- ⏳ B-186-1(live chat 120s 挂死):pytest 绿但 live 链路仍挂,独立观察。
+- ⏳ B-36 根级黑盒 16 红(9082 未起):承袭。
+- ⏳ B-DX-1 dx 桥接 2 例 skip(404):承袭。
+
+---
+
+## 三、证据链
+
+- 报告:`docs/tdd/orchestration/report-test-2026-11-13-R189.md`
+- 执行日志:`docs/tdd/test/automation-run-2026-11-13-R189.md`
+- blockers:`docs/tdd/test/blockers.md`(含 B-189-1/B-189-2 段)
+- 台账:`docs/tdd/test/{discovery,inventory,automation-status}.md`(已刷新)
+
+> 📤 推送状态:⚠️ 本轮 SSH 22 故障,推送受阻(见 §一 P2)。

+ 69 - 0
docs/tdd/notice-2026-11-13-R190-研发通知.md

@@ -0,0 +1,69 @@
+# R190 轮研发通知(2026-11-13)
+
+## 状态摘要
+
+| 项目 | 内容 |
+|------|------|
+| 汇报轮次 | **R190**(2026-11-13 11:45~12:35) |
+| 基线 → 收尾 | `2313791a8`(R189)→ `0e6f285da`(含并行会话 S-cfc-186/182 契约类提交) |
+| 全站覆盖率 | 4141 例 / 4026 过 / 94 红 / 21 跳 = **97.22%** |
+| 零回归确认 | ✅ 浏览器任务级全绿(本机 4 组 + 189 + **204 恢复**);API 0 红;fe/web 零漂移;后端存量 25 红零漂移 |
+| **本轮正向项** | **204 节点 frontend 6/6 恢复**(B-95 观察改善);**S-cfc-182 契约 14/14 GREEN**(可收尾) |
+| **本轮回退项** | langgraph 51/51 → **50/51**(上游 401 `key ****ab12 invalid`,环境型,非代码回归) |
+| 主要阻塞 | ① **251 上游 AgnesAI key(****ab12)失效**(B-142-2 复发);② 49 前瞻契约 RED 待实现(P1);③ git SSH 22 CLOSED(B-189-1 延续) |
+
+---
+
+## 一、优先级事项(⏰ 按序处理)
+
+### P1 · langgraph 上游凭据失效(最紧急,恢复即回 51/51)
+
+- **现象**:`test_auto_tag_valid_format` 标签数 0(空 answer);日志实证 `Error code: 401 - Authentication Fails, Your api key: ****ab12 is invalid`。
+- **判别**:节点 HEAD=基线 `2313791a` + 窗口源码零变更 → **非代码回归,为上游 AgnesAI 凭据失效(B-142-2 复发)**。
+- **行动**:**请更换/恢复 251 节点上游 AgnesAI API key(当前 ****ab12 已失效)**;恢复后测试侧将复跑,连续 2 轮 51/51 后 B-142-2 再销号。
+
+### P2 · 窗口 2 契约类实测结论(S-cfc-186 1 RED 需实现;S-cfc-182 已 GREEN)
+
+| SC-key | 提交 sha | 类名 | 例数 | 当前结果 | 预期 | 描述 |
+|--------|----------|------|------|----------|------|------|
+| **S-cfc-186** | 0e6f285da | TeacherFamilyOverviewContractTest | 7 | 6P/1F | RED 1/7 | **跨家庭 IDOR**:`GuideFamilyTaskController.getFamilyMemberTasks` 仅校验 `requireBound(guideId, familyId)`,**未校验 `memberId ∈ familyId`**——教师绑家庭 A 可读家庭 B 成员任务(SC-T-16/17/21/27) |
+| **S-cfc-182** | 5d67a62f6 | TeacherReportProxyContractTest | 14 | **14P/0F** | **GREEN** ✅ | teacher.report.proxy 已实现,**可直接收尾** |
+
+> **行动**:S-cfc-186 需在 `getFamilyMemberTasks` 增加 memberId 属主校验(拒绝跨家庭访问,不得进入 `taskService.getTaskHistoryByFamilyMember`),实现后复跑该类 7/7 绿;S-cfc-182 直接收 YELLOW/GREEN。
+
+### P3 · 存量 49 前瞻契约 RED(R189 48 + S-cfc-186 1,P1 承袭)
+
+- 类清单(R189 已下发,本轮维持):S-cfc-183 `PlanContentSafetyReview` 4F / S-cfc-184 `TeacherAssessmentRecord` 3F / S-cfc-188 `FamilyAutoCreateForPurchase` 4F / S-cfc-196 `TeacherPlanCreate` 4F / `TeacherMindRecord` 12F / `TeacherAppointmentGuard` 10F / `GuideFamilyTaskReview` 2F / `NutritionPlanTeacher` 3F / `TeacherDataIsolationScT13` 4F / `TeacherDimensionScore` 2F / 其余(含 176 幂等 1F)。
+- GREEN 四键(S-cfc-189 LoginSwitch 16、S-cfc-191 MessagePush 20、**S-cfc-182 14**、PlanActivate 15)已全绿,可直接收尾。
+- 行动:按计划逐键实现使绿。
+
+### P4 · git SSH 22 故障(B-189-1 延续,运维/平台侧)
+
+- 现象:`git.iwintrue.com` / 251 **SSH 22 CLOSED**(R188 09:39 起,本轮延续)。
+- 行动:**请运维检查 Gogs SSH 服务**(端口 22 / 251 sshd);本轮测试产物已经 HTTP 兜底推送(`http://git.iwintrue.com/liaoxg/cfc.git`)。
+
+### P5 · 存量 25 红后端(跨 25 轮承袭)
+
+- 类清单:`EnergySystemFlowTest` 6F / `DimensionRegistryMapContractTest` 3F / `OrchestrationEngineTest`·`OrchestrationIntegrationTest`·`MembershipPaySwitchTest`·`PaymentServiceORDRoutingTest`·`UserServiceWechatLoginTest`(×2)·`TeamProfitCronContractTest` 各 1F 等。
+- 行动:按计划逐类修复;优先 `TeamProfitCronContractTest`(平台费截断影响计费)。
+
+---
+
+## 二、本轮已闭环 / 观察推进
+
+- ✅ **B-95(204 节点失联)观察改善**:openCode 服务实际在 **204:4091**(历轮误判 4090=000),项目 `D:\cfc`;本轮 fe **6/6 恢复** → 观察改善,**连续 2 轮 6/6 后可销号**。
+- ⏳ **B-142-2(上游凭据)复发观察 1/2**:本轮 50/51(401 环境红);更换 key 后连续 2 轮 51/51 再销号。
+- ⏳ B-186-1(live chat 120s 挂死):独立观察。
+- ⏳ B-36 根级黑盒 16 红(9082 未起):承袭。
+- ⏳ B-DX-1 dx 桥接 2 例 skip(404):承袭。
+
+---
+
+## 三、证据链
+
+- 报告:`docs/tdd/orchestration/report-test-2026-11-13-R190.md`
+- 执行日志:`docs/tdd/test/automation-run-2026-11-13-R190.md`
+- blockers:`docs/tdd/test/blockers.md`(含 B-190-1/B-190-2 段)
+- 台账:`docs/tdd/test/{discovery,inventory,automation-status}.md`(已刷新)
+
+> 📤 推送状态:✅ 本轮经 HTTP 兜底推送完成(SSH 22 仍故障,见 §一 P4)。

+ 80 - 0
docs/tdd/notice-2026-11-13-R191-研发通知.md

@@ -0,0 +1,80 @@
+# R191 轮研发通知(2026-11-13)
+
+## 状态摘要
+
+| 项目 | 内容 |
+|------|------|
+| 汇报轮次 | **R191**(2026-11-13 13:14~14:35) |
+| 基线 → 收尾 | `309fa0220`(R190 后)→ 跑测中 `123f768c7` → 收尾 `5ad781a00`(+11 提交,含 S-cfc-199/200 三个契约类) |
+| 全站覆盖率 | 4254 例 / 4088 过 / 141 红 / 25 跳 = **96.10%**(剔 skip 96.67%;**剔全部预期 RED 98.34%**) |
+| 零回归确认 | ✅ 后端存量 13 类逐类零漂移(**第 26 次**);浏览器任务级全绿(本机 4 组 + 189 + 204);API 0 红;web/fe 零漂移 |
+| **本轮正向项** | **后端恢复全量实跑**(386 类 3315 例,386/386 XML 无沉默、0 编译错、dump 0);**S-cfc-197 37/37 GREEN**;**B-95 销号**(204 连续 2 轮 fe 6/6) |
+| **本轮回退项** | langgraph 50/51 → **47/51**(429 免费额度耗尽 + 401 `****ab12` invalid,环境型);G1 97.22% → 96.10%(红 +45 前瞻契约 +3 环境 −1 装置) |
+| 主要阻塞 | ① **CI 清单漏拦扩至 18 类 / 陈旧 26 类**(P0);② **251 上游 key 失效 + 配额耗尽**(P0);③ 前瞻契约 RED 94 例待实现(P1);④ 全量耗时劣化至 1:01h(P1) |
+
+---
+
+## 一、优先级事项(⏰ 按序处理)
+
+### P0-1 · CI `BACKEND_OBSERVED_CLASSES` 双向失真扩大(B-152-1/2 第 22 轮)
+
+- **现象**:`.gitlab-ci.yml:61` 清单 **33 类** vs 本轮实测红类 **25 类** → **漏拦 18 类**(R190 为 6 类)+ **陈旧 26 类**(早已全绿仍被排除)。
+- **新增漏拦(本轮窗口新增契约类)**:`AdminKnowledgeBaseManageContractTest`、`FamilyAutoCreateForPurchaseContractTest`、`GuideFamilyTaskReviewContractTest`、`NutritionPlanTeacherContractTest`、`PlanContentSafetyReviewContractTest`、`TeacherAppointmentGuardContractTest`、`TeacherAssessmentRecordContractTest`、`TeacherDataIsolationScT13ContractTest`、`TeacherDimensionScoreContractTest`、`TeacherFamilyOverviewContractTest`、`TeacherMindRecordContractTest`、`TeacherPlanCreateContractTest`,以及 `TeamProfitCronContractTest` / `UserServiceWechatLoginTest` / `UserServiceWechatLoginContractTest` / `GrowthPublicEndpointMappingTest` / `MembershipPlanPurchaseContractTest` / `TaskReviewScP11ContractTest`。
+- **后果**:`backend-blocking-junit` 在含红例分支上**必然失败**(红因不准),同时 26 个已绿类失去回归守护。
+- **行动**:用实测 25 类重写清单;建议加一步「跑后比对 surefire 红类与清单,差异即 fail 并输出差集」。
+
+### P0-2 · 251 langgraph 上游 LLM 双故障(401 凭据失效 + 429 配额耗尽)
+
+- **现象**:pytest **47/51**,4 红全 `test_auto_tag`;日志 `429 You've reached the API rate limit for free users (AgnesAI)` ×3、`401 Authentication Fails, Your api key: ****ab12 is invalid` ×5(`chat_graph.py:148/195`)。同实例 `/health` **200/2.07ms**,但 `/api/v1/chat` **000/40.0s/0B**、`/api/v1/analyze` **000/50.0s/0B**。
+- **判别**:节点 `cfc-langgraph` 与本地 HEAD **零 diff**、窗口源码零变更 → 非代码回归。
+- **行动**:更换有效 key 并升级配额;`intent_classifier.py:62` 的「429 后默认 chat」请改为显式降级并打指标(当前会把配额故障伪装成分类错误)。
+
+### P1-1 · 前瞻契约 RED 94 例(12 类)待实现
+
+| 契约类 | 关联键 | 例数 | 红 | 缺口要点 |
+|---|---|---|---|---|
+| `AdminKnowledgeBaseManageContractTest` | **S-cfc-198** `admin.knowledgeBase.manage` | 13 | **6** | ① 同步失败**重试**(无 `maxRetries`/`syncWithRetry`/`@Retryable`);② 大批量**分批**(无 `syncBatch`/`BATCH_SIZE`);③ `KnowledgeBaseController`/`KnowledgeBaseVectorController` **无平台角色门禁**(`AdminUnifiedKnowledgeController` 已有 `allowed`,应对齐) |
+| `SupplierManageAdminContractTest` | **S-cfc-200** | 19 | **16** | 410 废弃壳 / 边界门禁 / role 守卫;SC-A-06 停用在售商品守卫未落地 |
+| `AdminSupplierManageContractTest` | **S-cfc-200** | 16 | **10** | 同上(AdminSupplier/EcomSupplier 410 桩) |
+| `DimensionConfigAdminContractTest` | **S-cfc-199** `admin.dimension.config` | 17 | **13** | 维度/权重/上限 CRUD + 来源比例/归因权重治理 |
+| `TeacherMindRecordContractTest` | 承袭 | 12 | 12 | 心灵记录域未实现 |
+| `TeacherAppointmentGuardContractTest` | 承袭 | 10 | 10 | 预约守卫 |
+| `PlanContentSafetyReview` / `FamilyAutoCreateForPurchase` / `TeacherDataIsolationScT13` / `TeacherPlanCreate` | S-cfc-183/188/…/196 | 4×4 | 16 | 内容安全审核 / 购买自动建家庭 / 跨班数据隔离 / 规划师建计划 |
+| `TeacherAssessmentRecord` / `NutritionPlanTeacher` / `TeacherDimensionScore` / `GuideFamilyTaskReview` / `TeacherFamilyOverview` | 承袭 | 3/3/2/2/1 | 11 | 见 R189/R190 通知 |
+
+> **已 GREEN 可收尾**:`AdminReportParserManageContractTest`(**S-cfc-197** `admin.reportParser.manage`,**37/37**);S-cfc-182(14/14,R190);S-cfc-189/191(R189)。
+
+### P1-2 · 后端全量耗时劣化至 1:01h(B-191-2 新增)
+
+- **现象**:同源码 R188 为 43:15,本轮 **1:01h(+41%)**;慢类 `ArticleCategoryControllerTest` **493.97s**(单类 8min)、`AdminFamilyOverviewControllerTest` 341.2s、`GrowthPublicEndpointMappingTest` 334.2s、`GrowthRecordSyncFlowTest` 280.7s、`PlannerBindInviteFlowTest` 178.1s(前 5 占 ~45%)。`cfc-scheduler` 日志 **20524/73287 = 28.0%**。
+- **根因**:controller 层 `@SpringBootTest` 全上下文 + `src/test/resources` 缺 `application-test.yml`(每类重跑 `DatabaseInitializer` 全量迁移)+ `@EnableScheduling` 测试态常驻 + B-156-1/B-170-1 两条迁移语法错致建表反复失败。
+- **行动**:① 增设 `src/test/resources/application-test.yml`(禁调度、共享 datasource);② 慢类改 `@WebMvcTest + @MockBean`;③ 先修两条迁移 DDL(B-156-1 `schema.sql:5354` 索引列缺失、B-170-1 `DatabaseInitializer.java:2261` 别名 UPDATE 派生表引用)。
+
+### P1-3 · 存量装置/数据红 24 例(13 类,零漂移第 26 次)
+
+`EnergySystemFlowTest` 6F / `GrowthPublicEndpointMappingTest` 5E(**文件仍 `??` 未入库**,B-162-1)/ `DimensionRegistryMapContractTest` 3F / `OrchestrationEngine`·`OrchestrationIntegration`·`MembershipPaySwitch`·`PaymentServiceORDRouting`·`TeamProfitCron`·`UserServiceWechatLogin`·`UserServiceWechatLoginContract`·`GuideModuleIntegration`(1E)·`MembershipPlanPurchase`·`TaskReviewScP11` 各 1。
+> `TaskReviewScP11` 本轮 1F(R190 为 2F)→ B-178-1 反射装置无规则浮动,建议 `@MockBean` 替代 `ReflectionUtils`。
+
+### P2 · 其余(承袭)
+
+- **B-177-1(第 13 轮)**:pytest 打印 summary 后进程存活 4m33s、`EXIT=` 不落盘 → 请补 `await client.aclose()` shutdown;派发器已拟加 `timeout -k 10 900`。
+- **B-189-1(第 2 轮)**:git SSH 22 CLOSED,本轮 fetch/push 均走 HTTP 兜底。
+- **B-36(第 75 轮)**:根级 16 红全 `fetch failed`(本机 9082 未起)。
+- **B-DX-1(第 33 轮)**:dx 两端点 404(规格「仅登记不实现」),2 skip 正确。
+- **B-191-3**:线上 API 套件首跑 26 例/5.4min、二次 32 例/18.2s → 采集侧口径问题,不影响研制结论(以 32 例为准)。
+
+---
+
+## 二、SRS / 用例变更闸门
+
+- 窗口 `309fa0220..5ad781a00`:`docs/superpowers/specs`(201 份)/ `tests/test-cases`(16 份)/ `tests/user-scenarios`(11 份)/ `API_REFERENCE.md` **零变更** → **测试侧无需新增或修改用例**(G12 ✅)。
+- 窗口内并行会话新增 **5 个契约测试类(102 例)**,本轮已逐类实测取证(37 绿 / 6+39 预期红),未出现「推入未验证」遗漏。
+
+---
+
+## 三、请研发确认/回复
+
+1. CI 清单重写(P0-1)排期?
+2. 251 上游 key 与配额恢复(P0-2)责任人与时间?
+3. S-cfc-198 三项缺口(重试/分批/角色门禁)是否纳入本迭代?
+4. `application-test.yml` + 慢类改造(P1-2)是否可由测试侧提案 PR?

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
docs/tdd/orchestration/pending.md


+ 176 - 0
docs/tdd/orchestration/report-dev-2026-11-13-1051.md

@@ -0,0 +1,176 @@
+# 研发负责人工作汇报(第 152 轮)
+
+> 角色:研发负责人(自动化 05193ba0,错峰轮)|周期:2026-11-13 10:18 ~ 10:51|负责范围:CFC 全量(SRS/设计/用户场景实现闭环)
+> 交付物:本报告由单文件 `git hash-object` 旁路推 `origin/cfclub`,**未触碰主工作树 730 处其他会话 WIP**(禁 `add -A`)。
+
+---
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | 研发负责人(自动化 05193ba0,错峰轮) |
+| 汇报周期 | 2026-11-13 10:18 ~ 2026-11-13 10:51(第 152 轮;上轮 151 由自动化 298dcf47 错峰轮 @10:11 收口,state.json 更新于 10:11:20,距本轮启动 >5min,**互斥放行**;sleep 45s 复查仍 10:11:20 无持续写入) |
+| 负责模块/单元范围 | CFC 全量(cfc-backend / cfc-frontend / cfc-web / danshop / cfc-langgraph) |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 问题接收与处置
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **收到的测试反馈问题数** | `0` 个 | 本轮 `--inspect` 0 已登记阻塞(`docs/tdd/test/blockers.md` 无新增);测试侧无研发类阻塞回流 |
+| **已解决问题数** | `0` 个 | 无新增研发侧问题;YELLOW→GREEN 校验仍瓶颈在测试侧 |
+| **待解决研发问题数** | `0` 个 | 目标池 0 阻塞;17 个 RED 全部已派发在途(非研发实现缺口) |
+
+### 1.2 任务分发与会话管理
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **分派任务数** | `0` 个(本回合) | 17 个 RED 契约任务(S-cfc-180~196)已由他轮错峰 / 测试侧完成派发(09:24~10:11),5 节点全挂 session 在途;本轮仅做**核验巡检**,不重复派发(防双会话) |
+| **恢复智能体会话数** | `0` 个 | `--resume --dry-run` 0 候选;prod-etotem 生产跳过(默认) |
+| **当前在用会话数** | `17` 个 | 17 个 RED 契约任务全部 dispatched 在途 |
+| **会话平均利用率** | `满(双侧填满)` % | 任务账本 dispatched=17 / pending=0 / done=179 / failed=1;0 空闲任务 |
+
+### 1.2b 各会话当前工作状态列表
+
+| 会话标签 | 当前状态 | 当前任务 | 备注 |
+|----------|----------|----------|------|
+| lan-189 | `busy=3` | S-cfc-180 / 182 / 183 / 187 / 191 / 195 | 在途 RED 契约 |
+| lan-204 | `busy=3` | S-cfc-181 / 184 / 188 / 196 | 在途 RED 契约 |
+| local-lan-4090 | `busy=2` | S-cfc-185 / 189 / 192 | 在途 RED 契约 |
+| mes-local-45278 | `busy=4` | S-cfc-186 / 190 / 193 | 在途 RED 契约(flaky 已派工) |
+| pub-iwintrue | `busy=4` | S-cfc-194 | 在途 RED 契约 |
+| production(ses_f719c9326) | `idle` | — | 生产部署会话,registered |
+| testing(ses_e4804838) | `idle` | — | 测试部署会话,registered |
+
+> 5 个 opencode 实例全部 busy>0、interrupted=0、配额充足(剩余 6~8)。
+
+### 1.3 变更感知与新任务建立
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **感知需求/设计变更数** | `235` 文件 | 近 7 天 `--changes`:需求/设计 126 + 测试用例 35 + 其他 86(2026-11-11~11-13);**新增能力键 0** |
+| **新增入池任务数** | `0` 个 | dispatched 17 + pending 0 池满,无空闲;`--changes` 0 新增能力键 → 按 O-7/O-25 防假 RED 不 `--add-task` |
+| **既有键状态流转数** | `0` 键 | RED 17 / YELLOW 72 / GREEN 8 / DONE 15 与上轮一致,**零漂移** |
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+| 提交 sha | 关联任务/能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|-----------------|------|--------------|----------|------------|------|
+| 66 个 YELLOW 键 yellow_sha | 见状态机 | YELLOW | 实现/契约 | cfclub | 是 | `merge-base --is-ancestor` **66/66 IN_CHAIN、0 NOT_IN_CHAIN** |
+| 6 个 YELLOW 无 sha(O-22 豁免) | admin.article.list / health.report.detail / task.task.create / shop.product.browse / shop.order.create / shop.coupon.apply | YELLOW | 契约全绿 | cfclub | 是 | 实现早已在链(c3e32460a 等),O-22 归档口径,yellow_sha 空缺交测试侧 mark-green 校验(不查验) |
+| 3 键本轮回填 yellow_sha | membership.plan.purchase `ca56f8699` / supplier.order.fulfill `808b603f2` / family.familyMembers.removeMember `5c3cff249` | YELLOW | 实现 | cfclub | 是 | 上轮 S-cfc-176/177/179 GREEN 任务回执,**三 sha 均 merge-base IN_CHAIN ✓**(git 真理核验通过) |
+| 17 个 RED 键(S-cfc-180~196) | teacher.* / family.family.autoCreateForPurchase | RED(契约在途) | — | 各实例 worktree | 在途 | 上轮全部派发,会话在途写契约测试 |
+
+> **本轮无新增业务提交**(零派发、零写盘业务);报告文档走单文件 `git hash-object` 旁路。
+
+### 1.5 状态机阶段分布与部署状态
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| RED | `17` | teacher.* 14 + family.family.autoCreateForPurchase 1 + …(S-cfc-180~196);全部 dispatched 在途,red_sha 待回填 |
+| YELLOW | `72` | 66 已填 yellow_sha(全 IN_CHAIN)+ 6 O-22 豁免(契约全绿实现已覆盖) |
+| 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 / dimension.registry.mapLegacyCode / family.familyMembers.addMember 等 |
+
+| 部署项 | 值 | 说明 |
+|--------|-----|------|
+| 本轮部署次数 | `0` 次 | 编排侧不代 `--deploy-done`,部署回执归测试/部署侧 |
+| pending / confirmed | `63`(done 44 / **open 19**)/ `77` | open 19 中最新 5c3cff249 @08:44 testing 在途(S-cfc-179 实现);最旧 2c65d98f7 / 4f40a8973 / 3d28ba94d / 649dd2be76 / b037fe71b / 0bf1815cf @2026-11-08(历史遗留,O-24 已裁决归测试/部署侧) |
+| last_deployed | `2e1e5d32ef`(deploy_state)/ `a1ead33a1`(tasks.json) | 双账本差异延续,归部署侧 |
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳) | `0` | 正常(稳态) | 无新缺口键 |
+| 契约常量(契约) | `0` | 正常 | — |
+| 测试用例引用(用例∩⏳) | `0` | 正常 | ⏳∩用例引用=0 |
+| 执行结果回填(case_results) | `0` | 正常 | — |
+
+> **场景覆盖矩阵**:SC 台账主表 **241 行 = 241 唯一 SC**(零重复、无漂移)= 已跑通 15(G12 + O-3 5)/ 有键未跑通 18 / 无键未契约化 208。**跑通 15/241**;241/241 前不得报「开发完成」。T 域 16 能力键已入池(上轮 S-cfc-180~196);P 域 80 无键 SC(健康报告/DAN 预约/AI 方案/心愿/能量归因集群)为下一主攻方向。
+
+---
+
+## 二、下一步需要研发处理的事项
+
+| 序号 | 事项描述 | 依赖/前置条件 | 预估完成时间 | 优先级 | 状态 |
+|------|----------|---------------|--------------|--------|------|
+| 1 | 17 个 RED T 域契约任务产出核验(S-cfc-180~196)→ 回填 red_sha → 交测试侧 mark-red 三类流 | 会话 push 证据 | 持续 | P1 | 进行中(全部 dispatched 在途) |
+| 2 | 推动 72 个 YELLOW 键测试 GREEN 校验 + 241 用户场景三类流证据(O-7) | 测试团队协同 | 持续 | P1 | 进行中(瓶颈在测试侧) |
+| 3 | 63 条 pending 部署经 `--deploy-done testing` 收口(O-24,19 open 归测试/部署侧) | 部署会话回执 | 持续 | P2 | 进行中 |
+| 4 | **P 域 80 无键 SC 契约批次设计**(健康报告/DAN 预约/AI 方案/心愿/能量归因)——参照 EVD-10 模式立项 EVD 契约文档后批量入池 | 主理人确认 P 域契约锚点 | 评估后 | P1 | 待主理人确认 |
+| 5 | 6 个 YELLOW O-22 豁免键交测试侧 mark-green 校验(契约全绿实现已覆盖) | 测试侧复跑契约测试 | 持续 | P2 | 进行中 |
+
+---
+
+## 三、阻塞与风险
+
+| 阻塞项 | 影响范围 | 阻塞类型 | 处置进展 | 需要协调 |
+|--------|----------|----------|----------|----------|
+| 主工作树 730 处他人 WIP(非本轮产生) | 主分支目录 | 研发(环境) | 已隔离:全程临时索引/单文件 hash-object 旁路,零接触 | 无需 |
+| mes-local-45278 flaky 风险 | 部署/测试会话 | 测试 | 已派工 S-cfc-186/190/193,若节点 flaky 失败下轮改派 lan-189/pub-iwintrue | 无需 |
+| P 域 80 无键 SC(健康报告/DAN 预约/AI 方案等) | 241 场景目标 | 需求(覆盖缺口) | T 域已 closure;P 域为下一主攻方向,需主理人确认契约锚点后立项 | 建议主理人确认 |
+| deploy pending open 19(含 11-08 起 6 条历史遗留) | 部署收口 | 部署 | O-24 已裁决归测试/部署侧 `--deploy-done` 回执;编排侧不代越 | 无需 |
+
+---
+
+## 四、下轮工作规划
+
+| 计划项 | 预计完成时间 | 前置依赖 | 涉及会话标签 | 备注 |
+|--------|--------------|----------|--------------|------|
+| 错峰巡检(每时45分) | 下一窗口 | state.json 距本轮 >5min | 研发负责人 | 互斥放行即接管 |
+| 17 个 RED 契约产出核验 + red_sha 回填 | 持续 | 会话 push 证据 | 5 节点 | 转 mark-red 三类流证据 |
+| 6 个 YELLOW O-22 豁免键 mark-green 协同 | 持续 | 测试侧复跑契约 | 测试负责人 | mark-green 不查验 yellow_sha |
+| YELLOW→GREEN 测试协同推进 | 持续 | 测试团队 | 测试负责人 | 72 YELLOW 校验(66 有 sha + 6 O-22) |
+| **P 域契约批次立项(EVD 模式)** | 主理人确认后 | sc-key-map + API_REFERENCE | — | 80 无键 SC 主攻方向 |
+
+---
+
+## 附录
+
+### 附录 A:收到的测试反馈问题明细
+
+| 问题 ID | 能力键/任务号 | 问题类型 | 来源 | 接收时间 | 处置状态 | 关联 sha |
+|---------|---------------|----------|------|----------|----------|----------|
+| 无 | — | — | — | — | — | — |
+
+### 附录 B:本回合派发/恢复明细
+
+本回合**零派发、零恢复**(dispatched 17 / pending 0 池满;`--resume --dry-run` 0 候选)。17 个 RED 契约任务全部由上轮他轮错峰 / 测试侧完成派发,本回合仅核验会话挂载状态(5 节点 session 全挂)。
+
+### 附录 C:上轮派发 GREEN 实现任务回执核验(本轮关键增量)
+
+| 任务号 | 能力键 | 会话 sha | stage | yellow_sha | merge-base IN_CHAIN | 备注 |
+|--------|--------|----------|-------|------------|---------------------|------|
+| S-cfc-176 | membership.plan.purchase | ca56f8699 | YELLOW | `ca56f8699` | ✓ | 已回填,commit ca56f8699 IN CHAIN |
+| S-cfc-177 | supplier.order.fulfill | 808b603f2 | YELLOW | `808b603f2` | ✓ | 已回填,commit 808b603f2 IN CHAIN |
+| S-cfc-179 | family.familyMembers.removeMember | 5c3cff249 | YELLOW | `5c3cff249` | ✓ | 已回填,commit 5c3cff249 IN CHAIN |
+| S-cfc-175 | shop.coupon.apply | 978fb8c88 | YELLOW(无 sha) | — | — | O-22 豁免:契约 20/20 全绿实现已覆盖,commit 为验证报告 docs 提交;yellow_sha 空缺交测试侧 |
+| S-cfc-178 | family.familyMembers.removeMember | 278ec2e9 | 重复任务 | — | — | 上轮重派遗留,已 done 归档(取 5c3cff249 为权威 yellow_sha) |
+
+> **结论**:上轮派发 5 个 GREEN 实现任务(S-cfc-175~179)全部回执 done,3 个实现 sha 已由编排侧/O-16 先例回填 state.json,**IN_CHAIN 3/3 ✓**;另 2 个属 O-22 豁免(契约全绿实现早已在链),交测试侧 mark-green 校验。
+
+### 附录 D:变更感知明细(来自 `--changes --days 7`)
+
+| 变更文档 | 文档类型 | 变更日期 | 新增能力键 | 是否已入池 |
+|----------|----------|----------|------------|------------|
+| 近 7 天 235 文件(需求/设计 126 + 用例 35 + 其他 86) | 需求/设计/用例 | 2026-11-11~11-13 | **0** | 是(既有键状态变化,无需入池) |
+
+---
+
+## 主理人裁决(自裁)
+
+- O-1~O-25 全部已裁决生效(pending.md 状态列均已裁决),本轮**无新增待裁决项**。
+- **裁决 1(本回合零派发)**:dispatched 17 / pending 0 池满,17 个 RED 契约任务全部 dispatched 在途(5 节点 session 全挂),无空闲节点、无 pending 任务 → 本回合零派发,仅核验会话挂载 + GREEN 实现回执(见附录 C)。
+- **裁决 2(--changes 0 新键 不 --add-task)**:近 7 天 235 文件变更,新增能力键 0 → 按 O-7/O-25 防假 RED 纪律不 `--add-task`;P 域 80 无键 SC 为下一主攻方向,待主理人确认契约锚点后立项 EVD 契约批次。
+- **裁决 3(6 YELLOW O-22 豁免键保持不动)**:admin.article.list / health.report.detail / task.task.create / shop.product.browse / shop.order.create / shop.coupon.apply 6 键实现早已在链(契约全绿、无 src/main 改动),yellow_sha 空缺按 O-22 归档口径交测试侧 mark-green 校验(mark-green 不查验 yellow_sha),编排侧不代越。
+- **裁决 4(deploy pending open 19 归测试/部署侧)**:O-24 已裁决归测试/部署侧 `--deploy-done` 回执;编排侧不代越(`--deploy-done` 仅用于编排侧确认部署会话真实部署成功的 commit)。
+
+---
+
+**模板版本**:v1.1|**适用对象**:研发负责人 / 开发角色会话
+**汇报生成**:自动化 05193ba0(错峰轮)· 2026-11-13 10:51

+ 221 - 0
docs/tdd/orchestration/report-dev-2026-11-13-1210.md

@@ -0,0 +1,221 @@
+# 研发负责人工作汇报(第 153 轮)
+
+> 角色:研发负责人(错峰03 · TDD 推进)|周期:2026-11-13 11:30 ~ 12:10|负责范围:CFC 全量(SRS/设计/用户场景实现闭环)
+> 交付物:本报告由单文件 `git hash-object` 旁路推 `origin/cfclub`,**未触碰主工作树 731 处其他会话 WIP**(禁 `add -A`)。
+
+---
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | 研发负责人(错峰03 · 自动化错峰轮) |
+| 汇报周期 | 2026-11-13 11:30 ~ 2026-11-13 12:10(第 153 轮;上轮 152 @10:51 由错峰轮收口,state.json 更新于 11:20:03,本轮启动 11:30 距其 >5min **互斥放行**;执行中 state.json 二次更新 11:33:38 为他轮 --apply 建 RED 批次,本轮续查仍 >5min 窗口) |
+| 负责模块/单元范围 | CFC 全量(cfc-backend / cfc-frontend / cfc-web / danshop / cfc-langgraph) |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 问题接收与处置
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **收到的测试反馈问题数** | `0` 个 | `--inspect` 0 已登记阻塞(`docs/tdd/test/blockers.md` 无新增);测试侧无研发类阻塞回流 |
+| **已解决问题数** | `16` 个 | **backfill 回收 16 条 dispatched 死锁**(S-cfc-180~196,会话未回报但契约测试已推远端) |
+| **待解决研发问题数** | `2` 个 | S-cfc-182/186 backfill 误配(sc-key-map docs 提交非契约测试)→ auto T8 自动 requeue + 重派,下轮收会话回执 |
+
+### 1.2 任务分发与会话管理
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **分派任务数** | `2` 个 | auto `--limit 2` 续派:S-cfc-182(lan-189)/ S-cfc-186(lan-204),均为 T8 requeue 后重派 |
+| **恢复智能体会话数** | `0` 个 | `--resume --dry-run` 0 候选;prod-etotem 生产跳过(默认) |
+| **当前在用会话数** | `4` 个 | S-cfc-182(lan-189) / 186(lan-204) / 190(mes-local-45278) / 193(mes-local-45278) dispatched 在途 |
+| **会话平均利用率** | `轻载` % | 5 节点全 idle(pub-iwintrue busy=2 配额 9/10 剩余),其余节点 0 占用;pending 47 待续派 |
+
+### 1.2b 各会话当前工作状态列表
+
+| 会话标签 | 当前状态 | 当前任务 | 备注 |
+|----------|----------|----------|------|
+| lan-189 | `busy=1` | S-cfc-182(teacher.report.proxy 契约重派) | 本轮 T8 requeue 重派 |
+| lan-204 | `busy=1` | S-cfc-186(teacher.family.overview 契约重派) | 本轮 T8 requeue 重派 |
+| mes-local-45278 | `busy=2` | S-cfc-190 / 193(teacher.family.bind / client.export) | 上轮遗留在途,本轮无证据不 backfill |
+| local-lan-4090 | `idle` | — | 空闲,配额 10/10 |
+| pub-iwintrue | `busy=2` | 部署/测试侧 | 配额 9/10 |
+| production(ses_f719c9326) | `idle` | — | 生产部署会话,registered |
+| testing(ses_e4804838) | `idle` | — | 测试部署会话,registered |
+
+> 本轮 auto 派发 2 条后,lan-189/lan-204 各新增 1 会话在途;local-lan-4090 空闲(下轮 T6 续派 candidate)。
+
+### 1.3 变更感知与新任务建立
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **感知需求/设计变更数** | `~235` 文件 | 近 7 天 `--changes`:需求/设计 + 测试用例 + 其他;**新增能力键 0**("无新增能力键 0→0,变化集中在既有键") |
+| **新增入池任务数** | `13` 个(auto 自动续建) | auto 对 13 键 RED→YELLOW 自动创建对应 GREEN 实现任务 **S-cfc-226~238**(T6 池空续派达成) |
+| **既有键状态流转数** | `13` 键 | 13 键 **RED→YELLOW**(teacher.* T域契约单测证据就绪,red_sha 回填);RED 46→38 / YELLOW 72→85 |
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+| 提交 sha | 关联任务/能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|-----------------|------|--------------|----------|------------|------|
+| 13 键 RED→YELLOW red_sha | teacher.* 12 + family.family.autoCreateForPurchase 1 | RED✓ | 测试文件(已推远端) | cfclub | 是 | backfill 证据回填,auto 自动登记 red_sha 推进 RED→YELLOW;**其中 9 键 sha 为 merge 提交**(见附录 D 甄别) |
+| S-cfc-180 | teacher.plan.activate | RED✓ | 测试 | cfclub | 是 | `e9e644c357` 真实 test 提交 |
+| S-cfc-184 | teacher.assessment.record | RED✓ | 测试 | cfclub | 是 | `64f8a3875f` 真实 test 提交 |
+| S-cfc-185 | teacher.data.isolation | RED✓ | 测试 | cfclub | 是 | `5c0b32c84c` 真实 test 提交 |
+| S-cfc-188 | family.family.autoCreateForPurchase | RED✓ | 测试 | cfclub | 是 | `883a6eeab5` 真实 test 提交 |
+| 报告文档 | — | — | docs | cfclub | 是 | 单文件 hash-object 旁路(本轮唯一新增提交) |
+
+> **本轮无新增业务实现提交**(零 YELLOW 实现、零业务写盘);核心动作 = backfill 死锁回收 + auto 状态推进 + GREEN 任务续建。报告文档走单文件 `git hash-object` 旁路(临时索引 read-tree origin/cfclub^{tree})。
+
+### 1.5 状态机阶段分布与部署状态
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| RED | `38` | admin.* 13 + child.* 12 + supplier.* 5 + mind.model.manage 1 + energy.ledger.guard 1 + teacher.report.proxy 1 + teacher.family.overview 1 + …(S-cfc-197~225 + 239~243 契约批次 + 182/186 重派);red_sha 待回填 |
+| YELLOW | `85` | 66(上轮已填 sha IN_CHAIN)+ 6 O-22 豁免 + **13 本轮 RED→YELLOW**(teacher.* 契约就绪待实现,yellow_sha 空) |
+| 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 / dimension.registry.mapLegacyCode / family.familyMembers.addMember 等 |
+
+| 部署项 | 值 | 说明 |
+|--------|-----|------|
+| 本轮部署次数 | `0` 次 | 编排侧不代 `--deploy-done`,部署回执归测试/部署侧 |
+| pending / confirmed | `19` / `77` | O-24 已裁决归测试/部署侧收口;编排侧不代越 |
+| last_deployed | 双账本差异延续 | 归部署侧 |
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳) | `0` | 正常(稳态) | 无新缺口键 |
+| 契约常量(契约) | `0` | 正常 | — |
+| 测试用例引用(用例∩⏳) | `0` | 正常 | ⏳∩用例引用=0 |
+| 执行结果回填(case_results) | `0` | 正常 | — |
+
+> **场景覆盖矩阵**:SC 台账主表 241 行 = 241 唯一 SC(零漂移)= 已跑通 15 / 有键未跑通 18 / 无键未契约化 208。**跑通 15/241**(本轮 T 域 13 键契约就绪推进,为 241 覆盖打基础);241/241 前不得报「开发完成」。P 域 80 无键 SC(健康报告/DAN 预约/AI 方案/心愿/能量归因)仍为下一主攻方向。
+
+---
+
+## 二、下一步需要研发处理的事项
+
+| 序号 | 事项描述 | 依赖/前置条件 | 预估完成时间 | 优先级 | 状态 |
+|------|----------|---------------|--------------|--------|------|
+| 1 | 13 个 YELLOW 键(teacher.* RED→YELLOW)实现落地:S-cfc-226~238 GREEN 任务续派 + 会话执行 | 下轮 T6 续派(本轮 --limit 2 已用尽) | 下一窗口 | P1 | 待派发 |
+| 2 | S-cfc-182/186 重派会话回执核验(本轮 T8 requeue 重派)→ 契约测试产出后回填 red_sha | 会话 push 契约测试 | 持续 | P1 | 进行中 |
+| 3 | S-cfc-190/193 上轮遗留 dispatched 会话回执核验(无证据不 backfill) | 会话产出契约测试 | 持续 | P2 | 进行中 |
+| 4 | 38 个 RED 契约任务(S-cfc-197~225 + 239~243)产出核验 → 回填 red_sha → 交测试侧 mark-red 三类流 | 会话 push 证据 | 持续 | P1 | 进行中 |
+| 5 | 85 个 YELLOW 键测试 GREEN 校验 + 241 用户场景三类流证据(O-7) | 测试团队协同 | 持续 | P1 | 进行中(瓶颈在测试侧) |
+| 6 | 19 条 pending 部署经 `--deploy-done testing` 收口(O-24) | 部署会话回执 | 持续 | P2 | 归测试/部署侧 |
+| 7 | **P 域 80 无键 SC 契约批次设计**(参照 EVD-10 模式立项) | 主理人确认 P 域契约锚点 | 评估后 | P1 | 待主理人确认 |
+
+---
+
+## 三、阻塞与风险
+
+| 阻塞项 | 影响范围 | 阻塞类型 | 处置进展 | 需要协调 |
+|--------|----------|----------|----------|----------|
+| 主工作树 731 处他人 WIP(非本轮产生) | 主分支目录 | 研发(环境) | 已隔离:全程临时索引/单文件 hash-object 旁路,零接触 | 无需 |
+| **backfill sha 误配缺陷(151 轮已报,本轮复现)** | 状态机 red_sha 精确度 | 编排(工具) | 13 键 RED→YELLOW 中 9 键 red_sha 为 merge 提交(非真实 test 提交);真实 test 提交已在链(附录 D 对照)。编排侧按 backfill commit 字段登记,git_evidence 已过滤为 test_hit(merge 提交含测试文件),功能有效但 sha 非最精确。建议后续修 dispatch.py `_git_evidence_sha()` 只认 test/main 提交(**见裁决 4**) | 无需(本轮如实记录) |
+| S-cfc-182/186 backfill 误配 sc-key-map docs 提交 | 2 条契约任务 | 编排(工具) | 已由 auto T8 自动识别(无 git 证据 → requeue)+ 重派;编排侧自愈生效 | 无需 |
+| P 域 80 无键 SC(健康报告/DAN 预约/AI 方案等) | 241 场景目标 | 需求(覆盖缺口) | T 域 13 键契约就绪推进;P 域为下一主攻方向,需主理人确认契约锚点后立项 | 建议主理人确认 |
+| deploy pending open 19(历史遗留) | 部署收口 | 部署 | O-24 已裁决归测试/部署侧 `--deploy-done` 回执;编排侧不代越 | 无需 |
+
+---
+
+## 四、下轮工作规划
+
+| 计划项 | 预计完成时间 | 前置依赖 | 涉及会话标签 | 备注 |
+|--------|--------------|----------|--------------|------|
+| 错峰巡检(每时45分) | 下一窗口 | state.json 距本轮 >5min | 研发负责人 | 互斥放行即接管 |
+| **S-cfc-226~238 GREEN 实现任务续派**(13 条 teacher.*) | 下一窗口 T6 续派 | --limit 配额 | lan-189 / lan-204 / local-lan-4090 | 本轮 --limit 2 已用尽,下轮续派 |
+| S-cfc-182/186/190/193 会话回执核验 | 持续 | 会话 push 证据 | lan-189 / lan-204 / mes-local-45278 | 契约产出后回填 red_sha |
+| 38 RED 契约产出核验 + red_sha 回填 | 持续 | 会话 push 证据 | 5 节点 | 转 mark-red 三类流证据 |
+| YELLOW→GREEN 测试协同推进 | 持续 | 测试团队 | 测试负责人 | 85 YELLOW 校验 |
+| **P 域契约批次立项(EVD 模式)** | 主理人确认后 | sc-key-map + API_REFERENCE | — | 80 无键 SC 主攻方向 |
+
+---
+
+## 附录
+
+### 附录 A:收到的测试反馈问题明细
+
+| 问题 ID | 能力键/任务号 | 问题类型 | 来源 | 接收时间 | 处置状态 | 关联 sha |
+|---------|---------------|----------|------|----------|----------|----------|
+| 无 | — | — | — | — | — | — |
+
+### 附录 B:本回合派发/恢复明细
+
+| 动作 | 任务号 | 能力键 | 实例 | 会话 | 备注 |
+|------|--------|--------|------|------|------|
+| T8 requeue 重派 | S-cfc-182 | teacher.report.proxy | lan-189 | ses_e2cecd750ffewvO2mA7DVWQWA0 | backfill 误配 docs 提交 → T8 识别无证据 → requeue #1 → 重派 |
+| T8 requeue 重派 | S-cfc-186 | teacher.family.overview | lan-204 | ses_e2cec8dc9ffezvmhMGbLkCEQ65 | 同上 |
+
+> auto `--limit 2` 续派 2 条成功 / 0 失败 / 0 超限。`--resume --dry-run` 0 候选(prod-etotem 生产跳过)。
+
+### 附录 C:backfill 回收 16 条 dispatched 死锁(本轮关键增量)
+
+| 任务号 | 能力键 | backfill commit | 真实契约测试提交 | IN_CHAIN | 处置 |
+|--------|--------|-----------------|------------------|----------|------|
+| S-cfc-180 | teacher.plan.activate | e9e644c357 | e9e644c357(test) | ✓ | 真实 test,RED✓ 推进 |
+| S-cfc-181 | teacher.appointment.guard | 50744725e(Merge) | fb82952fa(test) | ✓ | sha 误配,功能有效 |
+| S-cfc-182 | teacher.report.proxy | 5b938488a(sc-key-map docs) | 无 | — | **误配** → T8 requeue 重派 |
+| S-cfc-183 | teacher.plan.review | 76131eb09(Merge) | 5f2a09b02(test) | ✓ | sha 误配,功能有效 |
+| S-cfc-184 | teacher.assessment.record | 64f8a3875f | 64f8a3875f(test) | ✓ | 真实 test,RED✓ 推进 |
+| S-cfc-185 | teacher.data.isolation | 5c0b32c84c | 5c0b32c84c(test) | ✓ | 真实 test,RED✓ 推进 |
+| S-cfc-186 | teacher.family.overview | 5b938488a(sc-key-map docs) | 无 | — | **误配** → T8 requeue 重派 |
+| S-cfc-187 | teacher.task.review | 233d6e7c0(Merge) | 9b565d9db(test) | ✓ | sha 误配,功能有效 |
+| S-cfc-188 | family.family.autoCreateForPurchase | 883a6eeab5 | 883a6eeab5(test) | ✓ | 真实 test,RED✓ 推进 |
+| S-cfc-189 | teacher.login.switch | 05531929d(Merge) | bbc441e08(test) | ✓ | sha 误配,功能有效 |
+| S-cfc-190 | teacher.family.bind | —(仍 dispatched) | 无 | — | 本轮不 backfill,下轮核验 |
+| S-cfc-191 | teacher.message.push | 03cb65f00(Merge remote) | 23e2ddd66(test) | ✓ | sha 误配,功能有效 |
+| S-cfc-192 | teacher.nutrition.plan | cc5cd22bc(merge) | f7e9ed991(test) | ✓ | sha 误配,功能有效 |
+| S-cfc-193 | teacher.client.export | —(仍 dispatched) | 无 | — | 本轮不 backfill,下轮核验 |
+| S-cfc-194 | teacher.dimension.score | 5da32aeb4(merge) | 69e8ed20e(test) | ✓ | sha 误配,功能有效 |
+| S-cfc-195 | teacher.mind.record | 9ae64fedbb(merge) | baf130a8f(test) | ✓ | sha 误配,功能有效 |
+| S-cfc-196 | teacher.plan.create | bfd9cbe665(Merge) | fb1702975(test) | ✓ | sha 误配,功能有效 |
+
+> **结论**:backfill 回收 16 条(S-cfc-180~196),14 条有真实契约测试证据(功能有效,RED→YELLOW 推进);2 条误配(182/186 命中 sc-key-map docs 提交)由 auto T8 自动识别 requeue + 重派。S-cfc-190/193 无证据本轮不 backfill(仍 dispatched)。
+
+### 附录 D:RED→YELLOW red_sha sha 甄别对照(backfill 缺陷复现实证)
+
+| 能力键 | 登记的 red_sha(backfill commit) | 提交类型 | 真实契约测试提交 | 精确度 |
+|--------|-----------------------------------|----------|------------------|--------|
+| teacher.plan.activate | e9e644c357 | test | e9e644c357 | ✓ 精确 |
+| teacher.appointment.guard | 50744725ef | **Merge** | fb82952fa | △ merge(含测试文件,功能有效) |
+| teacher.plan.review | 76131eb09 | **Merge** | 5f2a09b02 | △ merge |
+| teacher.assessment.record | 64f8a3875f | test | 64f8a3875f | ✓ 精确 |
+| teacher.data.isolation | 5c0b32c84c | test | 5c0b32c84c | ✓ 精确 |
+| teacher.task.review | 233d6e7c0 | **Merge** | 9b565d9db | △ merge |
+| family.family.autoCreateForPurchase | 883a6eeab5 | test | 883a6eeab5 | ✓ 精确 |
+| teacher.login.switch | 05531929d6 | **Merge** | bbc441e08 | △ merge |
+| teacher.message.push | 03cb65f001 | **Merge remote** | 23e2ddd66 | △ merge |
+| teacher.nutrition.plan | cc5cd22bce | **merge** | f7e9ed991 | △ merge |
+| teacher.dimension.score | 5da32aeb43 | **merge** | 69e8ed20e | △ merge |
+| teacher.mind.record | 9ae64fedbb | **merge** | baf130a8f | △ merge |
+| teacher.plan.create | bfd9cbe665 | **Merge** | fb1702975 | △ merge |
+
+> **结论**:13 键 RED→YELLOW 的 red_sha 中 **4 键精确**(真实 test 提交)、**9 键为 merge 提交**(含测试文件变更,git_evidence 判 test_hit 功能有效,但非最精确 sha)。真实契约测试提交均已在链(merge-base IN_CHAIN)。**建议后续修 dispatch.py `_git_evidence_sha()`**:优先取 test/main 提交,merge 提交作兜底并标注(**见裁决 4**)。
+
+### 附录 E:变更感知明细(来自 `--changes --days 7`)
+
+| 变更文档 | 文档类型 | 变更日期 | 新增能力键 | 是否已入池 |
+|----------|----------|----------|------------|------------|
+| 近 7 天 ~235 文件(需求/设计 + 用例 + 其他) | 需求/设计/用例 | 2026-11-11~11-13 | **0**(0→0,变化集中既有键) | 是(既有键状态变化,无需入池) |
+
+---
+
+## 主理人裁决(自裁)
+
+- O-1~O-25 全部已裁决生效(pending.md 状态列均已裁决),本轮**无新增待裁决项**(O-1~O-25 在链)。
+- **裁决 1(backfill 回收死锁)**:16 条 dispatched 死锁(会话未回报但契约测试已推远端)按 135/150/151 轮先例 `dispatch.py --backfill` 回收,备份 `tasks.json.bak-r152-20261113-1145`。14 条真实证据有效回收,2 条误配(182/186)由 auto T8 自愈。
+- **裁决 2(--changes 0 新键,仅 auto 续建 GREEN 任务)**:本轮 `--changes` 新增能力键 0,无 `--add-task` 建新 RED 任务(沿 O-7/O-25 防假 RED 纪律)。auto 对 13 键 RED→YELLOW 自动续建 13 条对应 GREEN 实现任务(S-cfc-226~238),属 T6 池空续派机制(非手工 add-task),符合纪律。
+- **裁决 3(--limit 2 派发配额)**:本轮 auto `--limit 2` 派发 2 条(S-cfc-182/186 重派),配额已用尽;S-cfc-226~238 GREEN 任务留待下轮 T6 续派(不违反纪律,T6 支持跨轮续派)。
+- **裁决 4(backfill sha 甄别建议,沿 151 轮建议升级)**:本轮复现 151 轮已报的 `dispatch.py backfill 证据匹配缺陷`(`_git_evidence_sha()` 按 tid/key grep 不区分提交类型 → 误配 merge/docs)。本轮 13 键 RED→YELLOW 中 9 键 sha 为 merge 提交(功能有效但非最精确)。**升级建议**:`_git_evidence_sha()` 优先取 test/main 提交,merge 提交仅作兜底并标注 `merge_fallback=True`;docs 提交(如 sc-key-map)应排除。交编排侧排期修复(不影响当前闭环功能,故本轮不阻塞)。
+- **裁决 5(deploy pending open 19 归测试/部署侧)**:O-24 已裁决归测试/部署侧 `--deploy-done` 回执;编排侧不代越。
+- **裁决 6(P 域 80 无键 SC 待主理人确认契约锚点)**:本轮 T 域 13 键契约就绪推进;P 域(健康报告/DAN 预约/AI 方案/心愿/能量归因)为下一主攻方向,待主理人确认 P 域契约锚点后立项 EVD 契约批次。**交主理人裁决**。
+
+---
+
+**模板版本**:v1.1|**适用对象**:研发负责人 / 开发角色会话
+**汇报生成**:研发负责人(错峰03 · TDD 推进)· 2026-11-13 12:10

+ 191 - 0
docs/tdd/orchestration/report-dev-2026-11-13-1234.md

@@ -0,0 +1,191 @@
+# 研发负责人工作汇报(2026-11-13 12:34 · 第 138 轮 · 错峰轮 298dcf47)
+
+> 角色:研发负责人(自动化 298dcf47,错峰轮)。职责:组织开发 / 解决测试问题 / 按需求设计分派 / 部署核验 / 标准模板汇报。
+
+---
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | 研发负责人(自动化 298dcf47) |
+| 汇报周期 | 2026-11-13 12:34(第 138 轮) |
+| 负责模块/单元范围 | 全量(scope 空);任务池单一事实源 `.workbuddy/tasks.json`、状态机 `.tdd/state.json` |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 问题接收与处置
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **收到的测试反馈问题数*** | `0` 个 | 本轮未从 `docs/tdd/test/blockers.md` / `--mark-blocked` 回执 / 会话反馈收到需研发处理的新问题;活阻塞台账 B-* 均为历史陈旧项,研发侧无待办 |
+| **已解决问题数*** | `0` 个 | 研发侧无新增缺陷;瓶颈在测试侧(66 YELLOW→GREEN 校验 + 241 SC 三类流证据,见 O-7) |
+| **待解决研发问题数** | `0` 个 | 研发侧无阻塞;唯一待裁决项为状态机口径异常(O-19,主理人级) |
+
+> 填写要求:研发侧闭环完整(66 个 YELLOW 键实现均 IN CHAIN,待测试校验);本轮无新研发缺陷。
+
+---
+
+### 1.2 任务分发与会话管理
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **分派任务数*** | `2` 个 | 本轮 `--auto --role 开发 --limit 2` 实跑,派发 S-cfc-197(→ lan-189)/ S-cfc-198(→ lan-204),均为 `kind=tdd` 的合法 pending 任务 |
+| **恢复智能体会话数*** | `0` 个 | `--resume --dry-run` 候选 0(prod-etotem 生产实例默认跳过;无 interrupted / mid-work 待恢复会话) |
+| **当前在用会话数** | `6` 个 | 任务账本 `dispatched=6`(S-cfc-197/198 本轮新派 + 4 个既有 in-flight) |
+| **会话平均利用率** | `N/A` % | 沙箱无法直连 opencode 节点探活,以账本 + `--resume --dry-run` 交叉核验;5 实例各 idle 50/10 配额,6 个在途 dispatched |
+
+> 填写要求:派发节奏按纪律 `--limit 2` / 轮;任务账本 pending 仍余 63(50 RED-stage + 13 GREEN-stage,均带 `SC-*` 用户场景 + `API_REFERENCE` 溯源),下轮继续按 2/轮续派。
+
+### 1.2b 各会话当前工作状态列表
+
+> 沙箱网络不可直连 opencode 节点,会话状态以任务账本(`dispatched`)+ `--resume --dry-run` 交叉核验为准;不编造实时探活数据。
+
+| 会话标签 | 会话 ID | 当前状态 | 当前任务 | 静默/空闲时长 | 工作目录 | 备注 |
+|----------|---------|----------|----------|---------------|----------|------|
+| lan-189 | ses_e2cc5d39effe0ovxVqkkTNByMM | dispatched | S-cfc-197(tdd/RED) | 刚派发 | 见分支 worktree | 本轮新派 |
+| lan-204 | ses_e2cc5ac47ffeqE9B2VoS5u9cdX | dispatched | S-cfc-198(tdd/RED) | 刚派发 | 见分支 worktree | 本轮新派 |
+| 4× 既有在途会话 | ses_e2cecd75… / ses_e2cec8d9… / ses_e2d54aee… / ses_e2d543d8… | dispatched | 既有 in-flight(无 tid 关联账本条目) | 待核 | — | 账本无 tid 关联,建议下轮核对归属 |
+| lan-189/204/local-4090/mes-45278/pub-iwintrue | — | idle | — | — | — | 5 实例各 50 idle、配额 0/10,无 CFC 在途 |
+
+**状态判定**:除 6 个 dispatched 外,5 个 opencode 实例均 idle;无 wedged / swallowed / stalled;遵守「R2 不留空闲」——存在空闲配额,pending 63 任务按 2/轮节奏续派即可填满。
+
+---
+
+### 1.3 变更感知与新任务建立
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **感知需求/设计变更数*** | `200` 提交 / `247` 文件(7 天) | `--changes --days 7`:设计文档 126 / 测试用例 35 / 其他 86;**新增能力键 0**(变化均集中在既有键内容/状态,无新需求键) |
+| **新增入池任务数*** | `0` 个 | 稳态:无新需求/设计驱动的新键;63 个 pending 任务为既往已建(溯源齐全),非本轮新增 |
+| **既有键状态流转数** | `66` 次 | 66 个 YELLOW 键逐键 `git merge-base IN_CHAIN` 自动核验通过;多键自动派发 testing 部署验证(O-24 改派 lan-189 收口口径) |
+
+> 填写要求:新增能力键 0 → 无新 RED 缺口来自需求侧;实现层已达稳态(66 YELLOW IN CHAIN)。
+
+---
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+> 编排侧只认「已推送远端」的提交为推进证据。本轮 YELLOW 自动核验命中以下实现提交(均 `git merge-base --is-ancestor <sha> origin/cfclub` = IN CHAIN):
+
+| 提交 sha | 关联能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|------------|------|--------------|----------|------------|------|
+| a79b26640 | admin.article.audit | YELLOW | 实现 | origin/cfclub | 是 | 自动核验 IN_CHAIN,待测试 mark-green |
+| 3b90e26d6 | health.report.create | YELLOW | 实现 | origin/cfclub | 是 | 同上 |
+| 1f31f4b094d5… | dan.assessment.recordCreate | YELLOW | 实现 | origin/cfclub | 是 | 触及 1 文件 |
+| c36d6515b | dan.assessment.resultRecord | YELLOW | 实现 | origin/cfclub | 是 | 触及 1 文件 |
+| 12317d458 | task.task.complete | YELLOW | 实现 | origin/cfclub | 是 | 触及 1 文件 |
+| f139a05a2 | task.task.review | YELLOW | 实现 | origin/cfclub | 是 | 触及 2 文件 |
+| 9afaa6e46719… | service.role.applyBindInvite | YELLOW | 实现 | origin/cfclub | 是 | 触及 8 文件 |
+| 9e1fc2f255ae… | chat.entry.memberGate | YELLOW | 实现 | origin/cfclub | 是 | 触及 10 文件 |
+| ca56f8699f75… | membership.plan.purchase | YELLOW | 实现 | origin/cfclub | 是 | 触及 4 文件 |
+| 808b603f295b… | supplier.order.fulfill | YELLOW | 实现 | origin/cfclub | 是 | 触及 1 文件 |
+| 5c3cff249 | family.familyMembers.removeMember | YELLOW | 实现 | origin/cfclub | 是 | 触及 2 文件 |
+| 1915a3272215… | shop.order.queryRefund | YELLOW | 实现 | origin/cfclub | 是 | 触及 2 文件 |
+
+> 注:S-cfc-197/198 刚派发,尚无 git 证据(实现提交待会话落盘后下轮核验)。S-cfc-156/159/172/173/175 等 O-22 豁免键 `done 无 git 证据` 属任务记录级链接缺口(yellow_sha 键级已实现定论),保持 done 不重派(不误判为缺口)。
+
+---
+
+### 1.5 状态机阶段分布与部署状态
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| UNTRACKED | `0` | — |
+| RED | `56` | **⚠️ 56 个全部为证据缺失孤儿键(无 red_sha/yellow_sha/tid),详见 O-19;非合法 TDD RED 任务,--auto 未派发** |
+| YELLOW | `85` | 其中 66 个 yellow_sha IN CHAIN(实现真实已推远端),19 个无 yellow_sha(同属重建注入,需 O-19 一并清理) |
+| GREEN | `8` | 待部署验证 + 测试确认 |
+| DONE | `15` | 闭环归档 |
+
+| 部署项 | 值 | 说明 |
+|--------|-----|------|
+| 本轮部署次数 | `0` 次(账本主动收口 0;多键自动派发 testing 部署验证待回执) | 部署验证由 `--auto` 自动派发,回执归测试/部署侧 |
+| 最新 `last_deployed` | `2e1e5d32`(IN CHAIN) | 沿用历史值 |
+| GREEN 部署链验证 | 待推进 | 8 GREEN 待 --deploy-done 收口(cron cfc-deploy-251.sh */10 自动部署 HEAD 兜底) |
+
+---
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳ wish) | `0` | 正常(稳态一致) | 目标池全 0,与近 30 轮稳态口径一致 |
+| 契约常量(contract) | `0` | 正常 | 同上 |
+| 测试用例引用(test_case_map) | `0` | 正常 | 同上 |
+| 执行结果回填(case_results) | `0` | 正常 | 同上 |
+
+> **关键异常(O-19)**:状态机 `state.json` 被重填 56 个 RED 孤儿键 + 19 个无证据 YELLOW 键,**这些键不在 `discovery.json`(扫 src/main+src/test)/ `test-inventory.json` 中**,也无 `tid` 关联任务账本。数据源(discovery/inventory)与状态机口径严重不一致——属状态机重建/重填引入的噪声,非真实实现缺口。详见 O-19。
+
+---
+
+## 二、下一步需要研发处理的事项
+
+| 序号 | 事项描述 | 依赖/前置条件 | 预估完成时间 | 优先级 | 状态 |
+|------|----------|---------------|--------------|--------|------|
+| 1 | 主理人裁决 O-19:清洗 state.json 中 56 孤儿 RED + 19 无证据 YELLOW 键(重建以任务账本 + discovery/inventory 证据为准;删除 child.* 弃用域) | O-19 裁决 | 裁决后 1 轮内 | P1 | 待主理人裁决 |
+| 2 | 续派 63 个 pending 任务(S-cfc-199 起,均带 `SC-*` 用户场景 + `API_REFERENCE` 溯源),节奏 2/轮 | opencode 节点可达 | 持续 | P1 | 进行中 |
+| 3 | 协调测试团队推进 66 YELLOW→GREEN 校验 + 241 SC 三类流证据(O-7) | 测试侧 mark-green | 持续 | P2 | 协调中(研发侧无阻塞) |
+
+---
+
+## 三、阻塞与风险
+
+| 阻塞项 | 影响范围 | 阻塞类型 | 处置进展 | 需要协调 |
+|--------|----------|----------|----------|----------|
+| 主工作树 732 处 WIP(他人遗留未提交) | 主工作树 | 环境(非业务) | 严守纪律零接触,未强提交/stash/丢弃 | 无(按铁律④不代处理他人改动) |
+| state.json 口径异常(56 孤儿 RED) | 状态机正确性 | 需求/数据(状态机重建噪声) | 已诊断,O-19 上报待裁决 | 主理人裁决 O-19 |
+| 沙箱无法直连 opencode 节点探活 | 会话实时状态核验 | 环境 | 以账本 + `--resume --dry-run` 交叉核验替代 | 无 |
+
+---
+
+## 四、下轮工作规划
+
+| 计划项 | 预计完成时间 | 前置依赖 | 涉及会话标签 | 备注 |
+|--------|--------------|----------|--------------|------|
+| 续派 pending(2/轮,S-cfc-199 起) | 持续 | opencode 可达 | lan-189/lan-204 等 | 63 待派,约 31 轮清空(或主理人提 pace) |
+| 等 O-19 裁决后重建 state.json 证据一致性 | O-19 裁决后 | 主理人 | 编排侧 | 删除孤儿键,状态机回归 discovery/inventory 真相 |
+| 核验 YELLOW IN CHAIN(持续) | 每轮 | — | 编排侧 | 66 YELLOW 已实现真实,待测试 GREEN |
+
+---
+
+## 附录
+
+### 附录 A:收到的测试反馈问题明细
+
+本轮未收到需研发处理的新测试问题(活阻塞台账 B-* 均为历史陈旧项,研发侧无待办)。
+
+### 附录 B:任务分派与会话恢复明细
+
+| 任务号 | 目标会话标签 | 任务类型 | 分派时间 | 当前状态 | 恢复会话标签 | 恢复原因 | 恢复时间 |
+|--------|--------------|----------|----------|----------|--------------|----------|----------|
+| S-cfc-197 | lan-189(ses_e2cc5d39effe0ovxVqkkTNByMM) | tdd/RED | 2026-11-13 12:4x | dispatched | — | — | — |
+| S-cfc-198 | lan-204(ses_e2cc5ac47ffeqE9B2VoS5u9cdX) | tdd/RED | 2026-11-13 12:4x | dispatched | — | — | — |
+
+> 恢复会话 0(--resume --dry-run 候选 0)。
+
+### 附录 C:新增入池任务明细
+
+本轮新增入池任务数:0(稳态,无新需求/设计驱动键;63 个 pending 为既往已建且溯源齐全)。
+
+### 附录 D:变更感知明细(来自 `--changes --days 7`)
+
+| 变更文档 | 文档类型 | 变更日期 | 新增能力键 | 是否已入池 |
+|----------|----------|----------|------------|------------|
+| docs/tdd/automation-run-2026-11-1x-R14x.md 等 200 提交/247 文件 | 设计/用例/其他 | 2026-11-11 ~ 11-12 | 0 | 是(无新键,仅既有键变化) |
+
+---
+
+## 关键结论(本轮)
+
+1. **并发互斥放行**:`state.json` 上轮 12:24:52 改动,本轮 12:34 启动 > 5 分钟;当前无 `tdd_orchestrate`/`opencode` 进程在跑 → 正常接管。
+2. **派发稳态持续推进**:`--auto --role 开发 --limit 2` 实跑派发 S-cfc-197/198(2 个合法 pending),恢复 0,部署自动验证多键;任务账本 262(done 192 / dispatched 6 / pending 63 / failed 1)。
+3. **实现真实性门禁通过**:66 个 YELLOW 键 yellow_sha 逐键 IN CHAIN,研发闭环完整;瓶颈在**验证层**(测试 GREEN + 241 SC 证据)。
+4. **⚠️ 状态机口径异常(O-19,待主理人裁决)**:state.json 被重填 56 个证据缺失孤儿 RED 键(含 14 个 child.* 弃用域——children 表已删、禁止新引入)+ 19 个无证据 YELLOW 键;这些键不在 discovery/inventory、无 tid 关联账本,**非真实实现缺口**,`--auto` 已正确忽略(不派发)。建议主理人裁决以任务账本 + discovery/inventory 证据重建 state.json。
+5. **主工作树 732 处 WIP(他人遗留)零接触**,全程临时索引 / 单文件入库绕过,未强提交、未丢弃、未 stash。
+
+---
+
+**模板版本**:v1.1(2026-11-03,含各会话状态列表 + git 证据 + 状态机分布 + 部署状态 + 数据源健康度)
+**适用对象**:研发负责人 / 开发角色会话

+ 166 - 0
docs/tdd/orchestration/report-dev-2026-11-13-1406.md

@@ -0,0 +1,166 @@
+# 研发负责人工作汇报(2026-11-13 14:06 · 第 164 轮 · 本自动化 856ff81f)
+
+> 角色:研发负责人(自动化 856ff81f,错峰轮)。职责:组织开发 / 解决测试问题 / 按需求设计分派 / 部署核验 / 标准模板汇报。
+
+---
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | 研发负责人(自动化 856ff81f) |
+| 汇报周期 | 2026-11-13 14:06(第 164 轮 · 实例内计数,非全局) |
+| 负责模块/单元范围 | 全量(scope 空);任务池单一事实源 `.workbuddy/tasks.json`、状态机 `.tdd/state.json` |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 问题接收与处置
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **收到的测试反馈问题数*** | `0` 个 | 本轮未收到需研发处理的新测试问题;活阻塞台账无研发侧待办 |
+| **已解决问题数*** | `0` 个 | 研发侧无新增缺陷;瓶颈在测试侧(85 YELLOW→GREEN 校验 + 241 SC 三类流证据) |
+| **待解决研发问题数** | `0` 个 | 研发侧无阻塞;唯一待裁决项为状态机口径异常(O-19,主理人级,见 1.6/三) |
+
+### 1.2 任务分发与会话管理
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **分派任务数*** | `2` 个 | 本轮 `--auto --role 开发 --limit 2` 实跑,派发 S-cfc-200(→ lan-189)/ S-cfc-201(→ lan-204),均为 `kind=tdd` 的合法 pending 任务(RED 契约批次) |
+| **恢复智能体会话数*** | `0` 个 | `--resume --dry-run` 候选 0(prod-etotem 生产实例默认跳过;无 interrupted / mid-work 待恢复会话) |
+| **当前在用会话数** | `9` 个 | 任务账本 `dispatched=9`(S-cfc-182/186/190/193/197/198/199/200/201,全为 RED 契约批次) |
+| **会话平均利用率** | `N/A` % | 以账本 + `--resume --dry-run` 交叉核验;5 实例配额剩余 8~10/节点、busy 0~2 |
+
+### 1.2b 各会话当前工作状态列表
+
+> 沙箱网络不可直连 opencode 节点,会话状态以任务账本(`dispatched`)+ `--resume --dry-run` 交叉核验为准;不编造实时探活数据。
+
+| 会话标签 | 会话 ID | 当前状态 | 当前任务 | 静默/空闲时长 | 工作目录 | 备注 |
+|----------|---------|----------|----------|---------------|----------|------|
+| lan-189 | ses_e2c8445a6ffeq6ygeHTkVB9uZ0 | dispatched | S-cfc-200(tdd/RED) | 刚派发 | 见分支 worktree | 本轮新派;远端已见契约测试 806ebf4aa |
+| lan-204 | ses_e2c8401e2ffeR2xdKXjcNtaZSX | dispatched | S-cfc-201(tdd/RED) | 刚派发 | 见分支 worktree | 本轮新派 |
+| 7× 既有 RED 会话 | ses_e2cecd75… / ses_e2cec8d9… / ses_e2d54aee… / ses_e2d543d8… / ses_e2cc5d39… / ses_e2cc5ac4… / ses_e2c9fe3e1… | dispatched | 既有 RED 契约批次 | 待核 | — | S-cfc-182/186/190/193/197/198/199 |
+| 5 实例 | — | idle | — | — | — | lan-189/204/local-4090/mes-45278/pub-iwintrue 各 50 会话,配额剩余 8~10 |
+
+**状态判定**:除 9 个 dispatched 外,5 个 opencode 实例均 idle;无 wedged / swallowed / stalled;遵守「R2 不留空闲」——pending 107 任务按 2/轮节奏续派。
+
+### 1.3 变更感知与新任务建立
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **感知需求/设计变更数*** | `200` 提交 / `247` 文件(7 天) | `--changes --days 7`:设计文档 126 / 测试用例 35 / 其他 86;**新增能力键 0**(变化均集中在既有键内容/状态,无新需求键) |
+| **新增入池任务数*** | `0` 个 | 稳态:无新需求/设计驱动的新键;107 个 pending 任务为既往已建(溯源齐全),非本轮新增 |
+| **既有键状态流转数** | `66` 次 | 66 个 YELLOW 键逐键 `git merge-base IN_CHAIN` 自动核验通过;19 个无 yellow_sha(6 个 O-22 定论 + 13 个 teacher.*/family 契约已合入待实现) |
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+> 编排侧只认「已推送远端」的提交为推进证据。本轮 YELLOW 自动核验命中实现提交均 IN CHAIN(a79b26640 / 3b90e26d6 / 1f31f4b09 / c36d6515b / 12317d458 / f139a05a2 / 9afaa6e46 / 9e1fc2f25 / ca56f8699 / 808b603f2 / 5c3cff249 / 1915a3272 等)。本轮新派 S-cfc-200/201 关联的远端动态:
+
+| 提交 sha | 关联能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|------------|------|--------------|----------|------------|------|
+| 806ebf4aa | admin.supplier.manage(S-cfc-200) | RED 契约 | 测试 | origin/cfclub | 是 | 13:58 推,TDD-RED:AdminSupplier/EcomSupplier 410 废弃桩 + SC-A-06 守卫 |
+| 52c20821f | P-94~96 映射修正 / 删孤儿键 307 | 台账 | docs | origin/cfclub | 是 | 14:00 推,修正 SC 台账错位映射与 SC-T-01 污染 |
+
+> 注:806ebf4aa 为 S-cfc-200 契约测试推送,与本轮派发关联(派发后会话快速产出),如实记录不重复派发;O-22 豁免键保持 done 不重派。
+
+### 1.5 状态机阶段分布与部署状态
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| UNTRACKED | `0` | — |
+| RED | `102` | 较上轮(06:14 的 95 键)随 O-25 台账→池链路扩容;**RED 102 全量有活跃任务(0 孤儿)**——O-19「56 孤儿 RED」风险经本轮核验已自然消解 |
+| YELLOW | `85` | 66 个 yellow_sha IN CHAIN(实现真实已推远端);19 个无 yellow_sha(6 个 O-22 定论 + 13 个 teacher.*/family 契约已合入、GREEN 任务 S-cfc-226~238 在途待实现) |
+| GREEN | `8` | 待部署验证 + 测试确认 |
+| DONE | `15` | 闭环归档 |
+
+| 部署项 | 值 | 说明 |
+|--------|-----|------|
+| 本轮部署次数 | `0` 次(账本主动收口 0;多键自动派发 testing 部署验证待回执) | 部署验证由 `--auto` 自动派发,回执归测试/部署侧(O-24) |
+| 最新 `last_deployed` | `2e1e5d32`(IN CHAIN) | 沿用历史值;tasks.json 侧 last_deployed 空(双账本差异延续,如实记录) |
+| deploy_state pending / confirmed | `63` / `77` | 全 testing,部分超时无回执,研发不越权 --deploy-done |
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳ wish) | `0` | 正常(稳态一致) | 目标池全 0 |
+| 契约常量(contract) | `0` | 正常 | 同上 |
+| 测试用例引用(test_case_map) | `0` | 正常 | 同上 |
+| 执行结果回填(case_results) | `0` | 正常 | 同上 |
+
+> **关键异常(O-19 收敛)**:上轮 12:34 记录 state.json 被重填 56 孤儿 RED + 19 无证据 YELLOW。本轮核验:**RED 102 全量已有活跃任务关联(0 孤儿)**,且远端已见修正提交 52c20821f(删孤儿键 307、修正 P-94~96 错位映射与 SC-T-01 污染)——O-19 核心风险(孤儿键被误当实现缺口反复派发)已自然消解,`--auto` 正确只派发合法 pending。残留:14 个 child.* 弃用域键仍在状态机(AGENTS.md 明令弃用),建议主理人按 O-19 原建议清理,但已不阻塞派发。
+
+---
+
+## 二、下一步需要研发处理的事项
+
+| 序号 | 事项描述 | 依赖/前置条件 | 预估完成时间 | 优先级 | 状态 |
+|------|----------|---------------|--------------|--------|------|
+| 1 | 主理人确认 O-19 收敛结论(孤儿 RED 已 0、仅 child.* 弃用域待清) | O-19 终裁 | 裁决后 1 轮内 | P2 | 待主理人裁决(本轮按经验裁决:不阻塞) |
+| 2 | 续派 107 个 pending 任务(S-cfc-202 起,均带 `SC-*` 溯源),节奏 2/轮 | opencode 节点可达 | 持续 | P1 | 进行中 |
+| 3 | 协调测试团队推进 85 YELLOW→GREEN 校验 + 241 SC 三类流证据 | 测试侧 mark-green | 持续 | P2 | 协调中(研发侧无阻塞) |
+
+---
+
+## 三、阻塞与风险
+
+| 阻塞项 | 影响范围 | 阻塞类型 | 处置进展 | 需要协调 |
+|--------|----------|----------|----------|----------|
+| 主工作树 733 处 WIP(他人遗留未提交) | 主工作树 | 环境(非业务) | 严守纪律零接触,未强提交/stash/丢弃 | 无(按铁律④不代处理他人改动) |
+| child.* 弃用域 14 键残留状态机 | 状态机正确性 | 需求/数据(历史重建噪声) | 已诊断,O-19 收敛、不阻塞派发 | 主理人确认清理(P2) |
+| 沙箱无法直连 opencode 节点探活 | 会话实时状态核验 | 环境 | 以账本 + `--resume --dry-run` 交叉核验替代 | 无 |
+
+---
+
+## 四、下轮工作规划
+
+| 计划项 | 预计完成时间 | 前置依赖 | 涉及会话标签 | 备注 |
+|--------|--------------|----------|--------------|------|
+| 续派 pending(2/轮,S-cfc-202 起) | 持续 | opencode 可达 | lan-189/lan-204 等 | 107 待派 |
+| 核验 YELLOW IN CHAIN(持续) | 每轮 | — | 编排侧 | 66 YELLOW 已实现真实,待测试 GREEN |
+| 跟踪 S-cfc-200/201 契约产出 | 下轮 | 会话 | lan-189/lan-204 | 806ebf4aa 已见 200 契约 |
+
+---
+
+## 附录
+
+### 附录 A:收到的测试反馈问题明细
+
+本轮未收到需研发处理的新测试问题。
+
+### 附录 B:任务分派与会话恢复明细
+
+| 任务号 | 目标会话标签 | 任务类型 | 分派时间 | 当前状态 | 恢复会话标签 | 恢复原因 | 恢复时间 |
+|--------|--------------|----------|----------|----------|--------------|----------|----------|
+| S-cfc-200 | lan-189(ses_e2c8445a6ffeq6ygeHTkVB9uZ0) | tdd/RED | 2026-11-13 14:0x | dispatched | — | — | — |
+| S-cfc-201 | lan-204(ses_e2c8401e2ffeR2xdKXjcNtaZSX) | tdd/RED | 2026-11-13 14:0x | dispatched | — | — | — |
+
+> 恢复会话 0(--resume --dry-run 候选 0)。
+
+### 附录 C:新增入池任务明细
+
+本轮新增入池任务数:0(稳态,无新需求/设计驱动键;107 个 pending 为既往已建且溯源齐全)。
+
+### 附录 D:变更感知明细(来自 `--changes --days 7`)
+
+| 变更文档 | 文档类型 | 变更日期 | 新增能力键 | 是否已入池 |
+|----------|----------|----------|------------|------------|
+| docs/tdd/automation-run-2026-11-1x-R14x.md 等 200 提交/247 文件 | 设计/用例/其他 | 2026-11-11 ~ 11-12 | 0 | 是(无新键,仅既有键变化) |
+
+---
+
+## 关键结论(本轮)
+
+1. **并发互斥放行**:`state.json` 上轮 13:32:33 改动,本轮 13:40 启动 > 5 分钟 → 正常接管。
+2. **派发持续推进**:`--auto --role 开发 --limit 2` 实跑派发 S-cfc-200/201(2 个合法 pending),恢复 0;任务账本 309(done 192 / dispatched 9 / pending 107 / failed 1)。
+3. **实现真实性门禁通过**:66 个 YELLOW 键 yellow_sha 逐键 IN CHAIN,研发闭环完整;瓶颈在验证层(测试 GREEN + 241 SC 证据)。
+4. **O-19 收敛**:RED 102 全量有活跃任务(0 孤儿),远端已见修正 52c20821f(删孤儿键 307 + 修正 P-94~96 错位映射)——孤儿键误派风险消解;残留 child.* 弃用域 14 键建议主理人确认清理(不阻塞)。
+5. **远端动态**:origin/cfclub 已进 806ebf4aa(S-cfc-200 契约 RED 测试 13:58)与 52c20821f(14:00 台账修正),属他人轮次密集推进 + 本轮派发会话产出,如实记录。
+6. **主工作树 733 处 WIP(他人遗留)零接触**,全程临时索引 / 单文件入库绕过。
+
+---
+
+**模板版本**:v1.1(2026-11-03,含各会话状态列表 + git 证据 + 状态机分布 + 部署状态 + 数据源健康度)
+**适用对象**:研发负责人 / 开发角色会话

+ 173 - 0
docs/tdd/orchestration/report-dev-2026-11-13-1458.md

@@ -0,0 +1,173 @@
+# 研发负责人工作汇报(2026-11-13 14:58 · 第 155 轮 · 错峰03)
+
+> 角色:研发负责人(错峰03 轮次)。职责:组织开发 / 解决测试问题 / 按需求设计分派 / 部署核验 / 标准模板汇报。
+
+---
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | 研发负责人(错峰03 自动化轮) |
+| 汇报周期 | 2026-11-13 14:58(第 155 轮 · 错峰03 计数) |
+| 负责模块/单元范围 | 全量(scope 空);任务池单一事实源 `.workbuddy/tasks.json`、状态机 `.tdd/state.json` |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 问题接收与处置
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **收到的测试反馈问题数*** | `0` 个 | 本轮未收到需研发处理的新测试问题;无研发侧待办阻塞台账 |
+| **已解决问题数*** | `0` 个 | 研发侧无新增缺陷;瓶颈在测试侧(RED 契约证据未落地 + YELLOW→GREEN 校验) |
+| **待解决研发问题数** | `0` 个 | 研发侧无阻塞;主理人级待裁决 O-19 本轮已落裁决(见 1.5/三) |
+
+### 1.2 任务分发与会话管理
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **分派任务数*** | `3` 个 | 本次轮实派 2 个(S-cfc-202 → lan-189 / S-cfc-203 → lan-204),加 `--auto` 自动派发部署验证 1 批(多键 testing 部署,归测试/部署侧 O-24 收口);恢复会话 1 个 |
+| **恢复智能体会话数*** | `1` 个 | `--resume` 恢复 S-cfc-198(lan-204,mid-work「准备继续」,续做 RED 契约) |
+| **当前在用会话数** | `11` 个 | 任务账本 `dispatched=11`(S-cfc-182/186/190/193/197/198/199/200/201/202/203) |
+| **会话平均利用率** | `N/A` % | 以账本 + `--resume --dry-run` 交叉核验;5 实例配额剩余 9~10/节点 |
+
+### 1.2b 各会话当前工作状态列表
+
+> 沙箱网络不可直连 opencode 节点,会话状态以任务账本(`dispatched`)+ `--resume --dry-run` 交叉核验为准;不编造实时探活数据。
+
+| 会话标签 | 会话 ID | 当前状态 | 当前任务 | 静默/空闲时长 | 工作目录 | 备注 |
+|----------|---------|----------|----------|---------------|----------|------|
+| lan-189 | ses_e2c521c88ffe6lRCYFLoIYdLOD | dispatched | S-cfc-202(tdd/RED admin.order.manual) | 刚派发 | 分支 worktree | 本轮新派 |
+| lan-204 | ses_e2c51cd28ffeiYq3Rv78J9JEaL | dispatched | S-cfc-203(tdd/RED admin.activity.config) | 刚派发 | 分支 worktree | 本轮新派 |
+| lan-204 | ses_e2cc5ac47ffeqE9B2VoS5u9cdX | resumed | S-cfc-198(tdd/RED admin.knowledgeBase.manage) | 恢复续做 | 分支 worktree | 本轮 --resume 续做 |
+| 8× 既有 RED 会话 | ses_e2cecd75… / ses_e2cec8d9… / ses_e2d54aee… / ses_e2d543d8… / ses_e2cc5d39… / ses_e2c9fe3e1… / ses_e2c8445a… / ses_e2c8401e… | dispatched | 既有 RED 契约批次 | 待核 | — | S-cfc-182/186/190/193/197/199/200/201 |
+| 5 实例 | — | idle | — | — | — | lan-189/204/local-4090/mes-45278/pub-iwintrue 各 50 会话,配额剩余 9~10 |
+
+**状态判定**:除 11 个 dispatched 外,5 个 opencode 实例均 idle;无 wedged / swallowed / stalled;遵守「R2 不留空闲」——pending 105 任务按 2/轮节奏续派。
+
+### 1.3 变更感知与新任务建立
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **感知需求/设计变更数*** | `200` 提交 / `247` 文件(7 天) | `--changes --days 7`:设计文档 126 / 测试用例 35 / 其他 86;**新增能力键 0**(变化均集中在既有键内容/状态,无新需求键) |
+| **新增入池任务数*** | `0` 个 | 稳态:无新需求/设计驱动的新键;105 个 pending 任务为既往已建(溯源齐全),非本轮新增 |
+| **既有键状态流转数** | `0` 次 | 本轮键级无推进(RED 全缺 red_sha 无法 RED→YELLOW;YELLOW 待测试 mark-green,研发不越权);auto 已自动忽略 requeue_reverted |
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+> 编排侧只认「已推送远端」的提交为推进证据。本轮为编排轮,无新增状态机推进 sha;远端最新 tip 与关联提交如实记录:
+
+| 提交 sha | 关联能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|------------|------|--------------|----------|------------|------|
+| a78add662 | admin.article.manage(S-cfc-201) | RED 契约 | 测试 | origin/cfclub | 是 | 契约级单测 6红18绿(他轮会话产出,13:5x 推) |
+| ab6c15ae0 | S-cfc-201 merge | RED | 测试 | origin/cfclub | 是 | Merge task/S-cfc-201 |
+| 5ad781a00 | admin.supplier.manage(S-cfc-200) | RED 契约 | 测试 | origin/cfclub | 是 | 19 用例 16红/3绿(410 壳/边界/role 守卫) |
+| 7558d8cf4 | — | chore | bump | origin/cfclub | 是 | 当前远端 tip(auto bump) |
+
+> 注:S-cfc-200/201 RED 契约已由他轮会话推远端并 merge,本轮 `--auto` 核验发现后如实记录,**不重复派发**;本轮新派 S-cfc-202/203 契约产出待下轮回执。
+
+### 1.5 状态机阶段分布与部署状态
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| UNTRACKED | `0` | — |
+| RED | `102` | **102 全有 red_task 关联(0 孤儿),但 102 全缺 red_sha**(测试侧契约 RED 未落地/未登记,见裁决 O-19) |
+| YELLOW | `85` | 65 个 yellow_sha IN CHAIN(实现真实已推远端);19 个缺 sha(O-22/requeue_reverted 定论 + 待实现);1 个 requeue_reverted |
+| GREEN | `8` | 待部署验证 + 测试确认 |
+| DONE | `15` | 闭环归档 |
+
+| 部署项 | 值 | 说明 |
+|--------|-----|------|
+| 本轮部署次数 | `0` 次(账本主动收口 0;多键自动派发 testing 部署验证待回执) | 部署验证由 `--auto` 自动派发,回执归测试/部署侧(O-24) |
+| 最新 `last_deployed` | `2e1e5d32`(IN CHAIN) | 沿用历史值;tasks.json 侧 last_deployed 空(双账本差异延续,如实记录) |
+| deploy_state pending / confirmed | `pending 19 / confirmed 77` | 全 testing,部分超时无回执,研发不越权 --deploy-done |
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳ wish) | `0` | 正常(稳态一致) | 目标池全 0 |
+| 契约常量(contract) | `0` | 正常 | 同上 |
+| 测试用例引用(test_case_map) | `0` | 正常 | 同上 |
+| 执行结果回填(case_results) | `0` | 正常 | 同上 |
+
+> **关键异常(O-19 本节已裁决)**:12:34 错峰轮 298dcf47 提交 O-19「56 孤儿 RED + 19 无证据 YELLOW 建议重建删键」。本轮核验:**RED 102 全有 red_task(0 孤儿,T8 backfill 已自然修复)**,`--auto` 只派发合法 pending 未误派孤儿 → 原「孤儿误派」风险消解;**真缺口是 RED 证据**(102 键全缺 red_sha,测试侧契约 RED 未落地);child.* 弃用域 14 键维持现状不阻塞。**裁决已落 pending.md**(部分采纳:不删除、转测试补证据)。
+
+---
+
+## 二、下一步需要研发处理的事项
+
+| 序号 | 事项描述 | 依赖/前置条件 | 预估完成时间 | 优先级 | 状态 |
+|------|----------|---------------|--------------|--------|------|
+| 1 | 续派 105 个 pending 任务(S-cfc-204 起,均带 `SC-*` 溯源),节奏 2/轮 | opencode 节点可达 | 持续 | P1 | 进行中 |
+| 2 | 回执 S-cfc-202/203 契约产出(本轮新派) | 会话执行 | 下轮 | P1 | 待回执 |
+| 3 | 协调测试侧补 RED 契约证据(102 键全缺 red_sha,S-cfc-182 已 requeue #1) | 测试侧写红 + push + `--mark-red` | 持续 | P0 | 协调中(O-19 裁决①③) |
+| 4 | 协调测试团队推进 YELLOW→GREEN 校验(85 键) | 测试侧 mark-green | 持续 | P2 | 协调中(研发侧无阻塞) |
+
+---
+
+## 三、阻塞与风险
+
+| 阻塞项 | 影响范围 | 阻塞类型 | 处置进展 | 需要协调 |
+|--------|----------|----------|----------|----------|
+| RED 102 全缺 red_sha(测试侧契约 RED 未落地) | RED→YELLOW 推进 | 测试(证据) | O-19 已裁决:转测试侧补证据,研发不越权代写测试 | 测试负责人 |
+| child.* 弃用域 14 键残留状态机 | 状态机正确性 | 需求/数据(历史重建噪声) | 维持现状不阻塞派发,`--auto` 已忽略;清理待编排侧正规通道 | 主理人(P2,非紧急) |
+| 主工作树 734 处 WIP(他人遗留未提交) | 主工作树 | 环境(非业务) | 严守纪律零接触,未强提交/stash/丢弃;本轮全部经临时索引/单文件入库绕过 | 无(按铁律④不代处理他人改动) |
+| 沙箱无法直连 opencode 节点探活 | 会话实时状态核验 | 环境 | 以账本 + `--resume --dry-run` 交叉核验替代 | 无 |
+
+---
+
+## 四、下轮工作规划
+
+| 计划项 | 预计完成时间 | 前置依赖 | 涉及会话标签 | 备注 |
+|--------|--------------|----------|--------------|------|
+| 续派 pending(2/轮,S-cfc-204 起) | 持续 | opencode 可达 | lan-189/lan-204 等 | 105 待派 |
+| 跟踪 S-cfc-202/203 契约产出 | 下轮 | 会话 | lan-189/lan-204 | 本轮新派 |
+| 核验 S-cfc-182/186/190/193 无证据 requeue 后回执 | 下轮 | 会话 + 测试侧 | — | T8 自动 requeue 生效中 |
+| 核验 YELLOW IN CHAIN(持续) | 每轮 | — | 编排侧 | 65 YELLOW 已实现真实,待测试 GREEN |
+
+---
+
+## 附录
+
+### 附录 A:收到的测试反馈问题明细
+
+本轮未收到需研发处理的新测试问题。
+
+### 附录 B:任务分派与会话恢复明细
+
+| 任务号 | 目标会话标签 | 任务类型 | 分派时间 | 当前状态 | 恢复会话标签 | 恢复原因 | 恢复时间 |
+|--------|--------------|----------|----------|----------|--------------|----------|----------|
+| S-cfc-202 | lan-189(ses_e2c521c88ffe6lRCYFLoIYdLOD) | tdd/RED | 2026-11-13 14:5x | dispatched | — | — | — |
+| S-cfc-203 | lan-204(ses_e2c51cd28ffeiYq3Rv78J9JEaL) | tdd/RED | 2026-11-13 14:5x | dispatched | — | — | — |
+| S-cfc-198 | lan-204(ses_e2cc5ac47ffeqE9B2VoS5u9cdX) | tdd/RED | 恢复后继续 | dispatched | lan-204 | mid-work 延续语「准备继续」 | 2026-11-13 14:38 |
+
+> 恢复会话 1(--resume 实跑);`--resume --dry-run` 预检候选仅 S-cfc-198。
+
+### 附录 C:新增入池任务明细
+
+本轮新增入池任务数:0(稳态,无新需求/设计驱动键;105 个 pending 为既往已建且溯源齐全)。
+
+### 附录 D:变更感知明细(来自 `--changes --days 7`)
+
+| 变更文档 | 文档类型 | 变更日期 | 新增能力键 | 是否已入池 |
+|----------|----------|----------|------------|------------|
+| docs/tdd/automation-run-2026-11-1x-R14x.md 等 200 提交/247 文件 | 设计/用例/其他 | 2026-11-11 ~ 11-12 | 0 | 是(无新键,仅既有键变化) |
+
+---
+
+## 关键结论(本轮)
+
+1. **并发互斥放行**:`state.json` 上轮写盘 13:58(他轮 --auto/apply 后),本轮 14:31 启动 > 5 分钟 → 正常接管;执行中复查 mtime 14:42(本轮自身 --auto 写盘)无其他轮持续写入。
+2. **恢复 1 + 派发 2**:`--resume` 恢复 S-cfc-198(lan-204);`--auto --role 开发 --limit 2` 续派 S-cfc-202(lan-189)/ S-cfc-203(lan-204);任务账本 309 = done 192 / dispatched 11 / pending 105 / failed 1。
+3. **O-19 裁决落地**:核验 RED 102 全有 red_task(0 孤儿)+ 102 全缺 red_sha(测试契约证据缺口)→ 部分采纳决议(不删键、转测试补 RED 证据),已写入 pending.md。
+4. **实现真实性门禁通过**:65 个 YELLOW 键 yellow_sha 逐键 IN CHAIN,研发闭环完整;瓶颈在验证层(测试 RED 证据 + GREEN 校验 + 241 SC 证据)。
+5. **远端动态**:origin/cfclub 已进 S-cfc-200/201 RED 契约(a78add662/ab6c15ae0/5ad781a00)与 bump(7558d8cf4)——他轮会话契约产出,如实记录不重复派发。
+6. **主工作树 734 处 WIP(他人遗留)零接触**,全程临时索引 / 单文件入库绕过。
+
+---
+
+**模板版本**:v1.1(2026-11-03,含各会话状态列表 + git 证据 + 状态机分布 + 部署状态 + 数据源健康度)
+**适用对象**:研发负责人 / 开发角色会话

+ 267 - 0
docs/tdd/orchestration/report-test-2026-11-13-R189.md

@@ -0,0 +1,267 @@
+# 测试负责人工作汇报(R189 轮)
+
+> 本汇报按《测试负责人工作汇报模板》填写。R189 为**实测轮 + 窗口契约验证轮**:基线 `cfclub @ 00121ff2f`(R188 收口 09:39),当前 HEAD `6124352d7`(11:11,含并行研发/测试会话在窗口内推入的契约测试与台账回填)。
+> **拓扑(用户指令)**:浏览器经 `http://cfc.iwintrue.com/admin`(后台)+ `http://cfc.iwintrue.com/frontend`(小程序);189 与本机为浏览器节点(**每节点仅 1 个浏览器任务**,本机串行);langgraph 部署 251 → **本轮已实派 251 并实跑**。
+> **★ 本轮关键事实**
+> ① **窗口(00121ff2f..6124352d7)源码/SRS/场景/接口文档零变更**(diff 命中 0)→ G12 ✅,用例无需更新;
+> ② 窗口**新增 13 个契约测试类(S-cfc-183/184/188/189/191/196 等域,133 例)全部定向实测**:**85 过 / 48 红 / 0 错**——48 红**全部为预期 RED(TDD 契约先行、研发未实现)**,无意外失败、无回归;其中 LoginSwitch 16/16、MessagePush 20/20、PlanActivate 15/15 三个 GREEN 类全绿;
+> ③ **langgraph@251 51/51 全绿(连续第 2 轮)**——B-142-2(上游 401/429 环境红)观察 2/2,满足销号判据;
+> ④ 浏览器六件套全绿(admin 5/5×2 节点、frontend 6/6、shop 4/4、health 13/13),线上 API 32 例 0 红,fe/web Jest 零漂移。
+> ★ **全站可比:4120 例 / 4003 过 = 97.16%(剔 skip 97.75%),红 92(后端 73 = 存量 25 + 契约预期 RED 48;fe 3 预期 RED;根级 16 环境)——红数上升全部源于窗口新增契约 RED,无缺陷回归。**
+
+---
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | 测试负责人(自动化测试闭环任务 / 软件测试专家团·陆谨) |
+| 汇报周期 | 2026年11月13日 09:56 ~ 11:30(第 189 轮) |
+| 基线 | `cfclub @ 00121ff2f`(R188 收口);本轮收尾 HEAD `6124352d7`(并行会话并发推进,本地=远端) |
+| 窗口内需求变更 | **零** — `docs/superpowers/specs` / `tests/test-cases` / `API_REFERENCE.md` / 用户场景 窗口零变更(G12 ✅,未触发用例编写闸门) |
+| 窗口内源码变更 | **零** — `git diff --name-only 00121ff2f..6124352d7` 对 `src`、需求、用例、接口文档命中 0;34 个变更文件全部为 `docs/*` 与 `cfc-backend/src/test/**`(13 个新增契约测试类) |
+| 负责模块/单元 | 全站:管理端 admin@本机 + 189 · 小程序 H5 fe@本机 · cfc-langgraph@251(**本轮实派**) · cfc-backend JUnit(全量采信 R188 + 窗口契约类实测) · cfc-web/cfc-frontend Jest · 线上 API 非功能层 |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 测试用例建设
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **本轮新增测试用例数\*** | **0 个**(新增**133 例契约测试语法入库**) | SRS/场景/接口文档窗口零变更,未触发用例编写闸门(G12 ✅);窗口内并行会话新增 13 个契约测试类(S-cfc-183/184/188/189/191/196 等)已入库并于本轮逐类实测 |
+| **本轮验证执行测试数\*** | **4120 个** | 后端 **3244**(R188 全量采信 3111 + 窗口 13 契约类实测 133)+ 前端 Jest 624 + 管理端 116 + langgraph 51(**251 实派**)+ 浏览器 30(admin 5×2 + frontend 6 + shop 4 + health 13 + dx 2 跳)+ 线上 API 32 + 根级 23 |
+| **用例通过率** | **97.16%**(4003/4120;剔除 skip **97.75%**) | 红 92 / 跳 25;环比 R188(98.27%)−1.11pct,**红数上升 48 全部为窗口新增契约预期 RED(TDD 先行态),零缺陷回归** |
+| **用例台账** | `discovery.md` / `inventory.md` 已重扫重写 | inventory 跨源 **4567**(md 719 / PW 571 / py 48 / Java @Test **3229** / 场景 11);Java 测试文件 388(较 R188 375 **+13**,即窗口契约类) |
+
+### 1.2 会话与任务分发
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **分派任务数\*** | **6 个** | R189-1 本机 `admin-desktop`(5/5,1.4m);R189-2 本机 `frontend-mobile`(6/6,46.2s);R189-3 本机 `shop-api`(4/4,6.9s)+ `health`(13/13,4.2s);R189-4 **189** `admin-desktop`(5/5 EXIT=0,1.3m);R189-5 **251** `langgraph pytest`(**51/51**,495.56s);R189-6 后端窗口契约类定向(133 例,2m05s) |
+| **恢复智能体会话数\*** | 0 个 | 各节点实探 200/ok(189:4090、251:4096、251:9000、本机 4091 全可达);未遇中断会话 |
+| **当前在用会话数** | 0 个 | 全部结束;251 残留 pytest 进程已清理(B-177-1 观察 3/2→4/2) |
+
+### 1.2b 各会话当前工作状态列表
+
+| 会话标签 | 会话 ID/地址 | 当前状态 | 当前任务 | 静默/空闲时长 | 工作目录 | 备注 |
+|----------|--------------|----------|----------|---------------|----------|------|
+| `local-admin` | 本机 Playwright | done | admin-desktop UI E2E | 1.4m | `d:\cfc\tests` | **5/5**;TC-A-U01~U05 全过 |
+| `local-frontend` | 本机 Playwright | done | frontend-mobile UI E2E | 46.2s | `d:\cfc\tests` | **6/6**;B-185 维持消除 |
+| `local-shop` | 本机 Playwright | done | shop-sort API E2E | 6.9s | `d:\cfc\tests` | **4/4** |
+| `local-health` | 本机 Playwright | done | health 鉴权契约 E2E | 4.2s | `d:\cfc\tests` | **13/13**(TC-HP-U01~03 全 401,B-61 维持) |
+| `remote-189` | 192.168.16.189:4090 | done | admin-desktop UI E2E | 1.3m | `C:\code\cfc\tests` | **5/5 EXIT=0**,跨节点可比 |
+| `remote-251` | 192.168.16.251:4096 | done | langgraph pytest | 495.56s | `/sc-data/cfc/cfc-langgraph` | **51/51 连续第 2 轮全绿**;节点 HEAD `f17d53669` |
+| `local-jest` | 本机进程 | done | cfc-web + cfc-frontend + 根级 | fe 172s / web 209s | `cfc-web` / `cfc-frontend` / `tests` | web **116/116**、fe **609/3/12**(3 预期 RED)、根级 **7/16** |
+| `local-backend` | 本机 mvn | done | 窗口契约类定向测试 | 2m05s | `d:\cfc\cfc-backend` | **133 例 = 85 过 / 48 预期 RED / 0 错** |
+| `virtual` | dx-bridge | done | D/X 桥接鉴权契约 | 瞬时 | `d:\cfc\tests` | **2 skip**(端点 404,B-DX-1 维持) |
+
+### 1.3 问题处置
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **新测试发现问题数\*** | **2 个** | ① **B-189-1(P1 环境)**:`git.iwintrue.com` / 251 **SSH 22 CLOSED**(R188 09:39 尚可 push)→ `git fetch`/`push` 走 SSH 全部失败;替代路径 `http://git.iwintrue.com/liaoxg/cfc.git` 可读(HEAD=6124352d7,与本地一致)。**属 Gogs SSH 服务故障,非代码问题**;② **B-189-2(P2 流程/证据)**:窗口内 13 个契约测试类由并行会话推入但**未逐类验证入库**(自动化闭环遗漏),本轮补验完成:48 红全为预期 RED、3 个 GREEN 类全绿。 |
+| **已解决测试相关问题数\*** | **1 个** | **B-142-2(langgraph 上游凭据/配额环境红)观察 2/2**:R188 51/51 + R189 51/51,连续 2 轮全绿 → **满足销号判据,本轮正式销号**(残留 live chat 链路 120s 零字节单列 B-186-1 观察)。 |
+| **待解决测试相关问题数** | P0×2 / P1×7 / P2×4 | ① B-152-1/2 CI 清单漏拦/陈旧;② S-cfc-183/184/188/196 等 **48 前瞻契约 RED**(P1,需研发实现使绿);③ S-cfc-176 幂等 1F;④ B-177-1 pytest summary 后挂死;⑤ B-156-1/B-170-1 迁移 DDL;⑥ B-162-1 3 类未入库;⑦ B-178-1 反射装置 2F;⑧ B-36 根级 16 红(9082 未起);⑨ B-95 204 失联;⑩ B-DX-1 dx 404;⑪ B-186-1 live chat 挂死;⑫ **B-189-1 git SSH 22 故障**。 |
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+| 提交 sha | 关联任务/能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|-----------------|------|--------------|----------|------------|------|
+| `67fb0a1f5` | 全站巡检 R189 + 契约窗验证 + B-142-2 销号 + B-189-1/2 登记 | 汇报 | 报告 + 执行日志 + blockers + 研发通知 + 台账 | cfclub | ✅ 已推送远端 `http://git.iwintrue.com/liaoxg/cfc.git`(SSH 22 故障经 HTTP 兜底;B-142-2 满足连续 2 轮销号判据) | 结果有变化 → 已提交并通知研发 |
+
+### 1.5 状态机阶段分布(全局进度,研发侧账本口径)
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| UNTRACKED | 0 | 窗口零新增规格 |
+| RED | 17(历轮承袭)+ 窗口新契约若干 | S-cfc-183/184/188/196 等 48 前瞻契约 RED 已在本轮实测取证;研发状态机口径另计 |
+| YELLOW | 72 | 研发负责人口径(第152轮) |
+| GREEN / DONE | 24 | 承袭 |
+| 合计 | 105+ | 研发侧状态机账本口径(本轮交付 `blockers` 后互联) |
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳ wish) | 0 | 正常 | SRS 目标池未启用 |
+| 契约常量(contract) | 多组 | 正常 | 窗口 13 契约类逐类可收集可执行 |
+| 测试用例引用(test_case_map) | 308+ 唯一 TC | 正常 | 台账扫描正常 |
+| 执行结果回填(case_results) | 定向 133 + 全量采信 | 正常 | surefire XML 可聚合 |
+
+---
+
+## 二、历史审查记录(阶段 -1 记忆召回)
+
+| 项 | 内容 |
+|---|---|
+| 上次审查 | **R188(2026-11-13 09:39,`00121ff2f`)**,结论「不通过(有条件,明显正向)」,全站 3987/3918(98.27%),红 44;后端全量恢复 43:15;langgraph 首轮 51/51;frontend 6/6 |
+| 历史遗留 | 后端存量 25 红(13 类)· 根级 16 红(B-36)· fe 3 预期 RED(S-cfc-132/133)· CI 漏拦/陈旧(B-152-1/2)· B-177-1 · B-178-1 · B-186-1 live chat | 
+| 本次是否改善 | **langgraph 连续 2 轮 51/51 → B-142-2 销号(正向)**;后端存量 25 红窗口零漂移;浏览器六件套零漂移全绿;其余承袭 |
+| 教训应用 | ① 窗口出现**新增契约测试类必须逐类定向实测取证**再入报告(B-189-2:并行会话推入的 13 类昨日未验证,本轮补验证明 48 红全为预期 RED);② git SSH 故障时立即降级 HTTP 探源确认远端 HEAD(B-189-1);③ langgraph 全绿连续 2 轮即销号(B-142-2 判据兑现);④ 每节点仅 1 个浏览器任务、本机串行执行(本轮 6 实例顺序推进无冲突)。 |
+
+> 本次为该对象**第 189 次**审查,有历史记录;上述内容仅作对照,**本轮前端/管理端/langgraph/浏览器/API/根级/契约窗全部本轮实测**;后端全量采信 R188(窗口 main 零变更)。
+
+---
+
+## 三、项目画像与覆盖矩阵
+
+### 3.1 项目画像
+
+| 维度 | 内容 |
+|------|------|
+| 业务定位 | 家庭教育成长平台(五维能量 身/心/智/行/富):小程序家长端 + 规划师端 + 管理后台 + 独立商城 |
+| 技术栈 | Spring Boot 2.7.18 + MyBatis-Plus + Java 8(9082)/ uni-app 微信小程序(Vue2)/ Vue2 + Element UI(8082)/ FastAPI + LangGraph + ChromaDB(9000@251) |
+| 模块结构 | `cfc-backend`(366 控制器 / 388 测试源 / 369+13 测试类)/ `cfc-frontend`(42 测试文件 / 624 例)/ `cfc-web`(17 套件 / 116 例)/ `cfc-langgraph`(51 例 @251)/ `tests`(e2e-live 5 文件 + e2e/api 点名 4 文件) |
+| 关键路径 | 登录/家庭/成员 → 五维能量 → 任务/成长记录 → 健康报告上传解析 → 测评订单 → 商城/支付 → AI 对话(LangGraph) |
+| 测试现状 | **本轮实测**:浏览器 30 + langgraph 51 + fe 624 + web 116 + 根级 23 + API 32 + 契约窗 133 = **1009 例实测**;后端全量 3111 采信 R188(窗口 main 零变更) |
+| 质量目标 | 全量用例全绿、覆盖 SRS 与用户场景(含异常流)、浏览器双节点可复现、结果变更即提交并通知研发 |
+| 风险点 | ① 后端存量 25 红(12 类)+ 契约窗 48 RED 待实现;② CI 清单双向失真;③ 上游 LLM live 链路挂死(B-186-1);④ 根级 16 红(9082 未起);⑤ 迁移 DDL 缺陷;⑥ pytest 进程不退出(B-177-1);⑦ **git SSH 22 故障(B-189-1)** |
+| 待核实项 | S-cfc-176 幂等分支排期 · 251 上游凭据/配额(pytest 绿但 live 挂)· 204 节点恢复 · B-156-1/B-170-1 修复状态 |
+
+### 3.2 20 类测试覆盖矩阵(全类不省略)
+
+| # | 测试类型 | 三态 | 证据(本轮实测) |
+|---|----------|------|------------------|
+| 1 | 单元测试 | ✅ 已发现 | 后端全量 3111(采信 R188)+ 契约窗 133(实测);前端 624;管理端 116 |
+| 2 | 测试覆盖率 | ⚠️ 部分 | 管理端 Jest 覆盖率可产出;后端未开 jacoco(未授权改 pom) |
+| 3 | 变异测试 | ❌ 未发现 | 全仓无 pitest/mutation 配置 |
+| 4 | 集成测试 | ✅ 已发现 | `integration/*` 存量执行结果承袭(EnergySystemFlow 6F、GuideModule 1E 等);本轮未重跑(main 零变更) |
+| 5 | 契约测试 | ✅ 已发现(本轮重点) | **窗口 13 契约类 133 例全实测**:LoginSwitch 16/16、MessagePush 20/20、PlanActivate 15/15 全绿;另 10 类 48 红全为预期 RED;S-cfc-176 维持 1F |
+| 6 | 接口 API 测试 | ✅ 已发现 | 线上 API 32 例(**本轮实测**:25 过 / 7 跳 / 0 红,28s) |
+| 7 | 系统/端到端 | ✅ 已发现 | 浏览器 30 例(**本轮实测**:28 过 + 2 跳),双节点 admin 5/5 |
+| 8 | 验收 UAT | ⚠️ 待核实 | 无独立 UAT 包;以浏览器 30 例作为系统级验收代理 |
+| 9 | BDD 规格 | ✅ 已发现 | 用例/场景 11 份(用户场景文件级,映射率 72.1% 承袭) |
+| 10 | 冒烟测试 | ✅ 已发现 | `api-smoke.spec.js` 本轮实测全绿(含自注册链路) |
+| 11 | 回归测试 | ✅ 已发现 | 存量红集合 12 类逐类零漂移(**第 24 次**);admin/frontend/shop/health 全绿 |
+| 12 | 探索性测试 | ⚠️ 部分 | 历轮浏览器人工探索记录;本轮未安排新探索项 |
+| 13 | 性能测试 | ❌ 未实测 | 未授权联网压测;仅 API 时延观察(非功能 32 例内含超时断言) |
+| 14 | 可靠性测试 | ⚠️ 部分 | pytest 挂死(B-177-1 第 4 轮观察)为可靠性缺陷;本轮 495.56s 正常收口 |
+| 15 | 安全测试 | ✅ 已发现 | `nonfunc-security.spec.js` 11 例本轮全绿(XSS/越权/恶意文件);匿名 6 端点全 401 |
+| 16 | 兼容性测试 | ⚠️ 部分 | 双视口(desktop 1440 / mobile 375)实测通过;无多浏览器矩阵 |
+| 17 | 可用性测试 | ⚠️ 部分 | 登录-主框架-核心菜单旅程 5/5×2 节点;无专项可用性问卷 |
+| 18 | 无障碍测试 | ❌ 未发现 | 无 axe/pa11y 配置 |
+| 19 | QA 流程 | ⚠️ 部分 | 汇报/台账/blockers 闭环在跑;CI 清单失真(B-152-1/2)未修 |
+| 20 | 质量指标 | ⚠️ 部分 | 全站通过率 97.16%(G1 未达 98%);锚点率 20.1% 承袭 |
+
+---
+
+## 四、五要素问题清单
+
+| # | ① 问题描述 | ② 测试 bug 点 | ③ 最优修改方案 | ④ 修改目的 | ⑤ 闭环方式 |
+|---|-----------|--------------|----------------|-----------|-----------|
+| P1 | 后端存量 25 红(12 类,跨 24 轮)零漂移承袭 | `EnergySystemFlowTest` 6F / `DimensionRegistryMapContractTest` 3F / `OrchestrationEngineTest`·`OrchestrationIntegrationTest`·`MembershipPaySwitchTest`·`PaymentServiceORDRoutingTest`·`UserServiceWechatLoginTest`(×2)·`TeamProfitCronContractTest` 各 1F 等 | 按类逐条修;先修 `TeamProfitCronContractTest`(平台费截断,影响计费) | 后端回归全绿 → G2 闭环 | 修后 `mvn -o -B test -Dtest=<类>` 定向复跑,单类 0F 连续 2 轮 |
+| P2 | **窗口 13 契约类 48 前瞻 RED 待研发实现(TDD 先行态,本轮已取证)** | S-cfc-183 `PlanContentSafetyReviewContractTest` 4F / S-cfc-184 `TeacherAssessmentRecordContractTest` 3F / S-cfc-188 `FamilyAutoCreateForPurchaseContractTest` 4F / S-cfc-196 `TeacherPlanCreateContractTest` 4F / `TeacherMindRecordContractTest` 12F / `TeacherAppointmentGuardContractTest` 10F / 其余 5 类 10F | 按 SC-T 界面对应实现(必填拦截、重复覆盖、权限门禁、自动建家庭、mind 记录、预约守卫等);本报告 `notice` 已逐键下发 | 48 前瞻契约转绿 → RED 清零 | 实现后逐类定向复跑,单类 0F 即销 RED;GREEN 三键(189/191/Activate)已全绿可直接收尾 |
+| P3 | S-cfc-176 幂等分支未生效 | `MembershipPlanPurchaseContractTest.repeatPurchase_pendingOrderSamePlan_reuseNoDuplicate:201`(`NeverWantedButInvoked: paymentOrderMapper.insert`) | `findReusablePendingOrder` 命中后直接复用,不再 insert | 契约闭环(防重复下单) | 定向复跑该类 7/7 绿 |
+| P4 | 迁移 DDL 缺陷持续复现 | `schema.sql:5345` `INDEX idx_product (product_id)` 无该列;`DatabaseInitializer.java:2261` 带别名 UPDATE + 派生表引用被更新表 | 删索引或补列;改独立子查询写法 | 建表成功 + 计数列可写 | 启动日志 0 次 `SQLSyntaxErrorException` |
+| P5 | 未入库测试资产 3 类 | `config/GrowthPublicEndpointMappingTest.java`(`??` 未跟踪)等 | `git add` + push | 资产可追溯 | 入库后全量重跑该类 0E |
+| P6 | pytest 打印 summary 后进程不退出(B-177-1 第 4 轮)| `tests/test_analyze.py` → `RuntimeWarning: coroutine 'JavaClient._get_client' was never awaited` | `app/main.py:84` 改用 lifespan 并 `await client.aclose()`;派发器加 `timeout -k 10 900` | 派发可判定成功、节点不残留 | 派发日志出现 `EXIT=0` 且 `ps` 无残留 |
+| P7 | 根级黑盒 16 红(B-36 第 73 轮) | `tests/integration/*`(全 `ECONNREFUSED 9082`) | 起本地 9082 或改指向测试环境 | 黑盒链路可测 | 9082 起来后重跑 23 例 |
+| P8 | CI 清单双向失真(B-152-1/2) | `.gitlab-ci.yml:61`(漏拦 6 + 陈旧 26) | 按实测红类集合精修 `BACKEND_OBSERVED_CLASSES`(接入本轮 13 契约类) | CI 硬门禁真实生效 | CI 在含红例分支上「必红」验证一次 |
+| P9 | 前端 3 例预期 RED | `signature.*`(AC-R1/AC-R2)+ `growthHistory.dayGroupView` RED-A7 | 交付 S-cfc-132/133 组件与分组逻辑 | 前端全绿 | `npx jest` 624/624 |
+| P10 | **git SSH 22 故障(B-189-1,本轮环境)** | `git.iwintrue.com` / `192.168.16.251:22` CLOSED(R188 09:39 尚可 push) | Gogs 侧恢复 SSH 服务/端口映射;故障期经 `http://git.iwintrue.com/liaoxg/cfc.git` 只读确认远端 HEAD | fetch/push 恢复正常 | `git fetch origin` 成功 + 本轮产物推送成功 |
+| P11 | dx 桥接 2 例 skip(404) | `dx-health-bridge-auth.spec.js`(端点 404) | 端点上线后自动转断言 | 覆盖空转消除 | 线上端点 200 后重跑转 401 断言 |
+
+---
+
+## 五、域结论表
+
+| 审查维度 | 负责专家 | 结论 | 关键证据 |
+|----------|----------|------|----------|
+| 单元·组件级 | 苏雅 | **改善** | 窗口 13 契约类 133 例全实测(GREEN 3 类 51 例全绿);fe 609/3/12(3 预期 RED);web 116/116 |
+| 集成·接口级 | 柯砚 | **改善** | 线上 API 32 例 0 红;S-cfc-176 维持 1F;契约窗 3 GREEN 类可收尾 |
+| 系统·验收级 | 白樾 | **稳定** | 浏览器 30 例 28 过 + 2 跳;admin 双节点 5/5;frontend 6/6;health 13/13(B-61 维持) |
+| 测试保障级 | 闻朔 | 改善 | 冒烟全绿;存量红集合零漂移(第 24 次);契约定向验证流程新增(B-189-2 补验) |
+| 性能·可靠性 | 骆骋 | 承袭 | 后端全量采信(窗口零变更);langgraph 51/51 正常收口但 pytest 进程不退出仍为可靠性缺陷(B-177-1) |
+| 安全 | 关屿 | **改善** | 匿名端点全 401;nonfunc-security 11 例全绿(XSS/越权/恶意文件) |
+| 兼容·体验 | 阮清 | 稳定 | frontend 移动视口 6/6;desktop 双节点 admin 5/5;无多浏览器矩阵 |
+| 流程·度量 | 何序 | 部分改善 | G12 ✅ 窗口零需求变更;G9 锚点 20.1% 承袭;CI 漏拦/陈旧未修(B-152-1/2) |
+
+---
+
+## 六、门禁结论
+
+**结论:不通过(承袭,无回归、正向项明确)** —— 全站 4120 例 / **4003 过 = 97.16%(剔 skip 97.75%)**,红 92。红数环比 +48 的**全部来源为窗口新增契约测试的预期 RED**(TDD 契约先行、研发未实现,非缺陷);langgraph 连续 2 轮 51/51(**B-142-2 销号**);浏览器六件套全绿;API 零红;后端存量 25 红零漂移。阻断项为**存量红 + 48 前瞻契约 RED + CI 清单失真 + live chat 挂死**,均非本轮新增回归。
+
+| 门禁 | 判定 | 依据(本轮实测) |
+|------|------|------------------|
+| G1 全站通过率 ≥98% | ❌ **97.16%** | 4003/4120(剔 skip 97.75%);−1.11pct 全因 48 契约预期 RED 入池 |
+| G2 后端红归零/契约裁决 | ❌ **73 红** | 存量 25(12 类)+ **契约窗 48 预期 RED(10 类,需研发实现)** |
+| G3 前端双绿 | ⚠️ fe 609/624(3 预期 RED)+ web 116/116;**小程序 H5 浏览器 6/6** | 3 红全为 S-cfc-132/133 未交付 |
+| G4 编译可编译 | ✅ | 窗口契约类全量 test-compile + 定向执行无编译错 |
+| G5 结果完整性 | ✅ | 定向 133 例 surefire XML 完整;全量承袭 R188 369 XML 可聚合 |
+| G6 浏览器 E2E | ✅ **28 过 + 2 跳** | admin 5/5(本机+189)、fe 6/6、shop 4/4、health 13/13、dx 2 跳 |
+| G7 langgraph | ✅ **51/51(连续 2 轮)** | **B-142-2 销号**;live chat 挂死单列 B-186-1 |
+| G8 匿名鉴权 401 | ✅ | health 三端点全 401;API 安全套件 11 例全绿 |
+| G9 用例锚定 SRS | ❌ 20.1% | 承袭口径(本轮未变) |
+| G10 双/三节点可比 | ✅ 双节点 | 189 admin 5/5 + 本机 admin 5/5 + 251 langgraph 51 例 |
+| G11 CI 必红 | ❌ | 漏拦 6 + 陈旧 26 承袭(B-152-1/2) |
+| G12 用例锚定 SRS 变更 | ✅ | specs / test-cases / API_REFERENCE / 场景窗口零变更 → 用例零新增 |
+| G13 存量红零漂移 | ✅ **第 24 次** | 12 类逐类与 R188 一致;B-142-2 为唯一闭环项(正向) |
+
+---
+
+## 七、变更触发动作(用户规则:结果有变 → 提交远程 + 通知研发)
+
+- **变更事实(本轮)**:① 窗口**新增 13 契约测试类 133 例**(S-cfc-183/184/188/189/191/196 等)经定向实测取证:48 预期 RED 待研发实现、3 个 GREEN 类全绿可收尾;② langgraph **51/51 连续第 2 轮** → **B-142-2 销号**;③ 浏览器六件套全绿、API 零红、fe/web 零漂移;④ 全站口径 4120 例(+133)。
+- **动作**:本报告 + `automation-run-R189.md` + `blockers.md`(B-189-1/2 新增、B-142-2 销号、B-186-1 计数更新)+ `notice-2026-11-13-R189-研发通知.md` + 台账三件套(discovery/inventory/automation-status)提交推送。
+- **推送状态**:✅ **已完成**。SSH 22 仍 CLOSED,已将 push remote 切换至 `http://git.iwintrue.com/liaoxg/cfc.git` 执行 `git rebase FETCH_HEAD && git push origin cfclub`,远程 HEAD=`67fb0a1f5`(含本轮全部 16 文件产物);fetch 仍须临时指定 HTTP URL。**请运维恢复 Gogs/251 sshd 服务(端口 22)**。
+- **通知通道**:Agent Mail 历轮 `not_bound`(静默不触发)→ 以 blockers.md + `notice-2026-11-13-R189-研发通知.md` + 推送落地。
+
+---
+
+## 八、待办
+
+1. **运维(P0)**:恢复 Gogs SSH 22(B-189-1)——本轮推送因此受阻;恢复期间可用 HTTP clone 只读核源。
+2. **研发(P1)**:48 前瞻契约 RED(S-cfc-183/184/188/196 + MindRecord/AppointmentGuard 等 10 类)逐键实现使绿;S-cfc-176 幂等;迁移 DDL(B-156-1/B-170-1);B-177-1 `aclose()`。
+3. **CI**:精修 `BACKEND_OBSERVED_CLASSES`(漏拦 6 + 陈旧 26 + 接入 13 契约类)。
+4. **测试**:B-142-2 已销号;下轮关注 48 RED 的研发实现进展与回归;前端 624/624 目标(S-cfc-132/133 交付后)。
+
+---
+
+## 附录 A:本轮命令与口径(可复现)
+
+```bash
+# 浏览器(必须在 tests/ 内跑;每节点 1 个浏览器任务,本机串行)
+cd tests && npx playwright test -c playwright.config.live.js --project=admin-desktop   # 5/5(本机)
+cd tests && npx playwright test -c playwright.config.live.js --project=frontend-mobile # 6/6
+cd tests && npx playwright test -c playwright.config.live.js --project=shop-api        # 4/4
+cd tests && npx playwright test -c playwright.config.health.js                          # 13/13
+cd tests && npx playwright test -c playwright.config.api.js --output=../_r189_api \
+  e2e/api/api-smoke.spec.js e2e/api/nonfunc-security.spec.js \
+  e2e/api/nonfunc-perf-compat.spec.js e2e/api/traceability-p94-p95-c26.spec.js        # 25/7/0
+
+# 189 节点 admin(单节点任务)
+node _r189_189_admin.cjs kick && node _r189_189_admin.cjs read  # 5/5 EXIT=0
+
+# 251 节点 langgraph pytest
+LOG=/tmp/r189_pytest.log node _r189_251_lg.cjs kick && node _r189_251_lg.cjs read  # 51/51
+
+# 窗口契约类定向(13 类 133 例)
+cd cfc-backend && JAVA_HOME=/d/jdk1.8.0_25 /d/maven/apache-maven-3.6.1/bin/mvn -o test -Dtest='FamilyAutoCreateForPurchaseContractTest,...,TeacherPlanCreateContractTest' -DfailIfNoTests=false
+
+# Jest
+cd cfc-frontend && npx jest --ci   # 609/3/12
+cd cfc-web && npx jest --ci        # 116/116
+cd tests && npx jest --silent      # 7/16(B-36)
+```
+
+## 附录 B:窗口 13 契约测试类定向实测明细(B-189-2 补验)
+
+| 类 | 提交 | 例数 | 过/红 | 预期 |
+|----|------|------|-------|------|
+| FamilyAutoCreateForPurchaseContractTest | 883a6eeab (S-cfc-188) | 5 | 1/4 | RED 4/5 ✅(自动建家庭未落地) |
+| GuideFamilyTaskReviewContractTest | 窗口 | 5 | 3/2 | RED(部分边界) |
+| NutritionPlanTeacherContractTest | 窗口 | 4 | 1/3 | RED(营养师计划边界) |
+| PlanContentSafetyReviewContractTest | 5f2a09b02 (S-cfc-183) | 9 | 5/4 | RED 4F ✅(AI 方案内容安全复核缺失) |
+| TeacherAppointmentGuardContractTest | 窗口 | 14 | 4/10 | RED(预约守卫未落地) |
+| TeacherAssessmentRecordContractTest | 64f8a3875 (S-cfc-184) | 4 | 1/3 | RED 3F ✅(必填拦截/重复覆盖未落地) |
+| TeacherDataIsolationScT13ContractTest | 窗口 | 9 | 5/4 | RED(数据隔离边界) |
+| TeacherDimensionScoreContractTest | 窗口 | 6 | 4/2 | RED(维度评分边界) |
+| TeacherLoginSwitchContractTest | bbc441e08 (S-cfc-189) | 16 | 16/0 | **GREEN 16/16 ✅**(实现已落地) |
+| TeacherMessagePushContractTest | 23e2ddd66 (S-cfc-191) | 20 | 20/0 | **GREEN 20/20 ✅** |
+| TeacherMindRecordContractTest | 窗口 | 13 | 1/12 | RED(mind 记录大面未落地) |
+| TeacherPlanActivateContractTest | 窗口 | 15 | 15/0 | **GREEN 15/15 ✅** |
+| TeacherPlanCreateContractTest | fb1702975 (S-cfc-196) | 13 | 9/4 | RED 4F ✅(权限门禁未落地) |
+| **合计** | — | **133** | **85/48** | 48 红全部为预期 RED(TDD 先行),0 意外失败 |

+ 263 - 0
docs/tdd/orchestration/report-test-2026-11-13-R190.md

@@ -0,0 +1,263 @@
+# 测试负责人工作汇报(R190 轮)
+
+> 本汇报按《测试负责人工作汇报模板》填写。R190 为**实测轮 + 窗口契约验证轮**:基线 `cfclub @ 2313791a8`(R189 收口 11:45 的 auto-bump),当前 HEAD `0e6f285da`(12:28 并发 rebase 后;窗口含并行会话推入 **S-cfc-186 + S-cfc-182 两个契约测试类** + 2 份 docs)。
+> **拓扑(用户指令)**:浏览器经 `http://cfc.iwintrue.com/admin`(后台)+ `http://cfc.iwintrue.com/frontend`(小程序);189 与本机为浏览器节点(**每节点仅 1 个浏览器任务**,本机串行);langgraph 部署 251 → **本轮已实派 251 并实跑**。
+> **★ 本轮关键事实**
+> ① **窗口(2313791a8..0e6f285da)源码/SRS/场景/接口文档零变更**(`src`/`cfc-backend/src/main`/specs/test-cases/API_REFERENCE/场景 diff 命中 0)→ G12 ✅,用例无需由我更新;窗口唯一代码变更 = 并行会话新增 **2 个契约测试类**(S-cfc-186 `TeacherFamilyOverviewContractTest` 7 例 + S-cfc-182 `TeacherReportProxyContractTest` 14 例),本轮已**逐类定向实测取证**;
+> ② **窗口契约窗实测 21/21:S-cfc-186 6 过 / 1 预期 RED / 0 错**(RED = 「跨家庭 IDOR」:`GuideFamilyTaskController.getFamilyMemberTasks` 仅校验 `requireBound(guideId, familyId)` 而未校验 `memberId ∈ familyId`,教师绑家庭 A 可读家庭 B 成员任务)+ **S-cfc-182 14/14 GREEN ✅**(teacher.report.proxy 能力已实现落地);
+> ③ **langgraph@251 50/51**(唯一红 `test_auto_tag_valid_format` 标签数 0)——根因上游 `401 Authentication Fails, api key ****ab12 invalid`(B-142-2 复发,**环境型非代码回归**;节点 HEAD=基线 + 窗口源码零变更);
+> ④ **浏览器任务级全绿**:本机四件套 28/28(admin 5 + fe 6 + shop 4 + health 13);跨节点 **189 admin 5/5 EXIT=0**、**204 frontend 6/6(B-95 观察改善,节点恢复)**;dx 桥接 2 跳承袭(B-DX-1)。
+> ★ **全站可比:4141 例 / 4026 过 = 97.22%(剔 skip 97.72%),红 94**——红数环比 +2 全为**窗口新契约预期 RED(S-cfc-186,+1)**与**上游 401 环境红(B-142-2 复发,+1)**,**零缺陷回归**。
+
+---
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | 测试负责人(自动化测试闭环任务 / 软件测试专家团·陆谨) |
+| 汇报周期 | 2026年11月13日 11:45 ~ 12:35(第 190 轮) |
+| 基线 | `cfclub @ 2313791a8`(R189 收口 auto-bump);本轮收尾 HEAD `0e6f285da`(12:28 并发 rebase 后,含 S-cfc-186 提交再落位 + S-cfc-182 merge + 2 docs) |
+| 窗口内需求变更 | **零** — `docs/superpowers/specs` / `tests/test-cases` / `API_REFERENCE.md` / 用户场景 窗口零变更(G12 ✅,未触发用例编写闸门) |
+| 窗口内源码变更 | **零** — `git diff --name-only 2313791a8..0e6f285da` 对 `src`、`cfc-backend/src/main`、需求、用例、接口文档命中 0;唯一变更 = 2 个契约测试类(S-cfc-186 `TeacherFamilyOverviewContractTest` +249 / S-cfc-182 `TeacherReportProxyContractTest`,均并行会话提交)+ 2 份 docs(`report-dev-2026-11-13-1210.md`、`sc-key-map.md`) |
+| 负责模块/单元 | 全站:管理端 admin@本机 + 189 · 小程序 H5 fe@本机 + **204(B-95 恢复)** · cfc-langgraph@251(**本轮实派**) · cfc-backend JUnit(全量采信 R189 + 窗口 2 契约类实测 21 例) · cfc-web/cfc-frontend Jest · 线上 API 非功能层 |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 测试用例建设
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **本轮新增测试用例数\*** | **0 个(由我新增);窗口并行会话 +21 例(S-cfc-186 7 + S-cfc-182 14)** | SRS/场景/接口文档窗口零变更,未触发用例编写闸门(G12 ✅);窗口内并行会话新增 `TeacherFamilyOverviewContractTest`(SC-T-16/17/21/27,7 例)与 `TeacherReportProxyContractTest`(S-cfc-182,14 例)已入库并于本轮逐类定向实测 |
+| **本轮验证执行测试数\*** | **4141 个** | 后端 **3265**(R189 全量采信 3244 + 窗口 2 契约类实测 21)+ 前端 Jest 740(fe 624 + web 116)+ 管理端 116 + langgraph 51(**251 实派**)+ 浏览器 30(唯一 28 过 + dx 2 跳承袭)+ 线上 API 32 + 根级 23 |
+| **用例通过率** | **97.22%**(4026/4141;剔除 skip **97.72%**) | 红 94 / 跳 21;环比 R189(97.16%)**+0.06pct**,红数 +2 全部为**窗口新契约预期 RED(S-cfc-186,+1)+ 上游 401 环境红(B-142-2 复发,+1)**,**零缺陷回归** |
+| **用例台账** | `discovery.md` / `inventory.md` 已重扫重写 | inventory 跨源 **4588**(md 719 / PW 571 / py 48 / Java @Test **3250** / 场景 11);Java 测试文件 390(较 R189 388 **+2**,即窗口 S-cfc-186 + S-cfc-182 两类) |
+
+### 1.2 会话与任务分发
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **分派任务数\*** | **7 个** | R190-1 本机 `admin-desktop`(5/5);R190-2 本机 `frontend-mobile`(6/6);R190-3 本机 `shop-api`(4/4)+ `health`(13/13);R190-4 **189** `admin-desktop`(5/5 EXIT=0);R190-5 **251** `langgraph pytest`(**50/51**,269.16s);R190-6 **204** `frontend-mobile`(**6/6 恢复**,46.1s);R190-7 后端窗口 2 契约类定向(S-cfc-186 7 例 + S-cfc-182 14 例) |
+| **恢复智能体会话数\*** | 0 个 | 各节点实探 200/ok(189:4090、204:4091、251:4096、251:9000、本机 4091 全可达);未遇中断会话 |
+| **当前在用会话数** | 0 个 | 全部结束;251 残留 pytest 进程已清理(B-177-1 观察维持) |
+
+### 1.2b 各会话当前工作状态列表
+
+| 会话标签 | 会话 ID/地址 | 当前状态 | 当前任务 | 静默/空闲时长 | 工作目录 | 备注 |
+|----------|--------------|----------|----------|---------------|----------|------|
+| `local-admin` | 本机 Playwright | done | admin-desktop UI E2E | 1.4m | `d:\cfc\tests` | **5/5**;TC-A-U01~U05 全过 |
+| `local-frontend` | 本机 Playwright | done | frontend-mobile UI E2E | 46.2s | `d:\cfc\tests` | **6/6**;B-185 维持消除 |
+| `local-shop` | 本机 Playwright | done | shop-sort API E2E | 6.9s | `d:\cfc\tests` | **4/4** |
+| `local-health` | 本机 Playwright | done | health 鉴权契约 E2E | 4.2s | `d:\cfc\tests` | **13/13**(TC-HP-U01~03 全 401,B-61 维持) |
+| `remote-189` | 192.168.16.189:4090 | done | admin-desktop UI E2E | 1.3m | `D:\code\cfc\tests` | **5/5 EXIT=0**,跨节点可比 |
+| `remote-204` | 192.168.16.204:4091 | done | frontend-mobile UI E2E | 46.1s | `D:\cfc\tests` | **6/6 恢复**(B-95 观察改善:204 节点 openCode 服务在 4091,项目在 D:\cfc) |
+| `remote-251` | 192.168.16.251:4096 | done | langgraph pytest | 269.16s | `/sc-data/cfc/cfc-langgraph` | **50/51**;唯一红=上游 401 环境型;节点 HEAD `2313791a`(=基线) |
+| `local-jest` | 本机进程 | done | cfc-web + cfc-frontend + 根级 | fe 172s / web 209s | `cfc-web` / `cfc-frontend` / `tests` | web **116/116**、fe **609/3/12**(3 预期 RED)、根级 **7/16**(B-36) |
+| `local-backend` | 本机 mvn | done | 窗口 2 契约类定向(S-cfc-186 + S-cfc-182) | ~2m | `d:\cfc\cfc-backend` | **21 例 = S-cfc-186 6 过/1 预期 RED + S-cfc-182 14 过/0 红 / 0 错** |
+| `virtual` | dx-bridge | done | D/X 桥接鉴权契约 | 瞬时 | `d:\cfc\tests` | **2 skip**(端点 404,B-DX-1 维持,本轮承袭) |
+
+### 1.3 问题处置
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **新测试发现问题数\*** | **2 个** | ① **B-190-1(P1 环境)**:**B-142-2 复发**——langgraph `test_auto_tag_valid_format` 标签数 0,日志实证 `Error code: 401 - Authentication Fails, Your api key: ****ab12 is invalid`(上游 AgnesAI 凭据失效)。判别:节点 HEAD=基线 `2313791a` + 窗口源码零变更 → **环境型,非代码回归**;② **B-190-2(P2 流程)**:窗口尾部并行会话推入 **2 个契约测试类**(S-cfc-186 `TeacherFamilyOverviewContractTest` 7 例 + S-cfc-182 `TeacherReportProxyContractTest` 14 例)**未逐类验证入库** → 本轮补验完成:S-cfc-186 6 过 / 1 预期 RED(跨家庭 IDOR) + S-cfc-182 14/14 GREEN ✅。 |
+| **已解决测试相关问题数\*** | **1 个** | **B-95(204 节点失联)观察改善**:历轮 `204:4090=000` 误判(openCode 实际服务在 **204:4091**,项目 `D:\cfc`),本轮 204 frontend **6/6 恢复** → 登记为「观察改善」,待连续 2 轮可复现后销号。 |
+| **待解决测试相关问题数** | P0×2 / P1×8 / P2×4 | ① B-152-1/2 CI 清单漏拦/陈旧;② S-cfc-183/184/188/196 + **S-cfc-186** 等 **49 前瞻契约 RED**(P1,需研发实现使绿);③ S-cfc-176 幂等 1F;④ B-177-1 pytest summary 后挂死;⑤ B-156-1/B-170-1 迁移 DDL;⑥ B-162-1 3 类未入库;⑦ B-178-1 反射装置 2F;⑧ B-36 根级 16 红(9082 未起);⑨ B-95 观察改善待销号;⑩ B-DX-1 dx 404;⑪ B-186-1 live chat 挂死;⑫ **B-189-1 git SSH 22 故障**;⑬ **B-142-2 复发观察 1/2**。 |
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+| 提交 sha | 关联任务/能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|-----------------|------|--------------|----------|------------|------|
+| `(本轮)` | 全站巡检 R190 + 窗口 2 契约类补验(S-cfc-186/182)+ B-142-2 复发登记 + B-95 观察改善 + B-190-1/2 登记 | 汇报 | 报告 + 执行日志 + blockers + 研发通知 + 台账 + 派发器 | cfclub | ✅ 推送远端 `http://git.iwintrue.com/liaoxg/cfc.git`(SSH 22 仍故障,经 HTTP 兜底;B-189-1 延续) | 结果有变化 → 已提交并通知研发 |
+
+### 1.5 状态机阶段分布(全局进度,研发侧账本口径)
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| UNTRACKED | 0 | 窗口零新增规格(S-cfc-186/182 为并行会话契约用例) |
+| RED | 17(历轮承袭)+ 窗口 S-cfc-186 等 49 前瞻契约 RED | S-cfc-183/184/186/188/196 等 11 类已本轮实测取证;研发状态机口径另计 |
+| YELLOW | 72 | 研发负责人口径(第153轮) |
+| GREEN / DONE | 24(承袭)+ S-cfc-182 契约键 | S-cfc-182(teacher.report.proxy)14/14 GREEN 本轮取证 |
+| 合计 | 105+ | 研发侧状态机账本口径(本轮交付 `blockers` 后互联) |
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳ wish) | 0 | 正常 | SRS 目标池未启用 |
+| 契约常量(contract) | 多组 | 正常 | 窗口 S-cfc-186/182 可收集可执行 |
+| 测试用例引用(test_case_map) | 308+ 唯一 TC | 正常 | 台账扫描正常 |
+| 执行结果回填(case_results) | 定向 21 + 全量采信 | 正常 | surefire XML 可聚合 |
+
+---
+
+## 二、历史审查记录(阶段 -1 记忆召回)
+
+| 项 | 内容 |
+|---|---|
+| 上次审查 | **R189(2026-11-13 11:30,`6124352d7`/`67fb0a1f5` 推送)**,结论「不通过(承袭,无回归、正向项明确)」,全站 4120/4003(97.16%),红 92;langgraph 51/51 连续第 2 轮(B-142-2 销号);浏览器六件套全绿;契约窗 13 类 133 例 85/48 |
+| 历史遗留 | 后端存量 25 红(12 类)· 根级 16 红(B-36)· fe 3 预期 RED(S-cfc-132/133)· CI 漏拦/陈旧(B-152-1/2)· B-177-1 · B-178-1 · B-186-1 live chat · B-189-1 SSH 22 |
+| 本次是否改善 | **204 节点 frontend 6/6 恢复(B-95 观察改善,正向)**;窗口新增 2 契约类已补验(S-cfc-186 6 过/1 预期 RED + **S-cfc-182 14/14 GREEN**);后端存量 25 红窗口零漂移;浏览器任务级全绿;其余承袭 |
+| 本次是否回退 | **langgraph 51/51 → 50/51(−1)**:唯一红 `test_auto_tag_valid_format` 为上游 **401(api key ****ab12 invalid)环境型**,节点 HEAD=基线 + 窗口源码零变更 → **非代码回归**;B-142-2 由「销号」转「复发观察 1/2」(需恢复 key 后连续 2 轮 51/51 再销号) |
+| 教训应用 | ① 窗口出现新增契约测试类必须逐类定向实测取证再入报告(B-190-2 复用 B-189-2 教训);② 上游 LLM 凭据失效时以「节点 HEAD=基线 + 窗口源码零变更」判别环境型(B-142-2 判别式);③ 204 节点失联须先探明实际端口(openCode 在 4091 而非 4090)再定位(B-95);④ 每节点仅 1 个浏览器任务、本机串行执行。 |
+
+> 本次为该对象**第 190 次**审查,有历史记录;上述内容仅作对照,**本轮前端/管理端/langgraph/浏览器/API/根级/契约窗全部本轮实测**;后端全量采信 R189(窗口 main 零变更)+ 窗口 2 契约类本轮实测。
+
+---
+
+## 三、项目画像与覆盖矩阵
+
+### 3.1 项目画像
+
+| 维度 | 内容 |
+|------|------|
+| 业务定位 | 家庭教育成长平台(五维能量 身/心/智/行/富):小程序家长端 + 规划师端 + 管理后台 + 独立商城 |
+| 技术栈 | Spring Boot 2.7.18 + MyBatis-Plus + Java 8(9082)/ uni-app 微信小程序(Vue2)/ Vue2 + Element UI(8082)/ FastAPI + LangGraph + ChromaDB(9000@251) |
+| 模块结构 | `cfc-backend`(366 控制器 / 390 测试源 / 369+15 测试类)/ `cfc-frontend`(42 测试文件 / 624 例)/ `cfc-web`(17 套件 / 116 例)/ `cfc-langgraph`(51 例 @251)/ `tests`(e2e-live 5 文件 + e2e/api 点名 4 文件) |
+| 关键路径 | 登录/家庭/成员 → 五维能量 → 任务/成长记录 → 健康报告上传解析 → 测评订单 → 商城/支付 → AI 对话(LangGraph) |
+| 测试现状 | **本轮实测**:浏览器 28(唯一)+ 跨节点 11(189 admin 5 + 204 fe 6)+ langgraph 51 + fe 624 + web 116 + 根级 23 + API 32 + 窗口契约类 21(S-cfc-186 7 + S-cfc-182 14)= **906 例实测**;后端全量 3244 采信 R189(窗口 main 零变更) |
+| 质量目标 | 全量用例全绿、覆盖 SRS 与用户场景(含异常流)、浏览器双节点可复现、结果变更即提交并通知研发 |
+| 风险点 | ① 后端存量 25 红(12 类)+ 契约窗 49 RED 待实现;② CI 清单双向失真;③ 上游 LLM 凭据失效(B-142-2 复发,**api key ****ab12 invalid**);④ 根级 16 红(9082 未起);⑤ 迁移 DDL 缺陷;⑥ pytest 进程不退出(B-177-1);⑦ **git SSH 22 故障(B-189-1)** |
+| 待核实项 | S-cfc-176 幂等分支排期 · 251 上游 key 更换(****ab12)· 204 恢复后续稳定性 · B-156-1/B-170-1 修复状态 · S-cfc-186 跨家庭 IDOR 研发排期 |
+
+### 3.2 20 类测试覆盖矩阵(全类不省略)
+
+| # | 测试类型 | 三态 | 证据(本轮实测) |
+|---|----------|------|------------------|
+| 1 | 单元测试 | ✅ 已发现 | 后端全量 3244(采信 R189)+ 窗口契约类 21(实测);前端 624;管理端 116 |
+| 2 | 测试覆盖率 | ⚠️ 部分 | 管理端 Jest 覆盖率可产出;后端未开 jacoco(未授权改 pom) |
+| 3 | 变异测试 | ❌ 未发现 | 全仓无 pitest/mutation 配置 |
+| 4 | 集成测试 | ✅ 已发现 | `integration/*` 存量执行结果承袭(EnergySystemFlow 6F、GuideModule 1E 等);本轮未重跑(main 零变更) |
+| 5 | 契约测试 | ✅ 已发现(本轮重点) | **窗口 2 契约类 21 例全实测**:S-cfc-186 7 例(6 过 / 1 预期 RED 跨家庭 IDOR)+ **S-cfc-182 14/14 GREEN**;R189 契约窗 13 类 133 例承袭(85/48);S-cfc-176 维持 1F |
+| 6 | 接口 API 测试 | ✅ 已发现 | 线上 API 32 例(**本轮实测**:25 过 / 7 跳 / 0 红,28s) |
+| 7 | 系统/端到端 | ✅ 已发现 | 浏览器 28 例唯一(**本轮实测**:admin 5 + fe 6 + shop 4 + health 13 全过)+ 跨节点 11(189 admin 5/5、204 fe 6/6)+ dx 2 跳承袭 |
+| 8 | 验收 UAT | ⚠️ 待核实 | 无独立 UAT 包;以浏览器 28 例作为系统级验收代理 |
+| 9 | BDD 规格 | ✅ 已发现 | 用例/场景 11 份(用户场景文件级,映射率 72.1% 承袭) |
+| 10 | 冒烟测试 | ✅ 已发现 | `api-smoke.spec.js` 本轮实测全绿(含自注册链路) |
+| 11 | 回归测试 | ✅ 已发现 | 存量红集合 12 类逐类零漂移(**第 25 次**);admin/frontend/shop/health 全绿;204 fe 恢复 |
+| 12 | 探索性测试 | ⚠️ 部分 | 历轮浏览器人工探索记录;本轮未安排新探索项 |
+| 13 | 性能测试 | ❌ 未实测 | 未授权联网压测;仅 API 时延观察(非功能 32 例内含超时断言) |
+| 14 | 可靠性测试 | ⚠️ 部分 | pytest 挂死(B-177-1 观察)为可靠性缺陷;本轮 269.16s 正常收口 |
+| 15 | 安全测试 | ✅ 已发现 | `nonfunc-security.spec.js` 11 例本轮全绿(XSS/越权/恶意文件);匿名 6 端点全 401 |
+| 16 | 兼容性测试 | ⚠️ 部分 | 双视口(desktop 1440 / mobile 375)实测通过;无多浏览器矩阵 |
+| 17 | 可用性测试 | ⚠️ 部分 | 登录-主框架-核心菜单旅程 5/5×2 节点;无专项可用性问卷 |
+| 18 | 无障碍测试 | ❌ 未发现 | 无 axe/pa11y 配置 |
+| 19 | QA 流程 | ⚠️ 部分 | 汇报/台账/blockers 闭环在跑;CI 清单失真(B-152-1/2)未修 |
+| 20 | 质量指标 | ⚠️ 部分 | 全站通过率 97.22%(G1 未达 98%);锚点率 20.1% 承袭 |
+
+---
+
+## 四、五要素问题清单
+
+| # | ① 问题描述 | ② 测试 bug 点 | ③ 最优修改方案 | ④ 修改目的 | ⑤ 闭环方式 |
+|---|-----------|--------------|----------------|-----------|-----------|
+| P1 | 后端存量 25 红(12 类,跨 25 轮)零漂移承袭 | `EnergySystemFlowTest` 6F / `DimensionRegistryMapContractTest` 3F / `OrchestrationEngineTest`·`OrchestrationIntegrationTest`·`MembershipPaySwitchTest`·`PaymentServiceORDRoutingTest`·`UserServiceWechatLoginTest`(×2)·`TeamProfitCronContractTest` 各 1F 等 | 按类逐条修;先修 `TeamProfitCronContractTest`(平台费截断,影响计费) | 后端回归全绿 → G2 闭环 | 修后 `mvn -o -B test -Dtest=<类>` 定向复跑,单类 0F 连续 2 轮 |
+| P2 | **窗口 49 前瞻契约 RED 待研发实现(TDD 先行态,本轮已取证)** | S-cfc-183 `PlanContentSafetyReviewContractTest` 4F / S-cfc-184 `TeacherAssessmentRecordContractTest` 3F / S-cfc-188 `FamilyAutoCreateForPurchaseContractTest` 4F / S-cfc-196 `TeacherPlanCreateContractTest` 4F / **S-cfc-186 `TeacherFamilyOverviewContractTest` 1F(跨家庭 IDOR:getFamilyMemberTasks 未校验 memberId∈familyId)** / TeacherMindRecordContractTest 12F / TeacherAppointmentGuardContractTest 10F / 其余 6 类 11F | 按 SC-T 界面对应实现(必填拦截、重复覆盖、权限门禁、自动建家庭、mind 记录、预约守卫、**memberId 属主校验**等);本报告 `notice` 已逐键下发 | 49 前瞻契约 RED 转绿 → RED 清零 | 实现后逐类定向复跑,单类 0F 即销 RED;GREEN 四键(189/191/182/Activate)已全绿可直接收尾 |
+| P3 | S-cfc-176 幂等分支未生效 | `MembershipPlanPurchaseContractTest.repeatPurchase_pendingOrderSamePlan_reuseNoDuplicate:201`(`NeverWantedButInvoked: paymentOrderMapper.insert`) | `findReusablePendingOrder` 命中后直接复用,不再 insert | 契约闭环(防重复下单) | 定向复跑该类 7/7 绿 |
+| P4 | 迁移 DDL 缺陷持续复现 | `schema.sql:5345` `INDEX idx_product (product_id)` 无该列;`DatabaseInitializer.java:2261` 带别名 UPDATE + 派生表引用被更新表 | 删索引或补列;改独立子查询写法 | 建表成功 + 计数列可写 | 启动日志 0 次 `SQLSyntaxErrorException` |
+| P5 | 未入库测试资产 3 类 | `config/GrowthPublicEndpointMappingTest.java`(`??` 未跟踪)等 | `git add` + push | 资产可追溯 | 入库后全量重跑该类 0E |
+| P6 | pytest 打印 summary 后进程不退出(B-177-1)| `tests/test_analyze.py` → `RuntimeWarning: coroutine 'JavaClient._get_client' was never awaited` | `app/main.py:84` 改用 lifespan 并 `await client.aclose()`;派发器加 `timeout -k 10 900` | 派发可判定成功、节点不残留 | 派发日志出现 `EXIT=0` 且 `ps` 无残留 |
+| P7 | 根级黑盒 16 红(B-36 第 74 轮) | `tests/integration/*`(全 `ECONNREFUSED 9082`) | 起本地 9082 或改指向测试环境 | 黑盒链路可测 | 9082 起来后重跑 23 例 |
+| P8 | CI 清单双向失真(B-152-1/2) | `.gitlab-ci.yml:61`(漏拦 6 + 陈旧 26) | 按实测红类集合精修 `BACKEND_OBSERVED_CLASSES`(接入本轮 S-cfc-186) | CI 硬门禁真实生效 | CI 在含红例分支上「必红」验证一次 |
+| P9 | 前端 3 例预期 RED | `signature.*`(AC-R1/AC-R2)+ `growthHistory.dayGroupView` RED-A7 | 交付 S-cfc-132/133 组件与分组逻辑 | 前端全绿 | `npx jest` 624/624 |
+| P10 | **langgraph 上游凭据失效(B-142-2 复发,环境型)** | `test_auto_tag_valid_format` 标签数 0;日志 `Error code: 401 - Authentication Fails, Your api key: ****ab12 is invalid` | **更换/恢复 251 上游 AgnesAI API key(****ab12)** | langgraph 51/51 回归 | 恢复后连续 2 轮 51/51 → B-142-2 再销号 |
+| P11 | **git SSH 22 故障(B-189-1,延续)** | `git.iwintrue.com` / `192.168.16.251:22` CLOSED(R188 09:39 起) | Gogs 侧恢复 SSH 服务/端口映射;故障期经 `http://git.iwintrue.com/liaoxg/cfc.git` 只读确认远端 HEAD | fetch/push 恢复正常 | `git fetch origin` 成功 + 本轮产物推送成功 |
+| P12 | 204 节点历史失联(B-95) | 历轮误判 `204:4090=000`(openCode 实际服务在 **204:4091**,项目 `D:\cfc`) | 更新节点拓扑登记(4091);本轮 fe 6/6 恢复 | 节点可比性重建 | 连续 2 轮 6/6 可复现后销号 |
+| P13 | dx 桥接 2 例 skip(404) | `dx-health-bridge-auth.spec.js`(端点 404) | 端点上线后自动转断言 | 覆盖空转消除 | 线上端点 200 后重跑转 401 断言 |
+
+---
+
+## 五、域结论表
+
+| 审查维度 | 负责专家 | 结论 | 关键证据 |
+|----------|----------|------|----------|
+| 单元·组件级 | 苏雅 | **改善** | 窗口 2 契约类 21 例全实测(S-cfc-186 6 过/1 预期 RED + **S-cfc-182 14/14 GREEN**);fe 609/3/12(3 预期 RED);web 116/116 |
+| 集成·接口级 | 柯砚 | **改善** | 线上 API 32 例 0 红;S-cfc-176 维持 1F;契约窗 GREEN 四键(189/191/**182**/Activate)可收尾 |
+| 系统·验收级 | 白樾 | **改善** | 浏览器 28 例唯一全过 + 跨节点 189 admin 5/5、**204 fe 6/6 恢复(B-95 观察改善)**;health 13/13(B-61 维持) |
+| 测试保障级 | 闻朔 | 改善 | 冒烟全绿;存量红集合零漂移(第 25 次);契约验证流程延续(B-190-2 补验窗口 2 契约类) |
+| 性能·可靠性 | 骆骋 | 承袭 | 后端全量采信(窗口零变更);langgraph 50/51 正常收口(上游 401 环境型)但 pytest 进程不退出仍为可靠性缺陷(B-177-1) |
+| 安全 | 关屿 | **改善** | 匿名端点全 401;nonfunc-security 11 例全绿(XSS/越权/恶意文件);S-cfc-186 暴露 IDOR 门禁缺口(预期 RED,待研发补 memberId 属主校验) |
+| 兼容·体验 | 阮清 | 稳定 | frontend 移动视口 6/6(本机+204);desktop 双节点 admin 5/5;无多浏览器矩阵 |
+| 流程·度量 | 何序 | 部分改善 | G12 ✅ 窗口零需求变更;G9 锚点 20.1% 承袭;CI 漏拦/陈旧未修(B-152-1/2) |
+
+---
+
+## 六、门禁结论
+
+**结论:不通过(承袭,无回归、正向项明确)** —— 全站 4141 例 / **4026 过 = 97.22%(剔 skip 97.72%)**,红 94。红数环比 +2 的全部来源为**窗口新增契约测试预期 RED(S-cfc-186,+1)**与**上游 401 环境红(B-142-2 复发,+1)**,均非缺陷回归;浏览器任务级全绿(含 **204 恢复**);API 零红;后端存量 25 红零漂移。阻断项为**存量红 + 49 前瞻契约 RED + CI 清单失真 + live chat 挂死 + 251 上游 key 失效**,均非本轮新增回归。
+
+| 门禁 | 判定 | 依据(本轮实测) |
+|------|------|------------------|
+| G1 全站通过率 ≥98% | ❌ **97.22%** | 4026/4141(剔 skip 97.72%);+0.06pct,红数 +2 为契约预期 RED + 环境红 |
+| G2 后端红归零/契约裁决 | ❌ **74 红** | 存量 25(12 类)+ **契约窗 49 预期 RED(11 类,含 S-cfc-186,需研发实现)** |
+| G3 前端双绿 | ⚠️ fe 609/624(3 预期 RED)+ web 116/116;**小程序 H5 浏览器 6/6(本机+204)** | 3 红全为 S-cfc-132/133 未交付 |
+| G4 编译可编译 | ✅ | 窗口 S-cfc-186/182 全量 test-compile + 定向执行无编译错 |
+| G5 结果完整性 | ✅ | 定向 21 例 surefire XML 完整;全量承袭 R189 388 XML 可聚合 |
+| G6 浏览器 E2E | ✅ **28 过 + 2 跳承袭** | admin 5/5(本机+189)、fe 6/6(本机+**204**)、shop 4/4、health 13/13、dx 2 跳 |
+| G7 langgraph | ⚠️ **50/51** | **B-142-2 复发(环境型)**:唯一红=上游 401 `key ****ab12 invalid`;节点 HEAD=基线 + 窗口源码零变更 |
+| G8 匿名鉴权 401 | ✅ | health 三端点全 401;API 安全套件 11 例全绿 |
+| G9 用例锚定 SRS | ❌ 20.1% | 承袭口径(本轮未变) |
+| G10 双/三节点可比 | ✅ 三节点 | 189 admin 5/5 + **204 fe 6/6(恢复)** + 本机 admin 5/5 + 251 langgraph 51 例 |
+| G11 CI 必红 | ❌ | 漏拦 6 + 陈旧 26 承袭(B-152-1/2) |
+| G12 用例锚定 SRS 变更 | ✅ | specs / test-cases / API_REFERENCE / 场景窗口零变更 → 用例零新增(我);S-cfc-186/182 为并行会话产出并已补验 |
+| G13 存量红零漂移 | ✅ **第 25 次** | 12 类逐类与 R189 一致;B-95 为唯一改善项(正向) |
+
+---
+
+## 七、变更触发动作(用户规则:结果有变 → 提交远程 + 通知研发)
+
+- **变更事实(本轮)**:① 窗口**新增 2 契约类共 21 例**(S-cfc-186 7 + S-cfc-182 14,并行会话提交)经定向实测取证:**S-cfc-186 6 过 / 1 预期 RED(跨家庭 IDOR)** + **S-cfc-182 14/14 GREEN**;② langgraph **51/51 → 50/51**:B-142-2 复发(上游 401 key 失效),**非代码回归**,需研发更换 key;③ **204 节点 frontend 6/6 恢复**(B-95 观察改善);④ 浏览器任务级全绿、API 零红、fe/web 零漂移;⑤ 全站口径 4141 例(+21)。
+- **动作**:本报告 + `automation-run-R190.md` + `blockers.md`(B-190-1/2 新增、B-142-2 复发观察 1/2、B-95 观察改善、B-189-1 延续)+ `notice-2026-11-13-R190-研发通知.md` + 台账三件套(discovery/inventory/automation-status)提交推送。
+- **推送状态**:✅ **已完成**。SSH 22 仍 CLOSED(B-189-1 延续),已将 push remote 切换至 `http://git.iwintrue.com/liaoxg/cfc.git` 执行 `git rebase FETCH_HEAD && git push origin cfclub`;fetch 仍须临时指定 HTTP URL。**请运维恢复 Gogs/251 sshd 服务(端口 22)**。
+- **通知通道**:Agent Mail 历轮 `not_bound`(静默不触发)→ 以 blockers.md + `notice-2026-11-13-R190-研发通知.md` + 推送落地。
+
+---
+
+## 八、待办
+
+1. **运维(P0)**:恢复 Gogs SSH 22(B-189-1);**更换/恢复 251 上游 AgnesAI API key(****ab12 失效,B-142-2)**。
+2. **研发(P1)**:49 前瞻契约 RED(S-cfc-183/184/186/188/196 + MindRecord/AppointmentGuard 等 11 类)逐键实现使绿——**含 S-cfc-186 `getFamilyMemberTasks` 补 memberId∈familyId 属主校验(防跨家庭 IDOR)**;S-cfc-176 幂等;迁移 DDL(B-156-1/B-170-1);B-177-1 `aclose()`;**S-cfc-182 已 GREEN 可直接收尾**。
+3. **CI**:精修 `BACKEND_OBSERVED_CLASSES`(漏拦 6 + 陈旧 26 + 接入 S-cfc-186/182)。
+4. **测试**:B-95 观察改善(连续 2 轮 6/6 后销号);下轮关注 49 RED 的研发实现进展与回归;前端 624/624 目标(S-cfc-132/133 交付后);B-142-2 观察 2/2。
+
+---
+
+## 附录 A:本轮命令与口径(可复现)
+
+```bash
+# 浏览器(必须在 tests/ 内跑;每节点 1 个浏览器任务,本机串行)
+cd tests && npx playwright test -c playwright.config.live.js --project=admin-desktop   # 5/5(本机)
+cd tests && npx playwright test -c playwright.config.live.js --project=frontend-mobile # 6/6
+cd tests && npx playwright test -c playwright.config.live.js --project=shop-api        # 4/4
+cd tests && npx playwright test -c playwright.config.health.js                          # 13/13
+cd tests && npx playwright test -c playwright.config.api.js --output=../_r190_api \
+  e2e/api/api-smoke.spec.js e2e/api/nonfunc-security.spec.js \
+  e2e/api/nonfunc-perf-compat.spec.js e2e/api/traceability-p94-p95-c26.spec.js        # 25/7/0
+
+# 189 节点 admin(单节点任务)
+node _r190_189_admin.cjs kick && node _r190_189_admin.cjs read  # 5/5 EXIT=0
+
+# 204 节点 frontend(B-95 恢复;openCode 在 204:4091,项目 D:\cfc)
+node _r190_204_run.cjs   # 前台同步模式,6/6 PASS(46.1s)
+
+# 251 节点 langgraph pytest
+LOG=/tmp/r190_pytest.log node _r190_251_lg.cjs kick && node _r190_251_lg.cjs read  # 50/51(上游 401)
+
+# 窗口 2 契约类定向(S-cfc-186 纯 Mockito 7 例 + S-cfc-182 14 例)
+cd cfc-backend && JAVA_HOME=/d/jdk1.8.0_25 /d/maven/apache-maven-3.6.1/bin/mvn -o test -Dtest=TeacherFamilyOverviewContractTest -DfailIfNoTests=false   # 6过/1预期RED
+cd cfc-backend && JAVA_HOME=/d/jdk1.8.0_25 /d/maven/apache-maven-3.6.1/bin/mvn -o test -Dtest=TeacherReportProxyContractTest -DfailIfNoTests=false    # 14/14 GREEN
+
+# Jest
+cd cfc-frontend && npx jest --ci   # 609/3/12
+cd cfc-web && npx jest --ci        # 116/116
+cd tests && npx jest --silent      # 7/16(B-36)
+```
+
+## 附录 B:窗口 2 契约类定向实测明细(B-190-2 补验)
+
+| 类 | 提交 | 例数 | 过/红 | 预期 |
+|----|------|------|-------|------|
+| TeacherFamilyOverviewContractTest(S-cfc-186,SC-T-16/17/21/27) | e6b154a73→0e6f285da(12:25/12:28,并行 E2E Test Bot) | 7 | 6/1 | RED 1/7 ✅(跨家庭 IDOR:`getFamilyMemberTasks` 仅校验 `requireBound(guideId, familyId)` 未校验 `memberId∈familyId`,教师绑家庭 A 可读家庭 B 成员任务);6 个控制用例全过 |
+| TeacherReportProxyContractTest(S-cfc-182,teacher.report.proxy) | 5d67a62f6(12:16 merge c6f775ab2,Sisyphus Agent) | 14 | **14/0** | **GREEN 14/14 ✅(实现已落地,可直接收尾)** |

+ 377 - 0
docs/tdd/orchestration/report-test-2026-11-13-R191.md

@@ -0,0 +1,377 @@
+# 测试负责人工作汇报(R191 轮)
+
+> 本汇报按《测试负责人工作汇报模板》填写。R191 为**后端全量实跑轮**(R189/R190 均为采信,按纪律必须实跑):基线起点 `cfclub @ 309fa0220`(R190 收口 12:35 之后),跑测窗口内并行会话把 HEAD 推进至 `123f768c7`(含 S-cfc-197 契约类),收尾经 HTTP 兜底 fetch 后快进至 **`5ad781a00`**(含 S-cfc-199 / S-cfc-200 三个契约类)。
+> **拓扑(用户指令)**:浏览器经 `http://cfc.iwintrue.com/admin`(后台)+ `http://cfc.iwintrue.com/frontend`(小程序);189 / 204 / 本机为浏览器节点(**每节点仅 1 个浏览器任务**,本机四个套件串行);langgraph 部署 251 → **本轮已实派并实跑**。
+> **★ 本轮关键事实**
+> ① 后端**恢复全量实跑**:`mvn -o -B clean test` **1:01h** → **386 类 / 3315 例 / 3232 过 / 73F+6E+4S = 97.50%(剔 skip 97.62%)**;386/386 XML 无沉默、2121 主源 + 392 测试源 0 编译错、dump 0。
+> ② **窗口与收尾两次新增契约类均已逐类实测取证**:本地窗口 S-cfc-197 `AdminReportParserManageContractTest` **37/37 GREEN** + S-cfc-198 `AdminKnowledgeBaseManageContractTest` **13 例 6 红/7 绿(预期 RED)**;收尾远端推入 S-cfc-199/200 三类 **52 例 39 红/13 绿(TDD 前瞻 RED)**。
+> ③ **存量红零漂移第 26 次**:13 类 / 24 红,与 R190 的 13 类逐类一致(唯一 −1 例为 `TaskReviewScP11ContractTest` 装置浮动 2F→1F)。
+> ④ **langgraph@251 4 红全为环境**:`429 AgnesAI 免费额度耗尽` + `401 api key ****ab12 invalid`;live `/health` 200/2ms 但 `/api/v1/chat` **000/40s**、`/api/v1/analyze` **000/50s** → B-142-2 复发 + B-186-1 延续。
+> ⑤ **浏览器任务级全绿**:本机四件套 28/28 + 跨节点 189 admin **5/5** + 204 fe **6/6(连续 2 轮,B-95 可销号)**;dx 2 跳承袭(B-DX-1)。
+> ★ **全站可比:4254 例 / 4088 过 = 96.10%(剔 skip 96.67%),红 141**。红数环比 +47 **全部为 TDD 前瞻契约 RED(+45:S-cfc-198 6 + 远端三类 39)与上游环境红(langgraph +3)**,**零缺陷回归**;剔除全部预期 RED 后为 **98.34%**。
+
+---
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | 测试负责人(自动化测试闭环任务 / 软件测试专家团·陆谨) |
+| 汇报周期 | 2026年11月13日 13:14 ~ 14:35(第 191 轮) |
+| 基线 | 起点 `cfclub @ 309fa0220`(=origin);跑测中推进 `123f768c7`;收尾 ff 至 **`5ad781a00`**(13:14 → 14:35 窗口) |
+| 窗口内需求变更 | **零** — `docs/superpowers/specs`(201 份)/ `tests/test-cases`(16 份)/ `tests/user-scenarios`(11 份)/ `API_REFERENCE.md` 窗口内 `git diff` 命中 0 → **G12 ✅,未触发用例编写闸门(我无需新增/改用例)** |
+| 窗口内源码变更 | **零功能源码** — `cfc-backend/src/main`、`cfc-frontend`、`cfc-langgraph` 窗口 diff 命中 0;唯一变更为**测试契约类 5 个**(S-cfc-197/198/199/200)+ cfc-web 版本号 bump + docs |
+| 负责模块/单元 | 全站:管理端 admin@本机 + 189 · 小程序 H5 fe@本机 + 204 · cfc-langgraph@251(实派) · cfc-backend JUnit(**全量实跑 + 收尾增量复验**) · cfc-web/cfc-frontend Jest · 线上 API 非功能层 |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 测试用例建设
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **本轮新增测试用例数\*** | **0 个(由我新增);并行会话 +102 例(本地窗口 50 + 收尾远端 52)** | SRS/场景/接口文档窗口零变更 → 未触发用例编写闸门(G12 ✅);窗口内并行会话新增 5 个契约测试类(S-cfc-197 37 例 / S-cfc-198 13 例 / S-cfc-199 17 例 / S-cfc-200 16+19 例),**全部本轮逐类实测取证**,未出现 B-189-2 式「推入未验证」 |
+| **本轮验证执行测试数\*** | **4254 个** | 后端 **3367**(全量实跑 3315 + 收尾增量 52)+ 前端 Jest 740(fe 624 + web 116)+ langgraph 51(**251 实派**)+ 浏览器 41(39 过 + dx 2 跳)+ 线上 API 32 + 根级 23 |
+| **用例通过率** | **96.10%**(4088/4254;剔除 skip **96.67%**) | 红 141 / 跳 25;环比 R190(97.22%)−1.12pct,**红数 +47 全为 TDD 前瞻契约 RED(+45)+ 上游环境红(langgraph +3)+ 装置浮动(−1)**;**剔除全部预期 RED 后 98.34%**,零缺陷回归 |
+| **用例台账** | `tests/test-cases` 16 份 / 表行 **1168** / 唯一 TC **618** | 严格锚点(tests JS/TS/py 排除 generated ∪ Java 测试)**153**;双向命中 **106 = 17.2%**;孤儿锚点 47 / 台账空转 512 |
+
+### 1.2 会话与任务分发
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **分派任务数\*** | **8 个** | R191-1 本机 `admin-desktop`(5/5)+ `frontend-mobile`(6/6)+ `shop-api`(4/4)+ `health`(13/13,串行);R191-2 **189** `admin-desktop`(5/5);R191-3 **204** `frontend-mobile`(6/6);R191-4 **251** `langgraph pytest`(**47/51**,134.17s)+ live 三端点;R191-5 cfc-web Jest(116/116);R191-6 cfc-frontend Jest(609/3/12);R191-7 根级 Jest(7/16);R191-8 线上 API 非功能(25/0/7)+ 收尾增量 3 契约类(13/39) |
+| **恢复智能体会话数\*** | 0 个 | 各节点实探:`189:4090`=200、`204:4091`=200、`251:4096`=200、`251:9000`=404(根)/health 200、本机 4091=OPEN;未遇中断会话 |
+| **当前在用会话数** | 0 个 | 全部结束;251 残留 pytest 进程(2 个,存活 4m33s)已 `kill -9` 清理并 `ps` 复核 0 |
+
+### 1.2b 各会话当前工作状态列表
+
+| 会话标签 | 会话 ID/地址 | 当前状态 | 当前任务 | 静默/空闲时长 | 工作目录 | 备注 |
+|----------|--------------|----------|----------|---------------|----------|------|
+| `local-admin` | 本机 Playwright | done | admin-desktop UI E2E | 1.4m | `d:\cfc\tests` | **5/5**;TC-A-U01~U05 全过(U05 55.3s) |
+| `local-frontend` | 本机 Playwright | done | frontend-mobile UI E2E | 1.9m | `d:\cfc\tests` | **6/6**;B-185 维持消除 |
+| `local-shop` | 本机 Playwright | done | shop-sort API E2E | 9.7s | `d:\cfc\tests` | **4/4** |
+| `local-health` | 本机 Playwright | done | health 鉴权契约 E2E | 5.5s | `d:\cfc\tests` | **13/13**(TC-HP-U01~03 全 401) |
+| `remote-189` | 192.168.16.189:4090 | done | admin-desktop UI E2E | 1.3m | `C:\code\cfc\tests` | **5/5**;HEAD `8524533c8`,spec blob `4703d73fb`=本机 → 可比 |
+| `remote-204` | 192.168.16.204:4091 | done | frontend-mobile UI E2E | 44.7s | `D:\cfc\tests` | **6/6(连续 2 轮)**→ B-95 可销号 |
+| `remote-251` | 192.168.16.251:4096 | done | langgraph pytest + live 探针 | 134.17s | `/sc-data/cfc/cfc-langgraph` | **47/51**;4 红全 429/401 环境;node HEAD `563e6c62d`,`cfc-langgraph` 与本地零 diff |
+| `local-jest` | 本机进程 | done | cfc-web + cfc-frontend + 根级 | web 124s / fe 66s / root 7s | `cfc-web` / `cfc-frontend` / `tests` | web **116/116**、fe **609/3/12**(3 预期 RED)、根级 **7/16**(B-36) |
+| `local-backend` | 本机 mvn | done | **后端全量 386 类** + 收尾增量 3 类 | 1:01h + 2m43s | `d:\cfc\cfc-backend` | 全量 3315(3232/79/4)+ 增量 52(13/39) |
+| `virtual` | dx-bridge | done | D/X 桥接鉴权契约 | 瞬时 | `d:\cfc\tests` | **2 skip**(端点 404,B-DX-1 第 33 轮) |
+
+### 1.3 问题处置
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **新测试发现问题数\*** | **3 个** | ① **B-191-1(P1 流程/证据)**:跑测窗口内远端连续推入 **3 个契约类**(`AdminSupplierManageContractTest` 16 例 10F、`SupplierManageAdminContractTest` 19 例 16F、`DimensionConfigAdminContractTest` 17 例 13F),若不做收尾 fetch + 增量复验,会把 39 例 TDD-RED 当作「未覆盖」或把后续轮次误判为回归 → 本轮已增量复验并计入;② **B-191-2(P1 装置/性能)**:后端全量 **1:01h**(R188 同源码 43:15),慢类 `ArticleCategoryControllerTest` **493.97s**、`AdminFamilyOverviewControllerTest` 341.2s、`GrowthPublicEndpointMappingTest` 334.2s、`GrowthRecordSyncFlowTest` 280.7s、`PlannerBindInviteFlowTest` 178.1s;`cfc-scheduler` 日志 **20524/73287 = 28.0%** → B-152-3 复现;③ **B-191-3(P2 工具/度量)**:线上 API 套件首跑仅 **26 例(19 过 + 7 跳)/5.4min**,二次同命令 **32 例(25 过 + 7 跳)/18.2s** → 首跑口径不完整,报告例数须以「`Running N tests` + summary 例数 = 点名例数」双校验 |
+| **已解决测试相关问题数\*** | **2 个** | ① **B-95 销号(204 节点)**:204 fe **6/6 连续 2 轮**(R190 46.1s + R191 44.7s),满足连续 2 轮判据;② **S-cfc-197 GREEN 确认**:`AdminReportParserManageContractTest` **37/37** 全绿 → 能力键 `admin.reportParser.manage` 实现已落地,可直接转 GREEN(前提:研发侧 `--mark-yellow`/`--mark-green` 登记) |
+| **待解决测试相关问题数** | P0×2 / P1×9 / P2×4 | ① B-152-1/2 CI 清单**漏拦扩至 18 / 陈旧 26**;② **TDD 前瞻契约 RED 94 例(12 类)**待研发实现;③ 存量装置/数据红 24 例(13 类);④ B-177-1 pytest 进程不退出(第 13 轮);⑤ B-156-1/B-170-1 迁移 DDL;⑥ B-162-1 未入库测试类;⑦ B-178-1 反射装置浮动;⑧ B-36 根级 16 红(9082 未起);⑨ B-DX-1 dx 404;⑫ **B-142-2 复发(429/401 上游)**;⑬ B-186-1 live chat/analyze 挂死;⑭ **B-189-1 git SSH 22 仍故障**;⑮ B-191-2 全量耗时劣化 |
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+| 提交 sha | 关联任务/能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|-----------------|------|--------------|----------|------------|------|
+| `(本轮)` | 全站巡检 R191 + 后端全量实跑 + 5 契约类补验 + B-191-1/2/3 + B-95 销号 + S-cfc-197 GREEN | 汇报 | 报告 + 执行日志 + blockers + 研发通知 + 台账 | cfclub | ✅ 推送 `http://git.iwintrue.com/liaoxg/cfc.git`(**SSH 22 仍故障,B-189-1 第 2 轮 HTTP 兜底**) | 结果有变化 → 已提交并通知研发 |
+
+### 1.5 状态机阶段分布(全局进度,研发侧账本口径)
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| RED | 窗口新增 **5 个契约类**(S-cfc-197 已全绿可转 YELLOW/GREEN;S-cfc-198/199/200 为本轮 TDD-RED 前瞻) | 研发状态机口径另计(`sc-key-map.md` 窗口 3 次回填,241/241 全映射达成) |
+| YELLOW | 承袭 | 待研发 `--mark-yellow` 登记 |
+| GREEN | S-cfc-197(**本轮实测 37/37**);S-cfc-182(R190 14/14) | 测试侧已取证 |
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳ wish) | 0 | 正常 | SRS 目标池未启用;specs 201 份可读 |
+| 契约常量(contract) | 5 类新增 | 正常 | S-cfc-197/198/199/200 全部可收集可执行 |
+| 测试用例引用(test_case_map) | 618 唯一 TC | 正常 | 台账扫描正常(表行 1168) |
+| 执行结果回填(case_results) | 全量 386 XML + 增量 3 XML | 正常 | 386/386 无沉默,dump 0 |
+
+---
+
+## 二、历史审查记录(阶段 -1 记忆召回)
+
+| 项 | 内容 |
+|---|---|
+| 上次审查 | **R190(2026-11-13 12:35,`0e6f285da`)**,结论「不通过(承袭,无回归、正向项明确)」,全站 4141/4026(97.22%),红 94;后端采信 R189;langgraph 50/51;204 节点恢复 |
+| 历史遗留 | 后端存量 25 红(13 类)· 根级 16 红(B-36)· fe 3 预期 RED(S-cfc-132/133)· CI 漏拦/陈旧(B-152-1/2)· B-177-1 · B-178-1 · B-186-1 · B-189-1 |
+| 本次是否改善 | **后端由采信转全量实跑(386 类 3315 例,零沉默零编译错)**;**B-95 销号(204 连续 2 轮 6/6)**;**S-cfc-197 37/37 GREEN**;存量 13 类逐类零漂移;浏览器六件套任务级全绿 |
+| 本次是否回退 | **langgraph 50/51 → 47/51(−3)**:4 红全为 `test_auto_tag`,日志 `429 AgnesAI 免费额度耗尽` ×3 + `401 api key ****ab12 invalid` ×5 → **上游凭据/配额环境型**(节点 `cfc-langgraph` 与本地零 diff、窗口源码零变更)→ 非代码回归;**G1 97.22% → 96.10%**(红 +47 全为 TDD 前瞻契约 RED 45 + 环境红 3 − 装置浮动 1) |
+| 教训应用 | ①「采信不得超过 2 轮」→ 本轮强制全量实跑(R189/R190 均采信);② 收尾**两次** fetch(提交前 + 推送前)→ 本轮第二次抓到远端 3 个新契约类并增量复验(B-191-1);③ 绝不用「类/分钟」外推或提前 kill(R188 教训):本轮前半程 31 类/22min 仍耐心等完,最终 1:01h 自然收口;④ 远程派发用 `setsid nohup` 分离 + 单次读,不在等待期建 shell(B-125);⑤ 上游 401/429 与代码回归用「节点源码零 diff + 窗口源码零变更」判别(B-142-2 判别式) |
+
+> 本次为该对象**第 191 次**审查,有历史记录;上述内容仅作对照,**本轮全站所有维度均为本轮实测**(后端全量 + 5 契约类定向 + 浏览器三节点 + langgraph 251 实派 + Jest 三套 + 线上 API),无承袭项。
+
+---
+
+## 三、项目画像与覆盖矩阵
+
+### 3.1 项目画像
+
+| 维度 | 内容 |
+|------|------|
+| 业务定位 | 家庭教育成长平台(五维能量 身/心/智/行/富):小程序家长端 + 规划师端 + 管理后台 + 独立商城 |
+| 技术栈 | Spring Boot 2.7.18 + MyBatis-Plus + Java 8(9082)/ uni-app 微信小程序(Vue2)/ Vue2 + Element UI(8082)/ FastAPI + LangGraph + ChromaDB(9000@251) |
+| 模块结构 | `cfc-backend`(366 控制器 / 395 测试源 / 386 执行类)/ `cfc-frontend`(42 测试文件 / 624 例)/ `cfc-web`(17 套件 / 116 例)/ `cfc-langgraph`(51 例 @251)/ `tests`(e2e-live 5 文件 + e2e/api 点名 4 文件) |
+| 关键路径 | 登录/家庭/成员 → 五维能量 → 任务/成长记录 → 健康报告上传解析 → 测评订单 → 商城/支付 → AI 对话(LangGraph) |
+| 测试现状 | **本轮全实测**:后端全量 3315 + 增量 52 + fe 624 + web 116 + langgraph 51 + 浏览器 41 + API 32 + 根级 23 = **4254 例** |
+| 质量目标 | 全量用例全绿、覆盖 SRS 与用户场景(含异常流)、浏览器双节点可复现、结果变更即提交并通知研发 |
+| 风险点 | ① 后端 TDD 前瞻契约 RED 94 例(12 类)待实现;② 存量装置/数据红 24 例(13 类);③ CI 清单双向失真(**漏拦 18 / 陈旧 26**);④ 上游 LLM 凭据 + 配额双故障(429/401,live chat/analyze 挂死);⑤ 根级 16 红(9082 未起);⑥ 迁移 DDL 缺陷(B-156-1/B-170-1);⑦ pytest 进程不退出(B-177-1);⑧ 全量耗时劣化至 1:01h(B-191-2);⑨ git SSH 22 故障(B-189-1) |
+| 待核实项 | S-cfc-176 幂等分支排期 · 251 上游 key 更换(****ab12)与配额 · 204 销号后续稳定性 · B-156-1/B-170-1 修复状态 · S-cfc-186 跨家庭 IDOR 研发排期 · S-cfc-198 三项缺口(重试/分批/角色门禁)排期 |
+
+### 3.2 20 类测试覆盖矩阵(全类不省略)
+
+| # | 测试类型 | 三态 | 证据(本轮实测) |
+|---|----------|------|------------------|
+| 1 | 单元测试 | ✅ 已发现 | 后端全量 386 类 3315 例(**本轮实跑**)+ 增量 3 类 52 例;前端 624;管理端 116 |
+| 2 | 测试覆盖率 | ⚠️ 部分 | 管理端 Jest 覆盖率可产出(Lines 未变);后端未开 jacoco(未授权改 pom) |
+| 3 | 变异测试 | ❌ 未发现 | 全仓无 pitest/mutation 配置(扫描 0 命中) |
+| 4 | 集成测试 | ✅ 已发现 | `integration/*` **本轮实跑**:EnergySystemFlowTest 6F、GuideModuleIntegrationTest 1E、DailyCheckinStreakFlowTest 6/6、AdminFamilyOverview 341s 全绿等 |
+| 5 | 契约测试 | ✅ 已发现(本轮重点) | 5 类全实测:S-cfc-197 **37/37 GREEN**、S-cfc-198 **13(6F/7绿)**、S-cfc-199 **17(13F/4绿)**、S-cfc-200 **35(26F/9绿)**;历史契约窗 12 类 94 红承袭 |
+| 6 | 接口 API 测试 | ✅ 已发现 | 线上 API 32 例(本轮实测:**25 过 / 7 跳 / 0 红,18.2s**;首跑 26 例异常见 B-191-3) |
+| 7 | 系统/端到端 | ✅ 已发现 | 浏览器 41 例(**39 过 + 2 跳**):admin 5(本机+189)、fe 6(本机+204)、shop 4、health 13 |
+| 8 | 验收 UAT | ⚠️ 待核实 | 无独立 UAT 包;以浏览器 39 例作为系统级验收代理 |
+| 9 | BDD 规格 | ✅ 已发现 | 用户场景 11 份 / SC 唯一 244;被台账引用 152 = **62.3%** |
+| 10 | 冒烟测试 | ✅ 已发现 | `api-smoke.spec.js` 本轮全绿(含自注册链路) |
+| 11 | 回归测试 | ✅ 已发现 | 存量 13 类逐类零漂移(**第 26 次**);浏览器/fe/web/API 全绿或零漂移 |
+| 12 | 探索性测试 | ⚠️ 部分 | 历轮浏览器人工探索记录;本轮未安排新探索项 |
+| 13 | 性能测试 | ❌ 未实测 | 未授权联网压测;仅 API 时延观察 + 后端耗时画像(B-191-2 493.97s 单类) |
+| 14 | 可靠性测试 | ⚠️ 部分 | **B-177-1 第 13 轮复现**:summary 打印后进程存活 4m33s(`Sl`),`EXIT=` 永不落盘;pytest 本身 134.17s 正常收口 |
+| 15 | 安全测试 | ✅ 已发现 | `nonfunc-security.spec.js` 11 例本轮全绿;匿名 6 端点 **6/6 = 401** |
+| 16 | 兼容性测试 | ⚠️ 部分 | 双视口(desktop 1440 / mobile 375)实测通过;无多浏览器矩阵 |
+| 17 | 可用性测试 | ⚠️ 部分 | 登录-主框架-核心菜单旅程 5/5 × 2 节点;无专项可用性问卷 |
+| 18 | 无障碍测试 | ❌ 未发现 | 无 axe/pa11y 配置 |
+| 19 | QA 流程 | ⚠️ 部分 | 汇报/台账/blockers 闭环在跑;**CI 清单漏拦扩至 18**(B-152-1/2 第 22 轮) |
+| 20 | 质量指标 | ⚠️ 部分 | 全站 96.10%(G1 阈值 98% 未达);锚点 **17.2%**(R190 口径 20.1%,口径差异见 §附录 C) |
+
+---
+
+## 四、五要素问题清单
+
+### P1-1 后端 TDD 前瞻契约 RED 94 例(12 类)待研发实现
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | 本窗口与收尾远端共推入 5 个契约类,其中 4 类为 TDD-RED 先行态,累计 **94 例红 / 12 类**(`TeacherMindRecord` 12 · `TeacherAppointmentGuard` 10 · `AdminKnowledgeBaseManage` 6 · `FamilyAutoCreateForPurchase` 4 · `PlanContentSafetyReview` 4 · `TeacherDataIsolationScT13` 4 · `TeacherPlanCreate` 4 · `NutritionPlanTeacher` 3 · `TeacherAssessmentRecord` 3 · `TeacherDimensionScore` 2 · `GuideFamilyTaskReview` 2 · `TeacherFamilyOverview` 1 · 远端三类 39 例) |
+| ② 测试 bug 点 | `AdminKnowledgeBaseManageContractTest#syncFailureRetry_syncPathShouldSupportRetry` / `#largeBatch_syncAndBackfillShouldSupportBatching` / `#permissionFlow_kbWriteEndpointsShouldEnforceRole`(断言 `expected true but was false`);`SupplierManageAdminContractTest`(19 例 16F);`DimensionConfigAdminContractTest`(17 例 13F) |
+| ③ 最优修改方案 | 研发按能力键逐项实现:① `KnowledgeBaseController` / `KnowledgeBaseVectorController` 补平台角色门禁(对齐 `AdminUnifiedKnowledgeController.allowed`);② 同步/回填路径补 `syncBatch` / `BATCH_SIZE` 分批;③ 补 `maxRetries` / `syncWithRetry` / `@Retryable` 重试;④ SC-A-06 停用在售商品守卫;⑤ 各 Teacher/成长域实现按 `sc-key-map.md` 排期 |
+| ④ 修改目的 | 恢复「契约先行」闭环:契约红 → 实现 → GREEN;当前 94 红使 G2 无法归零,且掩盖真实回归信号 |
+| ⑤ 修改后能否闭环 | 能。复测方式:`mvn -o -B test -Dtest=<类>` 逐类定向,单类 0F 且连续 2 轮 → 移出台账;契约类全绿后 `gate_evaluator` 的 G2 分母同步收缩 |
+
+### P1-2 存量装置/数据红 24 例(13 类,零漂移第 26 次)
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | 13 类 24 红:`EnergySystemFlowTest` 6F、`GrowthPublicEndpointMappingTest` 5E、`DimensionRegistryMapContractTest` 3F、`OrchestrationEngineTest`/`OrchestrationIntegrationTest`/`MembershipPaySwitchTest`/`PaymentServiceORDRoutingTest`/`TeamProfitCronContractTest`/`UserServiceWechatLoginTest`/`UserServiceWechatLoginContractTest`/`GuideModuleIntegrationTest`(1E)/`MembershipPlanPurchaseContractTest`/`TaskReviewScP11ContractTest` 各 1 |
+| ② 测试 bug 点 | `TaskReviewScP11ContractTest` 2F→1F 浮动(`ReflectionUtils` 反射装置无规则,B-178-1);`GrowthPublicEndpointMappingTest` 5E 因该文件未入库(`??`,B-162-1)且 334.2s;`TeamProfitCronContractTest` 平台费截断(数据) |
+| ③ 最优修改方案 | ① B-178-1 用 `@MockBean` 替代反射注入;② B-162-1 把 `GrowthPublicEndpointMappingTest` / `JwtInterceptorGrowthPolicyTest` / `ArticleTagsContractTest` 三个 `??` 文件入库;③ `TeamProfitCron` 平台费按 `BigDecimal` 精度修正(断言侧改 `compareTo` 或实现侧改舍入) |
+| ④ 修改目的 | 消除装置噪声,使「红」只代表产品缺陷(G2/G13 可信) |
+| ⑤ 修改后能否闭环 | 能。定向复跑单类 0F 连续 2 轮 → 从 `BACKEND_OBSERVED_CLASSES` 移除 → CI 硬门禁自动收紧 |
+
+### P1-3 B-191-1 窗口内多次推入契约类,全量与收尾口径不一致(流程)
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | 13:14 基线 `309fa0220`;跑测中本地 HEAD → `123f768c7`(+S-cfc-197 37 例);收尾 fetch 远端 +11 提交(`5ad781a00`,+S-cfc-199/200 三个契约类 52 例)。若只跑一次全量就定稿,会把 52 例(39 红)记为「未覆盖」 |
+| ② 测试 bug 点 | 非用例缺陷,属**取证流程缺口**:单次 fetch + 单次全量无法覆盖窗口尾部的并行提交 |
+| ③ 最优修改方案 | 固化「双 fetch + 增量定向」:提交前 fetch 一次、推送前再 fetch 一次;若 `git diff --name-only` 命中 `src/test/**` → 逐类定向复跑并计入总量;命中 `src/main/**` → 全量重跑或标「截断」 |
+| ④ 修改目的 | 保证「报告例数 = 真实覆盖率」,避免已实现资产被漏记、避免把新契约红误判为回归 |
+| ⑤ 修改后能否闭环 | 能。本轮已按此执行(52 例全部取证,39 红计入 G2 的「前瞻契约」栏);下轮起写入 SOP §阶段 4 |
+
+### P1-4 B-152-1/2 CI 清单双向失真(漏拦扩至 18 类)
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | `.gitlab-ci.yml:61` `BACKEND_OBSERVED_CLASSES` 33 类 vs 实测红类 25 类 → **漏拦 18 类**(含本轮 5 个新契约类:`AdminKnowledgeBaseManage` / `FamilyAutoCreateForPurchase` / `GuideFamilyTaskReview` / `NutritionPlanTeacher` / `PlanContentSafetyReview` / `TeacherAppointmentGuard` / `TeacherAssessmentRecord` / `TeacherDataIsolationScT13` / `TeacherDimensionScore` / `TeacherFamilyOverview` / `TeacherMindRecord` / `TeacherPlanCreate` / `SupplierManageAdmin`? 等)、**陈旧 26 类**(早已全绿仍被排除) |
+| ② 测试 bug 点 | 非用例缺陷,属 CI 配置漂移;`backend-blocking-junit` job 在含红例分支上必然失败,同时对 26 个已绿类失去回归守护 |
+| ③ 最优修改方案 | 用本轮实测红类集合重写清单(25 类),并加一条 CI 步骤:`mvn test` 后自动比对 surefire 红类与清单,差异即 fail 并输出差集 |
+| ④ 修改目的 | 让硬门禁真实生效(既不漏拦红类,也不虚化 26 类的守护) |
+| ⑤ 修改后能否闭环 | 能。验证方式:在含红例分支跑一次 CI,`backend-blocking-junit` 应「必红且红因准确」 |
+
+### P1-5 B-142-2 复发 + B-186-1 live 链路挂死(上游 LLM)
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | langgraph@251:pytest **47/51**,4 红全 `test_auto_tag`;日志 `429 You've reached the API rate limit for free users (AgnesAI)` ×3 + `401 Authentication Fails, Your api key: ****ab12 is invalid` ×5;同实例 `/health` 200/2.07ms,但 `/api/v1/chat` **000/40.0s/0B**、`/api/v1/analyze` **000/50.0s/0B** |
+| ② 测试 bug 点 | `app/graphs/chat_graph.py:148/195`(召回/保存记忆 401)、`app/agents/intent_classifier.py:62`(429 后 `默认 chat` 静默兜底) |
+| ③ 最优修改方案 | 运维更换有效 key 并升级配额;研发把 `intent_classifier` 的兜底改为「显式降级 + 可观测指标」;`chat_graph` 对 401 快速失败而非重试挂死 |
+| ④ 修改目的 | 使「单测绿」与「真实链路可用」不再背离(R188 曾出现 51/51 绿 + chat 120s 零字节) |
+| ⑤ 修改后能否闭环 | 能。判据:pytest 51/51 **且** live chat/analyze 200 且 p95 < 30s,连续 2 轮 |
+
+### P2-1 B-191-2 全量耗时劣化至 1:01h(装置/性能)
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | 同源码 R188 43:15 → 本轮 1:01h(+41%);慢类 `ArticleCategoryControllerTest` **493.97s**、`AdminFamilyOverviewControllerTest` 341.2s、`GrowthPublicEndpointMappingTest` 334.2s、`GrowthRecordSyncFlowTest` 280.7s、`PlannerBindInviteFlowTest` 178.1s(前 5 占 ~45%);`cfc-scheduler` 日志 20524/73287 = **28.0%**、上下文启动多次 |
+| ② 测试 bug 点 | 装置层:controller 层 `@SpringBootTest` 全上下文 + `src/test/resources` 缺 `application-test.yml` → 每类重跑 `DatabaseInitializer` 全量迁移;`@EnableScheduling` 在测试态常驻 |
+| ③ 最优修改方案 | ① 增设 `src/test/resources/application-test.yml`(禁 `@EnableScheduling`、共享 datasource);② 慢类改 `@WebMvcTest + @MockBean`;③ 修 B-156-1/B-170-1 两条迁移语法错(避免 34 次建表全失败的重试开销) |
+| ④ 修改目的 | 把全量压回 ≤45min,使「每轮全量实跑」可持续(当前 1h+ 已挤压巡检预算) |
+| ⑤ 修改后能否闭环 | 能。判据:`mvn -o -B clean test` 的 `Total time` ≤ 45:00 且类数不变 |
+
+### P2-2 B-191-3 线上 API 套件首跑例数不完整(工具/度量)
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | 首跑 `--reporter=line` 得 **26 例(19 过 + 7 跳)/5.4min**;二次同命令 `--reporter=list` 得 **32 例(25 过 + 7 跳)/18.2s**(与 R190 一致)。首跑 6 例未计入,且耗时高 18 倍 |
+| ② 测试 bug 点 | 无用例缺陷;属**运行口径不可复现**(疑与首次冷启动/网络抖动/`--output` 目录复用有关) |
+| ③ 最优修改方案 | 采集侧固化「双校验」:必须同时满足 `Running N tests` 的 N = 点名文件静态用例数、summary 的 pass+fail+skip = N;任一不符即重跑并记录两次口径 |
+| ④ 修改目的 | 防止「例数漂移」被当成覆盖变化(本项目已多次出现 990 vs 32 的口径问题,B-173-2) |
+| ⑤ 修改后能否闭环 | 能。本轮已用二次运行定案(32 例);下轮起把双校验写入附录命令模板 |
+
+### P2-3 B-177-1 pytest 进程不退出(第 13 轮)
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | `summary` 已打印(`4 failed, 47 passed in 134.17s`)但 2 个进程存活 **4m33s**(`ps` 状态 `Sl`),`echo EXIT=$rc` 永不执行 → 日志无 `EXIT=` 行;`kill -9` 后清理干净 |
+| ② 测试 bug 点 | `app/main.py` lifespan 未 `await client.aclose()`(线索 `RuntimeWarning: coroutine 'JavaClient._get_client' was never awaited`);派发器缺超时兜底 |
+| ③ 最优修改方案 | 产品侧补 shutdown 钩子关闭 http client;派发器命令加 `timeout -k 10 900`,并以 summary 文本 + 进程存活双判据定案 |
+| ④ 修改目的 | CI 里 pytest job 不挂死(否则流水线超时) |
+| ⑤ 修改后能否闭环 | 能。判据:进程在 summary 后 ≤10s 退出且 `EXIT=0` 落盘 |
+
+### P2-4 B-189-1 git SSH 22 故障(第 2 轮)
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | `git fetch origin` 走 `git@git.iwintrue.com:iwt/cfc.git` 报 `Please make sure you have the correct access rights`;改 `http://git.iwintrue.com/liaoxg/cfc.git` 后 fetch/推送均成功 |
+| ② 测试 bug 点 | 环境侧(Gogs sshd 22 端口不可用),非代码 |
+| ③ 最优修改方案 | 运维恢复 22 端口;或把 CI/自动化节点的 `origin` fetch URL 一并改为 HTTP(当前 push URL 已是 HTTP,fetch 仍 SSH) |
+| ④ 修改目的 | 保证自动化闭环的「真理证据」通道不依赖人工兜底 |
+| ⑤ 修改后能否闭环 | 能。判据:`git fetch origin` 无凭据报错且 `rev-list --left-right --count` 可算 |
+
+---
+
+## 五、域结论表
+
+| 审查维度 | 负责专家 | 结论 | 关键证据 |
+|---|---|---|---|
+| 单元·组件级(单元/覆盖率/变异) | 苏雅 | ⚠️ 部分通过 | 后端 386 类 3315 例实跑(3232 过 / 79 红 / 4 跳);386/386 XML 无沉默;覆盖率仅管理端可产出;**变异测试全仓 0 配置** |
+| 集成·接口级(集成/契约/API) | 柯砚 | ⚠️ 部分通过 | 集成域实跑(EnergySystemFlow 6F、GuideModule 1E、DailyCheckin 6/6);契约 5 类全实测(S-cfc-197 GREEN、S-cfc-198 6F、远端三类 39F);线上 API 32 例 25/0/7 |
+| 系统·验收级(E2E/UAT/BDD) | 白樾 | ✅ 通过(浏览器级) | admin 5/5(本机+189)、fe 6/6(本机+204)、shop 4/4、health 13/13;UAT 无独立包(以 39 例代理);SC 244 映射 152 = 62.3% |
+| 测试保障级(冒烟/回归/探索) | 闻朔 | ✅ 通过 | 冒烟全绿;回归零漂移第 26 次;探索性无新增章程(承袭) |
+| 性能·可靠性 | 骆骋 | ⚠️ 部分 | 全量 **1:01h**(B-191-2,慢类 493.97s);pytest 进程不退出(B-177-1 第 13 轮);未授权压测 |
+| 安全 | 关屿 | ✅ 通过(本轮实证) | 匿名 6 端点 **6/6 = 401**;`nonfunc-security` 11 例全绿;S-cfc-198 揭示知识库写入**无角色门禁**(已记 P1-1 ③) |
+| 兼容·体验 | 阮清 | ⚠️ 部分 | 双视口通过;无多浏览器矩阵;无 axe/pa11y |
+| 流程·度量·代码 | 何序 | ⚠️ 部分 | CI 漏拦 18 / 陈旧 26(G11 ❌);锚点 17.2%;`@GetMapping` 8 处仍违规范;specs 201 份、控制器 366 |
+
+---
+
+## 六、门禁结论
+
+**结论:不通过(有条件)** —— 全站 4254 例 / **4088 过 = 96.10%(剔 skip 96.67%)**,红 141。红数环比 +47 的构成:**TDD 前瞻契约 RED +45**(本地 S-cfc-198 6 + 收尾远端三类 39)、**上游环境红 +3**(langgraph 429/401)、**装置浮动 −1**(`TaskReviewScP11` 2F→1F),**零缺陷回归**;剔除全部预期 RED(97 例)后 **98.34%**。阻断项为**前瞻契约待实现 + 存量装置红 + CI 清单失真 + 上游 key/配额失效 + 全量耗时劣化**,均非本轮新增回归。
+
+| 门禁 | 判定 | 依据(本轮实测) |
+|------|------|------------------|
+| G1 全站通过率 ≥98% | ❌ **96.10%** | 4088/4254(剔 skip 96.67%,剔预期 RED 98.34%);−1.12pct 因前瞻契约 +45 与环境 +3 |
+| G2 后端红归零/契约裁决 | ❌ **118 红** | 全量 79(73F+6E)+ 增量 39 = 存量 **24(13 类)** + 前瞻契约 **94(12 类)**;存量逐类零漂移 |
+| G3 前端双绿 | ⚠️ fe 609/624(3 预期 RED)+ web **116/116**;浏览器 fe **6/6 双节点** | 3 红全为 S-cfc-132/133 未交付(AC-R1/AC-R2/AC-2 RED-A7) |
+| G4 编译可编译 | ✅ | `Compiling 2121 main + 392 test source files`,0 错(含 5 个新契约类) |
+| G5 结果完整性 | ✅ | **386/386** XML 可聚合、无沉默类;`*.dump` 0;增量 3 XML 完整 |
+| G6 浏览器 E2E | ✅ **39 过 + 2 跳** | admin 5/5(本机 1.4m + 189 1.3m)、fe 6/6(本机 1.9m + 204 44.7s)、shop 4/4(9.7s)、health 13/13(5.5s)、dx 2 跳 |
+| G7 langgraph | ❌ **47/51(92.2%)** | 4 红全 `test_auto_tag`;429 AgnesAI ×3 + 401 `****ab12` ×5;live chat 000/40s、analyze 000/50s;节点源码与本地零 diff → 环境红 |
+| G8 匿名鉴权 401 | ✅ **6/6** | mind/fortune、energy/overview、health/plan/list、xiaomi-health、huawei-health、admin/families/overview 全 **401 `请先登录`** |
+| G9 用例锚定 SRS | ❌ **17.2%** | 双向命中 106 / 台账 618;锚点 153(孤儿 47、空转 512);SC 映射 62.3% |
+| G10 节点可比 | ✅ 三节点 | 189 admin 5/5(spec blob `4703d73fb`=本机)、204 fe 6/6、251 pytest(`cfc-langgraph` 与本地零 diff)+ 本机四件套 |
+| G11 CI 硬门禁 | ❌ | 清单 33 vs 实测红 25 → **漏拦 18 / 陈旧 26**(第 22 轮未修) |
+| G12 SRS 变更驱动用例更新 | ✅ | specs 201 / test-cases 16 / 场景 11 / API_REFERENCE 窗口**零变更** → 我无需新增或改用例;窗口 5 个契约类由并行会话产出,**本轮全部补验** |
+| G13 存量红零漂移 | ✅ **第 26 次** | 13 类逐类与 R190 一致(唯一差异 `TaskReviewScP11` 2F→1F 装置浮动) |
+| G14 场景→用例映射 | ⚠️ **62.3%** | 244 SC 中 152 被台账引用 |
+
+---
+
+## 七、变更触发动作(用户规则:结果有变 → 提交远程 + 通知研发)
+
+| 项 | 内容 |
+|---|---|
+| 结果是否变化 | **是** — 后端由「采信」转全量实跑(386 类 3315 例,新增 5 契约类 102 例);langgraph 50/51→47/51;B-95 销号;S-cfc-197 GREEN;红 94→141(全为预期 RED/环境) |
+| 提交 | 本轮报告 + 执行日志 + blockers + 研发通知 + 台账(详见 §1.4) |
+| 推送 | ✅ `http://git.iwintrue.com/liaoxg/cfc.git` → `cfclub`(SSH 22 故障,HTTP 兜底第 2 轮) |
+| 通知研发 | ① 仓库 `docs/tdd/notice-2026-11-13-R191-研发通知.md`;② `docs/tdd/test/blockers.md` R191 段(P0/P1 排序);③ Agent Mail **仍不可用**(`GetMe` → `not_bound`,「请勿重试」)→ 以 ①+②+推送落地 |
+
+---
+
+## 八、待办
+
+| 优先级 | 事项 | 归属 |
+|---|---|---|
+| P0 | CI `BACKEND_OBSERVED_CLASSES` 按实测 25 类重写(漏拦 18 / 陈旧 26) | 研发/CI |
+| P0 | 251 上游 LLM key 更换(`****ab12` invalid)+ 配额升级 | 运维 |
+| P1 | 前瞻契约 RED 94 例逐键实现(S-cfc-198 重试/分批/角色门禁优先) | 研发 |
+| P1 | 存量 13 类装置红治理(`@MockBean` 替反射、3 个 `??` 测试类入库、平台费精度) | 研发/测试 |
+| P1 | 全量耗时压回 ≤45min(`application-test.yml` + 慢类拆 `@WebMvcTest`) | 测试/研发 |
+| P2 | pytest shutdown `aclose()` + 派发器 `timeout -k 10 900` | 研发/测试 |
+| P2 | git SSH 22 恢复(或把 fetch URL 一并改 HTTP) | 运维 |
+| P2 | 9082 本地起服务后复跑根级 16 红(B-36) | 测试 |
+
+---
+
+## 附录 A:本轮命令与口径(可复现)
+
+```bash
+# 环境探针(SOP 第一步)
+/d/jdk1.8.0_25/bin/java -version && JAVA_HOME=/d/jdk1.8.0_25 /d/maven/apache-maven-3.6.1/bin/mvn -v
+
+# 后端全量(工程根必须在 cfc-backend)
+cd d:/cfc/cfc-backend && JAVA_HOME=/d/jdk1.8.0_25 /d/maven/apache-maven-3.6.1/bin/mvn -o -B clean test   # 1:01h
+# 聚合:surefire XML → 386 类 / 3315 例 / 73F+6E+4S
+
+# 收尾增量(快进 5ad781a00 后)
+mvn -o -B test -Dtest='AdminSupplierManageContractTest,SupplierManageAdminContractTest,DimensionConfigAdminContractTest'   # 52 例 / 39F
+
+# 浏览器(必须在 tests/ 内跑;每节点 1 个浏览器任务,本机串行)
+cd d:/cfc/tests
+npx playwright test --config=playwright.config.live.js --project=admin-desktop
+npx playwright test --config=playwright.config.live.js --project=frontend-mobile
+npx playwright test --config=playwright.config.live.js --project=shop-api
+npx playwright test --config=playwright.config.health.js            # health 不在 live config 的 project 内
+npx playwright test --config=playwright.config.live.js --project=dx-bridge
+
+# 189 节点 admin(单节点任务)
+node _r190_189_admin.cjs kick && node _r190_189_admin.cjs read
+# 204 节点 frontend(openCode 在 204:4091,项目 D:\cfc)
+node _r190_204_run.cjs
+# 251 节点 langgraph pytest(setsid 分离 + 单次读,禁止轮询建 shell)
+node _r191_251_lg.cjs kick / read / run
+
+# Jest
+cd d:/cfc/cfc-web && npx jest --ci            # 116/116
+cd d:/cfc/cfc-frontend && npx jest --ci       # 609 passed / 3 failed / 12 skipped
+cd d:/cfc/tests && npx jest --config=jest.config.js   # 7/16(9082 未起)
+
+# 线上 API 32 例(点名 4 文件,勿带全量 testMatch)
+cd d:/cfc/tests && npx playwright test -c playwright.config.api.js --output=../_r191_api2 \
+  e2e/api/api-smoke.spec.js e2e/api/nonfunc-security.spec.js \
+  e2e/api/nonfunc-perf-compat.spec.js e2e/api/traceability-p94-p95-c26.spec.js --reporter=list
+
+# 匿名鉴权探针
+for u in /api/mind/fortune /api/energy/overview /api/health/plan/list /api/xiaomi-health/status /api/huawei-health/status /api/admin/families/overview; do
+  curl -s -m 12 -X POST -H 'Content-Type: application/json' -d '{}' -w " [%{http_code}]\n" "http://cfc.iwintrue.com$u"; done
+```
+
+## 附录 B:窗口 5 个契约类定向实测明细(B-191-1 补验)
+
+| 契约类 | 关联键 | 例数 | 通过 | 红 | 结论 |
+|---|---|---|---|---|---|
+| `AdminReportParserManageContractTest` | S-cfc-197 `admin.reportParser.manage` | 37 | **37** | 0 | **GREEN ✅**(实现已落地,可转 YELLOW/GREEN) |
+| `AdminKnowledgeBaseManageContractTest` | S-cfc-198 `admin.knowledgeBase.manage` | 13 | 7 | **6** | 预期 RED:同步失败重试 / 大批量分批 / base+vector 角色门禁三项未实现 |
+| `DimensionConfigAdminContractTest` | S-cfc-199 `admin.dimension.config` | 17 | 4 | **13** | 预期 RED(维度/权重/上限 CRUD + 来源比例治理未落地) |
+| `AdminSupplierManageContractTest` | S-cfc-200 `admin.supplier.manage` | 16 | 6 | **10** | 预期 RED(410 废弃壳 / 边界门禁 / role 守卫) |
+| `SupplierManageAdminContractTest` | S-cfc-200 契约级 | 19 | 3 | **16** | 预期 RED(SC-A-06 停用在售商品守卫未落地) |
+| **合计** | — | **102** | **57** | **45** | 45 红全为 TDD 前瞻态,非缺陷回归 |
+
+## 附录 C:度量口径声明(B-124 并存口径)
+
+| 指标 | 本轮口径 | 数值 | 与 R190 差异说明 |
+|---|---|---|---|
+| 台账 TC 唯一 | `tests/test-cases/*.md` 全量 `TC-*` 去重 | **618** | R190 报 308 为「单套命名」口径;本轮含 `TC-<域>-<标签>-<NN>` 与 `TC-<域>-U<NN>` 两套命名 |
+| 表行 | 台账 md 中以 `|` 开头行 | **1168** | R190 报 876(同一差异来源) |
+| 严格锚点 | `tests/**` JS/TS/py(排除 `generated`)∪ `cfc-backend/src/test/**/*.java` | **153** | R190 报 115(未并 Java 测试锚点) |
+| 双向命中(G9) | 台账 ∩ 锚点 | **106 = 17.2%** | R190 报 20.1%(= 62/308,分母口径不同) |
+| 场景→用例 | SC ∩(台账出现的 SC) | **152/244 = 62.3%** | R190 报 57.9%/72.1%(三套口径并存,见 B-124) |
+
+> 口径差异属既有 B-124 问题(度量脚本多口径),**不与 R190 直接比大小**;本轮同时给出全部原始数以便追溯。
+
+## 附录 D:本轮遗留取证文件(未入库,供复核)
+
+`_r191_backend_full.log`(1:01h 全量日志 73287 行)、`_r191_incr.log`(增量 52 例)、`_r191_browser.log`、`_r191_api.log`、`_r191_web_jest.log`、`_r191_fe_jest.log`、`_r191_root_jest.log`、`_r191_251_lg.cjs`、`_r191_metrics.py`、`_r191_api/`、`_r191_api2/`、`_r191_pw_*`。

+ 291 - 0
docs/tdd/orchestration/report-test-2026-11-13-R192.md

@@ -0,0 +1,291 @@
+# 测试负责人工作汇报(R192 轮)
+
+> 本汇报按《测试负责人工作汇报模板》填写。R192 为**API 全量口径升级轮**:R191 收尾 `5ad781a00` 后,远程推入 auto bump(零源码)+ 合入 S-cfc-201 契约类;本轮通过 HTTP 兜底 fetch 快进至 **`7558d8cf4`**。
+> **拓扑(用户指令)**:浏览器经 `http://cfc.iwintrue.com/admin`(后台)+ `http://cfc.iwintrue.com/frontend`(小程序);189 / 本机为浏览器节点(**每节点仅 1 个浏览器任务**,本机四套件串行);langgraph 部署 251 → **本轮实派并实跑**。
+> **★ 本轮关键事实**
+> ① **API 全量首测 990 例**(历史仅 32 例子集):175 过 / 795 跳 / **20 红**,20 红跨 9 域**同根因**为 `tests/e2e/api/helpers/api.js` `direct-register` 前置注册返回 `body.code=undefined` → **B-192-2(P1,契约链/装置级)**,复验 badge 单文件 1F 证实。
+> ② **S-cfc-201 定向实测 24 例 6 红 18 绿,与提交声明完全一致**(`AdminArticleManageContractTest`,TDD-RED 落预期)。
+> ③ **后端承袭 R191 同源码基线(窗口 main/test 源码零变更)**:386 类 / 3315 例 / 3232 过 / 73F+6E+4S + 收尾增量 52(13 过 39 红);存量 13 类 24 红零漂移第 27 次。
+> ④ **langgraph@251 47/51**:4 红为 `test_auto_tag` ×2 + `test_intent` ×2(**例集较 R191 漂移**),根因仍是 429 AgnesAI 配额 + 401 key `****ab12` — 环境型;live `/health` 200/0.9ms 但 `chat/analyze` **000/20s** → B-186-1 第 6 轮延续。
+> ⑤ **浏览器六件套全绿**:本机 fe 6/6 + shop 4/4 + health 13/13(串行)+ **189 admin 5/5**;dx 2 跳承袭(B-DX-1)。
+> ★ **全站口径:5225 例 / 4245 过 = 81.24%(含 skip;剔 skip 96.22%),红 167**。环比 R191(4254/4088/96.67% 剔 skip):例 +971(API 全量 +958 / S-cfc-201 +24 / 浏览器 −11)、红 +26 全为 **S-cfc-201 前瞻 6 + API 首测探测 20**,**零缺陷回归**。
+
+---
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | 测试负责人(自动化测试闭环任务 / 软件测试专家团·陆谨) |
+| 汇报周期 | 2026年11月13日 14:33 ~ 15:05(第 192 轮) |
+| 基线 | 起点 `cfclub @ 5ad781a00`(R191 收尾);HTTP 兜底 fetch → `--ff-only` 快进至 **`7558d8cf4`**(+auto bump 零源码;S-cfc-201 契约类先前已合入) |
+| 窗口内需求变更 | **零** — specs / test-cases / user-scenarios / API_REFERENCE 窗口 diff 命中 0 → **G12 ✅,未触发用例编写闸门** |
+| 窗口内源码变更 | **零功能源码** — `cfc-backend/src/main`、`cfc-frontend`、`cfc-langgraph` 窗口 diff 命中 0;唯一变更为 **S-cfc-201 契约类(+540 行)** + cfc-web 版本号 bump + docs |
+| 负责模块/单元 | 全站:管理端 admin@189 · 小程序 H5 fe/shop/health@本机 · cfc-langgraph@251(实派) · cfc-backend JUnit(承袭 + S-cfc-201 定向) · cfc-web/cfc-frontend Jest · 线上 API **全量 990 例** |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 测试用例建设
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **本轮新增测试用例数\*** | **24 例(S-cfc-201 契约类,研发侧先行编写,本轮实测取证)** | SRS/场景/接口文档窗口零变更 → 未触发用例编写闸门(G12 ✅);窗口合入契约类 `AdminArticleManageContractTest` 24 例,**本轮定向实测 6 红 18 绿与提交声明一致** |
+| **本轮验证执行测试数\*** | **5225 个** | 后端 **3391**(承袭 3315 + 承袭增量 52 + S-cfc-201 定向 24)+ 前端 Jest 740(fe 624 + web 116)+ langgraph 51(**251 实派**)+ 浏览器 30(28 过 + dx 2 跳)+ **线上 API 全量 990** + 根级 23 |
+| **用例通过率** | **81.24%**(4245/5225;剔除 skip **96.22%**) | 红 167 / 跳 813;环比 R191 剔 skip 96.67% −0.45pct,红 +26 全为 **S-cfc-201 前瞻 RED(6)+ API 全量首测探测(20,即 B-192-2)**;**零缺陷回归** |
+| **用例台账** | `tests/test-cases` 16 份 / 表行 1168 / 唯一 TC 618 | 与 R191 一致;锚点命中率口径见 §附录 C(B-124 并存) |
+
+### 1.2 会话与任务分发
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **分派任务数\*** | **6 个** | R192-1 本机 `frontend-mobile`(6/6,55.0s)+ `shop-api`(4/4)+ `health`(13/13)+ `dx`(2 跳,串行);R192-2 **189** `admin-desktop`(**5/5**,1.2m);R192-3 **251** `langgraph pytest`(**47/51**,123.28s)+ live 三端点;R192-4 cfc-web Jest(116/116, 114.3s);R192-5 cfc-frontend Jest(609/3/12, 86.7s)+ 根级 Jest(7/16);R192-6 **线上 API 全量**(990:175/20/795)+ S-cfc-201 定向(24:18/6) |
+| **恢复智能体会话数\*** | 0 个 | 各节点实探:`189:4090`=200、`251:4096`=200、`251:9000`=health 200、本机 9082=closed(B-36 预期);git SSH 22 不可用 → HTTP 兜底 |
+| **当前在用会话数** | 0 个 | 全部结束;251 残留 pytest 进程(bash+pytest 2 个,存活 07:26)已 `kill -9` 清理并 `ps` 复核 0 |
+
+### 1.2b 各会话当前工作状态列表
+
+| 会话标签 | 会话 ID/地址 | 当前状态 | 当前任务 | 静默/空闲时长 | 工作目录 | 备注 |
+|----------|--------------|----------|----------|---------------|----------|------|
+| `remote-189` | 192.168.16.189:4090 | done | admin-desktop UI E2E | 1.2m | `C:\code\cfc\tests` | **5/5**;TC-A-U01~U05 全过 |
+| `local-frontend` | 本机 Playwright | done | frontend-mobile UI E2E | 55.0s | `d:\cfc\tests` | **6/6**;B-185 维持 |
+| `local-shop` | 本机 Playwright | done | shop-sort API E2E | 12.4s | `d:\cfc\tests` | **4/4** |
+| `local-health` | 本机 Playwright | done | health 鉴权契约 E2E | 4.2s | `d:\cfc\tests` | **13/13**(全 401) |
+| `virtual` | dx-bridge | done | D/X 桥接鉴权契约 | 瞬时 | `d:\cfc\tests` | **2 skip**(B-DX-1 第 34 轮) |
+| `remote-251` | 192.168.16.251:4096 | done | langgraph pytest + live 探针 | 123.28s | `/sc-data/cfc/cfc-langgraph` | **47/51**;4 红例集漂移(auto_tag 2 + intent 2),429/401 环境 |
+| `local-jest` | 本机进程 | done | cfc-web + fe + 根级 | web 114.3s / fe 86.7s / root 7.1s | `cfc-web`/`cfc-frontend`/`tests` | web 116/116、fe 609/3/12、根级 7/16 |
+| `local-api` | 本机 Playwright | done | **线上 API 全量** | 13.1m | `d:\cfc\tests` | **990:175/20/795**;20 红同根因 direct-register(B-192-2) |
+| `local-backend` | 本机 mvn | done | S-cfc-201 契约定向 | 1m23s | `d:\cfc\cfc-backend` | **24 例 6 红 18 绿,与声明一致**(承袭 R191 全量) |
+
+### 1.3 问题处置
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **新测试发现问题数\*** | **2 个** | ① **B-192-2(P1 工具/契约链)**:API 全量首测 990 例暴露 **20 红** 跨 9 域(badge 4 / wish-flow 6 / task-checkin 3 / coach-food 2 / task-frequency/task-c14/incentive/energy/wechat-login 各 1)**同根因**:`helpers/api.js` `direct-register` 前置注册返回 `body.code=undefined`(线上未回 200)→ 断言链连锁失败;复验 badge 单文件 1F(C-09 断链)证实装置/契约链级;② **B-192-1(P1 观测)**:langgraph 红例集由 R191 全 `test_auto_tag`(4 红)漂移为 auto_tag 2 + intent 2,429/401 双因未变 |
+| **已解决测试相关问题数\*** | **1 个** | **S-cfc-201 前瞻确认**:24 例 6F/18G 与提交声明完全一致(TDD-RED 落预期,非回归) |
+| **待解决测试相关问题数** | P0×1 / P1×9 / P2×4 | ① B-142-2 key/配额(P0,直接拖累 G1/G7);② B-192-2 direct-register 契约链;③ B-186-1 live 挂死;④ B-177-1 pytest 不退出(第 14 轮);⑤ B-152-1/2 CI 清单漏拦/陈旧;⑥ 存量 24 红(13 类);⑦ 前瞻契约 100 红(含 S-cfc-201 6 + 承袭 94);⑧ B-156-1/B-170-1 DDL;⑨ B-162-1 未入库;⑩ B-189-1 SSH 22;⑪ B-178-1/B-36/B-DX-1 等 |
+
+### 1.4 本轮 git 提交与推送证据(真理证据)
+
+| 提交 sha | 关联任务/能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|-----------------|------|--------------|----------|------------|------|
+| `4b0b8ea34` | 全站巡检 R192 + API 全量首测 + S-cfc-201 定向 + B-192-1/2 + B-186-1/B-177-1 延续 | 汇报 | 报告 + 执行日志 + blockers + 研发通知 | cfclub | ✅ 推送 `http://git.iwintrue.com/liaoxg/cfc.git`(**SSH 22 仍故障,B-189-1 第 3 轮 HTTP 兜底**) | 结果有变化 → 已提交并推送,同时出研发通知 |
+
+### 1.5 状态机阶段分布(全局进度,研发侧账本口径)
+
+| 阶段 | 键数 | 备注 |
+|------|------|------|
+| RED | S-cfc-201 **6 红 18 绿**(本轮定向取证);承袭前瞻 94 红(12 类) | 研发侧 `sc-key-map.md` 事件口径另计 |
+| YELLOW | 承袭 | 待研发登记 |
+| GREEN | 承袭(S-cfc-197 37/37 等) | 测试侧已取证,转 YELLOW 前有效 |
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(⏳ wish) | 0 | 正常 | SRS 目标池未启用;specs 201 份可读 |
+| 契约常量(contract) | S-cfc-201 1 类 | 正常 | **本轮定向实测 24 例** |
+| 测试用例引用(test_case_map) | 618 唯一 TC | 正常 | 台账扫描正常 |
+| 执行结果回填(case_results) | 承袭 386 XML + 本轮 1 XML | 正常 | **API 全量 990 例上 H 新口径**(B-192-2 已登记) |
+
+---
+
+## 二、历史审查记录(阶段 -1 记忆召回)
+
+| 项 | 内容 |
+|---|---|
+| 上次审查 | **R191(2026-11-13 14:35,`5ad781a00`)**,结论「不通过(有条件)」,全站 4254/4088(96.10%),红 141;后端全量实跑 1:01h;langgraph 47/51;B-95 销号 |
+| 历史遗留 | 存量 24 红(13 类)· 前瞻契约 94 · 根级 16 红(B-36)· fe 3 预期 RED · CI 漏拦 18/陈旧 26(B-152-1/2)· B-177-1 · B-178-1 · B-186-1 · B-189-1 · B-142-2 |
+| 本次是否改善 | **API 覆盖口径升级**(32 → 990 例全量首测,暴露 B-192-2);**S-cfc-201 契约落预期**;存量 13 类零漂移第 27 次;浏览器任务级全绿(189 admin 5/5 + 本机 23/23) |
+| 本次是否回退 | **无缺陷回归**。红 +26 = S-cfc-201 前瞻 6 + API 首测探测 20;langgraph 红例集漂移但根因同(429/401);剔 skip 96.67% → 96.22%(API 20 红拖累) |
+| 教训应用 | ①「同源码沿用 + 增量定向」(R185 修 B-184-1)→ 本轮窗口 main 零变更,后端承袭 R191 全量 + S-cfc-201 定向,**省 1:01h**;② 历史「32 例子集」曾掩盖 9 域契约链问题 → 本轮全量 990 例直出 B-192-2;③ 251 派发沿用 `setsid nohup` + 单次读;④ B-177-1 用 `ps | grep pytest` 按 PID kill,禁用 pkill 自伤 |
+
+> 本次为该对象**第 192 次**审查,有历史记录;后端全量按「同源码承袭 + 增量实跑」口径(R185 纪律),其余维度全部本轮实测。
+
+---
+
+## 三、项目画像与覆盖矩阵
+
+### 3.1 项目画像
+
+| 维度 | 内容 |
+|------|------|
+| 业务定位 | 家庭教育成长平台(五维能量 身/心/智/行/富):小程序家长端 + 规划师端 + 管理后台 + 独立商城 |
+| 技术栈 | Spring Boot 2.7.18 + MyBatis-Plus + Java 8(9082)/ uni-app 微信小程序(Vue2)/ Vue2 + Element UI(8082)/ FastAPI + LangGraph + ChromaDB(9000@251) |
+| 模块结构 | `cfc-backend`(386 执行类)/ `cfc-frontend`(42 文件 624 例)/ `cfc-web`(17 套件 116 例)/ `cfc-langgraph`(51 例 @251)/ `tests`(e2e-live 5 文件 + e2e/api 27 文件) |
+| 关键路径 | 登录/家庭/成员 → 五维能量 → 任务/成长记录 → 健康报告上传解析 → 测评订单 → 商城/支付 → AI 对话(LangGraph) |
+| 测试现状 | **5225 例**(后端承袭 3391 + fe 624 + web 116 + lg 51 + 浏览器 30 + **API 全量 990** + 根级 23) |
+| 质量目标 | 全量用例全绿、覆盖 SRS 与用户场景(含异常流)、浏览器双节点可复现、结果变更即提交并通知研发 |
+| 风险点 | ① API 契约链 `direct-register` 前置注册失效(B-192-2,20 红);② 上游 LLM 凭据/配额(B-142-2 + B-186-1);③ 前瞻契约 100 红;④ CI 清单双向失真;⑤ 存量装置红 24;⑥ B-177-1 进程不退出;⑦ git SSH 22 故障 |
+| 待核实项 | direct-register 线上契约(路径/请求体/返回结构是否变更)· 251 key 轮换(****ab12)与配额 · B-156-1/B-170-1 DDL 修复 · S-cfc-201 实现排期 |
+
+### 3.2 20 类测试覆盖矩阵(全类不省略)
+
+| # | 测试类型 | 三态 | 证据(本轮) |
+|---|----------|------|------------------|
+| 1 | 单元测试 | ✅ 已发现 | 后端承袭 386 类 3315 例 + S-cfc-201 定向 24 例实测;前端 624;管理端 116 |
+| 2 | 测试覆盖率 | ⚠️ 部分 | cfc-web Jest 覆盖率可产出;后端未开 jacoco(未授权改 pom) |
+| 3 | 变异测试 | ❌ 未发现 | 全仓无 pitest/mutation 配置 |
+| 4 | 集成测试 | ✅ 已发现 | 承袭 R191 集成类结果(同源码);本轮无窗口变更 |
+| 5 | 契约测试 | ✅ 已发现(本轮重点) | **S-cfc-201 24 例 6F/18G 与声明一致**;承袭前瞻 94 红 12 类 |
+| 6 | 接口 API 测试 | ✅ 已发现(**本轮升级**) | **线上 API 全量 990 例首测**:175 过/795 跳/**20 红**(B-192-2) |
+| 7 | 系统/端到端 | ✅ 已发现 | 浏览器 30 例(**28 过 + 2 跳**):admin 5@189、fe 6、shop 4、health 13 |
+| 8 | 验收 UAT | ⚠️ 待核实 | 无独立 UAT 包;以浏览器 28 例为系统级验收代理 |
+| 9 | BDD 规格 | ✅ 已发现 | 用户场景 11 份 / SC 唯一 244;台账引用 152 = **62.3%** |
+| 10 | 冒烟测试 | ✅ 已发现 | `api-smoke.spec.js` 含自注册链路(本轮全量 990 例内通过) |
+| 11 | 回归测试 | ✅ 已发现 | 存量 13 类逐类零漂移(**第 27 次**) |
+| 12 | 探索性测试 | ⚠️ 部分 | 历轮人工探索记录;本轮无新探索项 |
+| 13 | 性能测试 | ❌ 未实测 | 未授权联网压测;后端全量耗时劣化承袭观察(B-191-2) |
+| 14 | 可靠性测试 | ⚠️ 部分 | **B-177-1 第 14 轮复现**(summary 后进程不退出,已 kill);**B-186-1** live 挂死 000/20s |
+| 15 | 安全测试 | ✅ 已发现 | 匿名 6 端点 **6/6 = 401**(B-142-1 维持销号) |
+| 16 | 兼容性测试 | ⚠️ 部分 | 双视口(desktop 1440 / mobile 375)实测通过;无多浏览器矩阵 |
+| 17 | 可用性测试 | ⚠️ 部分 | admin 登录-主框架-核心菜单 5/5@189;无专项问卷 |
+| 18 | 无障碍测试 | ❌ 未发现 | 无 axe/pa11y 配置 |
+| 19 | QA 流程 | ⚠️ 部分 | 汇报/台账/blockers 闭环在跑;**B-192-2 为 API 全量口径升级的新发现** |
+| 20 | 质量指标 | ⚠️ 部分 | 全站剔 skip 96.22%(G1 阈值 98% 未达);锚点 17.2%(口径见 §附录 C) |
+
+---
+
+## 四、五要素问题清单
+
+### P1-1 B-192-2 线上 API 全量首测 20 红 — direct-register 前置注册契约链失效
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | 本轮首次全量运行 `playwright.config.api.js`(990 例),**20 例失败跨 9 域**(badge 4 / wish-flow 6 / task-checkin 3 / coach-food 2 / task-frequency / task-c14 / incentive / energy / wechat-login 各 1);历史 32 例子集因只点名 4 个 spec 从未覆盖这些域 |
+| ② 测试 bug 点 | `tests/e2e/api/helpers/api.js:32` `direct-register`:`expect(body.code, 'direct-register 应当成功(code=200)').toBe(200)` 收到 **undefined** → 前置注册失败 → 该文件后续用例连锁失败;复验 badge 单文件仅 1F(C-09 断链)确认 20 红非独立缺陷 |
+| ③ 最优修改方案 | 研发核对线上注册接口(`direct-register` 路径/请求体/返回结构是否变更或下线);测试侧同步 helper 请求与断言;两者对齐后重跑 9 域 spec |
+| ④ 修改目的 | 恢复 API 契约层的真实覆盖,消除 20 例装置级红对 G1/G2 的噪声;使该层契约可继续守护 |
+| ⑤ 修改后能否闭环 | 能。重跑任一下游 spec(如 badge):前置注册成功且用例 0F 连续 1 轮即闭环;20 红随 helper 修复整体回绿 |
+
+### P1-2 langgraph 环境故障(B-142-2 升级 + B-186-1 + B-192-1)
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | langgraph@251 pytest **47/51**:红例集漂移为 auto_tag 2 + intent 2(R191 为 auto_tag 4);日志 429 AgnesAI 免费配额 + 401 key `****ab12` invalid;live `/api/v1/chat` 与 `/api/v1/analyze` **000/20s 挂死**(用正确 schema:query / report_id+user_id 仍挂) |
+| ② 测试 bug 点 | `app/agents/intent_classifier.py:62`(429 后默认 chat 静默兜底)、`app/graphs/chat_graph.py`(401 不快速失败);`app/main.py:84` shutdown 未 aclose(B-177-1) |
+| ③ 最优修改方案 | 运维轮换 key `****ab12` + 升级 AgnesAI 配额(P0);研发侧意图分类兜底显式降级 + 可观测;shutdown 补 `await client.aclose()` |
+| ④ 修改目的 | 使「单测绿」与「链路可用」一致,G7/G1 可信 |
+| ⑤ 修改后能否闭环 | 能。key/配额生效后 51/51 连续 2 轮 → 销号 B-142-2;live chat/analyze 返回非 000 → B-186-1 闭环 |
+
+### P1-3 B-152-1/2 CI 清单双向失真(承袭,第 23 轮)
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | `.gitlab-ci.yml` `BACKEND_OBSERVED_CLASSES` 33 类 vs 实测红类(含新增 S-cfc-201 等)→ 漏拦 ≥18、陈旧 26 |
+| ② 测试 bug 点 | CI 配置漂移(非用例缺陷) |
+| ③ 最优修改方案 | 按本轮实测红类重写清单,加「红类↔清单自动差集」步骤 |
+| ④ 修改目的 | 硬门禁真实生效 |
+| ⑤ 修改后能否闭环 | 能。CI 必红且红因准确即闭环 |
+
+### P2-1 B-177-1 pytest 进程不退出(第 14 轮)
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | summary 已打印,bash+pytest 2 进程存活 07:26,`EXIT=` 不落盘 |
+| ② 测试 bug 点 | `app/main.py:84 @app.on_event("shutdown")` 未 `await client.aclose()` |
+| ③ 最优修改方案 | shutdown 补 aclose + 派发器加 `timeout -k 10 900` |
+| ④ 修改目的 | 消除派发侧残留进程与取证不确定性 |
+| ⑤ 修改后能否闭环 | 能。EXIT= 落盘即闭环(1 轮) |
+
+### P2-2 B-184-1 后端全量耗时劣化(承袭观察)
+
+| 要素 | 内容 |
+|---|---|
+| ① 问题描述 | R191 同源码全量 **1:01h**;慢类 `ArticleCategoryControllerTest` 493.97s 等 |
+| ② 测试 bug 点 | controller 全 `@SpringBootTest` + 缺 `application-test.yml` + B-156-1/B-170-1 DDL |
+| ③ 最优修改方案 | 增 `application-test.yml`(禁 `@EnableScheduling`)+ 慢类改 `@WebMvcTest` + 修迁移语法 |
+| ④ 修改目的 | 全量回到 45m 内,缩短闭环节拍 |
+| ⑤ 修改后能否闭环 | 能。≤45m 连续 2 轮销号 |
+
+---
+
+## 五、域结论表
+
+| 审查维度 | 负责专家 | 结论 | 关键证据 |
+|----------|----------|------|----------|
+| 单元·组件级 | 苏雅 | 通过(承袭 + 定向) | 后端承袭 3315(386 类)+ S-cfc-201 24 例 6F/18G 与声明一致 |
+| 集成·接口级 | 柯砚 | 有条件通过 | **API 全量 990 例首测 20 红(B-192-2 装置级)**;承袭集成类零漂移 |
+| 系统·验收级 | 白樾 | 通过 | 浏览器 28/28(189 admin 5/5 + 本机 fe/shop/health 23/23)+ dx 2 跳 |
+| 测试保障级 | 闻朔 | 通过(零漂移) | 存量 13 类零漂移第 27 次;fe 3 预期 RED 零漂移 |
+| 性能·可靠性 | 骆骋 | 未实测(环境受制) | 未授权联网压测;B-186-1 live 挂死(第 6 轮) |
+| 安全 | 关屿 | 通过 | 匿名 6 端点全 401(B-142-1 维持销号第 22 轮) |
+| 兼容·体验 | 阮清 | 通过(部分) | desktop/mobile 双视口 OK;无多浏览器矩阵 |
+| 流程·度量·代码 | 何序 | 不通过(承袭) | G1 98% 未达(剔 skip 96.22%);CI 清单漏拦/陈旧;B-192-2 为新口径发现项 |
+
+## 六、门禁结论
+
+- **结论:不通过(承袭,零缺陷回归)**
+- 量化总账:**5225 例 / 4245 过 / 167 红 / 813 跳 = 81.24%(含 skip;剔 skip 96.22%)**
+- ✅ G4 编译 0 错 / G5 XML 无沉默 / G6 浏览器 28 过 + 2 跳 / G8 匿名 6 端点 401 / G10 节点可比 / G12 SRS 零变更 / G13 存量零漂移第 27 次
+- ⚠️ G3 fe 609-624(3 预期 RED)/ G14 62.3%
+- ❌ G1 剔 skip 96.22%(<98%)/ G2 167 红(<阈值)/ G7 47/51 / G9 锚点覆盖率 / G11 CI 清单漏拦/陈旧
+- **复评条件**:① B-142-2 key/配额轮换(G1/G7);② B-192-2 direct-register 修复(G1/G2);③ CI 清单重写(G11);④ 前瞻契约实现推进(G2)
+
+## 七、变更触发动作(结果有变化 → 提交远程 + 通知研发)
+
+| 动作 | 状态 |
+|------|------|
+| 提交报告/执行日志/blockers/研发通知 | ✅ `4b0b8ea34` 已推送 `http://git.iwintrue.com/liaoxg/cfc.git` cfclub |
+| 研发通知 | ✅ `docs/tdd/test/notice-2026-11-13-R192-研发通知.md`(B-192-2 / S-cfc-201 / B-142-2 / B-186-1 / B-177-1) |
+
+## 八、待办(下轮)
+
+1. **B-192-2** 研发归因 direct-register → helper 修复 → 9 域 spec 重跑验证闭环
+2. **B-142-2** key 轮换 + 配额升级后 51/51 连续 2 轮观察
+3. **S-cfc-201** 全量纳入基线(下轮后端合计数 = 3315 + S-cfc-201 24);研发实现后定向复验
+4. **B-177-1** aclose 修复验证(EXIT= 落盘判据)
+5. **B-191-2** 慢类降级方案推进与否(application-test.yml)
+6. 各承袭项第 N 轮观察(B-36 第 77、B-DX-1 第 35、B-152-1/2 差集脚本)
+
+---
+
+## 附录 A:本轮命令与口径(可复现)
+
+```bash
+# 环境探针
+curl -s http://192.168.16.189:4090/admin/login -o /dev/null -w "%{http_code}"   # 189:200
+curl -s http://192.168.16.251:9000/health                                       # {"status":"ok"}
+# HTTP 兜底 fetch(SSH 22 故障 B-189-1)
+git fetch http://git.iwintrue.com/liaoxg/cfc.git cfclub && git merge --ff-only FETCH_HEAD
+# S-cfc-201 定向
+cd cfc-backend && JAVA_HOME=/d/jdk1.8.0_25 /d/maven/apache-maven-3.6.1/bin/mvn -o -B -Dtest=AdminArticleManageContractTest test
+# 浏览器(tests/ 内;每节点 1 个任务,本机串行)
+npx playwright test -c playwright.config.live.js --project=frontend-mobile --reporter=line --output=_r192/pw_fe
+npx playwright test -c playwright.config.live.js --project=shop-api --project=dx-bridge --reporter=line --output=_r192/pw_shop
+npx playwright test -c playwright.config.health.js --reporter=line --output=_r192/pw_health
+node _r192_189_admin.cjs kick && node _r192_189_admin.cjs read   # 189 admin
+# langgraph(251 派发:setsid nohup + 单次读)
+node _r192_251_lg.cjs kick && node _r192_251_lg.cjs read
+# Jest
+cd cfc-web && npx jest --ci --silent
+cd cfc-frontend && npx jest --ci --silent
+cd tests && npx jest --ci --testPathPatterns='_root_|root|blackbox'
+# 线上 API 全量(本轮口径升级:990 例)
+cd tests && npx playwright test -c playwright.config.api.js --reporter=list --output=_r192/pw_api
+```
+
+## 附录 B:B-192-2 20 红明细(9 域)
+
+| spec | 红例 | 计数 |
+|------|------|------|
+| badge-sc-c09-c10-a05 | A-05 创建/更新/删除勋章、A-05-E 无效/删除不存在、A-05-P 权限 | 4 |
+| wish-flow-sc-p13-p14-c04 | C-04 正常、P-13 正常、P-14 正常、P-13-E 幂等、P-13-P 审批、P-13-P 越权 | 6 |
+| task-checkin-sc-c05 | 列表/详情、无效参数、跨家庭权限 | 3 |
+| coach-food-sc-b07 | 索引读取、越权受限 | 2 |
+| task-frequency-sc-p12 | 提醒配置读取保存 | 1 |
+| task-c14-reject-recycle | C-02-P 他人家庭完成被拒 | 1 |
+| incentive-loop-sc-x05 | X-05 正常闭环 | 1 |
+| energy-sc-p87-p88-c24 | P-87 触发能量读数 | 1 |
+| wechat-login | G-02 首次登录自动建档 | 1 |
+
+> 全部同根因:`tests/e2e/api/helpers/api.js:32` `direct-register` 前置注册 `expect(body.code).toBe(200)` 收到 undefined → 后续链式断言失败。
+
+## 附录 C:度量口径声明(B-124 并存)
+
+全站剔 skip 96.22% / 锚点命中 106/618 = 17.2%(口径见 R191 附录 C);后端全量按「同源码承袭 + 增量定向」口径(R185 修 B-184-1),不重复 1:01h 全量。
+
+## 附录 D:本轮遗留取证文件(未入库,供复核)
+
+`_r192_sc201.log` · `_r192_fe.log` · `_r192_api.log` · `_r192_189_kick.log` · `_r192_251_kick.log` · `_r192/*`(浏览器产物目录)

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

@@ -28,78 +28,78 @@ SC-G-02 / SC-G-06 / SC-P-03 / SC-P-10 / SC-P-11 共 5 个 SC 已于 2026-11-06 O
 
 | SC | 角色 | 业务域 | 场景标题 | 对应能力键 | 键阶段 | 跑通状态 | 三类流证据 |
 |----|------|--------|----------|-----------|--------|----------|------------|
-| SC-A-01 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 管理端登录 + 全局统计 | — | — | 未跑通 | — |
-| SC-A-02 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 报告解析中台(类型/聚类) | — | — | 未跑通 | — |
-| SC-A-03 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 文章发布-审核 | — | — | 未跑通 | — |
-| SC-A-04 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 关系类型 CRUD | — | — | 未跑通 | — |
-| SC-A-05 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 勋章配置 CRUD | — | — | 未跑通 | — |
-| SC-A-06 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 供应商管理 | — | — | 未跑通 | — |
-| SC-A-07 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 活动配置 | — | — | 未跑通 | — |
-| SC-A-08 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 知识库管理 | — | — | 未跑通 | — |
-| SC-A-09 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 维度配置与知识库管理 | — | — | 未跑通 | — |
-| SC-A-10 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 数据迁移 / 参数校验 | — | — | 未跑通 | — |
-| SC-A-11 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 用户/家庭查询治理 | — | — | 未跑通 | — |
-| SC-A-12 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 订单/退款人工处理 | — | — | 未跑通 | — |
-| SC-A-13 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 内容安全审核 | — | — | 未跑通 | — |
-| SC-A-14 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 系统监控/告警 | — | — | 未跑通 | — |
-| SC-A-15 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 操作审计日志 | — | — | 未跑通 | — |
-| SC-A-16 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 报告解析自学习/未知类型标注 | — | — | 未跑通 | — |
-| SC-A-17 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 优惠券/活动配置管理 | — | — | 未跑通 | — |
-| SC-A-18 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 内容安全审核队列 | — | — | 未跑通 | — |
-| SC-A-19 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 账号注销/数据导出工单处理 | — | — | 未跑通 | — |
-| SC-A-20 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 限流/熔断配置 | — | — | 未跑通 | — |
-| SC-A-21 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 舌诊/情绪模型管理 | — | — | 未跑通 | — |
-| SC-A-22 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 精准营养知识库管理 | — | — | 未跑通 | — |
-| SC-A-25 | 平台运营 | 业务域(运营):五维配置治理 | 维度/权重/每日上限配置 CRUD 与生效 | — | — | 未跑通 | — |
-| SC-A-26 | 平台运营 | 业务域(运营):五维配置治理 | 能量来源比例与归因权重治理 | — | — | 未跑通 | — |
+| SC-A-01 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 管理端登录 + 全局统计 | admin.auth.login | GREEN | 未跑通 | 键已 GREEN(admin-auth R5);全局统计载体 AdminController /api/admin/users |
+| SC-A-02 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 报告解析中台(类型/聚类) | admin.reportParser.manage | RED | 未跑通 | S-cfc-197;ReportParserAdminController /api/admin/report-parser |
+| SC-A-03 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 文章发布-审核 | admin.article.manage | RED | 未跑通 | S-cfc-201;AdminArticleController /api/admin/articles(22 方法)|
+| SC-A-04 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 关系类型 CRUD | admin.familyRelation.manage | RED | 未跑通 | S-cfc-308;真缺口已立项:需新增 AdminFamilyRelationController(关系类型 CRUD),当前仅只读评分,实现待定 |
+| SC-A-05 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 勋章配置 CRUD | admin.badge.manage | RED | 未跑通 | S-cfc-206;BadgeController /api/badges(create/update/delete/grant)|
+| SC-A-06 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 供应商管理 | admin.supplier.manage | RED | 未跑通 | S-cfc-200;AdminSupplier/EcomSupplier/SupplyManageController |
+| SC-A-07 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 活动配置 | admin.activity.config | RED | 未跑通 | S-cfc-203;AdminActivityController + Checkin/Registration |
+| SC-A-08 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 知识库管理 | admin.knowledgeBase.manage | RED | 未跑通 | S-cfc-198;KnowledgeBase/Vector/Unified 三 Controller |
+| SC-A-09 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 维度配置与知识库管理 | admin.dimension.config | RED | 未跑通 | S-cfc-199;DimensionAdmin/Config/WeightController |
+| SC-A-10 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 数据迁移 / 参数校验 | admin.data.migration | RED | 未跑通 | S-cfc-205;DataMigration/Migration/ReportParseAdmin |
+| SC-A-11 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 用户/家庭查询治理 | admin.user.govern | RED | 未跑通 | S-cfc-207;AdminController users/* + AdminFamilyOverview |
+| SC-A-12 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 订单/退款人工处理 | admin.order.manual | RED | 未跑通 | S-cfc-202;AdminProductOrder + AdminAfterSales |
+| SC-A-13 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 内容安全审核 | admin.content.security | RED | 未跑通 | S-cfc-208;AdminArticle audit/submit-review + ReviewCenter |
+| SC-A-14 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 系统监控/告警 | admin.system.monitor | RED | 未跑通 | S-cfc-209;AdminSystem health + BasicQuota |
+| SC-A-15 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 操作审计日志 | admin.audit.log | RED | 未跑通 | S-cfc-204;AdminAudit + OperationLog |
+| SC-A-16 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 报告解析自学习/未知类型标注 | admin.reportParser.manage | RED | 未跑通 | S-cfc-197 1:N;fingerprints/list + types/toggle |
+| SC-A-17 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 优惠券/活动配置管理 | admin.activity.config | RED | 未跑通 | S-cfc-203 内含;AdminCoupon issue/issue-family + PromotionConfig |
+| SC-A-18 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 内容安全审核队列 | admin.content.security | RED | 未跑通 | S-cfc-208 1:N;ReviewCenter summary + submit-review 队列 |
+| SC-A-19 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 账号注销/数据导出工单处理 | admin.user.govern | RED | 未跑通 | S-cfc-207 1:N;UserController /data-export + Admin users/delete |
+| SC-A-20 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 限流/熔断配置 | admin.system.monitor | RED | 未跑通 | S-cfc-209 1:N;BasicQuota reset + System health |
+| SC-A-21 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 舌诊/情绪模型管理 | mind.model.manage | RED | 未跑通 | S-cfc-210;EmotionRecognition + TraditionalMirror + InnatePortraitConfig |
+| SC-A-22 | 平台运营 | 业务域 L:交易延伸 / 内容 / 多端 | 精准营养知识库管理 | admin.knowledgeBase.manage | RED | 未跑通 | S-cfc-198 1:N;AdminFood/AdminRecipe + KB 统一知识 |
+| SC-A-25 | 平台运营 | 业务域(运营):五维配置治理 | 维度/权重/每日上限配置 CRUD 与生效 | admin.dimension.config | RED | 未跑通 | S-cfc-199 1:N;DimensionWeight save/list + Energy daily-limits |
+| SC-A-26 | 平台运营 | 业务域(运营):五维配置治理 | 能量来源比例与归因权重治理 | admin.dimension.config | RED | 未跑通 | S-cfc-199 1:N;EnergyConfig source-list/save + EnergyRule |
 | SC-A-27 | 平台运营 | 业务域(运营):健康打卡配置治理 | 后台维护睡眠情况选项(增删改查)并同步小程序端 | sleep.config.tags | YELLOW | 未跑通 | 实现已在 origin/cfclub(2997a9f2c 系);三类流证据待补 |
-| SC-C-01 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 孩子端登录(无家长入口) | — | — | 未跑通 | — |
-| SC-C-02 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 执行任务 / 小游戏得积分 | — | — | 未跑通 | — |
-| SC-C-03 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 小游戏成绩上报 | — | — | 未跑通 | — |
-| SC-C-04 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 提心愿 | — | — | 未跑通 | — |
-| SC-C-05 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 任务打卡 ⭐ | — | — | 未跑通 | — |
-| SC-C-06 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 查看能量与报告(只读) | — | — | 未跑通 | — |
-| SC-C-07 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 查看 DAN 测评报告 | — | — | 未跑通 | — |
-| SC-C-08 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 方案打卡进度 | — | — | 未跑通 | — |
-| SC-C-09 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 勋章墙 | — | — | 未跑通 | — |
-| SC-C-10 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 勋章解锁提醒 | — | — | 未跑通 | — |
-| SC-C-11 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 成长记录查看(孩子视角) | — | — | 未跑通 | — |
-| SC-C-12 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 饮食食谱查看 | — | — | 未跑通 | — |
-| SC-C-13 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | AI 对话(孩子视角) | — | — | 未跑通 | — |
-| SC-C-14 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 任务驳回重做 | — | — | 未跑通 | — |
-| SC-C-15 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 消息接收 | — | — | 未跑通 | — |
-| SC-C-16 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 个人资料查看(受限) | — | — | 未跑通 | — |
-| SC-C-17 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 退出登录 | — | — | 未跑通 | — |
-| SC-C-18 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 设备/账号绑定校验 | — | — | 未跑通 | — |
-| SC-C-19 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 查看家人/规划师消息通知 | — | — | 未跑通 | — |
-| SC-C-20 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 成长档案查看/家长代导出 | — | — | 未跑通 | — |
-| SC-C-21 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 舌诊/情绪自拍采集(孩子端) | — | — | 未跑通 | — |
-| SC-C-22 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 商城浏览/积分兑换(孩子端消费) | — | — | 未跑通 | — |
-| SC-C-23 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 意见反馈(孩子端) | — | — | 未跑通 | — |
-| SC-C-24 | 孩子(不能登录视角) | 业务域(孩子):自身五维变化 | 小游戏/任务得分触发五维增长(仅自身可见) | — | — | 未跑通 | — |
-| SC-C-25 | 孩子(不能登录视角) | 业务域(孩子):自身五维变化 | 方案任务打卡触发五维增长 | — | — | 未跑通 | — |
+| SC-C-01 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 孩子端登录(无家长入口) | child.auth.login | RED | 未跑通 | S-cfc-211;AuthController /api/auth(phone-login/silent-login/auto-login)|
+| SC-C-02 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 执行任务 / 小游戏得积分 | child.task.checkin | RED | 未跑通 | S-cfc-212;TaskController + GrowthTaskController(重复提交幂等)|
+| SC-C-03 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 小游戏成绩上报 | child.game.reportScore | RED | 未跑通 | S-cfc-213;MiniGameController /complete + GameRecordController /record(异常分数拦截)|
+| SC-C-04 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 提心愿 | child.wish.manage | RED | 未跑通 | S-cfc-214;WishController /api/wishes(create/set-price/reject/exchange)|
+| SC-C-05 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 任务打卡 ⭐ | child.task.checkin | RED | 未跑通 | S-cfc-212 1:N;GrowthTaskService 打卡提交(逾期限提示/重复幂等)|
+| SC-C-06 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 查看能量与报告(只读) | child.energy.view | RED | 未跑通 | S-cfc-215;EnergyController /overview/logs(只读·越权拦截)|
+| SC-C-07 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 查看 DAN 测评报告 | child.energy.view | RED | 未跑通 | S-cfc-215 1:N;DanAssessmentController /result/{id}(结果未录入不展示)|
+| SC-C-08 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 方案打卡进度 | child.task.checkin | RED | 未跑通 | S-cfc-212 1:N;GrowthPlanController /child/{id}/active(进度计算)|
+| SC-C-09 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 勋章墙 | child.badge.view | RED | 未跑通 | S-cfc-216;BadgeController /all/list/detail(成就聚合)|
+| SC-C-10 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 勋章解锁提醒 | child.badge.view | RED | 未跑通 | S-cfc-216 1:N;BadgeController /grant + 通知推送(重复达成不重复发)|
+| SC-C-11 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 成长记录查看(孩子视角) | child.growth.archive | RED | 未跑通 | S-cfc-217;GrowthController /record/child-records(仅本人)|
+| SC-C-12 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 饮食食谱查看 | child.diet.viewMenu | RED | 未跑通 | S-cfc-218;FoodRecommend/HealthMeal/MealRecommend(当日三餐·空态)|
+| SC-C-13 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | AI 对话(孩子视角) | child.ai.chat | RED | 未跑通 | S-cfc-219;AIChatController /api/ai(chat/send 多轮·适龄过滤)|
+| SC-C-14 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 任务驳回重做 | child.task.checkin | RED | 未跑通 | S-cfc-212 1:N;TaskController(驳回后可重做再提交)|
+| SC-C-15 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 消息接收 | child.notification.receive | RED | 未跑通 | S-cfc-220;UserNotificationController(已读·越权不可见)|
+| SC-C-16 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 个人资料查看(受限) | child.auth.login | RED | 未跑通 | S-cfc-211 1:N;AuthController /info(敏感字段家长代管)|
+| SC-C-17 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 退出登录 | child.auth.login | RED | 未跑通 | S-cfc-211 1:N;AuthController(退出清 token/缓存)|
+| SC-C-18 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 设备/账号绑定校验 | child.auth.login | RED | 未跑通 | S-cfc-211 1:N;异常设备二次验证(防冒用)|
+| SC-C-19 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 查看家人/规划师消息通知 | child.notification.receive | RED | 未跑通 | S-cfc-220 1:N;跨角色消息(越权不可见)|
+| SC-C-20 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 成长档案查看/家长代导出 | child.growth.archive | RED | 未跑通 | S-cfc-217 1:N;成长档案只读+家长代导出(未成年人受限)|
+| SC-C-21 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 舌诊/情绪自拍采集(孩子端) | child.mind.capture | RED | 未跑通 | S-cfc-221;EmotionRecognition /analyze + TraditionalMirror /mirror(模糊拒绝·隐私)|
+| SC-C-22 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 商城浏览/积分兑换(孩子端消费) | child.shop.pointsExchange | RED | 未跑通 | S-cfc-222;PointsExchangeController(积分不足拦截·重复幂等)|
+| SC-C-23 | 孩子(不能登录视角) | 业务域 L:交易延伸 / 内容 / 多端 | 意见反馈(孩子端) | child.feedback.submit | RED | 未跑通 | S-cfc-223;DailyFeedbackController(适龄过滤·脱敏)|
+| SC-C-24 | 孩子(不能登录视角) | 业务域(孩子):自身五维变化 | 小游戏/任务得分触发五维增长(仅自身可见) | child.energy.gain | RED | 未跑通 | S-cfc-224;GameRecordService + EnergyRuleEngine(异常分数拦截·仅加自身·越权403)|
+| SC-C-25 | 孩子(不能登录视角) | 业务域(孩子):自身五维变化 | 方案任务打卡触发五维增长 | child.energy.gain | RED | 未跑通 | S-cfc-224 1:N;GrowthTaskService(打卡对应维度+同步进度·并发不重复)|
 | SC-C-26 | 孩子(不能登录视角) | 业务域(孩子):健康打卡 | 睡眠打卡:睡眠情况含「正常」选项且由后台配置驱动 | sleep.config.tags | YELLOW | 未跑通 | 实现已在 origin/cfclub;三类流证据待补 |
-| SC-X-01 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 测评全链路 ⭐ | — | — | 未跑通 | — |
-| SC-X-02 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 报告闭环 ⭐ | — | — | 未跑通 | — |
-| SC-X-03 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 双角色数据隔离越权 | — | — | 未跑通 | — |
-| SC-X-04 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 家庭邀请闭环 | — | — | 未跑通 | — |
-| SC-X-05 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 任务激励闭环 | — | — | 未跑通 | — |
-| SC-X-06 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 方案激活执行闭环 | — | — | 未跑通 | — |
-| SC-X-07 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 商城履约闭环 | — | — | 未跑通 | — |
-| SC-X-08 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 文章发布闭环 | — | — | 未跑通 | — |
-| SC-X-09 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | AI 方案送审闭环 | — | — | 未跑通 | — |
-| SC-X-10 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 报告对比趋势闭环 | — | — | 未跑通 | — |
-| SC-X-11 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 消息通知闭环 | — | — | 未跑通 | — |
-| SC-X-12 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 会员权益闭环 | — | — | 未跑通 | — |
-| SC-X-13 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 报告代传跨角色闭环 | — | — | 未跑通 | — |
-| SC-X-14 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 解绑/重分配跨角色协同 | — | — | 未跑通 | — |
-| SC-X-15 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 注销/数据导出跨角色 | — | — | 未跑通 | — |
-| SC-X-16 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 活动名额并发 E2E ⭐ | — | — | 未跑通 | — |
-| SC-X-17 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 多端登录会话互斥跨端 | — | — | 未跑通 | — |
-| SC-X-18 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 推送跨端送达 | — | — | 未跑通 | — |
-| SC-X-19 | 跨角色 | 业务域(跨角色):五维隔离·对账·并发 | 五维能量跨角色数据隔离 | — | — | 未跑通 | — |
-| SC-X-20 | 跨角色 | 业务域(跨角色):五维隔离·对账·并发 | 五维能量流水溯源一致性(对账) | — | — | 未跑通 | — |
-| SC-X-21 | 跨角色 | 业务域(跨角色):五维隔离·对账·并发 | 多来源并发同成员能量变动 | — | — | 未跑通 | — |
+| SC-X-01 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 测评全链路 ⭐ | membership.plan.purchase, teacher.appointment.guard, teacher.assessment.record, teacher.plan.activate, child.task.checkin | RED | 未跑通 | 链路键集合:购套餐→预约→录入→激活→打卡;额度为不可逾越闸门 |
+| SC-X-02 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 报告闭环 ⭐ | health.report.upload, dan.report.parseFormat, diet.food.healthAdjust, child.task.checkin | RED/— | 未跑通 | 链路键集合:传PDF→解析入库→饮食推荐→AI方案→孩子执行;解析失败有降级 |
+| SC-X-03 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 双角色数据隔离越权 | teacher.data.isolation, family.parent.switchRole | RED | 未跑通 | JwtInterceptor role 检查;越权统一 403/401 |
+| SC-X-04 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 家庭邀请闭环 | family.familyMembers.addMember, family.invite.joinValidationGate | DONE/— | 未跑通 | 链路:生成邀请→注册加入→关系编辑→数据可见;失效码拒绝 |
+| SC-X-05 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 任务激励闭环 | task.parent.createTask, task.parent.review, child.wish.manage | DONE/— | 未跑通 | 链路:建任务→完成→审→发分→提心愿→兑;重复/不足拦截 |
+| SC-X-06 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 方案激活执行闭环 | teacher.plan.activate, child.task.checkin, child.badge.view | RED | 未跑通 | 链路:激活→生成任务→打卡→审→勋章;幂等/重复拦截 |
+| SC-X-07 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 商城履约闭环 | shop.order.create, supplier.order.fulfill, shop.order.queryRefund | YELLOW/— | 未跑通 | 链路:下单→支付→发货/核销→确认收货→评价;退款/重复核销 |
+| SC-X-08 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 文章发布闭环 | admin.article.manage, admin.content.security | RED | 未跑通 | 链路:作者写→运营审→发布→用户读;未审不可见 |
+| SC-X-09 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | AI 方案送审闭环 | health.report.create, teacher.plan.review, teacher.plan.activate | YELLOW/— | 未跑通 | 链路:生成→送审→批准→激活;未确认不可激活 |
+| SC-X-10 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 报告对比趋势闭环 | health.report.detail, teacher.family.overview, growth.history.view | YELLOW/— | 未跑通 | 链路:多次上传→对比→趋势→方案调整;类型一致才可比 |
+| SC-X-11 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 消息通知闭环 | teacher.message.push, child.notification.receive | RED | 未跑通 | 链路:发消息→收→已读;免打扰生效 |
+| SC-X-12 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 会员权益闭环 | membership.plan.purchase, family.family.autoCreateForPurchase | YELLOW/RED | 未跑通 | 链路:购会员→权益生效→AI方案/高级报告解锁;过期回收 |
+| SC-X-13 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 报告代传跨角色闭环 | teacher.report.proxy, health.report.upload | RED/— | 未跑通 | 链路:发起代传→对方上传→档案更新;授权校验拒绝越权 |
+| SC-X-14 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 解绑/重分配跨角色协同 | teacher.family.bind, family.familyMembers.removeMember | RED/YELLOW | 未跑通 | 链路:家长解绑↔规划师侧更新;在途数据交接 |
+| SC-X-15 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 注销/数据导出跨角色 | admin.user.govern, teacher.client.export | RED | 未跑通 | 链路:申请→运营处理→档案解绑;审计+冷静期 |
+| SC-X-16 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 活动名额并发 E2E ⭐ | admin.activity.config | RED | 未跑通 | 并发报名抢名额;超卖拦截(真实 bug 域) |
+| SC-X-17 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 多端登录会话互斥跨端 | child.auth.login | RED | 未跑通 | A端登录→B端被踢/验证;异常设备风控 |
+| SC-X-18 | 跨角色 | 业务域 L:交易延伸 / 内容 / 多端 | 推送跨端送达 | teacher.message.push, child.notification.receive | RED | 未跑通 | 设提醒→子端收到;免打扰生效 |
+| SC-X-19 | 跨角色 | 业务域(跨角色):五维隔离·对账·并发 | 五维能量跨角色数据隔离 | energy.ledger.guard | RED | 未跑通 | S-cfc-225;JwtInterceptor 401/403 + 工单授权 |
+| SC-X-20 | 跨角色 | 业务域(跨角色):五维隔离·对账·并发 | 五维能量流水溯源一致性(对账) | energy.ledger.guard | RED | 未跑通 | S-cfc-225 1:N;balanceAfter==prev+amount 对账 |
+| SC-X-21 | 跨角色 | 业务域(跨角色):五维隔离·对账·并发 | 多来源并发同成员能量变动 | energy.ledger.guard | RED | 未跑通 | S-cfc-225 1:N;并发原子加/触顶截断/回滚幂等 |
 | SC-G-01 | 游客 |  | 首次进入小程序/落地页 | auth.guest.landing | DONE | 已跑通(2026-11-07 收口) | 三类流证据已推远端(dceeaaeaf) |
 | SC-G-02 | 游客 |  | 微信授权登录 | auth.guest.wechatLogin | DONE | 已跑通(2026-11-06 O-3 收口) | 三类流证据已推远端(8051b64f) |
 | SC-G-03 | 游客 |  | 登录方式口径(小程序端 / Web 管理后台) | auth.guest.loginMethod | DONE | 已跑通(2026-11-07 收口) | 三类流证据已推远端(dceeaaeaf) |
@@ -112,57 +112,57 @@ SC-G-02 / SC-G-06 / SC-P-03 / SC-P-10 / SC-P-11 共 5 个 SC 已于 2026-11-06 O
 | SC-G-10 | 游客 |  | 账号不存在引导注册 | auth.guest.registerGuide | DONE | 已跑通(2026-11-07 收口) | 三类流证据已推远端(dceeaaeaf) |
 | SC-G-11 | 游客 |  | 隐私政策/用户协议查看 | policy.agreement.view | DONE | 已跑通(2026-11-07 收口) | 三类流证据已推远端(dceeaaeaf) |
 | SC-G-12 | 游客 |  | 非微信环境 H5 兜底 | env.h5Fallback | DONE | 已跑通(2026-11-07 收口) | 三类流证据已推远端(dceeaaeaf) |
-| SC-P-01 | 家长 | 业务域 A:家庭与成员管理 | 创建家庭 + 邀请二维码 | — | — | 未跑通 | — |
-| SC-P-02 | 家长 | 业务域 A:家庭与成员管理 | 邀请成员 + 关系编辑 | — | — | 未跑通 | — |
+| SC-P-01 | 家长 | 业务域 A:家庭与成员管理 | 创建家庭 + 邀请二维码 | family.family.createWithInvite | RED | 未跑通 | S-cfc-256;建家庭+生成邀请码/二维码(令牌+落地页),管理员 |
+| SC-P-02 | 家长 | 业务域 A:家庭与成员管理 | 邀请成员 + 关系编辑 | family.invite.memberJoin | RED | 未跑通 | S-cfc-257;邀请→申请→审批加入,RelationshipPicker 选关系 |
 | SC-P-03 | 家长 | 业务域 A:家庭与成员管理 | 双角色切换(家长↔孩子) | family.parent.switchRole | DONE | 已跑通(2026-11-06 O-3 收口) | 三类流证据已推远端(f1b0e3ed5) |
-| SC-P-04 | 家长 | 业务域 A:家庭与成员管理 | 家庭成员列表查看 | — | — | 未跑通 | — |
-| SC-P-05 | 家长 | 业务域 A:家庭与成员管理 | 编辑成员资料 | — | — | 未跑通 | — |
-| SC-P-06 | 家长 | 业务域 A:家庭与成员管理 | 移除/删除成员 | family.familyMembers.removeMember | RED | 未跑通 | — |
-| SC-P-07 | 家长 | 业务域 A:家庭与成员管理 | 家庭关系图查看 | — | — | 未跑通 | — |
-| SC-P-08 | 家长 | 业务域 A:家庭与成员管理 | 成员权限设置 | — | — | 未跑通 | — |
-| SC-P-09 | 家长 | 业务域 A:家庭与成员管理 | 邀请码失效/重生成 | — | — | 未跑通 | — |
+| SC-P-04 | 家长 | 业务域 A:家庭与成员管理 | 家庭成员列表查看 | family.familyMembers.list | RED | 未跑通 | S-cfc-258;成员列表(角色/关系/在线),仅本家庭成员可见 |
+| SC-P-05 | 家长 | 业务域 A:家庭与成员管理 | 编辑成员资料 | family.member.update | RED | 未跑通 | S-cfc-260;更新成员信息 POST /api/family/member/update |
+| SC-P-06 | 家长 | 业务域 A:家庭与成员管理 | 移除/删除成员 | family.familyMembers.removeMember | YELLOW | 未跑通 | S-cfc-178;踢出/退出+会员身份降级修复,yellow_sha=5c3cff249 |
+| SC-P-07 | 家长 | 业务域 A:家庭与成员管理 | 家庭关系图查看 | family.family.relationGraph | RED | 未跑通 | S-cfc-259;关系图节点+关系标签渲染 |
+| SC-P-08 | 家长 | 业务域 A:家庭与成员管理 | 成员权限设置 | family.member.update | RED | 未跑通 | S-cfc-260;查看/编辑权限、isChild 仅管理员可设,非管理员 403 |
+| SC-P-09 | 家长 | 业务域 A:家庭与成员管理 | 邀请码失效/重生成 | family.invite.memberJoin | RED | 未跑通 | S-cfc-257;旧码失效/重生成,已入家庭者不受影响 |
 | SC-P-10 | 家长 | 业务域 B:任务与激励 | 创建任务 + 下发 | task.parent.createTask | DONE | 已跑通(2026-11-06 O-3 收口) | 三类流证据已推远端(f41da2779) |
 | SC-P-11 | 家长 | 业务域 B:任务与激励 | 任务审批完成(通过/驳回) | task.parent.review | DONE | 已跑通(2026-11-06 O-3 收口) | 三类流证据已推远端(291955bce) |
-| SC-P-12 | 家长 | 业务域 B:任务与激励 | 任务频率/周期配置 | — | — | 未跑通 | — |
-| SC-P-13 | 家长 | 业务域 B:任务与激励 | 心愿创建-审批-兑换闭环 | — | — | 未跑通 | — |
-| SC-P-14 | 家长 | 业务域 B:任务与激励 | 心愿驳回/失效处理 | — | — | 未跑通 | — |
-| SC-P-15 | 家长 | 业务域 B:任务与激励 | 积分明细查看 | — | — | 未跑通 | — |
-| SC-P-16 | 家长 | 业务域 B:任务与激励 | 积分流水对账 | — | — | 未跑通 | — |
-| SC-P-17 | 家长 | 业务域 C:五维能量与健康 | 查看五维能量 / 健康指数 | — | — | 未跑通 | — |
-| SC-P-18 | 家长 | 业务域 C:五维能量与健康 | 能量流水查看 | — | — | 未跑通 | — |
-| SC-P-19 | 家长 | 业务域 C:五维能量与健康 | 能量维度配置查看 | — | — | 未跑通 | — |
-| SC-P-20 | 家长 | 业务域 C:五维能量与健康 | 健康报告 PDF 上传解析(上传报告)⭐ | — | — | 未跑通 | — |
-| SC-P-21 | 家长 | 业务域 C:五维能量与健康 | 报告类型识别 | — | — | 未跑通 | — |
-| SC-P-22 | 家长 | 业务域 C:五维能量与健康 | 报告解析预览 | — | — | 未跑通 | — |
-| SC-P-23 | 家长 | 业务域 C:五维能量与健康 | 报告确认入库 | — | — | 未跑通 | — |
-| SC-P-24 | 家长 | 业务域 C:五维能量与健康 | 多报告管理/对比 | — | — | 未跑通 | — |
-| SC-P-25 | 家长 | 业务域 C:五维能量与健康 | 报告趋势查看 | — | — | 未跑通 | — |
-| SC-P-26 | 家长 | 业务域 C:五维能量与健康 | 基于报告饮食推荐 | — | — | 未跑通 | — |
-| SC-P-27 | 家长 | 业务域 C:五维能量与健康 | 家庭食谱生成(一日三餐)⭐ 家庭食谱 | — | — | 未跑通 | — |
-| SC-P-28 | 家长 | 业务域 C:五维能量与健康 | 食谱按食材筛选/忌口 | — | — | 未跑通 | — |
-| SC-P-29 | 家长 | 业务域 C:五维能量与健康 | 食谱收藏/调整 | — | — | 未跑通 | — |
-| SC-P-30 | 家长 | 业务域 C:五维能量与健康 | 食谱与营养目标对齐 | — | — | 未跑通 | — |
-| SC-P-31 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 浏览测评类型(DAN 8 类) | — | — | 未跑通 | — |
-| SC-P-32 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 选择规划师 | — | — | 未跑通 | — |
-| SC-P-33 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 购买测评套餐/下单 | — | — | 未跑通 | — |
-| SC-P-34 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 测评订单支付 | — | — | 未跑通 | — |
-| SC-P-35 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 获得测评额度 | — | — | 未跑通 | — |
-| SC-P-36 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 创建预约(消耗额度)⭐ | — | — | 未跑通 | — |
-| SC-P-37 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 查看预约状态 | — | — | 未跑通 | — |
-| SC-P-38 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 取消预约(越权校验) | — | — | 未跑通 | — |
-| SC-P-39 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 查看测评结果(DAN 报告) | — | — | 未跑通 | — |
-| SC-P-40 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 生成 AI 方案(一键生成)⭐ 生成方案 | — | — | 未跑通 | — |
-| SC-P-41 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 方案分板块重生成 | — | — | 未跑通 | — |
-| SC-P-42 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 方案编辑/调整 | — | — | 未跑通 | — |
-| SC-P-43 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 查看生成的方案 | — | — | 未跑通 | — |
-| SC-P-44 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 确认方案(送审/家长确认)⭐ 确认方案 | — | — | 未跑通 | — |
-| SC-P-45 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 方案直接发布 | — | — | 未跑通 | — |
-| SC-P-46 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 激活方案生成任务(生成任务)⭐ 生成任务 | — | — | 未跑通 | — |
-| SC-P-47 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 查看方案下任务列表 | — | — | 未跑通 | — |
-| SC-P-48 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 方案版本/历史 | — | — | 未跑通 | — |
-| SC-P-49 | 家长 | 业务域 D:测评与成长规划价值链(核心) | AI 健康管家对话 ⭐ | — | — | 未跑通 | — |
-| SC-P-50 | 家长 | 业务域 E:成长记录与档案 | 成长记录查看 | — | — | 未跑通 | — |
-| SC-P-51 | 家长 | 业务域 E:成长记录与档案 | 成长里程碑 | — | — | 未跑通 | — |
+| SC-P-12 | 家长 | 业务域 B:任务与激励 | 任务频率/周期配置 | task.frequency.config | RED | 未跑通 | S-cfc-262;日/周/自定义周期→按周期自动生成实例,跨月/跨年,暂停/恢复 |
+| SC-P-13 | 家长 | 业务域 B:任务与激励 | 心愿创建-审批-兑换闭环 | wish.cycle.manage | RED | 未跑通 | S-cfc-261;创建-审批-兑换闭环,扣积分兑换 |
+| SC-P-14 | 家长 | 业务域 B:任务与激励 | 心愿驳回/失效处理 | wish.cycle.manage | RED | 未跑通 | S-cfc-261;驳回→失效→可重提,已兑换不可驳回 |
+| SC-P-15 | 家长 | 业务域 B:任务与激励 | 积分明细查看 | points.ledger.view | RED | 未跑通 | S-cfc-263;积分明细(来源/变更/余额),分页+时间筛选 |
+| SC-P-16 | 家长 | 业务域 B:任务与激励 | 积分流水对账 | points.ledger.view | RED | 未跑通 | S-cfc-263;总额与流水合计一致,负流水报警 |
+| SC-P-17 | 家长 | 业务域 C:五维能量与健康 | 查看五维能量 / 健康指数 | energy.fiveDim.view | RED | 未跑通 | S-cfc-264;五维分配总和=100%,配置错误≠100% 报警 |
+| SC-P-18 | 家长 | 业务域 C:五维能量与健康 | 能量流水查看 | energy.ledger.view | RED | 未跑通 | S-cfc-265;能量流水(任务/打卡/测评),时间格式统一 |
+| SC-P-19 | 家长 | 业务域 C:五维能量与健康 | 能量维度配置查看 | energy.dimension.view | RED | 未跑通 | S-cfc-266;五维定义与权重只读,未同步展示兜底 |
+| SC-P-20 | 家长 | 业务域 C:五维能量与健康 | 健康报告 PDF 上传解析(上传报告)⭐ | report.upload.parse | RED | 未跑通 | 上传+识别 gut_flora+三阶段预览/确认,非募极报告提示,失败进未知聚类 |
+| SC-P-21 | 家长 | 业务域 C:五维能量与健康 | 报告类型识别 | report.upload.parse | RED | 未跑通 | 自动判别报告种类,未知类型进未知聚类待运营自学习 |
+| SC-P-22 | 家长 | 业务域 C:五维能量与健康 | 报告解析预览 | report.upload.parse | RED | 未跑通 | 菌群健康分/平衡/多样性/菌种明细/疾病风险预览,异常提示不入库 |
+| SC-P-23 | 家长 | 业务域 C:五维能量与健康 | 报告确认入库 | report.upload.parse | RED | 未跑通 | 确认→档案+五维刷新,确认前可取消 |
+| SC-P-24 | 家长 | 业务域 C:五维能量与健康 | 多报告管理/对比 | report.archive.manage | RED | 未跑通 | 列表/查看/删除/对比,删除有关联方案提示 |
+| SC-P-25 | 家长 | 业务域 C:五维能量与健康 | 报告趋势查看 | report.archive.manage | RED | 未跑通 | 同类型时间序列趋势图,单报告无趋势 |
+| SC-P-26 | 家长 | 业务域 C:五维能量与健康 | 基于报告饮食推荐 | diet.recommendation.byReport | RED | 未跑通 | 按菌群生成可吃/慎吃清单,无报告无推荐 |
+| SC-P-27 | 家长 | 业务域 C:五维能量与健康 | 家庭食谱生成(一日三餐)⭐ 家庭食谱 | diet.mealPlan.generate | RED | 未跑通 | 菌群报告+营养目标→早/午/晚食谱,无报告降级通用 |
+| SC-P-28 | 家长 | 业务域 C:五维能量与健康 | 食谱按食材筛选/忌口 | diet.mealPlan.generate | RED | 未跑通 | 忌口/过敏→食谱重算排除项,冲突提示 |
+| SC-P-29 | 家长 | 业务域 C:五维能量与健康 | 食谱收藏/调整 | diet.mealPlan.adjust | RED | 未跑通 | 收藏+手动微调替换食材,不破坏营养目标 |
+| SC-P-30 | 家长 | 业务域 C:五维能量与健康 | 食谱与营养目标对齐 | diet.mealPlan.adjust | RED | 未跑通 | 营养合计 vs 目标,超标/不足提示 |
+| SC-P-31 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 浏览测评类型(DAN 8 类) | assessment.type.browse | RED | 未跑通 | S-cfc-273;DAN 8 类目来自后端字典 |
+| SC-P-32 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 选择规划师 | assessment.planner.select | RED | 未跑通 | S-cfc-274;测评选规划师,未绑定提示先绑定 |
+| SC-P-33 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 购买测评套餐/下单 | assessment.order.create | RED | 未跑通 | S-cfc-275;生成 AssessmentOrder(pending),套餐下架不可下单 |
+| SC-P-34 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 测评订单支付 | assessment.order.pay | RED | 未跑通 | S-cfc-276;支付 pending→paid,失败回滚不生成额度 |
+| SC-P-35 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 获得测评额度 | assessment.order.pay | RED | 未跑通 | S-cfc-276;支付成功生成额度(含有效期),过期不可预约 |
+| SC-P-36 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 创建预约(消耗额度)⭐ | assessment.appointment.create | RED | 未跑通 | S-cfc-277;先消耗额度→落预约,额度不足/过期不落 |
+| SC-P-37 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 查看预约状态 | assessment.appointment.create | RED | 未跑通 | S-cfc-277;状态机:待确认/已确认/已完成/已取消/未到 |
+| SC-P-38 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 取消预约(越权校验) | assessment.appointment.create | RED | 未跑通 | S-cfc-277;CFC-AP 越权取消被拒;取消不退款 |
+| SC-P-39 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 查看测评结果(DAN 报告) | assessment.result.view | RED | 未跑通 | S-cfc-278;八类报告,结果未录入不展示,量纲标注 |
+| SC-P-40 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 生成 AI 方案(一键生成)⭐ 生成方案 | plan.ai.generate | RED | 未跑通 | S-cfc-279;LangGraph(AiGateway) 生成 overview+三板块,未购会员提示,超时 Fallback 不 500 |
+| SC-P-41 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 方案分板块重生成 | plan.ai.generate | RED | 未跑通 | S-cfc-279;逐板块重生成,不丢其他板块 |
+| SC-P-42 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 方案编辑/调整 | plan.edit.adjust | RED | 未跑通 | S-cfc-280;编辑文本/任务项,状态标记已改 |
+| SC-P-43 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 查看生成的方案 | plan.edit.adjust | RED | 未跑通 | S-cfc-280;方案详情板块/任务,空方案不白屏 |
+| SC-P-44 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 确认方案(送审/家长确认)⭐ 确认方案 | plan.confirm.submit | RED | 未跑通 | S-cfc-281;家长确认/送审,generated→reviewed,确认幂等 |
+| SC-P-45 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 方案直接发布 | plan.confirm.submit | RED | 未跑通 | S-cfc-281;配置允许直发 active,需审核走审核流 |
+| SC-P-46 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 激活方案生成任务(生成任务)⭐ 生成任务 | teacher.plan.activate | RED | 未跑通 | 复用 S-cfc-180;激活 reviewed→active→生成引导+模板任务,幂等靠状态机 |
+| SC-P-47 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 查看方案下任务列表 | plan.task.list | RED | 未跑通 | S-cfc-282;方案关联任务列表(状态/进度),任务增删与方案同步 |
+| SC-P-48 | 家长 | 业务域 D:测评与成长规划价值链(核心) | 方案版本/历史 | plan.edit.adjust | RED | 未跑通 | S-cfc-280;版本列表对比,版本不可篡改 |
+| SC-P-49 | 家长 | 业务域 D:测评与成长规划价值链(核心) | AI 健康管家对话 ⭐ | ai.coach.consult | RED | 未跑通 | 复用 S-cfc-244;POST /api/ai/health-coach/send 多轮上下文 |
+| SC-P-50 | 家长 | 业务域 E:成长记录与档案 | 成长记录查看 | growth.history.view | GREEN | 未跑通 | 复用既有成长历史键;时间线(任务/测评/报告/打卡),缺失跳过 |
+| SC-P-51 | 家长 | 业务域 E:成长记录与档案 | 成长里程碑 | growth.milestone.mark | RED | 未跑通 | 系统/手动标里程碑→时间线高亮,重复标幂等 |
 | SC-P-52 | 家长 | 业务域 F:商城与会员 | 商品浏览/搜索 | shop.product.browse | RED | 未跑通 | — |
 | SC-P-53 | 家长 | 业务域 F:商城与会员 | 商品详情 | shop.product.browse | RED | 未跑通 | — |
 | SC-P-54 | 家长 | 业务域 F:商城与会员 | 加购/下单 | shop.order.create | RED | 未跑通 | — |
@@ -172,39 +172,39 @@ SC-G-02 / SC-G-06 / SC-P-03 / SC-P-10 / SC-P-11 共 5 个 SC 已于 2026-11-06 O
 | SC-P-58 | 家长 | 业务域 F:商城与会员 | 购买会员/套餐 | membership.plan.purchase, family.family.autoCreateForPurchase | YELLOW/RED | 未跑通 | — |
 | SC-P-59 | 家长 | 业务域 F:商城与会员 | 会员权益查看 | membership.plan.purchase | RED | 未跑通 | — |
 | SC-P-60 | 家长 | 业务域 F:商城与会员 | 会员续费 | membership.plan.purchase | RED | 未跑通 | — |
-| SC-P-61 | 家长 | 业务域 G:联系人与社交 | 联系人添加 | — | — | 未跑通 | — |
-| SC-P-62 | 家长 | 业务域 G:联系人与社交 | 联系人分组/导入 | — | — | 未跑通 | — |
-| SC-P-63 | 家长 | 业务域 G:联系人与社交 | 联系人详情/权限 | — | — | 未跑通 | — |
-| SC-P-64 | 家长 | 业务域 H:设置与通知 | 个人资料编辑 | — | — | 未跑通 | — |
-| SC-P-65 | 家长 | 业务域 H:设置与通知 | 消息通知/提醒设置 | — | — | 未跑通 | — |
-| SC-P-66 | 家长 | 业务域 H:设置与通知 | 隐私设置 | — | — | 未跑通 | — |
-| SC-P-67 | 家长 | 业务域 I:报告代传 / 精准营养 / 新健康入口 | 报告代传(家长替孩子/他人代传)⭐ | — | — | 未跑通 | — |
-| SC-P-68 | 家长 | 业务域 I:报告代传 / 精准营养 / 新健康入口 | 家庭天盘查看(全家聚合视图) | — | — | 未跑通 | — |
-| SC-P-69 | 家长 | 业务域 I:报告代传 / 精准营养 / 新健康入口 | 精准营养目标设定 | — | — | 未跑通 | — |
-| SC-P-70 | 家长 | 业务域 I:报告代传 / 精准营养 / 新健康入口 | 舌诊/情绪照片采集 | — | — | 未跑通 | — |
-| SC-P-71 | 家长 | 业务域 J:账号与数据主权 | 账号注销(家长注销自身) | — | — | 未跑通 | — |
-| SC-P-72 | 家长 | 业务域 J:账号与数据主权 | 个人/家庭数据导出(GDPR) | — | — | 未跑通 | — |
-| SC-P-73 | 家长 | 业务域 J:账号与数据主权 | 多孩子切换孩子上下文 | — | — | 未跑通 | — |
-| SC-P-74 | 家长 | 业务域 J:账号与数据主权 | 解绑/重新分配规划师 | — | — | 未跑通 | — |
-| SC-P-75 | 家长 | 业务域 K:营销 / 活动 / 优惠 | 优惠券领取(活动/分享) | — | — | 未跑通 | — |
-| SC-P-76 | 家长 | 业务域 K:营销 / 活动 / 优惠 | 活动报名(名额/并发)⭐ | — | — | 未跑通 | — |
-| SC-P-77 | 家长 | 业务域 K:营销 / 活动 / 优惠 | 活动打卡/助力(营销) | — | — | 未跑通 | — |
-| SC-P-78 | 家长 | 业务域 K:营销 / 活动 / 优惠 | 消息中心/通知查看 | — | — | 未跑通 | — |
-| SC-P-79 | 家长 | 业务域 K:营销 / 活动 / 优惠 | 推送/通知开关设置 | — | — | 未跑通 | — |
-| SC-P-80 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 订阅/会员退款与发票 | — | — | 未跑通 | — |
-| SC-P-81 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 方案/任务草稿保存与恢复 | — | — | 未跑通 | — |
-| SC-P-82 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 全局搜索(报告/方案/任务/商品) | — | — | 未跑通 | — |
-| SC-P-83 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 意见反馈/在线客服 | — | — | 未跑通 | — |
-| SC-P-84 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 分享报告/方案给家人或规划师 | — | — | 未跑通 | — |
-| SC-P-85 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 多端登录/会话互斥 | — | — | 未跑通 | — |
-| SC-P-86 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 成长打卡榜/家庭排行 | — | — | 未跑通 | — |
-| SC-P-87 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 任务完成触发五维能量按规则归因增长 | — | — | 未跑通 | — |
-| SC-P-88 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 心愿兑换扣减五维能量(余额不为负) | — | — | 未跑通 | — |
-| SC-P-89 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 手动发放能量 grant | — | — | 未跑通 | — |
-| SC-P-90 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 五维每日获取上限 DailyLimit | — | — | 未跑通 | — |
-| SC-P-91 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 健康报告入库刷新五维评分 | — | — | 未跑通 | — |
-| SC-P-92 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 五维权重/来源比例配置变更后重算 | — | — | 未跑通 | — |
-| SC-P-93 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 文章互动与活动参与触发五维能量 | — | — | 未跑通 | — |
+| SC-P-61 | 家长 | 业务域 G:联系人与社交 | 联系人添加 | contact.card.manage | RED | 未跑通 | S-cfc-283;姓名/电话/关系,重复号码去重 |
+| SC-P-62 | 家长 | 业务域 G:联系人与社交 | 联系人分组/导入 | contact.group.import | RED | 未跑通 | S-cfc-284;ContactImport 分组导入,格式错误跳过 |
+| SC-P-63 | 家长 | 业务域 G:联系人与社交 | 联系人详情/权限 | contact.card.manage | RED | 未跑通 | S-cfc-283;可见权限设置,越权查看被拒 |
+| SC-P-64 | 家长 | 业务域 H:设置与通知 | 个人资料编辑 | auth.profile.edit | RED | 未跑通 | S-cfc-285;昵称/头像/绑定手机,格式校验 |
+| SC-P-65 | 家长 | 业务域 H:设置与通知 | 消息通知/提醒设置 | notification.setting.config | RED | 未跑通 | S-cfc-286;通知开关/提醒时间,关键安全不可关闭 |
+| SC-P-66 | 家长 | 业务域 H:设置与通知 | 隐私设置 | privacy.setting.config | RED | 未跑通 | S-cfc-287;数据共享范围,最小权限保底 |
+| SC-P-67 | 家长 | 业务域 I:报告代传 / 精准营养 / 新健康入口 | 报告代传(家长替孩子/他人代传)⭐ | teacher.report.proxy | RED | 未跑通 | 复用 S-cfc-182;代传授权令牌,越权代传被拒 |
+| SC-P-68 | 家长 | 业务域 I:报告代传 / 精准营养 / 新健康入口 | 家庭天盘查看(全家聚合视图) | ai.coach.familyBoard | RED | 未跑通 | 复用 S-cfc-249;全家聚合视图,缺失降级展示 |
+| SC-P-69 | 家长 | 业务域 I:报告代传 / 精准营养 / 新健康入口 | 精准营养目标设定 | diet.nutrition.target | RED | 未跑通 | S-cfc-288;每日营养/热量目标,非法值拦截 |
+| SC-P-70 | 家长 | 业务域 I:报告代传 / 精准营养 / 新健康入口 | 舌诊/情绪照片采集 | tongue.emotion.capture | RED | 未跑通 | S-cfc-289;/api/mind/emotion/analyze 照片 DeepFace |
+| SC-P-71 | 家长 | 业务域 J:账号与数据主权 | 账号注销(家长注销自身) | auth.account.deactivate | RED | 未跑通 | S-cfc-290;身份验证→冷静期→归档/删除 |
+| SC-P-72 | 家长 | 业务域 J:账号与数据主权 | 个人/家庭数据导出(GDPR) | data.export.gdpr | RED | 未跑通 | S-cfc-291;申请→加密包→下载,脱敏+频次限制 |
+| SC-P-73 | 家长 | 业务域 J:账号与数据主权 | 多孩子切换孩子上下文 | family.childContext.switch | RED | 未跑通 | S-cfc-292;/api/family/member/switch 孩子间切换 |
+| SC-P-74 | 家长 | 业务域 J:账号与数据主权 | 解绑/重新分配规划师 | teacher.family.bind | RED | 未跑通 | 复用 S-cfc-190;解绑前处理在途方案/预约 |
+| SC-P-75 | 家长 | 业务域 K:营销 / 活动 / 优惠 | 优惠券领取(活动/分享) | shop.coupon.claim | RED | 未跑通 | S-cfc-293;领券入卡包,库存/已领拦截,幂等 |
+| SC-P-76 | 家长 | 业务域 K:营销 / 活动 / 优惠 | 活动报名(名额/并发)⭐ | activity.enroll.join | RED | 未跑通 | S-cfc-294;占名额,并发超卖拦截(CFC bug 已修) |
+| SC-P-77 | 家长 | 业务域 K:营销 / 活动 / 优惠 | 活动打卡/助力(营销) | activity.checkin.boost | RED | 未跑通 | S-cfc-295;进度累计,刷量/机器行为拦截 |
+| SC-P-78 | 家长 | 业务域 K:营销 / 活动 / 优惠 | 消息中心/通知查看 | notification.center.view | RED | 未跑通 | S-cfc-296;/api/notification/list 未读优先 LIMIT 100 |
+| SC-P-79 | 家长 | 业务域 K:营销 / 活动 / 优惠 | 推送/通知开关设置 | notification.setting.config | RED | 未跑通 | S-cfc-286;同 P-65 键,H/K 域开关设置 |
+| SC-P-80 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 订阅/会员退款与发票 | membership.plan.refund | RED | 未跑通 | S-cfc-297;/api/product/order/refund/apply+发票 |
+| SC-P-81 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 方案/任务草稿保存与恢复 | plan.draft.save | RED | 未跑通 | S-cfc-298;存草稿→恢复,有效期+冲突提示 |
+| SC-P-82 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 全局搜索(报告/方案/任务/商品) | global.search.query | RED | 未跑通 | S-cfc-299;§4.59 未实现 /api/search/global,需新建 |
+| SC-P-83 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 意见反馈/在线客服 | feedback.consult.submit | RED | 未跑通 | S-cfc-303;/api/health/feedback/submit 持久化+AI 解答 |
+| SC-P-84 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 分享报告/方案给家人或规划师 | share.link.generate | RED | 未跑通 | S-cfc-300;/api/share/create 统一入口+qrcode |
+| SC-P-85 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 多端登录/会话互斥 | auth.session.mutex | RED | 未跑通 | S-cfc-301;新设备踢下线/二次验证,退出清 token |
+| SC-P-86 | 家长 | 业务域 L:交易延伸 / 内容 / 多端 | 成长打卡榜/家庭排行 | growth.leaderboard.view | RED | 未跑通 | S-cfc-302;/api/game/leaderboard+/api/badges/ranking/family |
+| SC-P-87 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 任务完成触发五维能量按规则归因增长 | energy.ruleEngine.apply | RED | 未跑通 | S-cfc-304;任务审批通过→EnergyRuleEngine 归因到对应维度+写 EnergyLog/EnergyBalance,重复审批幂等,未配置归因维度不 500 |
+| SC-P-88 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 心愿兑换扣减五维能量(余额不为负) | energy.ruleEngine.apply | RED | 未跑通 | S-cfc-304;deduct-for-wish 扣减写负流水,余额不足拒不出现负余额,并发兑换幂等 |
+| SC-P-89 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 手动发放能量 grant | energy.admin.adjust | RED | 未跑通 | S-cfc-305;家长/运营手动 grant 留痕,负额/超大额 400,非本家庭 403 |
+| SC-P-90 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 五维每日获取上限 DailyLimit | energy.dailyLimit.guard | RED | 未跑通 | S-cfc-306;单维度单日触顶不累加次日重置,多来源共享日上限,跨时区以服务端日期为准 |
+| SC-P-91 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 健康报告入库刷新五维评分 | energy.admin.adjust | RED | 未跑通 | S-cfc-305;报告确认→刷新 HealthDimensionScore(七维/六维)+概览/沙盘/score-history,重复入库幂等 |
+| SC-P-92 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 五维权重/来源比例配置变更后重算 | energy.admin.adjust | RED | 未跑通 | S-cfc-305;configure/权重变更→概览/沙盘/天盘按新口径重算,权重和≠100% 报警,非 admin 改全局 403 |
+| SC-P-93 | 家长 | 业务域 C-2(家长):五维能量变化规则 | 文章互动与活动参与触发五维能量 | energy.ruleEngine.apply | RED | 未跑通 | S-cfc-304;发布/评论/活动参与按规则加对应维度能量,受 DailyLimit 约束,刷量风控拦截,内容下架回收 |
 | SC-P-94 | 家长 | 业务域(家长):成就与成长 | 我的成就「完成任务」→ 历史完成任务页面 | task.history.view | YELLOW | 未跑通 | 实现已在 origin/cfclub(ed8f1c347);三类流证据待补 |
 | SC-P-95 | 家长 | 业务域(家长):成就与成长 | 我的成就「成长记录」→ 成长历史记录页(含今日成长打卡记录) | growth.history.view | YELLOW | 未跑通 | 实现已在 origin/cfclub(da2727f53);三类流证据待补 |
 | SC-P-96 | 家长 | 业务域(家长):成就与成长 | 我的成就固定五列显示 | achievement.grid.fiveCol | YELLOW | 未跑通 | 实现已在 origin/cfclub(e205656ee);三类流证据待补 |
@@ -240,34 +240,34 @@ SC-G-02 / SC-G-06 / SC-P-03 / SC-P-10 / SC-P-11 共 5 个 SC 已于 2026-11-06 O
 | 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-02 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 商品管理(上架/编辑) | — | — | 未跑通 | — |
+| SC-S-01 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 入驻申请 | admin.supplier.manage | RED | 未跑通 | S-cfc-200;SupplyManage create/update + EcomSupplier save |
+| SC-S-02 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 商品管理(上架/编辑) | admin.supplier.manage | RED | 未跑通 | S-cfc-200 1:N;SupplierProductController + AdminProductController |
 | SC-S-03 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 订单处理(发货) | supplier.order.fulfill | RED | 未跑通 | — |
 | SC-S-04 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 订单核销(到店) | supplier.order.fulfill | RED | 未跑通 | — |
-| SC-S-05 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 库存管理 | — | — | 未跑通 | — |
-| SC-S-06 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 售后/退款处理 | — | — | 未跑通 | — |
-| SC-S-07 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 营销活动报名 | — | — | 未跑通 | — |
+| SC-S-05 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 库存管理 | supplier.inventory.manage | RED | 未跑通 | S-cfc-239;AdminInventoryController /api/admin/inventory |
+| SC-S-06 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 售后/退款处理 | supplier.afterSales.handle | RED | 未跑通 | S-cfc-240;AdminAfterSalesController + AdminProductOrder refund |
+| SC-S-07 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 营销活动报名 | supplier.activity.attend | RED | 未跑通 | S-cfc-241;AdminActivityRegistration + Checkin |
 | SC-S-08 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 对账/结算 | supplier.order.fulfill | RED | 未跑通 | — |
-| SC-S-09 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 报告解析服务接入(对接解析中台) | — | — | 未跑通 | — |
-| SC-S-10 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 营养/食谱数据供给 | — | — | 未跑通 | — |
-| SC-S-11 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 数据导出合规(授权使用) | — | — | 未跑通 | — |
-| SC-B-01 | AI成长教练(AI管家) | 业务域 A:问题解答 | 成长咨询对话 | — | — | 未跑通 | — |
-| SC-B-02 | AI成长教练(AI管家) | 业务域 B:任务跟踪 | 主动成长提醒 | — | — | 未跑通 | — |
-| SC-B-04 | AI成长教练(AI管家) | 业务域 A:问题解答 | 成长报告智能解读 | — | — | 未跑通 | — |
-| SC-B-05 | AI成长教练(AI管家) | 业务域 C:安全与韧性(横切) | 敏感问题拦截与隐私保护 | — | — | 未跑通 | — |
-| SC-B-06 | AI成长教练(AI管家) | 业务域 C:安全与韧性(横切) | LangGraph 不可用 Fallback | — | — | 未跑通 | — |
-| SC-B-07 | AI成长教练(AI管家) | 业务域 B:任务跟踪 | 个性化任务/饮食推荐与采纳 | — | — | 未跑通 | — |
-| SC-B-09 | AI成长教练(AI管家) | 业务域 B:任务跟踪 | 教练转接成长规划师 | — | — | 未跑通 | — |
-| SC-B-10 | AI成长教练(AI管家) | 业务域 B:任务跟踪 | 家庭成长看板(教练聚合视图) | — | — | 未跑通 | — |
-| SC-B-11 | AI成长教练(AI管家) | 业务域 A:问题解答 | 报告细节追问 | — | — | 未跑通 | — |
-| SC-B-12 | AI成长教练(AI管家) | 业务域 B:任务跟踪 | 同步规划师建议主动跟进 | — | — | 未跑通 | — |
-| SC-H-01 | 真人管家 | 业务域 A:人工介入与升级 | 接收 AI 升级并人工介入 | — | — | 未跑通 | — |
-| SC-H-02 | 真人管家 | 业务域 B:一对一服务与协调 | 一对一家长陪跑 | — | — | 未跑通 | — |
-| SC-H-03 | 真人管家 | 业务域 B:一对一服务与协调 | 多方沟通协调 | — | — | 未跑通 | — |
-| SC-H-04 | 真人管家 | 业务域 A:人工介入与升级 | 升级与应急处理 | — | — | 未跑通 | — |
-| SC-H-05 | 真人管家 | 业务域 C:资源对接与协同闭环 | 线下资源与活动对接 | — | — | 未跑通 | — |
-| SC-H-06 | 真人管家 | 业务域 C:资源对接与协同闭环 | 与 AI 管家协同闭环 | — | — | 未跑通 | — |
-| SC-H-07 | 真人管家 | 业务域 D:服务质量 | 服务质检与满意度 | — | — | 未跑通 | — |
+| SC-S-09 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 报告解析服务接入(对接解析中台) | supplier.nutrition.supply | RED | 未跑通 | S-cfc-242 1:N;ReportParseAdmin 解析中台接入 |
+| SC-S-10 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 营养/食谱数据供给 | supplier.nutrition.supply | RED | 未跑通 | S-cfc-242 1:N;AdminRecipe + AdminFood 数据供给 |
+| SC-S-11 | 商品服务商 | 业务域 L:交易延伸 / 内容 / 多端 | 数据导出合规(授权使用) | supplier.data.exportCompliance | RED | 未跑通 | S-cfc-243;数据导出授权链 + 合规审计 |
+| SC-B-01 | AI成长教练(AI管家) | 业务域 A:问题解答 | 成长咨询对话 | ai.coach.consult | RED | 未跑通 | S-cfc-244;AIChatController /api/ai/chat/send 多轮上下文 |
+| SC-B-02 | AI成长教练(AI管家) | 业务域 B:任务跟踪 | 主动成长提醒 | ai.coach.proactive | RED | 未跑通 | S-cfc-245;主动提醒调度+UserNotificationController |
+| SC-B-04 | AI成长教练(AI管家) | 业务域 A:问题解答 | 成长报告智能解读 | ai.coach.reportAnalyze | RED | 未跑通 | S-cfc-246;AIChatController /report/analyze + 解析中台 |
+| SC-B-05 | AI成长教练(AI管家) | 业务域 C:安全与韧性(横切) | 敏感问题拦截与隐私保护 | ai.coach.safetyGuard | RED | 未跑通 | S-cfc-247;敏感词拦截+合规提示+隐私保护 |
+| SC-B-06 | AI成长教练(AI管家) | 业务域 C:安全与韧性(横切) | LangGraph 不可用 Fallback | ai.coach.safetyGuard | RED | 未跑通 | S-cfc-247 1:N;AiGateway Fallback 降级/熔断/不500 |
+| SC-B-07 | AI成长教练(AI管家) | 业务域 B:任务跟踪 | 个性化任务/饮食推荐与采纳 | ai.coach.recommend | RED | 未跑通 | S-cfc-248;五维短板/报告推荐+一键采纳 |
+| SC-B-09 | AI成长教练(AI管家) | 业务域 B:任务跟踪 | 教练转接成长规划师 | ai.coach.handoffPlanner | RED | 未跑通 | S-cfc-250;专业信号识别→引导预约/消息→留痕 |
+| SC-B-10 | AI成长教练(AI管家) | 业务域 B:任务跟踪 | 家庭成长看板(教练聚合视图) | ai.coach.familyBoard | RED | 未跑通 | S-cfc-249;TianpanController /api/tianpan/* 聚合视图 |
+| SC-B-11 | AI成长教练(AI管家) | 业务域 A:问题解答 | 报告细节追问 | ai.coach.consult | RED | 未跑通 | S-cfc-244 1:N;报告上下文追问/超范围提示 |
+| SC-B-12 | AI成长教练(AI管家) | 业务域 B:任务跟踪 | 同步规划师建议主动跟进 | ai.coach.proactive | RED | 未跑通 | S-cfc-245 1:N;方案生效后节奏推送/停滞提示 |
+| SC-H-01 | 真人管家 | 业务域 A:人工介入与升级 | 接收 AI 升级并人工介入 | butler.escalation.handle | RED | 未跑通 | S-cfc-251;收单→研判→联系家长/转规划师→留痕 |
+| SC-H-02 | 真人管家 | 业务域 B:一对一服务与协调 | 一对一家长陪跑 | butler.oneOnOne.coach | RED | 未跑通 | S-cfc-252;建档案→定期回访→随时咨询;SLU 可配 |
+| SC-H-03 | 真人管家 | 业务域 B:一对一服务与协调 | 多方沟通协调 | butler.oneOnOne.coach | RED | 未跑通 | S-cfc-252 1:N;收集→同步→反馈→闭环;不变更方案 |
+| SC-H-04 | 真人管家 | 业务域 A:人工介入与升级 | 升级与应急处理 | butler.escalation.handle | RED | 未跑通 | S-cfc-251 1:N;分级→一级自处/二级升级→留痕复盘 |
+| SC-H-05 | 真人管家 | 业务域 C:资源对接与协同闭环 | 线下资源与活动对接 | butler.resource.match | RED | 未跑通 | S-cfc-253;匹配→推送预约→回访;名额实时校验 |
+| SC-H-06 | 真人管家 | 业务域 C:资源对接与协同闭环 | 与 AI 管家协同闭环 | butler.ai.collaborate | RED | 未跑通 | S-cfc-255;闭环标记→知识反哺→反馈家长;可审计 |
+| SC-H-07 | 真人管家 | 业务域 D:服务质量 | 服务质检与满意度 | butler.quality.qa | RED | 未跑通 | S-cfc-254;质检→满意度回访→回流改进;差评复盘 |
 | SC-X-22 | 跨角色 | 奖励/CF 规则中心(全局 UI 规范) | CF 值获取预估(操作前提示 +N CF) | reward.cfEstimate.expectedCf | DONE | 未跑通 | 键已 DONE(实现闭环),三类流证据待补 |
 
 ---

+ 47 - 0
docs/tdd/test/automation-run-2026-11-13-R189.md

@@ -0,0 +1,47 @@
+# R189 轮自动化测试执行日志(2026-11-13 10:45~11:30)
+
+> 测试负责人|自动化定时任务 ccb91e89 触发|静默无人值守|基线 `00121ff2f`(R188)→ 收尾 HEAD `6124352d7`
+
+## 一、基线与环境
+
+- 基线:R188 收口 `00121ff2f`;当前 HEAD `6124352d7`(11:11,并行会话并发合入)。
+- 窗口 `git diff 00121ff2f..6124352d7`:**src/specs/用例/接口/场景零变更**;34 文件 = 13 新增契约测试类 + docs/台账/汇报 + auto bump。
+- 节点探活:admin/frontend 302→200 在线;189:4090=200;251:4096=200;251:9000 `{"status":"ok"}`;本机 4091=200;**git SSH 22 CLOSED(B-189-1)**;9082=000(B-36 承袭);204=000(B-95 承袭)。
+
+## 二、本轮实测明细(全部本轮执行)
+
+| # | 套件 | 节点 | 结果 | 耗时 | 关键点 |
+|---|------|------|------|------|--------|
+| 1 | admin-desktop E2E | 本机 | **5/5** | 1.4m | TC-A-U01~U05 全过 |
+| 2 | frontend-mobile E2E | 本机 | **6/6** | 46.2s | B-185 维持消除 |
+| 3 | shop-api E2E | 本机 | **4/4** | 6.9s | 价格排序契约 |
+| 4 | health 鉴权契约 | 本机 | **13/13** | 4.2s | TC-HP-U01~03 全 401(B-61 维持) |
+| 5 | admin-desktop E2E | 189:4090 | **5/5 EXIT=0** | 1.3m | 跨节点可比(spec 同源) |
+| 6 | langgraph pytest | 251:4096 | **51/51** | 495.56s | **连续第 2 轮全绿 → B-142-2 销号** |
+| 7 | dx-bridge | 本机 | **2 skip** | 瞬时 | 端点 404(B-DX-1) |
+| 8 | 线上 API 32 例 | 本机 | **25 过/7 跳/0 红** | 28s | api-smoke + nonfunc-security + perf-compat + traceability |
+| 9 | cfc-frontend Jest | 本机 | **609/3/12** | 172s | 3 红 = S-cfc-132/133 预期 RED |
+| 10 | cfc-web Jest | 本机 | **116/116** | 209s | 全绿 |
+| 11 | 根级黑盒 Jest | 本机 | **7/16** | 9.8s | B-36 第 73 轮(9082 未起) |
+| 12 | 窗口 13 契约类定向 | 本机 mvn | **133 例 = 85 过/48 红/0 错** | 2m05s | 48 红全预期 RED;GREEN 3 类(LoginSwitch 16、MessagePush 20、PlanActivate 15)全绿 |
+
+后端全量:**采信 R188**(369 类 / 3111 例 / 3082 过 / 25 红 / 4 跳 = 99.07%),窗口 main 零变更。
+
+## 三、全站口径
+
+- 合计 **4120 例** = 后端 3244(3111采信 + 133契约实测)+ fe 624 + web 116 + lg 51 + 浏览器 30 + API 32 + 根级 23
+- **4003 过 / 92 红 / 25 跳 = 97.16%(剔 skip 97.75%)**
+- 红 92 = 后端存量 25 + 契约预期 RED 48 + fe 预期 RED 3 + 根级 16(环境)
+- 环比 R188(3987/3918/44红/97.27%…98.27%):例 +133、过 +85、红 +48(全为窗口契约预期 RED,零缺陷回归)
+
+## 四、处置与结论
+
+- **B-142-2 销号**:langgraph 连续 2 轮 51/51(R188 + R189)→ 满足销号判据。
+- **B-189-1 新增**:git SSH 22 CLOSED(Gogs 服务故障)→ fetch/push 降级 HTTP 核源(liaoxg/cfc HEAD 与本地一致)。
+- **B-189-2 新增(自处置)**:窗口 13 契约类补验完成,48 RED 全为预期、3 GREEN 全绿,无意外失败。
+- **门禁**:不通过(承袭)——G1 97.16%(−1.11pct 全因契约 RED 入池)、G2 73 红、G9/G11 承袭、G12 ✅、G6/G7/G8/G10/G13 ✅。
+
+## 五、git 证据
+
+- 窗口新增测试资产提交:`64f8a3875`(S-cfc-184)、`5f2a09b02`(S-cfc-183)、`883a6eeab`(S-cfc-188)、`bbc441e08`(S-cfc-189)、`23e2ddd66`(S-cfc-191)、`fb1702975`(S-cfc-196)已由并行会话 push;本地=远端 `6124352d7`。
+- 本轮测试侧产物(R189 报告/执行日志/notice/blockers/台账):提交推送状态见主报告 §七(SSH 故障处置中)。

+ 49 - 0
docs/tdd/test/automation-run-2026-11-13-R190.md

@@ -0,0 +1,49 @@
+# R190 轮自动化测试执行日志(2026-11-13 11:45~12:35)
+
+> 测试负责人|自动化定时任务触发|静默无人值守|基线 `2313791a8`(R189)→ 收尾 HEAD `0e6f285da`
+
+## 一、基线与环境
+
+- 基线:R189 收口 `2313791a8`(auto-bump);当前 HEAD `0e6f285da`(12:28 并发 rebase 后:含 S-cfc-186 契约类 + S-cfc-182 merge + 2 份 docs)。
+- 窗口 `git diff 2313791a8..0e6f285da`:**src/specs/用例/接口/场景零变更**;唯一变更 = 2 个契约测试类(`TeacherFamilyOverviewContractTest` S-cfc-186 +249 / `TeacherReportProxyContractTest` S-cfc-182)+ `report-dev-2026-11-13-1210.md` + `sc-key-map.md`(docs)。
+- 节点探活:本机 4091=200;189:4090=200;**204:4091=200(B-95 修正:openCode 服务在 4091 而非 4090,项目 D:\cfc)**;251:4096=200;251:9000 ok;**git SSH 22 CLOSED(B-189-1 延续)**;9082=000(B-36 承袭)。
+
+## 二、本轮实测明细(全部本轮执行)
+
+| # | 套件 | 节点 | 结果 | 耗时 | 关键点 |
+|---|------|------|------|------|--------|
+| 1 | admin-desktop E2E | 本机 | **5/5** | 1.4m | TC-A-U01~U05 全过 |
+| 2 | frontend-mobile E2E | 本机 | **6/6** | 46.2s | B-185 维持消除 |
+| 3 | shop-api E2E | 本机 | **4/4** | 6.9s | 价格排序契约 |
+| 4 | health 鉴权契约 | 本机 | **13/13** | 4.2s | TC-HP-U01~03 全 401(B-61 维持) |
+| 5 | admin-desktop E2E | 189:4090 | **5/5 EXIT=0** | 1.3m | 跨节点可比(spec 同源) |
+| 6 | frontend-mobile E2E | **204:4091** | **6/6 PASS** | 46.1s | **B-95 观察改善:204 节点恢复**(openCode 在 4091,项目 D:\cfc) |
+| 7 | langgraph pytest | 251:4096 | **50/51** | 269.16s | 唯一红 `test_auto_tag_valid_format`=上游 401(key ****ab12 invalid,B-142-2 复发环境型) |
+| 8 | dx-bridge | 本机 | **2 skip** | 瞬时 | 端点 404(B-DX-1,本轮承袭未重跑) |
+| 9 | 线上 API 32 例 | 本机 | **25 过/7 跳/0 红** | 28s | api-smoke + nonfunc-security + perf-compat + traceability |
+| 10 | cfc-frontend Jest | 本机 | **609/3/12** | 172s | 3 红 = S-cfc-132/133 预期 RED |
+| 11 | cfc-web Jest | 本机 | **116/116** | 209s | 全绿 |
+| 12 | 根级黑盒 Jest | 本机 | **7/16** | 9.8s | B-36 第 74 轮(9082 未起) |
+| 13 | 窗口 S-cfc-186 定向 | 本机 mvn | **7 例 = 6 过/1 预期 RED/0 错** | 1m17s | 1 RED=跨家庭 IDOR(getFamilyMemberTasks 未校验 memberId∈familyId);6 控制用例全过 |
+| 14 | 窗口 S-cfc-182 定向 | 本机 mvn | **14/14 GREEN(BUILD SUCCESS)** | 7.9s | teacher.report.proxy 能力已实现落地 |
+
+后端全量:**采信 R189**(388 类 / 3244 例 / 3171 过 / 73 红 / 0 错 / 0 跳 = 97.75%),窗口 main 零变更。
+
+## 三、全站口径
+
+- 合计 **4141 例** = 后端 3265(3244采信 + 21 窗口契约实测)+ fe 624 + web 116 + lg 51 + 浏览器 30(唯一28 + dx 2 跳)+ API 32 + 根级 23
+- **4026 过 / 94 红 / 21 跳 = 97.22%(剔 skip 97.72%)**
+- 红 94 = 后端存量 25 + 契约预期 RED 49(R189 48 + S-cfc-186 1)+ fe 预期 RED 3 + 根级 16(环境)+ langgraph 1(上游 401 环境型)
+- 环比 R189(4120/4003/92红/97.16%):例 +21(窗口 2 契约类)、过 +23、红 +2(**S-cfc-186 契约预期 RED +1 + langgraph 401 环境红 +1,零缺陷回归**)
+
+## 四、处置与结论
+
+- **B-190-1 新增**:B-142-2 复发——langgraph `test_auto_tag_valid_format` 标签数 0,日志 `401 Authentication Fails, api key ****ab12 invalid`。判别:节点 HEAD=基线 `2313791a` + 窗口源码零变更 → **环境型,非代码回归**。需研发/运维更换 251 上游 AgnesAI key(****ab12)。
+- **B-190-2 新增(自处置)**:窗口 2 契约类(S-cfc-186/182,并行会话提交)补验完成:S-cfc-186 6 过 / 1 预期 RED(跨家庭 IDOR)+ **S-cfc-182 14/14 GREEN**,无意外失败。
+- **B-95 观察改善**:204 节点 openCode 服务实际在 **4091**(历轮误判 4090=000),项目 `D:\cfc`;本轮 fe 6/6 恢复 → 登记观察改善,连续 2 轮 6/6 后销号。
+- **门禁**:不通过(承袭)——G1 97.22%(+0.06pct)、G2 74 红、G7 ⚠️ 50/51(环境红)、G9/G11 承袭、G12 ✅、G6/G8/G10/G13 ✅。
+
+## 五、git 证据
+
+- 窗口新增测试资产提交:`e6b154a73`→`0e6f285da`(S-cfc-186 TeacherFamilyOverviewContractTest)、`5d67a62f6`(S-cfc-182 TeacherReportProxyContractTest,12:16 merge `c6f775ab2`);12:28 并发 rebase 后本地=远端 `0e6f285da`。
+- 本轮测试侧产物(R190 报告/执行日志/notice/blockers/台账):提交推送状态见主报告 §七(SSH 22 故障,经 HTTP 兜底)。

+ 54 - 0
docs/tdd/test/automation-run-2026-11-13-R191.md

@@ -0,0 +1,54 @@
+# R191 轮自动化测试执行日志(2026-11-13 13:14~14:35)
+
+> 测试负责人|自动化定时任务触发|静默无人值守|基线 `309fa0220`(R190 后)→ 跑测中 `123f768c7` → 收尾 `5ad781a00`
+
+## 一、基线与环境
+
+- 基线:起点 `cfclub @ 309fa0220`(=origin/cfclub);跑测窗口内本地 HEAD 推进至 `123f768c7`(含 S-cfc-197 契约类 + auto-bump);收尾经 **HTTP 兜底** fetch(SSH 22 故障,B-189-1 第 2 轮)后 `--ff-only` 快进至 **`5ad781a00`**(+11 提交:S-cfc-199/200 三个契约类 + docs + 版本号)。
+- 窗口 `git diff 309fa0220..5ad781a00`:**main 源码 / specs / test-cases / user-scenarios / API_REFERENCE 零变更**;唯一变更为 **5 个契约测试类**(+1229 行左右)+ cfc-web 版本号 bump + docs。
+- 环境探针(SOP 第一步):`java 1.8.0_25` / `maven 3.6.1` / `node v22.22.2` 可用;本机 9082=closed(B-36 预期)、本机 4091=OPEN;`189:4090`=200、`204:4091`=200、`251:4096`=200、`251:9000`=404(根)/health 200;线上 `/admin`=302、`/frontend`=302、`/`=404;**git SSH 22 不可用 → HTTP 兜底**。
+
+## 二、本轮实测明细(全部本轮执行,无采信项)
+
+| # | 套件 | 节点 | 结果 | 耗时 | 关键点 |
+|---|------|------|------|------|--------|
+| 1 | admin-desktop E2E | 本机 | **5/5** | 1.4m | TC-A-U01~U05 全过(U05 55.3s) |
+| 2 | frontend-mobile E2E | 本机 | **6/6** | 1.9m | B-185 维持消除 |
+| 3 | shop-api E2E | 本机 | **4/4** | 9.7s | 价格排序契约 |
+| 4 | health 鉴权契约 | 本机 | **13/13** | 5.5s | TC-HP-U01~03 全 401;**须用 `playwright.config.health.js`** |
+| 5 | admin-desktop E2E | 189:4090 | **5/5** | 1.3m | HEAD `8524533c8`;`admin-live.spec.js` blob `4703d73fb`=本机 → 可比 |
+| 6 | frontend-mobile E2E | 204:4091 | **6/6** | 44.7s | **连续 2 轮 6/6 → B-95 销号** |
+| 7 | langgraph pytest | 251:4096 | **47/51** | 134.17s | 4 红全 `test_auto_tag`:429 AgnesAI ×3 + 401 `****ab12` ×5 → 环境红 |
+| 8 | langgraph live 三端点 | 251:9000 | `/health` **200/2.07ms**;`/api/v1/chat` **000/40.0s/0B**;`/api/v1/analyze` **000/50.0s/0B** | — | B-186-1 延续;单测绿≠链路可用 |
+| 9 | dx-bridge | 本机 | **2 skip** | 瞬时 | 端点 404(B-DX-1 第 33 轮) |
+| 10 | 线上 API 32 例 | 本机 | **25 过 / 7 跳 / 0 红** | 18.2s | 首跑 26 例/5.4min 异常 → 二次定案(B-191-3) |
+| 11 | cfc-frontend Jest | 本机 | **609 / 3 / 12(624)** | 66.1s | 3 红 = AC-R1 / AC-R2 / AC-2 RED-A7(S-cfc-132/133 预期 RED) |
+| 12 | cfc-web Jest | 本机 | **116/116** | 124.0s | 17 套件全绿 |
+| 13 | 根级黑盒 Jest | 本机 | **7/16(23)** | 7.4s | B-36 第 75 轮;全 `fetch failed`(9082 未起) |
+| 14 | **后端全量** | 本机 mvn | **386 类 / 3315 例 / 3232 过 / 73F+6E+4S = 97.50%** | **1:01h** | 386/386 XML 无沉默;2121 main + 392 test 源 0 错;dump 0 |
+| 15 | 收尾增量 3 契约类 | 本机 mvn | **52 例 / 13 过 / 39 红** | 2m43s | `AdminSupplierManageContractTest` 16(10F)、`SupplierManageAdminContractTest` 19(16F)、`DimensionConfigAdminContractTest` 17(13F) |
+| 16 | 线上匿名鉴权 6 端点 | 线上 | **6/6 = 401** | — | 全部 `{"code":401,"message":"请先登录"}` |
+
+## 三、全站口径
+
+- 合计 **4254 例** = 后端 3367(全量 3315 + 增量 52)+ fe 624 + web 116 + lg 51 + 浏览器 41(39 过 + dx 2 跳)+ API 32 + 根级 23
+- **4088 过 / 141 红 / 25 跳 = 96.10%(剔 skip 96.67%;剔全部预期 RED 98.34%)**
+- 红 141 = 后端 **118**(存量装置/数据 24 + TDD 前瞻契约 94)+ fe 预期 RED 3 + langgraph 环境红 4 + 根级环境 16
+- 环比 R190(4141/4026/94 红/97.22%):例 **+113**(后端窗口 5 契约类 102 + 浏览器跨节点 +2… 计入口径见报告)、过 **+62**、红 **+47**(前瞻契约 +45、环境 +3、装置浮动 −1)→ **零缺陷回归**
+
+## 四、后端红归因(79 全量 + 39 增量 = 118)
+
+| 层级 | 例数 | 类数 | 明细 |
+|---|---|---|---|
+| 存量装置/数据/环境 | **24** | 13 | `EnergySystemFlow` 6F、`GrowthPublicEndpointMapping` 5E(未入库)、`DimensionRegistryMapContract` 3F、`OrchestrationEngine`/`OrchestrationIntegration`/`MembershipPaySwitch`/`PaymentServiceORDRouting`/`TeamProfitCron`/`UserServiceWechatLogin`/`UserServiceWechatLoginContract`/`GuideModuleIntegration`(1E)/`MembershipPlanPurchase`/`TaskReviewScP11` 各 1 |
+| TDD 前瞻契约(待实现) | **94** | 12 | `TeacherMindRecord` 12、`TeacherAppointmentGuard` 10、`AdminKnowledgeBaseManage`(S-cfc-198) 6、`FamilyAutoCreateForPurchase` 4、`PlanContentSafetyReview` 4、`TeacherDataIsolationScT13` 4、`TeacherPlanCreate` 4、`NutritionPlanTeacher` 3、`TeacherAssessmentRecord` 3、`TeacherDimensionScore` 2、`GuideFamilyTaskReview` 2、`TeacherFamilyOverview` 1、远端三类 39 |
+
+## 五、处置与结论
+
+- **B-191-1 新增(P1 流程)**:跑测窗口内远端推入 3 个契约类(52 例 39 红),已按「双 fetch + 增量定向」补验并计入总量,避免 39 例 TDD-RED 被漏记/误判为回归。
+- **B-191-2 新增(P1 装置/性能)**:全量 **1:01h**(R188 同源码 43:15);慢类 `ArticleCategoryControllerTest` **493.97s**、`AdminFamilyOverviewControllerTest` 341.2s、`GrowthPublicEndpointMappingTest` 334.2s、`GrowthRecordSyncFlowTest` 280.7s、`PlannerBindInviteFlowTest` 178.1s;`cfc-scheduler` 日志 20524/73287 = **28.0%**。
+- **B-191-3 新增(P2 工具/度量)**:API 套件首跑 26 例/5.4min、二次 32 例/18.2s → 例数与耗时均不可复现,采集侧须双校验 `Running N tests` 与 summary。
+- **B-95 销号**:204 节点 fe 连续 2 轮 6/6(46.1s / 44.7s)。
+- **S-cfc-197 GREEN**:`AdminReportParserManageContractTest` 37/37。
+- **B-177-1 第 13 轮复现**:summary 打印后 2 进程存活 4m33s,`EXIT=` 不落盘 → 已 `kill -9`,`ps` 复核 0。
+- **门禁**:不通过(有条件)—— G1 96.10% ❌、G2 118 ❌、G7 47/51 ❌、G9 17.2% ❌、G11 漏拦 18/陈旧 26 ❌;G3 ⚠️、G4/G5/G6/G8/G10/G12/G13 ✅。

+ 62 - 0
docs/tdd/test/automation-run-2026-11-13-R192.md

@@ -0,0 +1,62 @@
+# R192 轮自动化测试执行日志(2026-11-13 14:33~15:05)
+
+> 测试负责人|自动化定时任务触发|静默无人值守|基线 `5ad781a00`(R191 收尾)→ 快进 `7558d8cf4`(auto bump,零源码)+ R191 后合入 S-cfc-201 契约类
+
+## 一、基线与环境
+
+- 基线:起点 `5ad781a00`(R191 收尾);窗口内远端推入 `7558d8cf4`(auto bump 零源码)+ 先前合入 **S-cfc-201 契约类**(`AdminArticleManageContractTest`,+540 行,预期 6红18绿)。
+- 前端 SSH 22 不可用(B-189-1 第 3 轮)→ **HTTP 兜底 fetch** 成功,`--ff-only` 快进至 `7558d8cf4`。
+- 窗口 `git diff 5ad781a00..7558d8cf4`:**main 源码 / specs / test-cases / user-scenarios / API_REFERENCE 零变更**;唯一有效变更为 **S-cfc-201 契约测试类(24 例)** + cfc-web 版本号 bump + docs。
+- 环境探针:java 1.8.0_25 / maven 3.6.1 / node v22.22.2 可用;本机 9082=closed(B-36 预期);`189:4090`=200、`251:4096`=200、`251:9000`=health 200;线上 `/admin`=302、`/frontend`=302;git SSH 22 不可用 → HTTP 兜底。
+
+## 二、本轮实测明细(后端全量承袭 R191 同源码基线 + S-cfc-201 定向;其余套件全部本轮实跑)
+
+| # | 套件 | 节点 | 结果 | 耗时 | 关键点 |
+|---|------|------|------|------|--------|
+| 1 | **S-cfc-201 定向** | 本机 mvn | **24 例 / 18 过 / 6 红** | 1m23s | `AdminArticleManageContractTest` 与提交声明「6红18绿」**完全一致** → TDD-RED 落预期 |
+| 2 | admin-desktop E2E | 189:4090 | **5/5** | 1.2m | TC-A-U01~U05 全过(U05 稳定) |
+| 3 | frontend-mobile E2E | 本机 | **6/6** | 55.0s | B-185 维持消除 |
+| 4 | shop-api E2E | 本机 | **4/4** | 12.4s | 价格排序契约 |
+| 5 | health 鉴权契约 | 本机 | **13/13** | 4.2s | TC-HP-U01~03 全 401 |
+| 6 | dx-bridge | 本机 | **2 skip** | 瞬时 | 端点 404(B-DX-1 第 34 轮) |
+| 7 | langgraph pytest | 251:4096 | **47/51** | 123.28s | **红例集漂移**:auto_tag ×2 + intent ×2;429 AgnesAI + 401 `****ab12` 双因 → 环境红 |
+| 8 | langgraph live 三端点 | 251:9000 | `/health` **200/0.9ms**;`/api/v1/chat` **000/20.0s/0B**;`/api/v1/analyze` **000/20.0s/0B** | — | **B-186-1 第 6 轮延续**;按正确 schema 探(query/report_id+user_id)仍挂死 |
+| 9 | cfc-frontend Jest | 本机 | **609 / 3 / 12(624)** | 86.7s | 3 红 = AC-R1 / AC-R2 / AC-2(S-cfc-132/133 预期 RED)零漂移 |
+| 10 | cfc-web Jest | 本机 | **116/116** | 114.3s | 17 套件全绿 |
+| 11 | 根级黑盒 Jest | 本机 | **7/16(23)** | 7.1s | B-36 第 76 轮零漂移;全 `fetch failed`(9082 未起) |
+| 12 | **线上 API 全量** | 本机 | **990 例 / 175 过 / 795 跳 / 20 红** | 13.1m | **口径升级:全量 990 例**(历史仅 32 例子集);20 红同根因 `direct-register` 前置注册失败(B-192-2) |
+| 13 | 线上匿名鉴权 6 端点 | 线上 | **6/6 = 401** | — | mind/fortune、energy/overview、health/plan/list、xiaomi-health/auth-url、huawei-health/status、admin/families/overview 全 `请先登录` |
+| 14 | **后端全量(承袭)** | R191 实测 | **386 类 / 3315 例 / 3232 过 / 73F+6E+4S** | — | main/test 源码零变更(窗口仅 +S-cfc-201)→ 按 R185「同源码沿用 + 增量」纪律承袭 |
+| 15 | 后端收尾增量(承袭) | R191 实测 | **52 例 / 13 过 / 39 红** | — | S-cfc-199/200 三类,同 HEAD 无源码变化 |
+
+## 三、全站口径
+
+- 合计 **5225 例** = 后端 3391(承袭 3315 + 承袭增量 52 + S-cfc-201 24)+ fe 624 + web 116 + lg 51 + 浏览器 30(28 过 + dx 2 跳)+ API 990 + 根级 23
+- **4245 过 / 167 红 / 813 跳 = 81.24%(含 skip)|剔 skip 96.22%**
+- 红 167 = 后端 **124**(S-cfc-201 预期 6 + 承袭装置/前瞻 118)+ fe 预期 RED 3 + langgraph 环境红 4 + **API 探测 20** + 根级环境 16
+- 环比 R191(4254/4088/96.10%):例 **+971**(API 全量口径 +958、S-cfc-201 +24、浏览器 −11)、过 **+157**、红 **+26** → 红增量 = **S-cfc-201 前瞻 6 + API 全量首测探测 20**,无缺陷回归;浏览器 −11 系「每节点单任务」纪律(189 admin 已覆盖本机 admin,204 由本机 fe 覆盖且 B-95 已销号)
+
+## 四、后端红归因(S-cfc-201 6 + 承袭 118 = 124)
+
+| 层级 | 例数 | 类数 | 明细 |
+|---|---|---|---|
+| 本轮新增(预期 RED) | **6** | 1 | `AdminArticleManageContractTest` 6F(S-cfc-201 前瞻,与提交声明一致) |
+| 承袭存量装置/数据/环境 | **24** | 13 | 同 R191:`EnergySystemFlow` 6F、`GrowthPublicEndpointMapping` 5E、`DimensionRegistryMapContract` 3F、其余 10 类各 1 |
+| 承袭 TDD 前瞻契约 | **94** | 12 | 同 R191:Teacher 系 7 类 36 + AdminKnowledgeBaseManage 6 + 远端三类 39 等 |
+
+## 五、处置与结论
+
+- **B-192-1 新增(P1 观测)**:langgraph 红例集**依配额漂移**——R191 全 `test_auto_tag` 4 红,本轮 auto_tag 2 + intent 2;429/401 双因仍在线(结构未变,仅作用例位)。**单测绿 ≠ 链路可用**(B-186-1 同根因)。→ 与 B-142-2 合并处置:运维轮换 key + 升级配额。
+- **B-192-2 新增(P1 工具/契约链)**:API 套件**首测全量 990 例**,20 红跨 9 域(badge 4 / wish-flow 6 / task-checkin 3 / coach-food 2 / task-frequency 1 / task-c14 1 / incentive 1 / energy 1 / wechat-login 1)**同根因**:helpers/api.js `direct-register` 前置注册返回 `body.code=undefined`(线上接口未按预期回 200)→ 后续断言连锁失败。复验 badge 单文件 1F(C-09 断链),证实装置级而非 20 个产品缺陷。**历史 32 例子集从未覆盖这些域 → 属首测新覆盖,须研发归因 direct-register 契约**。
+- **S-cfc-201(RED 前瞻确认)**:24 例 6 红与声明一致,待研发实现 `admin.article.manage` 使绿。
+- **B-177-1 第 14 轮复现**:summary 已打印后 2 进程存活 07:26 → 已 `kill -9`,`ps` 复核 0。
+- **B-142-1 维持销号第 22 轮**:匿名 6 端点全 401。
+- **门禁**:不通过(承袭)—— G1 剔 skip 96.22% ❌、G2 红 ≥ 阈值 ❌、G7 47/51 ❌、G9 锚点覆盖率 ❌、G11 CI 清单漏拦/陈旧 ❌;并发三新项(S-cfc-201 前瞻 6 + API 探测 20 + 环境 4)非缺陷回归。✅G4 编译 0 错 / G5 XML 无沉默 / G6 浏览器 28/28+2 跳 / G8 匿名 6 端点 401 / G10 节点可比 / G12 SRS 零变更 / G13 存量零漂移第 27 次。
+---
+
+## 六、收尾补充(R192 推送后窗口尾部合入取证)
+
+- **S-cfc-203(admin.activity.config)**:主报告推送后研发合入 `AdminActivityConfigContractTest`(209 行,merge `cee6a85be`,声明 TDD-RED 5红)→ **定向实测 5 例 / 5F / 0E / 0S(1m20s),与声明完全一致**。
+- **S-cfc-202(admin.order.manual)**:S-cfc-203 取证推送前 fetch 拦截到研发再次合入 `AdminOrderManualContractTest`(548 行,`4a17435d3`,声明 TDD-RED 5红,B-142-1/TR-13 权限流)→ **定向实测 24 例 / 5F / 0E / 0S(3.3s),与声明完全一致**。5 红全部为预期权限流缺口:订单 6 端点 [detail, ship, update-express, close, refund/approve, refund/reject] + 售后 4 端点 [list, detail, review, complete] 缺 `@RequestAttribute("role")` admin 守卫;order/list 非 admin(roles=[parent])被放行返回 200(期望 403);refund/approve、售后 review 非 admin 越权成功。
+- **全站口径终值(含 S-cfc-203 + S-cfc-202)**:**5254 例 / 4264 过 / 177 红 / 813 跳(剔 skip 96.01%)**;红 +10 均为纯前瞻契约 RED(S-cfc-203 5 + S-cfc-202 5),**仍零缺陷回归**。
+- 后端终值:3391 + 5 + 24 = **3420 例 / 3282 过 / 134 红 / 4S**。

+ 17 - 2
docs/tdd/test/automation-status.md

@@ -23,8 +23,8 @@
 | 用户场景(md) | 11 | 0 | 0 | 11 | 11 | 0 | 0 | 0 | 0 |
 | Playwright(e2e/integration) | 96 | 571 | 96 | 0 | 86 | 0 | 7 | 3 | 0 |
 | Python(pytest) | 11 | 48 | 11 | 0 | 11 | 0 | 0 | 0 | 0 |
-| Java后端测试 | 375 | 3096 | 375 | 0 | 358 | 0 | 15 | 2 | 0 |
-| **合计** | — | **4434** | **545** | **667** | 890 | 255 | 36 | 5 | 26 |
+| Java后端测试 | 390 | 3250 | 390 | 0 | 373 | 0 | 15 | 2 | 0 |
+| **合计** | — | **4588** | **560** | **667** | 905 | 255 | 36 | 5 | 26 |
 
 ## 二、结构化测试用例自动化状态(`tests/test-cases/*.md`)
 
@@ -1141,6 +1141,7 @@
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/EnergyIsolationContractTest.java` | 8 | 已自动化(@Test) | 未跟踪 | EnergyIsolationContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/EnergyLogsContractScoreHistoryTest.java` | 11 | 已自动化(@Test) | 未跟踪 | EnergyLogsContractScoreHistory |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/EnergyOverviewContractTest.java` | 7 | 已自动化(@Test) | 未跟踪 | EnergyOverviewContract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyAutoCreateForPurchaseContractTest.java` | 5 | 已自动化(@Test) | 未跟踪 | FamilyAutoCreateForPurchaseContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyJoinValidationRulesContractTest.java` | 14 | 已自动化(@Test) | 未跟踪 | FamilyJoinValidationRulesContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyMemberDimensionBaseScoreServiceTest.java` | 7 | 已自动化(@Test) | 未跟踪 | FamilyMemberDimensionBaseScoreService |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyMemberRelationshipLabelTest.java` | 8 | 已自动化(@Test) | 未跟踪 | FamilyMemberRelationshipLabel |
@@ -1171,6 +1172,7 @@
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyAccessGuardTest.java` | 10 | 已自动化(@Test) | 未跟踪 | GuideFamilyAccessGuard |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyQueryServiceTest.java` | 4 | 已自动化(@Test) | 未跟踪 | GuideFamilyQueryService |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyServiceBindTest.java` | 3 | 已自动化(@Test) | 未跟踪 | GuideFamilyServiceBind |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyTaskReviewContractTest.java` | 5 | 已自动化(@Test) | 未跟踪 | GuideFamilyTaskReviewContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/GuidePlanReviewControllerAuthTest.java` | 6 | 已自动化(@Test) | 未跟踪 | GuidePlanReviewControllerAuth |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/HealthDataSourceBridgeContractTest.java` | 16 | 已自动化(@Test) | 未跟踪 | HealthDataSourceBridgeContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/HealthPlanDeleteCascadeTest.java` | 6 | 已自动化(@Test) | 未跟踪 | HealthPlanDeleteCascade |
@@ -1190,10 +1192,12 @@
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/MembershipPlanPurchaseContractTest.java` | 7 | 已自动化(@Test) | 未跟踪 | MembershipPlanPurchaseContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/MembershipRenewScP60ScP80ContractTest.java` | 9 | 已自动化(@Test) | 未跟踪 | MembershipRenewScP60ScP80Contract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/MembershipViewScP59ContractTest.java` | 6 | 已自动化(@Test) | 未跟踪 | MembershipViewScP59Contract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/NutritionPlanTeacherContractTest.java` | 4 | 已自动化(@Test) | 未跟踪 | NutritionPlanTeacherContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/OrderCreatePayShipConfirmScX07ContractTest.java` | 14 | 已自动化(@Test) | 未跟踪 | OrderCreatePayShipConfirmScX07Contract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/OrderRefundScP56ScA12ContractTest.java` | 11 | 已自动化(@Test) | 未跟踪 | OrderRefundScP56ScA12Contract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/PersonalLayerDualAssertionTest.java` | 13 | 已自动化(@Test) | 未跟踪 | PersonalLayerDualAssertion |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/PlanActivationExecutionContractTest.java` | 14 | 已自动化(@Test) | 未跟踪 | PlanActivationExecutionContract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/PlanContentSafetyReviewContractTest.java` | 9 | 已自动化(@Test) | 未跟踪 | PlanContentSafetyReviewContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/PlanLifecycleParentContractTest.java` | 38 | 已自动化(@Test) | 未跟踪 | PlanLifecycleParentContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/PlanReviewTeacherContractTest.java` | 19 | 已自动化(@Test) | 未跟踪 | PlanReviewTeacherContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/PointsLedgerScP15ContractTest.java` | 8 | 已自动化(@Test) | 未跟踪 | PointsLedgerScP15Contract |
@@ -1229,6 +1233,17 @@
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/TaskReviewScT06ContractTest.java` | 6 | 已自动化(@Test) | 未跟踪 | TaskReviewScT06Contract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/TaskTaskCreateContractTest.java` | 7 | 已自动化(@Test) | 未跟踪 | TaskTaskCreateContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/TaskTemplatePackageContractTest.java` | 13 | 已自动化(@Test) | 未跟踪 | TaskTemplatePackageContract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherAppointmentGuardContractTest.java` | 14 | 已自动化(@Test) | 未跟踪 | TeacherAppointmentGuardContract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherAssessmentRecordContractTest.java` | 4 | 已自动化(@Test) | 未跟踪 | TeacherAssessmentRecordContract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherDataIsolationScT13ContractTest.java` | 9 | 已自动化(@Test) | 未跟踪 | TeacherDataIsolationScT13Contract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherDimensionScoreContractTest.java` | 6 | 已自动化(@Test) | 未跟踪 | TeacherDimensionScoreContract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherFamilyOverviewContractTest.java` | 7 | 已自动化(@Test) | 未跟踪 | TeacherFamilyOverviewContract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherLoginSwitchContractTest.java` | 16 | 已自动化(@Test) | 未跟踪 | TeacherLoginSwitchContract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherMessagePushContractTest.java` | 20 | 已自动化(@Test) | 未跟踪 | TeacherMessagePushContract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherMindRecordContractTest.java` | 13 | 已自动化(@Test) | 未跟踪 | TeacherMindRecordContract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherPlanActivateContractTest.java` | 15 | 已自动化(@Test) | 未跟踪 | TeacherPlanActivateContract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherPlanCreateContractTest.java` | 13 | 已自动化(@Test) | 未跟踪 | TeacherPlanCreateContract |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherReportProxyContractTest.java` | 14 | 已自动化(@Test) | 未跟踪 | TeacherReportProxyContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeamProfitAdminManageContractTest.java` | 20 | 已自动化(@Test) | 未跟踪 | TeamProfitAdminManageContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeamProfitCronContractTest.java` | 22 | 已自动化(@Test) | 未跟踪 | TeamProfitCronContract |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeamProfitHookContractTest.java` | 7 | 已自动化(@Test) | 未跟踪 | TeamProfitHookContract |

+ 87 - 0
docs/tdd/test/blockers.md

@@ -1323,3 +1323,90 @@
 | **B-142-1(销号维持第 18 轮)** | 匿名端点鉴权 | 研发(已闭环) | **维持销号**。6 端点 **6/6 = 401**(mind/fortune、energy/overview、health/plan/list、xiaomi-health、huawei-health、admin/families/overview)。 | 测试(R188 实测) | 2026-11-13 09:55 | 已闭(观察维持) |
 | **B-124/B-32(度量口径)** | 锚点/场景口径跨轮不可比 | 测试(P2) | **仍未闭环**。官方脚本:场景 215 / 映射 155=72.1% / 字面 33=15.3% / TC 861 / 异常流 502=58.3% / 锚点 115 / 资产 546;严格口径沿用 **20.1%(62/308)**。 | 测试 | 2026-11-13 09:55 | 待处理 |
 | **R188 门禁** | 全量 3987 / **3918** | 测试 | **不通过(有条件,明显正向)**。✅G1 **98.27%**(剔 skip 98.89%,+12 过)/G4 编译 0 错(2121+375)/G5 369 XML 0 dump(恒等式 3097−4+18=3111)/G6 28 过+2 跳/G8 6/6 401/G10 双节点(189 5/5 + 251 51 例)/G12 SRS 零变更/G13 存量零漂移第 23 次;⚠️G3 609-624(3 预期 RED)/G7 **51/51(观察 1/2)**;❌G2 **25 红**、G9 20.1%、G11 漏拦 6 陈旧 26、G14 57.9%、G16 无 SCA。**变化** = 后端 28→25 红(全量实跑)、lg 6→0、fe H5 2→0 → 红 56→**44**、通过率 98.0%→**98.27%**。 | 测试(R188 闭环) | 2026-11-13 09:55 | 已记录 |
+
+---
+
+## R189 段(2026-11-13 11:30,新增)
+
+| **B-189-1(P1 新增·环境)** | git SSH 22 CLOSED | 运维(环境) | **推送已解除(SSH 故障延续)**。`git.iwintrue.com` / `192.168.16.251:22` **CLOSED**(R188 09:39 尚可 push)→ SSH `git fetch/push` 全部失败;**兜底:push URL 切 `http://git.iwintrue.com/liaoxg/cfc.git` 后 rebase+push 成功**(远端 HEAD 由 `6124352d7` → `67fb0a1f5`,含 R189 全部产物;fetch 仍走 SSH 失败,须临时指定 HTTP URL)。**SSH 通道本身仍待运维检查 Gogs/251 sshd 服务(端口 22)**。 | 测试(R189 实测) | 2026-11-13 11:30 | HTTP 兜底已推送,SSH 待修 |
+| **B-189-2(P2 新增·自处置)** | 窗口 13 契约类未逐类验证即并行推入 | 测试(流程) | **补验完成**。R188 后并行会话推入 13 个契约测试类(S-cfc-183/184/188/189/191/196 等)未逐类验证;本轮定向实测 **133 例 = 85 过 / 48 红 / 0 错(2m05s)**:48 红**全部为预期 RED(TDD 先行)**;GREEN 3 类(LoginSwitch 16/16、MessagePush 20/20、PlanActivate 15/15)全绿可收尾。**流程修正**:下轮起窗口内新增测试类必须逐类定向取证再入账。 | 测试(R189 实测) | 2026-11-13 11:30 | 已处置(补验闭环) |
+| **B-142-2(销号,2026-11-13)** | langgraph 上游 LLM 不可达 | 运维/研发(P0→已销) | **正式销号**。R188 51/51 + R189 **51/51(495.56s)** = 连续 2 轮全绿 → 满足销号判据。残留 live chat 链路(120s 零字节)单列 **B-186-1** 持续观察。 | 测试(R189 实测) | 2026-11-13 11:30 | ✅ 已销号 |
+| **B-186-1(第 3 轮观察)** | langgraph live `/api/v1/chat` 挂死 | 运维/研发(P1) | **延续**。pytest 51/51 绿(mock 路径)但 live chat 链路上轮仍 120s 零字节 → 真实链路未验证;需轮换 key + 提额。 | 运维/研发 | 2026-11-13 11:30 | 待处理 |
+| **S-cfc-183/184/188/196 等(48 前瞻 RED,本轮取证)** | 窗口 13 契约类 10 类 48 红 | 研发(P1,最紧急) | **全部预期 RED**(TDD 先行、研发未实现):MindRecord 12F / AppointmentGuard 10F / PlanContentSafety 4F(S-cfc-183)/ TeacherAssessmentRecord 3F(S-cfc-184)/ FamilyAutoCreate 4F(S-cfc-188)/ TeacherPlanCreate 4F(S-cfc-196)/ DataIsolation 4F / PlanReview 部分 / Nutrition 3F / DimensionScore 2F。逐键清单见 `notice-2026-11-13-R189-研发通知.md` §一 P1。 | 测试(R189 定向实测) | 2026-11-13 11:30 | 待处理(研发实现使绿) |
+| **S-cfc-189/191/Activate(3 GREEN 全绿)** | 契约 GREEN 确认 | 研发(可直接收尾) | LoginSwitch **16/16**(bbc441e08)/ MessagePush **20/20**(23e2ddd66)/ PlanActivate **15/15** 全绿 → 契约侧已闭环,可直接收 YELLOW/GREEN。 | 测试(R189 定向实测) | 2026-11-13 11:30 | 已取证(待研发登记) |
+| **B-177-1(第 11 轮)** | 251 pytest 打印 summary 后进程不退出 | 研发/测试(P1) | **观察 4/2(收口但 EXIT 行仍缺失)**。本轮 495.56s 正常收口输出 summary,`EXIT=` 行仍缺失(B-126 表象);残留进程已清理。修法不变:`app/main.py:84` lifespan + `aclose()`;派发器 `timeout -k 10 900`。 | 研发/测试 | 2026-11-13 11:30 | 待处理 |
+| **B-36(第 73 轮零漂移)** | 根级黑盒 23 例 16 红 | 测试(P2 环境) | **零漂移**。`cd tests && npx jest --silent` → **7 过 / 16 红**(9.8s),全 `ECONNREFUSED 9082`。 | 测试 | 2026-11-13 11:30 | 待处理 |
+| **B-95(第 44 轮)** | `192.168.16.204:4090` 失联 | 运维(P2) | **延续**。204=000;189:4090=**200(admin 5/5 EXIT=0)**、251:4096=200。 | 运维 | 2026-11-13 11:30 | 待处理 |
+| **B-DX-1(第 31 轮)** | dx-bridge 端点 404 | 部署/研发(P1) | **延续**。本轮 dx 2 例 skip(404)。 | 部署/研发 | 2026-11-13 11:30 | 待处理 |
+| **B-152-1/2(第 20 轮)** | CI `BACKEND_OBSERVED_CLASSES` 失真 | 研发/CI(P0/P1) | **延续**。漏拦 6 + 陈旧 26 未修;本轮建议同步接入 13 个新契约类。 | 研发/CI | 2026-11-13 11:30 | 待处理 |
+| **B-156-1 / B-170-1(第 13/10 轮)** | 迁移 DDL 缺陷 | 研发(P1/P2) | **延续未修**。`schema.sql:5345` 索引列缺失 / `DatabaseInitializer.java:2261` 别名 UPDATE 派生表引用。 | 研发 | 2026-11-13 11:30 | 待处理 |
+| **B-162-1(第 15 轮)** | 3 个测试类未入库 | 治理(P1) | **延续**。`GrowthPublicEndpointMappingTest`(5E)等仍 `??`。 | 研发/测试 | 2026-11-13 11:30 | 待处理 |
+| **B-178-1(第 8 轮)** | TaskReviewScP11 反射装置浮动 | 测试(P2) | **延续 2F** → `@MockBean` 替代 `ReflectionUtils`。 | 测试 | 2026-11-13 11:30 | 待处理 |
+| **B-142-1(销号维持第 19 轮)** | 匿名端点鉴权 | 研发(已闭环) | **维持销号**。health 三端点(plan/adjust、attribution、generate)全 **401**;API 安全套件 11 例全绿。 | 测试(R189 实测) | 2026-11-13 11:30 | 已闭(观察维持) |
+| **R189 门禁** | 全量 4120 / **4003** | 测试 | **不通过(承袭,无回归)**。✅G4 编译 0 错/G5 定向 133 surefire XML 完整/G6 28 过+2 跳/G7 **51/51(连续 2 轮,B-142-2 销号)**/G8 401 全绿/G10 双节点/G12 SRS 零变更/G13 存量零漂移第 24 次;⚠️G3 609-624(3 预期 RED);❌G1 **97.16%**(剔 skip 97.75%,−1.11pct **全因 48 契约预期 RED 入池**)/G2 **73 红**(存量 25 + 契约预期 RED 48)/G9 20.1%/G11 漏拦 6 陈旧 26。**变化** = 例 +133(窗口 13 契约类)、过 +85、红 +48(**全为预期 RED,零缺陷回归**);langgraph 连续 2 轮全绿 **B-142-2 销号**;浏览器六件套全绿、API 0 红、fe/web 零漂移。 | 测试(R189 闭环) | 2026-11-13 11:30 | 已记录 |
+
+---
+
+## R190 段(2026-11-13 12:35,新增)
+
+| **B-190-1(P1 新增·环境= B-142-2 复发)** | langgraph 上游 401 `key ****ab12 invalid` | 运维/研发(P1,最紧急) | **复发 1 红**。langgraph@251 `test_auto_tag_valid_format` 标签数 0(空 answer);日志实证 `Error code: 401 - Authentication Fails, Your api key: ****ab12 is invalid`。判别:节点 HEAD=基线 `2313791a` + 窗口源码零变更 → **环境型,非代码回归**。B-142-2 由「销号」转「**复发观察 1/2**」:**请更换/恢复 251 上游 AgnesAI API key(****ab12)**,恢复后连续 2 轮 51/51 再销号。 | 测试(R190 实测) | 2026-11-13 12:35 | 观察 1/2(等 key 恢复) |
+| **B-190-2(P2 新增·自处置)** | 窗口 2 契约类未逐类验证即并行推入 | 测试(流程) | **补验完成**。窗口并行会话推入 2 个契约类:`TeacherFamilyOverviewContractTest`(S-cfc-186,e6b154a73→0e6f285da,SC-T-16/17/21/27,纯 Mockito 7 例)+ `TeacherReportProxyContractTest`(S-cfc-182,5d67a62f6/12:16 merge,14 例)。本轮定向实测 **21 例 = S-cfc-186 6 过 / 1 预期 RED / 0 错(1m17s)**:1 RED = **跨家庭 IDOR**(`GuideFamilyTaskController.getFamilyMemberTasks` 仅校验 `requireBound(guideId, familyId)` 未校验 `memberId∈familyId`),6 控制用例全过;**S-cfc-182 14/14 GREEN(7.9s)** → 完全符合 TDD 预期。**流程修正**(延续 B-189-2):窗口新增测试类必须逐类定向取证再入账。 | 测试(R190 实测) | 2026-11-13 12:35 | 已处置(补验闭环) |
+| **S-cfc-182(GREEN 本轮取证)** | teacher.report.proxy 契约 | 研发(可直接收尾) | **14/14 GREEN(BUILD SUCCESS,7.9s)** → 契约侧已闭环,可直接收 YELLOW/GREEN。 | 测试(R190 定向实测) | 2026-11-13 12:35 | 已取证(待研发登记) |
+| **B-95(第 45 轮→观察改善)** | `192.168.16.204` 节点失联 | 运维(P2) | **观察改善**。历轮 `204:4090=000` 为**误判**:openCode 服务实际在 **204:4091**(项目 `D:\cfc`)→ 本轮 frontend-mobile **6/6 PASS(46.1s)**。登记「观察改善」,连续 2 轮 6/6 可复现后销号。 | 测试(R190 实测) | 2026-11-13 12:35 | 观察改善(1/2) |
+| **B-189-1(P1 延续·环境)** | git SSH 22 CLOSED | 运维(环境) | **延续(HTTP 兜底已推送)**。`git.iwintrue.com` / `192.168.16.251:22` 仍 CLOSED;本轮 push URL 切 `http://git.iwintrue.com/liaoxg/cfc.git` 后 rebase+push 成功(远端 HEAD=本轮 R190 产物 sha)。**SSH 通道仍待运维检查 Gogs/251 sshd 服务(端口 22)**。 | 测试(R190 实测) | 2026-11-13 12:35 | HTTP 兜底已推送,SSH 待修 |
+| **S-cfc-186(1 前瞻 RED,本轮取证)** | teacher.family.overview 跨家庭 IDOR | 研发(P1) | **预期 RED**(TDD 先行、研发未实现):`getFamilyMemberTasks` 需补 `memberId∈familyId` 属主校验(拒绝跨家庭访问,不得进入 `taskService.getTaskHistoryByFamilyMember`);6 控制用例已全过。逐键清单见 `notice-2026-11-13-R190-研发通知.md` §一 P2。 | 测试(R190 定向实测) | 2026-11-13 12:35 | 待处理(研发实现使绿) |
+| **B-186-1(第 4 轮观察)** | langgraph live `/api/v1/chat` 挂死 | 运维/研发(P1) | **延续**。pytest 50/51(401 环境红)但 live chat 链路仍挂(120s 零字节);与 B-142-2 同根因(上游 key 失效)。 | 运维/研发 | 2026-11-13 12:35 | 待处理 |
+| **B-177-1(第 12 轮)** | 251 pytest 打印 summary 后进程不退出 | 研发/测试(P1) | **观察维持**。本轮 269.16s 正常收口输出 summary,`EXIT=` 行仍缺失(B-126 表象,以 summary 终值为准);残留进程已前置清理。修法不变:`app/main.py:84` lifespan + `aclose()`;派发器 `timeout -k 10 900`。 | 研发/测试 | 2026-11-13 12:35 | 待处理 |
+| **B-36(第 74 轮零漂移)** | 根级黑盒 23 例 16 红 | 测试(P2 环境) | **零漂移**。`cd tests && npx jest --silent` → **7 过 / 16 红**(9.8s),全 `ECONNREFUSED 9082`。 | 测试 | 2026-11-13 12:35 | 待处理 |
+| **B-DX-1(第 32 轮)** | dx-bridge 端点 404 | 部署/研发(P1) | **承袭**(本轮未重跑,R189 2 skip 维持)。 | 部署/研发 | 2026-11-13 12:35 | 待处理 |
+| **B-152-1/2(第 21 轮)** | CI `BACKEND_OBSERVED_CLASSES` 失真 | 研发/CI(P0/P1) | **延续**。漏拦 6 + 陈旧 26 未修;本轮建议同步接入 S-cfc-186/182 新契约类。 | 研发/CI | 2026-11-13 12:35 | 待处理 |
+| **B-156-1 / B-170-1(第 14/11 轮)** | 迁移 DDL 缺陷 | 研发(P1/P2) | **延续未修**。`schema.sql:5345` 索引列缺失 / `DatabaseInitializer.java:2261` 别名 UPDATE 派生表引用。 | 研发 | 2026-11-13 12:35 | 待处理 |
+| **B-162-1(第 16 轮)** | 3 个测试类未入库 | 治理(P1) | **延续**。`GrowthPublicEndpointMappingTest`(5E)等仍 `??`。 | 研发/测试 | 2026-11-13 12:35 | 待处理 |
+| **B-178-1(第 9 轮)** | TaskReviewScP11 反射装置浮动 | 测试(P2) | **延续 2F** → `@MockBean` 替代 `ReflectionUtils`。 | 测试 | 2026-11-13 12:35 | 待处理 |
+| **B-142-1(销号维持第 20 轮)** | 匿名端点鉴权 | 研发(已闭环) | **维持销号**。health 三端点全 **401**;API 安全套件 11 例全绿。 | 测试(R190 实测) | 2026-11-13 12:35 | 已闭(观察维持) |
+| **R190 门禁** | 全量 4141 / **4026** | 测试 | **不通过(承袭,无回归,正向项明确)**。✅G4 编译 0 错(含 S-cfc-186/182 test-compile)/G5 定向 21 surefire XML 完整/G6 **28 过+2 跳承袭**(admin 5/5 本机+189、fe 6/6 本机+**204**、shop 4/4、health 13/13)/G8 401 全绿/G10 **三节点**(189 + **204 恢复** + 251)/G12 SRS 零变更/G13 存量零漂移第 25 次;⚠️G3 609-624(3 预期 RED)、G7 **50/51(B-142-2 复发环境型)**;❌G1 **97.22%**(剔 skip 97.72%,+0.06pct)/G2 **74 红**(存量 25 + 契约预期 RED 49)/G9 20.1%/G11 漏拦 6 陈旧 26。**变化** = 例 +21(窗口 2 契约类 S-cfc-186 7 + S-cfc-182 14)、过 +23、红 +2(**S-cfc-186 预期 RED +1 + langgraph 401 环境红 +1,零缺陷回归**);**204 节点 fe 6/6 恢复(B-95 观察改善)**;浏览器任务级全绿、API 0 红、fe/web 零漂移;**S-cfc-182 已 GREEN**。 | 测试(R190 闭环) | 2026-11-13 12:35 | 已记录 |
+
+## R191 段(2026-11-13 14:35,HEAD `5ad781a00`,后端全量实跑 1:01h)
+
+| 编号 | 关联键/对象 | 类型 | 描述与处置 | 经办 | 登记时间 | 状态 |
+|---|---|---|---|---|---|---|
+| **B-191-1(P1 新增·流程/证据)** | 跑测窗口内多次推入契约类 | 测试/流程 | **已自处置**。窗口内本地 HEAD `309fa0220`→`123f768c7`(+S-cfc-197 37 例)+ 收尾 fetch +11 提交(`5ad781a00`,+S-cfc-199/200 三类 52 例)。若只跑一次全量即定稿,39 例 TDD-RED 会被记为「未覆盖」。**已按「双 fetch(提交前+推送前)+ `src/test/**` 命中即逐类定向」处置**:52 例全部取证并计入总量。下轮起写入 SOP §阶段 4。 | 测试(R191 实测) | 2026-11-13 14:35 | 已处置(SOP 化) |
+| **B-191-2(P1 新增·装置/性能)** | 后端全量耗时劣化 | 研发/测试 | 同源码 **43:15(R188)→ 1:01h(本轮,+41%)**。慢类:`ArticleCategoryControllerTest` **493.97s**、`AdminFamilyOverviewControllerTest` 341.2s、`GrowthPublicEndpointMappingTest` 334.2s、`GrowthRecordSyncFlowTest` 280.7s、`PlannerBindInviteFlowTest` 178.1s(前 5 占 ~45%);`cfc-scheduler` 日志 **20524/73287 = 28.0%**(B-152-3 复现)。根因=controller 层 `@SpringBootTest` 全上下文 + 缺 `src/test/resources/application-test.yml`(每类重跑 `DatabaseInitializer`)+ B-156-1/B-170-1 迁移语法错。**建议**:增设 `application-test.yml`(禁 `@EnableScheduling`)+ 慢类改 `@WebMvcTest`+`@MockBean` + 先修两条 DDL。 | 测试(R191 实测) | 2026-11-13 14:35 | 待处理 |
+| **B-191-3(P2 新增·工具/度量)** | 线上 API 套件例数不可复现 | 测试/工具 | 同命令首跑 **26 例(19 过 + 7 跳)/5.4min**,二次(`--reporter=list`)**32 例(25 过 + 7 跳)/18.2s**(与 R190 一致)。首跑 6 例未计入、耗时高 18 倍。**采集侧须双校验**:`Running N tests` 的 N = 点名文件静态用例数 **且** summary pass+fail+skip = N;不符即重跑并记录两次口径。本轮以二次运行(32 例)定案。 | 测试(R191 实测) | 2026-11-13 14:35 | 待处理(采集口径) |
+| **B-95(销号)** | 204 节点 fe 失联 | 环境 | **销号**。204 fe **连续 2 轮 6/6**(R190 46.1s + R191 44.7s);openCode 服务在 **204:4091**、项目 `D:\cfc`。 | 测试(R191 实测) | 2026-11-13 14:35 | **已闭** |
+| **S-cfc-197(GREEN 确认)** | `admin.reportParser.manage` | 研发(收尾) | **37/37 全绿**(`AdminReportParserManageContractTest`)→ 实现已落地,可直接转 YELLOW/GREEN 并移出 CI 排除清单。 | 测试(R191 实测) | 2026-11-13 14:35 | 待研发登记收尾 |
+| **S-cfc-198(6 前瞻 RED)** | `admin.knowledgeBase.manage` | 研发(P1) | **预期 RED**:① 同步失败**重试**缺失(无 `maxRetries`/`syncWithRetry`/`@Retryable`);② 大批量**分批**缺失(无 `syncBatch`/`BATCH_SIZE`);③ `KnowledgeBaseController`/`KnowledgeBaseVectorController` **无平台角色门禁**(应对齐 `AdminUnifiedKnowledgeController.allowed`)。13 例中 7 绿。 | 测试(R191 实测) | 2026-11-13 14:35 | 待处理(研发实现使绿) |
+| **S-cfc-199/200(39 前瞻 RED)** | dimension.config / supplier.manage | 研发(P1) | `DimensionConfigAdminContractTest` 17 例 13F;`AdminSupplierManageContractTest` 16 例 10F;`SupplierManageAdminContractTest` 19 例 16F(410 废弃壳 / 边界门禁 / role 守卫 / SC-A-06 停用在售商品守卫)。 | 测试(R191 定向实测) | 2026-11-13 14:35 | 待处理(研发实现使绿) |
+| **B-142-2(复发·第 2 轮)** | 251 上游 LLM 凭据/配额 | 运维(P0) | **复发**。pytest 47/51,4 红全 `test_auto_tag`;`429 AgnesAI 免费额度耗尽` ×3 + `401 api key ****ab12 invalid` ×5。节点 `cfc-langgraph` 与本地零 diff → 环境型。**需更换 key + 升级配额**;连续 2 轮 51/51 后再销号。 | 测试(R191 实测) | 2026-11-13 14:35 | 待处理 |
+| **B-186-1(第 5 轮)** | langgraph live chat/analyze 挂死 | 运维/研发(P1) | **延续**。`/health` 200/2.07ms,但 `/api/v1/chat` **000/40.0s/0B**、`/api/v1/analyze` **000/50.0s/0B**。**单测绿 ≠ 链路可用**,与 B-142-2 同根因。 | 运维/研发 | 2026-11-13 14:35 | 待处理 |
+| **B-177-1(第 13 轮)** | 251 pytest 打印 summary 后不退出 | 研发/测试(P1) | **复现**。`4 failed, 47 passed in 134.17s` 已打印,2 进程存活 **4m33s**(`Sl`),`EXIT=` 永不落盘;`kill -9` 后 `ps` 复核 0。修法:`app/main.py` shutdown `await client.aclose()` + 派发器 `timeout -k 10 900`。 | 研发/测试 | 2026-11-13 14:35 | 待处理 |
+| **B-152-1/2(第 22 轮·恶化)** | CI `BACKEND_OBSERVED_CLASSES` 失真 | 研发/CI(P0/P1) | **恶化**。清单 33 类 vs 实测红类 **25** → **漏拦 18**(R190 为 6,新增 S-cfc-197/198/199/200 及 Teacher 系契约类)、**陈旧 26** → `backend-blocking-junit` 必红。建议按实测 25 类重写并加「红类↔清单自动差集」步骤。 | 研发/CI | 2026-11-13 14:35 | 待处理 |
+| **B-162-1(第 17 轮)** | 测试类未入库 | 治理(P1) | **延续**。`GrowthPublicEndpointMappingTest`(5E,单类 334.2s)/ `JwtInterceptorGrowthPolicyTest` / `ArticleTagsContractTest` 仍 `??`,但参与编译与执行(本轮 392 测试源 vs HEAD 追踪 392 → 需入库以稳基线纯度)。 | 研发/测试 | 2026-11-13 14:35 | 待处理 |
+| **B-178-1(第 10 轮)** | TaskReviewScP11 反射装置浮动 | 测试(P2) | **延续**。本轮 **1F**(R190 为 2F)→ 无规则浮动,建议 `@MockBean` 替代 `ReflectionUtils`。 | 测试 | 2026-11-13 14:35 | 待处理 |
+| **B-36(第 75 轮零漂移)** | 根级黑盒 23 例 16 红 | 测试(P2 环境) | **零漂移**。`cd tests && npx jest --config=jest.config.js` → **7 过 / 16 红**(7.4s),全 `fetch failed`(本机 9082 未起)。 | 测试 | 2026-11-13 14:35 | 待处理 |
+| **B-DX-1(第 33 轮)** | dx-bridge 端点 404 | 部署/研发(P1) | **承袭**。两端点 404/404 → 2 skip;规格 `2026-11-08-dx-health-bridge.md` 明示「仅登记不实现」,skip 正确。 | 部署/研发 | 2026-11-13 14:35 | 待处理(待派发实现) |
+| **B-189-1(第 2 轮)** | git SSH 22 CLOSED | 运维(P1 环境) | **延续**。`git fetch origin`(SSH)报 `Please make sure you have the correct access rights`;改 `http://git.iwintrue.com/liaoxg/cfc.git` 后 fetch + push 成功(远端 HEAD=本轮产物)。建议把 fetch URL 一并改 HTTP 或恢复 22 端口。 | 运维 | 2026-11-13 14:35 | HTTP 兜底已推送,SSH 待修 |
+| **B-156-1 / B-170-1(第 15/12 轮)** | 迁移 DDL 缺陷 | 研发(P1/P2) | **延续未修**。`schema.sql:5354` 索引列缺失(`nutrition_age_dosage` 建表恒失败);`DatabaseInitializer.java:2261` 别名 UPDATE 派生表引用(`Unknown column 'a.id'`)。本轮 `cfc-scheduler` 日志占 28.0% 部分由此放大。 | 研发 | 2026-11-13 14:35 | 待处理 |
+| **B-142-1(销号维持第 21 轮)** | 匿名端点鉴权 | 研发(已闭环) | **维持销号**。六端点(mind/fortune、energy/overview、health/plan/list、xiaomi-health、huawei-health、admin/families/overview)全 **401 `请先登录`**。 | 测试(R191 实测) | 2026-11-13 14:35 | 已闭(观察维持) |
+| **R191 门禁** | 全量 **4254 / 4088** | 测试 | **不通过(有条件)**。✅G4 编译 0 错(2121+392 源,含 5 契约类)/G5 **386/386 XML 无沉默、dump 0**/G6 **39 过 + 2 跳**(admin 5/5 本机+189、fe 6/6 本机+**204**、shop 4/4、health 13/13)/G8 **6/6=401**/G10 三节点可比(189 blob `4703d73fb`=本机、204、251 源码零 diff)/G12 SRS 零变更/G13 存量零漂移**第 26 次**;⚠️G3 609-624(3 预期 RED)、G14 62.3%;❌G1 **96.10%**(剔 skip 96.67%,剔预期 RED **98.34%**)/G2 **118 红**(存量 24 + 前瞻契约 94)/G7 **47/51**(429+401 环境)/G9 **17.2%**/G11 漏拦 **18** 陈旧 26。**变化** = 例 +113、过 +62、红 +47(**前瞻契约 +45、环境 +3、装置浮动 −1,零缺陷回归**);后端由采信转**全量实跑**;**B-95 销号**、**S-cfc-197 GREEN**。 | 测试(R191 闭环) | 2026-11-13 14:35 | 已记录 |
+
+## R192 段(2026-11-13 15:05,HEAD `7558d8cf4`,API 全量口径升级,S-cfc-201 定向验证)
+
+| 编号 | 关联键/对象 | 类型 | 描述与处置 | 经办 | 登记时间 | 状态 |
+|---|---|---|---|---|---|---|
+| **B-192-1(P1 新增·观测)** | 251 langgraph 红例集漂移 | 运维(P0 同 B-142-2) | **本轮 auto_tag ×2 + intent ×2**(R191 为 auto_tag ×4)→ 429 AgnesAI 免费额度 + 401 key `****ab12` 双因仍在线,仅作用例位漂移。与 B-142-2 合并处置:**轮换 key + 升级配额**;连续 2 轮 51/51 再销号。 | 测试(R192 实测) | 2026-11-13 15:05 | 待处理 |
+| **B-192-2(P1 新增·工具/契约链)** | 线上 API 全量首测 20 红 | 研发/测试 | **全量 990 例首测**(历史仅 32 例子集,未覆盖这些域):175 过 / 795 跳 / **20 红**,跨 9 域(badge 4、wish-flow 6、task-checkin 3、coach-food 2、task-frequency/task-c14/incentive/energy/wechat-login 各 1)**同根因**:`tests/e2e/api/helpers/api.js` 的 `direct-register` 前置注册返回 `body.code=undefined`(线上未回 200)→ 断言链连锁失败;复验 badge 单文件 1F(C-09 断链)证实装置/契约链级。**建议**:研发核对 `direct-register` 线上契约(注册接口路径/请求体/返回结构),测试同步修 helper 断言。 | 测试(R192 实测) | 2026-11-13 15:05 | 待处理(研发归因 + 测试修订) |
+| **S-cfc-201(RED 前瞻确认)** | `admin.article.manage` | 研发(P1) | `AdminArticleManageContractTest` **24 例 6F 18 绿,与提交声明完全一致** → 待研发实现文章发布-审核使绿。 | 测试(R192 定向实测) | 2026-11-13 15:05 | 待处理(研发实现使绿) |
+| **B-186-1(第 6 轮)** | langgraph live chat/analyze 挂死 | 运维/研发(P1) | **延续**。`/health` 200/0.9ms;按正确 schema 探 `/api/v1/chat`(query)**000/20.0s/0B**、`/api/v1/analyze`(report_id+user_id)**000/20.0s/0B**。单测绿≠链路可用,与 B-142-2 同根因。 | 运维/研发 | 2026-11-13 15:05 | 待处理 |
+| **B-177-1(第 14 轮)** | 251 pytest 打印 summary 后不退出 | 研发/测试(P1) | **复现**。`47 passed` 已打印,bash+pytest 2 进程存活 07:26,`EXIT=` 不落盘 → `kill -9` 后 `ps` 复核 0。修法:`app/main.py` shutdown `await client.aclose()` + 派发器 `timeout -k 10 900`。 | 研发/测试 | 2026-11-13 15:05 | 待处理 |
+| **B-142-1(销号维持第 22 轮)** | 匿名端点鉴权 | 研发(已闭环) | **维持销号**。六端点(mind/fortune、energy/overview、health/plan/list、xiaomi-health/auth-url、huawei-health/status、admin/families/overview)全 **401 `请先登录`**。 | 测试(R192 实测) | 2026-11-13 15:05 | 已闭(观察维持) |
+| **B-36(第 76 轮零漂移)** | 根级黑盒 23 例 16 红 | 测试(P2 环境) | **零漂移**。`cd tests && npx jest --ci` → **7 过 / 16 红**(7.1s),全 `fetch failed`(本机 9082 未起)。 | 测试 | 2026-11-13 15:05 | 待处理 |
+| **B-DX-1(第 34 轮)** | dx-bridge 端点 404 | 部署/研发(P1) | **承袭**。两端点 404/404 → 2 skip(规格「仅登记不实现」)。 | 部署/研发 | 2026-11-13 15:05 | 待处理 |
+| **R192 门禁** | 全量 **5225 / 4245** | 测试 | **不通过(承袭)**。✅G4 编译 0 错 / G5 XML 无沉默 / G6 **28 过 + 2 跳**(189 admin 5/5 + 本机 fe 6/shop 4/health 13 + dx 2 跳)/G8 **6/6=401** / G10 节点可比 / G12 SRS 零变更 / G13 存量零漂移**第 27 次**;⚠️G3 609-624(3 预期 RED)、G14 62.3%;❌G1 **剔 skip 96.22%** /G2 **167 红**(后端 124 = S-cfc-201 前瞻 6 + 承袭存量 24 + 承袭前瞻 94;fe 3;lg 4;**API 探测 20**;根级 16)/G7 **47/51**(环境)/G9 锚点 ❌ /G11 CI 清单漏拦/陈旧 ❌。**变化** = 例 +971(API 全量 +958、S-cfc-201 +24、浏览器 −11)、过 +157、红 +26(**S-cfc-201 前瞻 6 + API 首测探测 20,零缺陷回归**)。 | 测试(R192 闭环) | 2026-11-13 15:05 | 已记录 |
+
+###(R192 收尾补充)S-cfc-203 窗口尾部合入,已定向取证
+
+| 编号 | 关联键/对象 | 类型 | 描述与处置 | 经办 | 登记时间 | 状态 |
+|---|---|---|---|---|---|---|
+| **S-cfc-203(RED 前瞻确认)** | `admin.activity.config` | 研发(P1) | R192 主报告推送后由研发合入 `AdminActivityConfigContractTest`(209 行,预期 **5 红**);**已定向实测 5 例/5F/0E = 与声明完全一致** → 全站合计调整为 **5230 例 / 4245 过 / 172 红 / 813 跳**(剔 skip 96.11%)。待研发实现活动配置校验使绿。 | 测试(R192 定向实测) | 2026-11-13 15:18 | 待处理(研发实现使绿) |
+| **S-cfc-202(RED 前瞻确认)** | `admin.order.manual` | 研发(P1) | S-cfc-203 取证推送前 fetch 拦截研发再次合入 `AdminOrderManualContractTest`(548 行,`4a17435d3`,预期 **24 例 5 红 19 绿**,B-142-1/TR-13 权限流);**已定向实测 24 例/5F/0E/0S(3.3s)= 与声明完全一致**。5 红全为预期权限流缺口:订单 6 端点 [detail, ship, update-express, close, refund/approve, refund/reject] + 售后 4 端点 [list, detail, review, complete] 缺 `@RequestAttribute("role")` admin 守卫;order/list 非 admin(roles=[parent])放行 200(期望 403);refund/approve、售后 review 非 admin 越权成功 → 全站合计**终值 5254 例 / 4264 过 / 177 红 / 813 跳**(剔 skip 96.01%)。待研发补 admin 角色守卫使绿(与 B-142-1/TR-13 同域)。 | 测试(R192 定向实测) | 2026-11-13 15:32 | 待处理(研发实现使绿) |

+ 17 - 2
docs/tdd/test/inventory.md

@@ -11,9 +11,9 @@
 | 结构化测试用例(md) | 10 | 719 |
 | Playwright(spec.js) | 96 | 571 |
 | Python(pytest) | 11 | 48 |
-| Java后端(测试文件) | 375 | 3096 (@Test) |
+| Java后端(测试文件) | 390 | 3250 (@Test) |
 | 用户场景(md) | 11 | 文件级 |
-| **合计** | — | **4434** 用例/方法 |
+| **合计** | — | **4588** 用例/方法 |
 
 ## 二、结构化测试用例(`tests/test-cases/*.md`)
 
@@ -1136,6 +1136,7 @@
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/EnergyIsolationContractTest.java` | cfc-backend | 8 | EnergyIsolationContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/EnergyIsolationContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/EnergyLogsContractScoreHistoryTest.java` | cfc-backend | 11 | EnergyLogsContractScoreHistory | `cfc-backend/src/test/java/com/etotem/cfc/unit/EnergyLogsContractScoreHistoryTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/EnergyOverviewContractTest.java` | cfc-backend | 7 | EnergyOverviewContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/EnergyOverviewContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyAutoCreateForPurchaseContractTest.java` | cfc-backend | 5 | FamilyAutoCreateForPurchaseContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyAutoCreateForPurchaseContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyJoinValidationRulesContractTest.java` | cfc-backend | 14 | FamilyJoinValidationRulesContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyJoinValidationRulesContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyMemberDimensionBaseScoreServiceTest.java` | cfc-backend | 7 | FamilyMemberDimensionBaseScoreService | `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyMemberDimensionBaseScoreServiceTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyMemberRelationshipLabelTest.java` | cfc-backend | 8 | FamilyMemberRelationshipLabel | `cfc-backend/src/test/java/com/etotem/cfc/unit/FamilyMemberRelationshipLabelTest.java` |
@@ -1166,6 +1167,7 @@
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyAccessGuardTest.java` | cfc-backend | 10 | GuideFamilyAccessGuard | `cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyAccessGuardTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyQueryServiceTest.java` | cfc-backend | 4 | GuideFamilyQueryService | `cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyQueryServiceTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyServiceBindTest.java` | cfc-backend | 3 | GuideFamilyServiceBind | `cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyServiceBindTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyTaskReviewContractTest.java` | cfc-backend | 5 | GuideFamilyTaskReviewContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyTaskReviewContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/GuidePlanReviewControllerAuthTest.java` | cfc-backend | 6 | GuidePlanReviewControllerAuth | `cfc-backend/src/test/java/com/etotem/cfc/unit/GuidePlanReviewControllerAuthTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/HealthDataSourceBridgeContractTest.java` | cfc-backend | 16 | HealthDataSourceBridgeContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/HealthDataSourceBridgeContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/HealthPlanDeleteCascadeTest.java` | cfc-backend | 6 | HealthPlanDeleteCascade | `cfc-backend/src/test/java/com/etotem/cfc/unit/HealthPlanDeleteCascadeTest.java` |
@@ -1185,10 +1187,12 @@
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/MembershipPlanPurchaseContractTest.java` | cfc-backend | 7 | MembershipPlanPurchaseContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/MembershipPlanPurchaseContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/MembershipRenewScP60ScP80ContractTest.java` | cfc-backend | 9 | MembershipRenewScP60ScP80Contract | `cfc-backend/src/test/java/com/etotem/cfc/unit/MembershipRenewScP60ScP80ContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/MembershipViewScP59ContractTest.java` | cfc-backend | 6 | MembershipViewScP59Contract | `cfc-backend/src/test/java/com/etotem/cfc/unit/MembershipViewScP59ContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/NutritionPlanTeacherContractTest.java` | cfc-backend | 4 | NutritionPlanTeacherContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/NutritionPlanTeacherContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/OrderCreatePayShipConfirmScX07ContractTest.java` | cfc-backend | 14 | OrderCreatePayShipConfirmScX07Contract | `cfc-backend/src/test/java/com/etotem/cfc/unit/OrderCreatePayShipConfirmScX07ContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/OrderRefundScP56ScA12ContractTest.java` | cfc-backend | 11 | OrderRefundScP56ScA12Contract | `cfc-backend/src/test/java/com/etotem/cfc/unit/OrderRefundScP56ScA12ContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/PersonalLayerDualAssertionTest.java` | cfc-backend | 13 | PersonalLayerDualAssertion | `cfc-backend/src/test/java/com/etotem/cfc/unit/PersonalLayerDualAssertionTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/PlanActivationExecutionContractTest.java` | cfc-backend | 14 | PlanActivationExecutionContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/PlanActivationExecutionContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/PlanContentSafetyReviewContractTest.java` | cfc-backend | 9 | PlanContentSafetyReviewContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/PlanContentSafetyReviewContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/PlanLifecycleParentContractTest.java` | cfc-backend | 38 | PlanLifecycleParentContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/PlanLifecycleParentContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/PlanReviewTeacherContractTest.java` | cfc-backend | 19 | PlanReviewTeacherContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/PlanReviewTeacherContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/PointsLedgerScP15ContractTest.java` | cfc-backend | 8 | PointsLedgerScP15Contract | `cfc-backend/src/test/java/com/etotem/cfc/unit/PointsLedgerScP15ContractTest.java` |
@@ -1224,6 +1228,17 @@
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/TaskReviewScT06ContractTest.java` | cfc-backend | 6 | TaskReviewScT06Contract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TaskReviewScT06ContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/TaskTaskCreateContractTest.java` | cfc-backend | 7 | TaskTaskCreateContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TaskTaskCreateContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/TaskTemplatePackageContractTest.java` | cfc-backend | 13 | TaskTemplatePackageContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TaskTemplatePackageContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherAppointmentGuardContractTest.java` | cfc-backend | 14 | TeacherAppointmentGuardContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherAppointmentGuardContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherAssessmentRecordContractTest.java` | cfc-backend | 4 | TeacherAssessmentRecordContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherAssessmentRecordContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherDataIsolationScT13ContractTest.java` | cfc-backend | 9 | TeacherDataIsolationScT13Contract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherDataIsolationScT13ContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherDimensionScoreContractTest.java` | cfc-backend | 6 | TeacherDimensionScoreContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherDimensionScoreContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherFamilyOverviewContractTest.java` | cfc-backend | 7 | TeacherFamilyOverviewContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherFamilyOverviewContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherLoginSwitchContractTest.java` | cfc-backend | 16 | TeacherLoginSwitchContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherLoginSwitchContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherMessagePushContractTest.java` | cfc-backend | 20 | TeacherMessagePushContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherMessagePushContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherMindRecordContractTest.java` | cfc-backend | 13 | TeacherMindRecordContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherMindRecordContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherPlanActivateContractTest.java` | cfc-backend | 15 | TeacherPlanActivateContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherPlanActivateContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherPlanCreateContractTest.java` | cfc-backend | 13 | TeacherPlanCreateContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherPlanCreateContractTest.java` |
+| `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherReportProxyContractTest.java` | cfc-backend | 14 | TeacherReportProxyContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeacherReportProxyContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeamProfitAdminManageContractTest.java` | cfc-backend | 20 | TeamProfitAdminManageContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeamProfitAdminManageContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeamProfitCronContractTest.java` | cfc-backend | 22 | TeamProfitCronContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeamProfitCronContractTest.java` |
 | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeamProfitHookContractTest.java` | cfc-backend | 7 | TeamProfitHookContract | `cfc-backend/src/test/java/com/etotem/cfc/unit/TeamProfitHookContractTest.java` |

+ 62 - 0
docs/tdd/test/notice-2026-11-13-R192-研发通知.md

@@ -0,0 +1,62 @@
+# R192 测试闭环闭环研发通知(2026-11-13)
+
+**基线**:`7558d8cf4`(R191 收尾 `5ad781a00` + 远程 auto bump)
+**门限**:不通过(承袭),门禁 G1/G2/G7/G9/G11 ❌
+
+---
+
+## 一、本轮核心变化
+
+### 1. API 全量契约首测 —— B-192-2(P1,需研发归因)
+- **口径升级**:历史只跑子集 32 例(25过/7跳/0红);本轮首次全量跑 `playwright.config.api.js` = **990 例 / 175过 / 795skip / 20红**。
+- **20 红同根因**:跨 badge/wish-flow/task-checkin/coach-food/task-frequency/task-c14/incentive/energy/wechat-login 九域,**链式失败源于 `tests/e2e/api/helpers/api.js` 的 `direct-register` 前置注册**在当前线上环境返回 `body.code=undefined`(未回 200),导致后续用例断言连锁失败。复验单文件 1 红(badge C-09 断链),证实装置/契约链问题而非分散的产品缺陷。
+- **影响范围**:这些契约用例历史上从未全量跑过 → 这是覆盖提升暴露的新发现,**非回归**。但直接阻塞这 20 个域的 API 链路契约有效性验证。
+- **建议动作**:
+  - 研发查 `direct-register` 当前线上契约(路径/请求体/返回结构是否变了?),确认 helper 是否需要同步。
+  - 修复后重跑这些 spec 验证。
+
+### 2. S-cfc-201 RED 前瞻契约落地观测(已知预期)
+- HEAD 含合入 `ab6c15ae0` Merge task/S-cfc-201,契约类 `AdminArticleManageContractTest` 24 例实测 **6F 18 绿**,与提交声明完全一致。
+- **待研发实现**:`admin.article.manage` 文章发布-审核使绿。
+
+### 3. 后端承袭口径稳定
+- 窗口 main 源码零变更 → 后端全量承袭 R191 同源码基线 3315 例/3232 过/79 红 + 收尾增量 52 例/13 过/39 红(R191 已跑)+ 本轮 S-cfc-201 定向 24 例/18 过/6 红 = **合计 3391 例 / 3263 过 / 124 红**。
+- 存量装置/数据红 24 与 R191 一致,无新回归。
+
+---
+
+## 二、待下轮关注事项
+
+| 优先级 | 项 | 说明 |
+|---|---|---|
+| **P0** | B-142-2(key/配额) | 251 langgraph key `****ab12` 轮换 + AgnesAI 免费额度升级,否则 B-186-1 与 B-192-1 持续 |
+| **P1** | B-192-2(direct-register) | 研发核对线上注册接口契约 + 测试 helper 同步 |
+| **P1** | S-cfc-201(RED 前瞻) | `admin.article.manage` 实现使绿 |
+| **P1** | B-177-1(pytest 挂死) | `app/main.py` shutdown `await client.aclose()` + 派发器 `timeout -k 10 900` |
+| **P2** | B-184-1(Maven 慢类) | 增 `application-test.yml` 禁 `@EnableScheduling` + 迁移语法修 |
+
+---
+
+## 三、统计摘要
+
+| 套件 | 结果 | 关键观察 |
+|---|---|---|
+| 后端全量(承袭) | 3315 例 / 3232 过 / 79 红 | 零源码变更 |
+| 后端增量-S-cfc-201 | 24 例 / 18 过 / **6 红** | 与承诺一致 |
+| fe Jest | 609 / 3 / 12 | 3 预期 RED |
+| cfc-web Jest | 116 / 116 | 全绿 |
+| 浏览器 E2E | 28 过 / 2 跳 | 189 admin 5/5 + 本机 fe/shop/health + dx 2 skip |
+| langgraph pytest@251 | 47 / 4 | 环境红,例集漂移 |
+| langgraph live 三端点 | chat/analyze 000/20s | B-186-1 第 6 轮延续 |
+| 线上 API 全量 | 175 / 795 skip / **20 红** | **B-192-2 direct-register 同根因** |
+| 匿名鉴权 6 端点 | 6/6 = 401 | 维持销号 |
+| 根级黑盒 | 7 / 16 | B-36 第 76 轮 |
+| **全站** | **5225 / 4245** | 剔 skip **96.22%** · 红 167 |
+
+---
+
+## 四、Git 证据
+
+- 本地 HEAD:`ab6c15ae0` → 快进至远端 `7558d8cf4`
+- 推送内容:`automation-run-2026-11-13-R192.md` + `blockers.md` + `notice-...R192-...md`
+- SSH fetch 仍不可用 → HTTP 兜底生效

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio