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

test(diet): 边界回归套件改名并补食材推荐变更发布说明

QaBoundaryVerificationTest -> DietRecommendationBoundaryTest(类名与文件头同步,12 项 64 断言逐字保留,纳入常规回归);新增发布说明覆盖 5 项 CR + 2 个 Bugfix、迁移 465/466、接口契约、测试结论、发布前后清单、残留项与回滚提示。主交付 181e830be,QA_PASS YES(后端 52/52、前端 5 suites / 91 passed / 2 skipped)。
asus 1 день назад
Родитель
Сommit
b42b5463b5

+ 4 - 2
cfc-backend/src/test/java/com/etotem/cfc/unit/QaBoundaryVerificationTest.java → cfc-backend/src/test/java/com/etotem/cfc/unit/DietRecommendationBoundaryTest.java

@@ -46,11 +46,13 @@ import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
 /**
- * T5 独立验收临时测试(QA / Edward)。不修改任何业务代码与基线测试,跑完即删除。
+ * 食材推荐食谱边界 / 错误路径常规回归套件(QA / Edward)。
+ * 来源:T5 独立验收临时套件 QaBoundaryVerificationTest,第 2 轮回归全绿后纳入常规回归。
+ * 断言与用例逐字保留,未因源码修复而削弱。不修改任何业务代码与基线测试。
  * 覆盖:删除后 status 白名单 / 非法 menu_json / 菜数 N=0..7 / AI 不足·超出 / AI 空菜单 /
  * view_key 老客户端与双视角 / regenerate / 家庭总览分段聚合。
  */
