|
@@ -0,0 +1,290 @@
|
|
|
|
|
+# 食材推荐/查询 · 报告入口收敛 + 家庭总览持久化最小值
|
|
|
|
|
+
|
|
|
|
|
+> 任务号:T26-A / T26-B(合为一个开发批次 T28)
|
|
|
|
|
+> 生成日期:2026-10-25
|
|
|
|
|
+> 基线:`origin/cfclub` = `eb71d0aa6`
|
|
|
|
|
+> 需求来源:主理人 2026-10-25 20:13 口头指令(2 条)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 一、需求原文与验收口径
|
|
|
|
|
+
|
|
|
|
|
+### 需求 1:从报告进「食材推荐」和「食材查询」,上面的选成员功能就不要有了,只取当前报告的
|
|
|
|
|
+
|
|
|
|
|
+**当前行为**(已核查):
|
|
|
|
|
+
|
|
|
|
|
+| 入口 | 目标页 | 当前问题 |
|
|
|
|
|
+|---|---|---|
|
|
|
|
|
+| `pages/health/gut-flora-detail.vue:1023` `goToFoods()` → `/pages/health/gut-flora-foods-detail?reportId=X` | 食材推荐详情页 | 顶部 `member-bar`(成员切换条)始终渲染(`v-if="memberList.length > 1"`),`loadMemberList()` 无条件调用 → 从报告进来仍可切换成员,与"只取当前报告"矛盾 |
|
|
|
|
|
+| `pages/health/gut-flora-detail.vue:1026` `goToCaution()` → `/pages/diet/food-query?reportId=X&memberId=Y` | 食材查询页 | 顶部 `member-bar`(家庭总览/个人/成员三态切换)渲染;`loadFoods()` 同时传 `reportId` 和 `memberId`;`loadFamilyData()` 也无条件调用 → 报告上下文与家庭总览混杂 |
|
|
|
|
|
+
|
|
|
|
|
+**已存在的正确先例**(本任务照抄其模式):
|
|
|
|
|
+`pages/growth/caution-detail.vue:85-90` —— 报告入口(有 `reportId`)时**不调用** `loadMemberList()`,成员条自然不渲染。
|
|
|
|
|
+
|
|
|
|
|
+**验收标准**:
|
|
|
|
|
+1. AC-01 从报告进入 `gut-flora-foods-detail`(带 `reportId`)→ 顶部**不出现**成员切换条;页面数据只来自该报告(`/api/health/foods` 的 `reportId` 分支,即 `foodService.listByReportId(reportId)`)。
|
|
|
|
|
+2. AC-02 从报告进入 `food-query`(带 `reportId`)→ 顶部**不出现**成员切换条(家庭总览/个人/成员三态都不出现);列表只显示该报告的食材;**不请求** `/api/health/foods/by-family`。
|
|
|
|
|
+3. AC-03 非报告入口(不带 `reportId`,如 `pages/diet/index.vue:75` 的「食材查询」快捷入口、`pages/growth-main/index.vue:356/359`)→ 成员切换条**保持现状**,家庭总览功能不变。
|
|
|
|
|
+4. AC-04 报告入口时 `/api/health/foods` 的请求参数**只带 `reportId`,不带 `memberId`**(避免 `reportId != null` 时 `memberId` 被忽略造成的语义混淆与多余参数)。
|
|
|
|
|
+5. AC-05 `gut-flora-foods-detail` 的「编辑」功能(`editHealthReport(reportId, {foods})`)在报告入口下**保持可用**(编辑后写回该报告,不影响)。
|
|
|
|
|
+
|
|
|
|
|
+### 需求 2:食材查询的家庭总览,应该有一个总览的持久值,是家庭成员每项食材最新的最小值
|
|
|
|
|
+
|
|
|
|
|
+**当前行为**(已核查):
|
|
|
|
|
+- 前端 `pages/diet/food-query.vue:320-366` `buildFamilyOverview()` 在**浏览器内实时计算**:`avgScore = Math.round(totalScore / memberScores.length)`,即**各成员算术平均分**。
|
|
|
|
|
+- 数据源 `/api/health/foods/by-family`(`HealthReportController.java:1904-1966`)返回 `{total, byMember, memberNames}`,其中 `byMember` 来自 `foodRecommendService.getByFamily(familyId)` → 逐成员查 `food_recommend_idx`(该表存的是**每个成员的最新**推荐指数,由 `FoodRecommendService.recalculateForUser()` 全量覆盖维护)。
|
|
|
|
|
+- **问题**:① 总览值不落库,每次进页面重算,成员多/食材多时开销随 N×M 增长;② 语义是"平均",而家庭共同饮食场景应取**最保守值(最小值)**——家庭里最不耐受/最该戒备的那个人的指数才是采购与共同进餐该遵守的下限;③ 平均值会把"某人高度戒备"稀释成"看起来还行",存在健康误导风险。
|
|
|
|
|
+
|
|
|
|
|
+**目标口径**:
|
|
|
|
|
+> 家庭总览的每一项食材,取**家庭成员该项食材最新推荐指数的最小值**,并把这个最小值作为**持久化值**落库。
|
|
|
|
|
+
|
|
|
|
|
+**关键澄清("最新"的定义)**:
|
|
|
|
|
+- "某成员某项食材的最新值" = `food_recommend_idx` 中该 `user_id + food_id` 的 `index_score`。该表由 `recalculateForUser(userId)` 在报告确认入库时**全量覆盖**维护(新增/更新/删除三种分支),因此它天然就是"最新值",无需再按时间取 max。
|
|
|
|
|
+- "最小值" = 在该家庭的**可登录成员**(`family_members.user_id IS NOT NULL`)中,对该项食材有指数记录的成员,取 `MIN(index_score)`。
|
|
|
|
|
+- 某成员该项食材**无记录**(没做过含该食材的报告)→ **不计入** min 计算(不能当 0,0 是"中性"而非"未知")。若某食材**全部成员都无记录** → 该食材不出现在家庭总览中。
|
|
|
|
|
+
|
|
|
|
|
+**验收标准**:
|
|
|
|
|
+1. AC-06 新增持久化表 `family_food_overview`,按 `family_id + food_id` 唯一,存:`min_index_score`(持久最小值)、参与计算的成员数、计算时间戳、源数据指纹。
|
|
|
|
|
+2. AC-07 家庭总览接口 `/api/health/foods/by-family` 返回体新增持久化总览(字段名 `overview`),每项含 `foodId / foodName / category / minIndexScore / memberCount / memberScores`;**`minIndexScore` 必须来自持久化表**,不是前端算的平均值。
|
|
|
|
|
+3. AC-08 前端 `food-query.vue` 的家庭总览改为**直接渲染后端返回的持久化总览**,`minIndexScore` 取代原 `avgScore`(保留成员明细 `memberScores` 展示);前端**不再做 min/avg 计算**。
|
|
|
|
|
+4. AC-09 持久化值在 `food_recommend_idx` 发生变化后被**重算更新**(幂等),不允许出现"改了报告但家庭总览还是旧值"。
|
|
|
|
|
+5. AC-10 重算为**按需 + 幂等**:同一 family 重复重算结果一致;某成员被移出家庭后,其数据不再计入。
|
|
|
|
|
+6. AC-11 历史家庭(无持久化记录)在首次访问时能被补算(backfill),不返回空总览。
|
|
|
|
|
+7. AC-12 `minIndexScore < 0` 的食材在总览中标注为「戒备」;`>= 0` 标注「适合」,与既有 `getStatusText` 口径一致。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 二、技术方案
|
|
|
|
|
+
|
|
|
|
|
+### 2.1 后端:持久化家庭食材总览
|
|
|
|
|
+
|
|
|
|
|
+#### 2.1.1 数据库迁移(迁移编号 415,接续现有 414)
|
|
|
|
|
+
|
|
|
|
|
+在 `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` 的 `runMigrations()` 中新增:
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+迁移415: 创建 family_food_overview 表(家庭 × 食材 的持久化最小值总览)
|
|
|
|
|
+CREATE TABLE IF NOT EXISTS family_food_overview (
|
|
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
|
|
+ family_id BIGINT NOT NULL COMMENT '家庭ID',
|
|
|
|
|
+ food_id BIGINT NOT NULL COMMENT '食材ID',
|
|
|
|
|
+ food_name VARCHAR(100) COMMENT '食材名称快照',
|
|
|
|
|
+ category VARCHAR(50) COMMENT '分类快照',
|
|
|
|
|
+ min_index_score INT NOT NULL COMMENT '家庭成员该项食材最新推荐指数的最小值',
|
|
|
|
|
+ member_count INT NOT NULL DEFAULT 0 COMMENT '参与计算的成员数(有该食材记录的)',
|
|
|
|
|
+ member_scores JSON COMMENT '成员明细 [{userId,nickname,score}]',
|
|
|
|
|
+ source_fingerprint VARCHAR(64) COMMENT '源数据指纹(成员指数快照哈希),用于幂等与变更检测',
|
|
|
|
|
+ calculated_at DATETIME NOT NULL COMMENT '本次计算时间',
|
|
|
|
|
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
|
|
+ UNIQUE KEY uk_family_food (family_id, food_id),
|
|
|
|
|
+ INDEX idx_family (family_id)
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+同步更新 `cfc-backend/src/main/resources/schema.sql`(AGENTS.md 硬要求:迁移后必须同步 schema.sql)。
|
|
|
|
|
+
|
|
|
|
|
+> ⚠️ 迁移编号 415 必须在开工前**再次确认**当时 `DatabaseInitializer.java` 的最新编号;若已被其他会话占用则顺延,不得与他人冲突。
|
|
|
|
|
+
|
|
|
|
|
+#### 2.1.2 实体 / Mapper
|
|
|
|
|
+
|
|
|
|
|
+- 新增 `entity/FamilyFoodOverview.java`:`@TableName("family_food_overview")`,字段与上表对应(`memberScores` 用 `@TableField(typeHandler = JacksonTypeHandler.class)` 或直接存 String,由项目既有惯例决定——**先看 `family_members` 等表里 JSON 字段的既有处理方式再定**,不要自创风格)。
|
|
|
|
|
+- 新增 `mapper/FamilyFoodOverviewMapper.java`:继承 `BaseMapper<FamilyFoodOverview>`。
|
|
|
|
|
+
|
|
|
|
|
+#### 2.1.3 服务:`FamilyFoodOverviewService`(新增)
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+@Service
|
|
|
|
|
+public class FamilyFoodOverviewService {
|
|
|
|
|
+ /** 重算并持久化某家庭的食材总览(幂等) */
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ public int recalculate(Long familyId);
|
|
|
|
|
+
|
|
|
|
|
+ /** 读取持久化总览;无记录时先 backfill 再返回 */
|
|
|
|
|
+ public List<FamilyFoodOverview> getOverview(Long familyId);
|
|
|
|
|
+
|
|
|
|
|
+ /** 从最新的 food_recommend_idx 重算(供调用方触发) */
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+`recalculate(familyId)` 算法:
|
|
|
|
|
+1. 查 `family_members` where `family_id = ?` AND `user_id IS NOT NULL` → 可登录成员列表(带 nickname)。
|
|
|
|
|
+2. 对每个成员查 `food_recommend_idx` where `user_id = ?` → 该成员全部食材最新指数。
|
|
|
|
|
+3. 按 `food_id` 聚合到 `foodId → List<(userId, nickname, indexScore)>`。
|
|
|
|
|
+4. 对每个 `foodId`:
|
|
|
|
|
+ - `minIndexScore = MIN(indexScore)`(**只对有记录的成员取 min**)
|
|
|
|
|
+ - `memberCount = 该食材有记录的成员数`
|
|
|
|
|
+ - `memberScores = [{userId, nickname, score}]`
|
|
|
|
|
+ - `sourceFingerprint = SHA-256(familyId + 排序后的 (foodId:userId:score) 序列)`
|
|
|
|
|
+5. **全量覆盖式写入**(幂等):按 `family_id` 删除该家庭不再出现的食材行 + upsert 现有行(`SELECT` 后 `updateById`/`insert`)。
|
|
|
|
|
+6. `calculated_at = now`。
|
|
|
|
|
+
|
|
|
|
|
+**指纹用途**:同一 family 的指纹不变 → 跳过写入(省 IO);指纹变了 → 更新。
|
|
|
|
|
+
|
|
|
|
|
+#### 2.1.4 触发点(保证 AC-09 / AC-10)
|
|
|
|
|
+
|
|
|
|
|
+在 `FoodRecommendService.recalculateForUser(Long userId)` **末尾**(`normalizeIndicatorPipeline` 快照之后)触发家庭总览重算:
|
|
|
|
|
+- 由 `userId` 反查其所属 `family_id`(`family_members` where `user_id = ?`),可能一个用户属于多个家庭 → **逐个 family 重算**。
|
|
|
|
|
+- 包裹 try/catch,失败**只 warn 不抛出**,绝不能因为总览重算失败而让报告确认入库主流程回滚(与 `normalizeIndicatorPipeline` 现有处理方式一致)。
|
|
|
|
|
+
|
|
|
|
|
+> 注意:`recalculateForUser` 被多处调用(报告确认入库、代传确认入库、AI 解读后等),这正是我们需要的触发密度。
|
|
|
|
|
+
|
|
|
|
|
+#### 2.1.5 接口改造:`HealthReportController.getFoodsByFamily`
|
|
|
|
|
+
|
|
|
|
|
+在现有 `{total, byMember, memberNames}` 之外**新增** `overview` 字段:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+result.put("overview", familyFoodOverviewService.getOverview(familyId));
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+`overview` 元素结构:
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "foodId": 12, "foodName": "三文鱼", "category": "水产",
|
|
|
|
|
+ "minIndexScore": -18, "memberCount": 3,
|
|
|
|
|
+ "memberScores": [{"userId": 1018, "nickname": "小宇宙", "score": -18},
|
|
|
|
|
+ {"userId": 1020, "nickname": "妈妈", "score": 22}],
|
|
|
|
|
+ "calculatedAt": "2026-10-25 20:30:00"
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**保留** `total` / `byMember` / `memberNames` 原字段不动(向后兼容,其他页面/调用方可能在用)。
|
|
|
|
|
+
|
|
|
|
|
+#### 2.1.6 另一处入口:`FoodRecommendController`
|
|
|
|
|
+
|
|
|
|
|
+`/api/food/recommendation/by-family`(`FoodRecommendController.java:54`)同样按 family 返回 `Map<Long, List<FoodRecommendIndex>>`,也**新增** `overview` 字段,保持两个接口口径一致。
|
|
|
|
|
+
|
|
|
|
|
+### 2.2 前端:食材查询页家庭总览改读持久值
|
|
|
|
|
+
|
|
|
|
|
+`cfc-frontend/pages/diet/food-query.vue`:
|
|
|
|
|
+
|
|
|
|
|
+1. `loadFamilyData()` 中新增:从 `res.data.overview` 取持久化总览,映射为 `familyOverview`:
|
|
|
|
|
+ ```js
|
|
|
|
|
+ self.familyOverview = (res.data.overview || []).map(function(o) {
|
|
|
|
|
+ var min = o.minIndexScore
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: o.foodId, name: o.foodName, category: o.category,
|
|
|
|
|
+ score: min, // 最小值取代原 avgScore
|
|
|
|
|
+ scoreClass: self.getScoreClass(min),
|
|
|
|
|
+ statusText: self.getStatusText(min),
|
|
|
|
|
+ status: min >= 0 ? 'suitable' : 'unsuitable',
|
|
|
|
|
+ memberScores: o.memberScores || [],
|
|
|
|
|
+ memberCount: o.memberCount || 0
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ ```
|
|
|
|
|
+2. 删除 `buildFamilyOverview()` 中前端计算 `avgScore` 的逻辑(前端不再算 min/avg)。**保留 `buildFamilyOverview` 作为后端未返回 `overview` 时的降级路径**(降级时按 min 计算,比 avg 更安全),但正常路径不触发。
|
|
|
|
|
+3. 模板 `item.avgScore` → 改为 `item.score`(已映射为 min)。
|
|
|
|
|
+4. `family-overview` 区块标题文案:`🏠 家庭推荐概况(共 N 人)` → 补充口径说明,如 `🏠 家庭推荐概况(共 N 人 · 取家庭最保守值)`,让用户知道这个值是"家人中最该注意的那个"。
|
|
|
|
|
+
|
|
|
|
|
+### 2.3 前端:报告入口隐藏成员切换(需求 1)
|
|
|
|
|
+
|
|
|
|
|
+#### 2.3.1 `pages/health/gut-flora-foods-detail.vue`(食材推荐)
|
|
|
|
|
+
|
|
|
|
|
+- 模板:`v-if="memberList.length > 1"` → `v-if="!reportId && memberList.length > 1"`
|
|
|
|
|
+- `onLoad`:`if (!this.reportId) { this.loadMemberList() }`(照抄 `caution-detail.vue` 模式)
|
|
|
|
|
+- `loadData()`:请求参数在有 `reportId` 时**只传 reportId**,不传 `currentMemberId`:
|
|
|
|
|
+ ```js
|
|
|
|
|
+ var mid = (!this.reportId && this.currentMemberId > 0) ? this.currentMemberId : null
|
|
|
|
|
+ getHealthFoods(self.reportId, mid)
|
|
|
|
|
+ ```
|
|
|
|
|
+- `loadTrends()`:报告入口时**不查趋势**(趋势是"个人跨报告"概念,报告快照无历史)——照抄 `caution-detail.vue:132-135` 的 `if (!this.reportId) { this.loadTrends() }` 模式。
|
|
|
|
|
+- `switchMember` 在报告入口下不可达(成员条不渲染),无需改动逻辑。
|
|
|
|
|
+
|
|
|
|
|
+#### 2.3.2 `pages/diet/food-query.vue`(食材查询)
|
|
|
|
|
+
|
|
|
|
|
+- 模板 `member-bar`:`v-if="memberList.length > 1 || familyMode"` → `v-if="!reportId && (memberList.length > 1 || familyMode)"`
|
|
|
|
|
+- `mode-bar`(推荐值 ↓/↑ 排序条,仅家庭模式):报告入口下也**不显示**(它属于家庭总览语境)→ `v-if="!reportId && familyMode"`
|
|
|
|
|
+- `onLoad`:报告入口时**不调用** `loadMembers()` 与 `loadFamilyData()`:
|
|
|
|
|
+ ```js
|
|
|
|
|
+ if (this.reportId) { this.filterType = 'unsuitable'; this.sortType = 'asc' }
|
|
|
|
|
+ else { this.loadMembers(); this.loadFamilyData() }
|
|
|
|
|
+ this.loadFoods()
|
|
|
|
|
+ ```
|
|
|
|
|
+- `loadFoods()`:有 `reportId` 时**只传 reportId**,不传 `memberId`:
|
|
|
|
|
+ ```js
|
|
|
|
|
+ if (self.reportId) { data.reportId = self.reportId } else if (self.currentMemberId) { data.memberId = self.currentMemberId }
|
|
|
|
|
+ ```
|
|
|
|
|
+- `currentView` 报告入口下固定为 `'personal'`(默认已是),确保列表区渲染。
|
|
|
|
|
+- **非报告入口完全不受影响**(AC-03)。
|
|
|
|
|
+
|
|
|
|
|
+### 2.4 不改动的部分(明确边界)
|
|
|
|
|
+
|
|
|
|
|
+- `pages/body-detail/health-report.vue`(报告详情页内的食材推荐区块):已经只按 `reportId` 请求 `/api/health/foods`,无成员切换条 → **不动**。
|
|
|
|
|
+- `pages/health/gut-flora-detail.vue` 的入口跳转参数:保持现状(`goToFoods` 已传 reportId;`goToCaution` 已传 reportId + memberId)。
|
|
|
|
|
+- `pages/growth/caution-detail.vue`、`pages/health/gut-flora-foods-detail.vue` 的**非报告**入口行为:不变。
|
|
|
|
|
+- `food_recommend_idx` 表结构:**不改**(它已天然是"最新值"来源)。
|
|
|
|
|
+- `report_food_suitability` 表:**不改**。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 三、文件改动清单
|
|
|
|
|
+
|
|
|
|
|
+### 后端(cfc-backend)
|
|
|
|
|
+| 文件 | 改动 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| `config/DatabaseInitializer.java` | 新增迁移415:`family_food_overview` 表 |
|
|
|
|
|
+| `src/main/resources/schema.sql` | 同步 `family_food_overview` 建表 |
|
|
|
|
|
+| `entity/FamilyFoodOverview.java` | **新增** |
|
|
|
|
|
+| `mapper/FamilyFoodOverviewMapper.java` | **新增** |
|
|
|
|
|
+| `service/FamilyFoodOverviewService.java` | **新增**(recalculate / getOverview / backfill) |
|
|
|
|
|
+| `service/FoodRecommendService.java` | `recalculateForUser()` 末尾触发家庭总览重算 |
|
|
|
|
|
+| `controller/HealthReportController.java` | `getFoodsByFamily()` 新增 `overview` 返回字段 |
|
|
|
|
|
+| `controller/FoodRecommendController.java` | `by-family` 新增 `overview` 返回字段 |
|
|
|
|
|
+
|
|
|
|
|
+### 前端(cfc-frontend)
|
|
|
|
|
+| 文件 | 改动 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| `pages/diet/food-query.vue` | ① 报告入口隐藏 member-bar / mode-bar,不加载成员与家庭数据,请求只传 reportId;② 家庭总览改读后端持久化 `overview`(minIndexScore),前端不计算 avg |
|
|
|
|
|
+| `pages/health/gut-flora-foods-detail.vue` | 报告入口隐藏 member-bar,不查趋势,请求只传 reportId |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 四、验收与自检清单
|
|
|
|
|
+
|
|
|
|
|
+### 编译 / 构建
|
|
|
|
|
+- [ ] `cd cfc-backend && mvn -pl cfc-backend -am compile` 通过(无新增编译错误)
|
|
|
|
|
+- [ ] 小程序构建通过(`cfc-frontend` 按项目既有构建方式;注意 AGENTS.md 小程序限制:禁可选链 `?.`、禁 CSS Grid、禁 `new Date(string)`、`:key` 只能是真实属性或循环索引别名)
|
|
|
|
|
+- [ ] `git diff --cached --stat` 确认 staged 仅含本任务文件(**禁止 `git add .`**)
|
|
|
|
|
+
|
|
|
|
|
+### 功能验证(需在有测试库的环境实跑)
|
|
|
|
|
+- [ ] 迁移415 幂等:重复启动后端不报错,表存在且索引正确
|
|
|
|
|
+- [ ] 报告入口:`/pages/health/gut-flora-foods-detail?reportId=X` 无成员条;`/pages/diet/food-query?reportId=X` 无成员条、不请求 by-family
|
|
|
|
|
+- [ ] 非报告入口:`/pages/diet/food-query` 成员条正常、家庭总览正常
|
|
|
|
|
+- [ ] 持久化总览值 = 家庭成员该项食材最新指数的**最小值**(人工核对 ≥2 个成员的原始 `food_recommend_idx` 行)
|
|
|
|
|
+- [ ] 成员新增/移除报告后,家庭总览持久值随之更新(AC-09)
|
|
|
|
|
+- [ ] 某成员删除某食材后,该食材在总览中的 `memberCount` 相应减少;若无人有该食材则总览中消失
|
|
|
|
|
+- [ ] `minIndexScore < 0` 的食材状态显示「戒备」
|
|
|
|
|
+
|
|
|
|
|
+### 回归检查
|
|
|
|
|
+- [ ] `pages/diet/index.vue` 的「食材查询」快捷入口正常(不带 reportId)
|
|
|
|
|
+- [ ] `pages/growth-main/index.vue` 的食材查询入口正常
|
|
|
|
|
+- [ ] `pages/body-detail/health-report.vue` 食材推荐区块不受影响
|
|
|
|
|
+- [ ] `pages/growth/caution-detail.vue` 不受影响
|
|
|
|
|
+- [ ] `/api/food/recommendation/by-family` 原字段 `byMember` 结构未变(向后兼容)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 五、风险与边界
|
|
|
|
|
+
|
|
|
|
|
+| 风险 | 处置 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| **迁移编号冲突**:415 可能已被其他会话占用 | 开工前先 `grep -n "迁移[0-9]\{3\}" DatabaseInitializer.java | tail` 确认最新编号,顺延不覆盖 |
|
|
|
|
|
+| **JSON 字段处理风格**:项目内 JSON 列的既有写法需对齐 | 先查既有表(如 `family_members`/`diet_recommendations`)的 JSON 字段处理方式,照抄 |
|
|
|
|
|
+| **重算性能**:家庭成员多时按成员逐次查 `food_recommend_idx` 有 N 次查询 | 先用单条 `IN (userIds)` 批量查再内存聚合,**不要 N 次单查** |
|
|
|
|
|
+| **主流程被拖累**:总览重算抛异常导致报告入库回滚 | 触发点 try/catch + 只 warn,参照现有 `normalizeIndicatorPipeline` 处理 |
|
|
|
|
|
+| **前端空态**:后端 `overview` 未返回时前端白屏 | 保留降级路径(前端按 min 计算),并加空态提示 |
|
|
|
|
|
+| **老数据无持久化记录** | `getOverview()` 检测到该 family 无记录时先 backfill 再返回(AC-11) |
|
|
|
|
|
+| **不可逆操作** | 本任务**不涉及**删数据/删行/改 UNIQUE 约束/迁移外键;若实现中发现必须做,先停下回报主理人确认 |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 六、交付纪律(务必遵守)
|
|
|
|
|
+
|
|
|
|
|
+1. 基线:`git fetch origin cfclub` → 基于 `origin/cfclub` 新建分支 `feature/food-overview-persistent-min`(**不要合并主干**)
|
|
|
|
|
+2. 只 `git add` 自己新增/修改的文件(**禁止 `git add .`**)
|
|
|
|
|
+3. 先 `mvn -pl cfc-backend -am compile` 通过,再做小程序构建
|
|
|
|
|
+4. 完成后**自行** `git commit`(提交信息写明任务号与改动摘要)并 `git push -u origin feature/food-overview-persistent-min`——**无需等待主理人确认即可提交推送**
|
|
|
|
|
+5. **不要自行合并 cfclub**
|
|
|
|
|
+6. 回报:commit 哈希、改动文件清单、编译/构建结果、实测验证结论
|
|
|
|
|
+7. 若规格与代码实际不符(找不到对应文件/接口形态与描述不同),**停下并回报**,不要凭空实现
|