--- name: 计算器结果保存食谱 overview: 在用户采纳营养计划时,将食谱计算器的配餐方案提取并保存到 v2_recipes 食谱表,并在首页"精选食谱"中混合展示用户自己的食谱(排在前面)。 todos: - id: db-migration content: 新建 SQL 变更文件,v2_recipes 表新增 source 和 source_id 字段 status: completed - id: entity-update content: V2Recipe.java 实体类新增 source 和 sourceId 属性 status: completed - id: adopt-save-recipe content: ToolCalculatorServiceImpl.adopt() 中新增保存食谱到 v2_recipes 的逻辑(含幂等检查) status: completed - id: home-mix-recipes content: ToolHomeServiceImpl.getRecommendedRecipes() 混入当前用户的计算器食谱 status: completed - id: home-return-fields content: 推荐食谱接口返回字段补充 description、source、totalProtein status: completed - id: frontend-recipe-card content: 首页 index.vue 食谱卡片展示适配(tag、desc 等字段映射) status: completed isProject: false --- # 食谱计算器结果保存到食谱表 ## 整体流程 ```mermaid sequenceDiagram participant User as 用户 participant FE as 前端 participant Ctrl as ToolController participant CalcSvc as ToolCalculatorServiceImpl participant RecipeDao as V2RecipeDao participant HomeSvc as ToolHomeServiceImpl User->>FE: 点击"采纳计划" FE->>Ctrl: POST /calculator/adopt Ctrl->>CalcSvc: adopt(resultId) CalcSvc->>CalcSvc: 创建营养计划 (已有逻辑) CalcSvc->>RecipeDao: 保存一条食谱到 v2_recipes CalcSvc-->>FE: 返回 adoptResponse Note over FE: 首页刷新时 FE->>Ctrl: GET /home/recipes Ctrl->>HomeSvc: getRecommendedRecipes() HomeSvc->>RecipeDao: 查询推荐食谱 + 用户自己的食谱 HomeSvc-->>FE: 返回混合列表 ``` ## 1. 数据库变更 - v2_recipes 新增字段 需要新增两个字段以追踪食谱来源: ```sql ALTER TABLE v2_recipes ADD COLUMN source VARCHAR(20) DEFAULT 'manual' COMMENT '来源:manual(手动)/calculator(计算器)/ai(AI生成)' AFTER sort_order, ADD COLUMN source_id BIGINT DEFAULT NULL COMMENT '来源ID(如计算器结果ID)' AFTER source; ``` 同时新增一个变更 SQL 文件记录此次 DDL。 ## 2. 后端 - 实体类更新 **文件**: [V2Recipe.java](msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/model/tool/V2Recipe.java) 新增两个字段: ```java @ApiModelProperty(value = "来源:manual/calculator/ai") private String source; @ApiModelProperty(value = "来源ID(计算器结果ID等)") private Long sourceId; ``` ## 3. 后端 - 采纳时保存食谱 **文件**: [ToolCalculatorServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCalculatorServiceImpl.java) 在 `adopt()` 方法中,步骤 5(创建营养计划)之后,新增步骤"保存食谱到 v2_recipes": - 注入 `V2RecipeDao` - 从 `V2CalculatorResult` 中解析 `mealPlanJson` 得到 MealPlan - 构建 `V2Recipe` 对象: - `userId` = 当前用户ID - `name` = "每日营养配餐 - {ckdStage}",如 "每日营养配餐 - CKD 5期" - `description` = "蛋白质 {proteinIntake}g/天 | 能量 {energyIntake}kcal/天" - `coverImage` = 取午餐第一道菜的图片 URL(最具代表性) - `mealType` = null(整日配餐,非单餐) - `category` = "营养配餐" - `tagsJson` = `["AI配餐", "{ckdStage}"]` - `ingredientsJson` = 聚合三餐所有食材 - `stepsJson` = 存放完整 mealPlanJson(早/午/晚餐详情),复用此字段存储配餐详情 - `totalProtein` = result.getProteinIntake() - `totalEnergy` = result.getEnergyIntake() - `suitableStagesJson` = `["{ckdStage}"]` - `suitableDialysis` = result.getHasDialysis() - `status` = "published" - `isRecommend` = 0(不进入官方推荐,通过 source + userId 查询) - `isOfficial` = 0 - `source` = "calculator" - `sourceId` = resultId - 幂等处理:先检查是否已存在 `source='calculator' AND source_id=resultId` 的记录,避免重复 ## 4. 后端 - 首页推荐食谱混入用户食谱 **文件**: [ToolHomeServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolHomeServiceImpl.java) 修改 `getRecommendedRecipes()` 方法: 1. 如果用户已登录,先查询该用户的食谱(`user_id = currentUserId AND status = 'published' AND source = 'calculator'`,按 `created_at DESC`,取最新 1 条) 2. 再查询官方推荐食谱(现有逻辑,`is_recommend = 1`) 3. 将用户食谱排在前面,官方推荐排在后面,合并后返回 4. 总数仍控制在 `limit` 范围内(如用户有 1 条自己的食谱,则官方推荐取 limit-1 条) 5. 缓存 key 需要区分用户(登录用户的缓存 key 加上 userId) ## 5. 后端 - 推荐食谱接口返回字段补充 **文件**: [ToolHomeServiceImpl.java](msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolHomeServiceImpl.java) 在返回数据 map 中增加字段,使前端能够区分并展示更多信息: ```java map.put("id", recipe.getRecipeId()); map.put("name", recipe.getName()); map.put("coverImage", recipe.getCoverImage()); map.put("totalEnergy", recipe.getTotalEnergy()); map.put("description", recipe.getDescription()); // 新增 map.put("source", recipe.getSource()); // 新增:标记来源 map.put("totalProtein", recipe.getTotalProtein()); // 新增 ``` ## 6. 前端 - 首页食谱展示适配 **文件**: [index.vue (首页)](msh_single_uniapp/pages/tool_main/index.vue) 调整食谱卡片展示逻辑: - `item.tag`:如果 `source === 'calculator'` 显示 "我的配餐",否则显示 "推荐" - `item.tagClass`:根据来源使用不同样式(如用户食谱用不同颜色) - `item.desc`:使用 `description` 字段(如 "蛋白质 75.6g/天 | 能量 2205kcal/天") - `item.time`:可不显示或显示 "每日配餐" - `item.views`:使用 `viewCount` 或隐藏 修改 `loadData` 中对 `recipeList` 的处理逻辑,将后端返回的数据映射为前端需要的格式。 ## 7. 前端 - 食谱详情页适配 点击首页食谱卡片跳转到 `recipe-detail` 页面,需要确认该页面能正确显示来自计算器的食谱内容(特别是 `stepsJson` 中存储的 mealPlan 数据)。如果需要特殊处理,在详情页根据 `source` 字段做渲染分支。 ## 涉及文件清单 | 层级 | 文件 | 改动 | | -------- | --------------------------------------------- | ------------------------------ | | SQL | `docs/sql/v2_recipes_add_source.sql`(新建) | 新增 source、source_id 字段 | | Entity | `V2Recipe.java` | 新增 source、sourceId 字段 | | Service | `ToolCalculatorServiceImpl.java` | adopt() 中新增保存食谱逻辑 | | Service | `ToolHomeServiceImpl.java` | getRecommendedRecipes() 混入用户食谱 | | Frontend | `msh_single_uniapp/pages/tool_main/index.vue` | 食谱卡片展示适配 |