-class QaBoundaryVerificationTest {
+class DietRecommendationBoundaryTest {
 
     private static final ObjectMapper OM = new ObjectMapper();
     private static final String DATE = "2026-11-10";

+ 154 - 0
docs/2026-11-11-食材推荐食谱变更-发布说明.md

@@ -0,0 +1,154 @@
+# 食材推荐 · 今日食谱增量变更 发布说明
+
+| 项 | 内容 |
+|---|---|
+| 发布日期 | 2026-11-11 |
+| 分支 | `cfclub` |
+| 主交付提交 | `181e830be feat(diet): 食材推荐-今日食谱增量变更(菜数硬保证/删除菜品/翻页修复/家庭总览聚合口径)` |
+| 收尾提交 | 边界回归套件改名 + 本说明(与本次一并入库) |
+| 关联文档 | `docs/需求分析/2026-11-10-食材推荐食谱需求变更.md`(PRD v1.1,38 条 AC)、`docs/需求分析/2026-11-10-食材推荐食谱需求变更-影响面与任务分解.md`(架构 v1.1,T1~T5) |
+| 验收结论 | `QA_PASS: YES`(第 2 轮回归,硬上限内一次通过),**0 阻断项** |
+| 涉及模块 | `cfc-backend` / `cfc-frontend`(uni-app 小程序)/ `cfc-langgraph` / DB 迁移 / `system_prompts` |
+| 代码规模 | 25 文件,4238 insertions / 217 deletions |
+
+---
+
+## 一、本次发布内容
+
+| # | 变更 | 说明 |
+|---|------|------|
+| CR-1 | 今日食谱按视角隔离 | 新增 `view_key`,成员切换后各自看到自己的食谱;家庭总览固定 `family` |
+| CR-2 | 「替换」改「删除」 | 删除菜品复用 `/update`,删除后不回空态、不静默补菜 |
+| CR-3 | 午晚餐菜数达标 | 三口及以下 4 菜 1 汤,超过三口每多一人加一道菜;AI 返回不足/超出由后端补齐/截断 |
+| CR-4 | 「重新生成」真正生效 | 修复旧实现只清空菜单、不生成的问题 |
+| CR-5 | 共餐人口径统一 | 后端优先读前端 `participant_member_ids`,不再只依赖 `meal_config` |
+| Bug-1 | 食材列表无法下滑翻页 | 修复 `scroll-view` 缺确定高度导致 `@scrolltolower` 永不派发 |
+| Bug-2 | AI 失败时空菜单落库(P1) | 归一化后加结构闸门,空/非法输出兜底到规则菜单 |
+
+### 家庭总览聚合口径(迁移 465,随本次一并发布)
+
+同一食材汇总家庭可登录成员的推荐指数:任一名成员 `< 0` → 取**全员最低值**(`aggregateRule=MIN_NEGATIVE`);全员 `>= 0` → 取**平均值**(四舍五入取整,0 分计入平均,`AVG_POSITIVE`)。展示值 `overviewScore`,`minIndexScore` 仍保留真实最小值供诊断/旧客户端兼容。指纹加入 `RULE_VERSION` 强制旧规则行失配重算,`getOverview()` 对历史行自动 backfill。
+
+---
+
+## 二、数据库与配置变更(发布前必读)
+
+### 迁移 465 · `family_food_overview`
+
+新增 `overview_score INT NULL`、`aggregate_rule VARCHAR(24) NULL`,`schema.sql` 已同步。
+
+### 迁移 466 · `diet_recommendations`
+
+| 项 | 内容 |
+|---|---|
+| DDL | `view_key VARCHAR(128) NULL`,值域 `member:<id>` / `family` / 存量 `NULL` |
+| 索引 | 普通索引 `idx_family_date_view (family_id, recommendation_date, view_key)`;**不加唯一索引**(避免 regenerate 历史同键记录冲突) |
+| M4 | 幂等将历史 `status='adjusted'` 归位为 `pending` |
+| M6 | 幂等更新 `system_prompts` 的 `content` / `default_content`,**仅当仍含旧种子句**,不覆盖运营自定义文案 |
+
+> **存量 `view_key` 不回填**。老客户端不传 `view_key` 时保持原有查询语义;升级首日各视角会出现空态,需重新生成一次食谱。
+
+所有迁移写在 `DatabaseInitializer.runMigrations()` 唯一入口,使用 `ensureColumn` / `indexExists` 兜底,**幂等可重复执行**。
+
+### Prompt 双来源
+
+- Java 侧 `get_prompt("menu_generate")` 读 `system_prompts` 表(生产真正生效来源);`cfc-langgraph/app/api/meal.py` 的 `MENU_GENERATE_TEMPLATE` 是本地/降级第二来源。
+- 两来源模板均**只含 4 个占位符** `{participant_count, date, foods_text, constraints_text}`,菜数要求经既有 `{constraints_text}` 注入,未新增占位符。
+- `prompt_service.py` 有 **300s 进程内缓存**(`_TTL_SECONDS=300`)。
+- Java `normalizeMenu()` 是菜数正确性的**硬保证**,prompt 仅软引导——即使文案未生效,菜数仍然正确。
+
+---
+
+## 三、接口契约变更
+
+| 接口 | 变更 |
+|---|---|
+| `/today` | 请求体改 `Map<String,Object>`,可选 `view_key`;传才追加视角过滤并按 `version,id` 倒序,不传保持旧查询 |
+| `/generate` | 请求体改 `Map<String,Object>`,可选 `view_key`;优先读 `participant_member_ids`(兼容 JSON 字符串 / 数组 / 数字),空才回落 `meal_config`;幂等键追加 `view_key` |
+| `/update` | 复用为删除菜品:**不再置 `status="adjusted"`**(否则 `/today` 白名单查不到);只写 `menu_json` + `updated_at`,**永不补菜**;非法 JSON / `meals` 非数组 → `400 menu_json格式非法` |
+| `/regenerate` | 改为调用 `generateRecommendation()` 全链路,按原视角覆盖、`version+1`、`status=pending`,不再插入空记录 |
+| `/api/meal/replace` | 是「换食材」返回 `Result<Food>`,本轮未复用;文档偏差已在 `API_REFERENCE.md` 标注 |
+
+全量保持项目约定:`@PostMapping`、统一 `Result<T>`、`@Resource`。
+
+---
+
+## 四、前端与小程序
+
+- `recommendation.vue`:`buildViewKey()`(family → `family`;成员 → `member:<id>`;成员列表未就绪返回 `null` 且不发请求);`loadRecommendation()` **全量 12 处调用点,本轮新增 5 处联动**(`switchTab('recipe')` / `switchView` / `switchMember` / `onRefresh` / `onShow`)。
+- 菜品操作收敛为单一「🗑 删除」,由 `dishActions='delete-only'` 数据位驱动(保留 `replaceDish` 回退入口);`dishStandard` 只读展示「当前 N 菜 1 汤 / 未达标准」,不触发补菜;`parseMenu` 透传 `dish.type`;补了局部 loading / 失败 / 空三态与防重复点击。
+- **翻页修复**:页面根 `min-height:100vh` → `height:100vh`,`.content` 补 `height:0; min-height:0`。这是 uni-app 竖向 `scroll-view` 拿到确定计算高度、派发 `@scrolltolower` 的前提;`overflow:hidden` 为 HEAD 既有、非本轮新增。
+- Vue2 / uni-app 约束保持:无可选链、无 CSS Grid、无新增 `:key` 表达式、无 `new Date(string)`,Options API。
+
+---
+
+## 五、测试与验收
+
+| 项 | 结果 |
+|---|---|
+| 后端测试 | 4 个测试类 **52 / 52**(`FamilyFoodOverviewServiceTest` 15、`DietRecommendationServiceTest` 17、`DietRecommendationBoundaryTest` 12、`ReportDietRecommendContractTest` 8),exit 0 |
+| 前端测试 | **5 suites / 91 passed / 2 skipped / 93 total**,exit 0 |
+| 编译 | `mvn clean compile` 全量 2118 源文件 BUILD SUCCESS |
+| Python | `py_compile` + 双来源占位符 / 渲染核对 OK |
+| QA 轮次 | 第 1 轮 `QA_PASS: NO`(发现 P1)→ 修复 → 第 2 轮 `QA_PASS: YES` |
+
+**防回归证据**:QA 用 `git show HEAD` 取原始 RED 态套件与改名后的 `DietRecommendationBoundaryTest.java` 逐行比对,所有 `assert*` 行集合 md5 完全一致(`8a3564495fa9f2fbdfc813c23778b360`),12 个 `@Test` / 64 条断言逐字未改——P1 转绿来自源码修复,不是削弱断言。另做独立形状验证,确认新闸门不会把「菜数偏少但结构合法」的 AI 输出误整单降级,且全文 `aiGateway.generateMenu(` 仅 1 处调用(无整份重试 AI)。
+
+---
+
+## 六、发布前检查清单
+
+1. **核对线上 `system_prompts.content` 换行符**。工作区 `seed_system_prompts.sql` 为 CRLF,而迁移 M6 的 `oldRequirements` / `newRequirements` 用 `\n`;若线上 `content` 当年由 Windows CRLF 导入,`REPLACE` 会**静默 no-op**,DB 里仍留旧冲突句。正确性由 Java 硬保证兜底,但文案不会更新。
+2. **刷新 prompt 缓存**:调 `clear_cache('menu_generate')`,或等待 300s TTL。该函数全仓无调用方,不会自动触发。
+3. **新装 / 升级环境**:手工执行根目录 `seed_system_prompts.sql`(注意其 `ON DUPLICATE KEY UPDATE` 只更新 `default_content`、**不覆盖** `content`;存量库 `content` 唯一自动更新通道是迁移 M6)。
+4. **确认迁移 465 / 466 在目标库执行成功**(`runMigrations()` 幂等,可重复执行;本机无 MySQL,DDL 幂等为静态审查 + SQLite DML 模拟)。
+5. 后端重新编译部署、小程序前端发版 / 提审。
+
+---
+
+## 七、发布后验证
+
+| # | 验证项 | 预期 |
+|---|--------|------|
+| 1 | **真机 / H5 触底冒烟**(唯一未自动化覆盖项) | 进「适合 / 全部」Tab(>20 条食材),滑到底追加下一批 20 条,到底后不重复追加;顺带看移动端 Safari / 自定义导航下 `100vh` 有无底部偏移,必要时用 `calc(100vh - env(safe-area-inset-bottom))` 收口 |
+| 2 | 视角隔离 | 切换成员 / 家庭总览,各自食谱不同、互不串数据;升级首日空态重新生成一次后恢复 |
+| 3 | 删除菜品 | 删除后整页不回空态、剩余菜品不被补齐、`/today` 仍能查到 |
+| 4 | 菜数 | N≤3 → 4 菜 1 汤;N=4 → 5 菜 1 汤;N=5 → 6 菜 1 汤;汤恒 1 道且排在末尾;早餐维持现状 |
+| 5 | AI 失败兜底 | 模拟 LangGraph 返回 `{"meals":[]}` / 非法 JSON,用户仍看到足量三餐而非空菜单 |
+| 6 | 重新生成 | 返回菜单非空、`version+1`、`status=pending`,不出现空记录 |
+
+> 第 1 项需要 H5 预览或微信开发者工具 / 真机,Jest 与 Node 只能验证 CSS 契约与 JS 数据流,**不等价于真实触摸滚动**。
+
+---
+
+## 八、已知残留与低危项(均不阻断)
+
+| 级别 | 项 | 处置 |
+|---|---|---|
+| 残留 | 真机触底滚动 / `100vh` 适配未验证 | 见第七节第 1 项,需用户侧补验后关闭 |
+| P2 | 存量 `view_key=NULL` 记录与多视角共存时,regenerate 幂等键不追加 `.eq(viewKey)`,`selectOne` 可能命中多行;老客户端 `/today` 会展示任意视角 | 按已裁定「存量不回填」不改代码;升级首日各视角重新生成一次缓解 |
+| P2 | seed CRLF 使 M6 可能 no-op、`clear_cache` 无调用方 / 300s TTL | 见第六节 1~2,运维动作 |
+| P3 | 删除失败时双重 toast(请求层与 `deleteDish` catch 各弹一次) | 不影响数据正确性,失败不 splice、可重试 |
+| P3 | HEAD 既有的 `:key` 表达式(L105 / L299)违反小程序硬约束 | 非本轮引入,低危观察 |
+| P3 | Jest `A worker process has failed to exit gracefully` | 既有 timer/handle 遗留,exit 0,`2 skipped` 亦为 HEAD 既有 `describe.skip` |
+| P3 | AI 用 `meal_type` 等别名字段时,闸门保守地整单降级为规则菜单;AI 只给午晚餐时不主动补早餐 | 均为保守 / 既有透传行为,不落坏数据,非本轮回归 |
+| 设计 | `normalizeMenu` 对所有餐次用 `toDish()` 重建,可能丢弃 `notes` / `nutrition` 子字段 | 前端 `parseMenu` 只消费 5 个字段,当前无影响 |
+
+---
+
+## 九、回滚提示
+
+- 代码回滚可 `revert 181e830be`,但 **DDL 迁移不会自动回滚**:`view_key` 列与 `family_food_overview` 两个新列会保留。这不影响老客户端——不传 `view_key` 仍走原查询,新增列可为 NULL。
+- `system_prompts.content` 若已被 M6 改写,回滚代码不会自动恢复文案,需按运营备份手工还原并清缓存。
+- 前端回滚到旧版本后不传 `view_key`,`/today` 恢复「所有人看到同一份食谱」的旧行为。
+
+---
+
+## 十、提交清单
+
+| 提交 | 内容 |
+|---|---|
+| `181e830be` | 25 文件:迁移 465/466、后端服务与实体、Controller、AiGateway、schema.sql、小程序页面、LangGraph 模板与模型、seed SQL、API 文档、两份需求文档、前后端测试 |
+| 本次收尾提交 | `QaBoundaryVerificationTest.java` → `DietRecommendationBoundaryTest.java`(类名同步,断言逐字保留,纳入常规回归)+ 本发布说明 |
+
+> 工作区另有 3 个与本轮无关的未跟踪脚本(`docs/参考资料/知识库/merge_knowledge.py`、`web/img/ref-screenshots/annotate.py`、`annotate_cell.py`),**不在本次提交范围内**。本仓库长期堆积未提交改动,`git add` 须显式指定文件并 `git diff --cached --stat` 复核后再 commit。