HarmonyOS RDB 实战|中式美食菜谱表设计:食材、步骤与购物清单怎么落库

中式美食现在已经有菜谱、搜索、收藏、浏览历史、餐桌方案和买菜清单这些入口。页面能跑起来是一回事,数据以后能不能继续扩展是另一回事。如果菜谱一直只当成一个大对象缓存,后面做搜索筛选、购物清单、推荐排序、厨房步骤流时,很多逻辑会慢慢挤到页面和 ViewModel 里。

这篇就从真实项目里的 RecipeFavoriteItemHistoryItemTablePlan 这些模型出发,拆一下中式美食后续如果迁到 HarmonyOS RDB,本地表应该怎么设计,Repository 边界应该怎么留,怎么验证这套设计不是纸上谈兵。

图 1|中式美食 RDB 菜谱表设计:菜谱、食材、步骤、用户行为和购物清单不要都挤在一个对象里,拆开以后搜索、清单和推荐才有继续扩展的空间。

本章位置

项目 内容
应用名称 中式美食
当前工程 Flutter/HarmonyOS 上架项目
当前真实模型 RecipeFavoriteItemHistoryItemTablePlan
当前真实文件 lib/data/models/recipe.dartlib/data/repositories/recipe_repository.dartlib/data/datasources/recipe_local_datasource.dart
本文重点 从当前缓存模型推导 HarmonyOS RDB 落库方案
读者收获 知道菜谱、食材、步骤、收藏浏览、购物清单应该怎么拆表

先看当前模型:Recipe 已经开始变重了

中式美食当前的菜谱模型已经不只是一个标题和图片:

dart 复制代码
class Recipe {
  const Recipe({
    required this.id,
    required this.name,
    required this.coverImage,
    required this.category,
    required this.cuisine,
    required this.difficulty,
    required this.cookTimeMinutes,
    required this.flavor,
    required this.ingredients,
    required this.steps,
    required this.tips,
    required this.tags,
    required this.favoriteCount,
    required this.viewCount,
    required this.updatedAt,
  });

  final String id;
  final String name;
  final String coverImage;
  final String category;
  final String cuisine;
  final String difficulty;
  final int cookTimeMinutes;
  final String flavor;
  final List<String> ingredients;
  final List<String> steps;
  final List<String> tips;
  final List<String> tags;
  final int favoriteCount;
  final int viewCount;
  final DateTime updatedAt;
}

这个模型放在页面里读起来很舒服,但从数据层看,它已经有几个明显信号:

字段 当前作用 后续问题
ingredients 展示食材 以后要生成买菜清单,不能一直当字符串数组
steps 展示做法步骤 厨房执行页需要当前步骤、完成状态、预计耗时
tags 搜索和推荐辅助 如果只塞在数组里,按标签统计和排序会变弱
favoriteCount 展示热度 用户自己的收藏行为和全局热度要分开
viewCount 展示浏览量 本机浏览历史、最近浏览和推荐排序要有独立行为记录

所以这篇不是说当前写法错了。当前阶段用模型和缓存先把产品跑通是对的。但下一阶段如果要写得更稳,就要把"展示模型"和"本地结构化数据"拆开。

为什么不能继续把菜谱当一个大 JSON

如果只是缓存首页推荐、详情页数据,一个大对象够用。中式美食现在已经不只是"看菜谱",它还有这些动作:

用户动作 数据层需要回答的问题
搜索"莲藕" 哪些菜谱的食材或标签命中了莲藕
收藏一道菜 这道菜是否被当前用户收藏,什么时候收藏
最近浏览 哪些菜最近看过,重复浏览怎么计数
生成买菜清单 多道菜里的同名食材怎么合并
进入厨房步骤 当前执行到第几步,步骤是否完成

这些问题用一个 Recipe JSON 也能硬做,但后面会变成很多 map/filter/sort 堆在一起。短期快,长期不稳。RDB 的价值就在这里:把有关系的数据拆成表,让搜索、清单、推荐和验证都有明确入口。

我会先拆五张表,不一口气做复杂

中式美食的 RDB 切入点不应该一开始就做"大而全数据库"。先拆五张表就够:

解决什么问题 关键字段
recipe 保存菜谱主体 idnamecategorycuisinedifficultycook_time_minutesflavorcover_imageupdated_at
recipe_ingredient 保存食材明细 idrecipe_idnameamount_textsort_order
recipe_step 保存步骤 idrecipe_idcontenttipsort_order
recipe_action 保存用户行为 idrecipe_idaction_typecountlast_at
shopping_item 保存买菜清单 idsource_recipe_idnameamount_textcheckedcreated_at

这五张表的边界比较清楚:

  • 菜谱主体负责"这道菜是什么"。
  • 食材表负责"它需要什么"。
  • 步骤表负责"怎么做"。
  • 行为表负责"用户怎么用过它"。
  • 购物清单负责"今天要买什么"。

Repository 边界要保留,页面不要直接碰 RDB

中式美食当前已经有 Repository 接口,比如:

dart 复制代码
abstract interface class RecipeRepository {
  Future<List<Recipe>> getRecommendedRecipes({bool forceRefresh = false});

  Future<List<RecipeCategory>> getCategories({bool forceRefresh = false});

  Future<Recipe?> getRecipeDetail(
    String recipeId, {
    bool forceRefresh = false,
  });
}

本地数据源也已经有独立接口:

dart 复制代码
abstract interface class RecipeLocalDatasource {
  Future<CacheEntry<List<Recipe>>?> readRecommendedRecipes();

  Future<void> cacheRecommendedRecipes(List<Recipe> recipes);

  Future<CacheEntry<Recipe>?> readRecipeDetail(String recipeId);

  Future<void> cacheRecipeDetail(Recipe recipe);
}

这个分层很好,后续迁到 HarmonyOS RDB 时,不要把页面改成直接查表。页面还是只找 ViewModel,ViewModel 还是只找 Repository。变化应该藏在 DataSource 里面。
可以把重构后的边界理解成这样:

负责什么 不应该做什么
Page 展示列表、详情、按钮状态 不写 SQL,不拼查询条件
ViewModel 管加载态、空态、错误态、用户动作 不知道数据来自 JSON、RDB 还是网络
Repository 聚合菜谱、收藏、历史、清单规则 不暴露表结构给页面
RDB DataSource 负责表读写、事务、迁移 不处理页面状态

这样改的好处是:如果先从缓存换到 RDB,页面可以少动。后面再加同步、推荐排序、视频拆菜草稿,也不会把页面越写越重。

食材为什么要独立成表

Recipe.ingredients 当前是 List<String>,展示没问题。但买菜清单会马上遇到三个问题:

  1. 同名食材来自多道菜,数量要合并。
  2. 用户勾选的是食材项,不是菜谱对象。
  3. 清单里要保留来源,用户才知道为什么要买这个。
    所以食材表不能只是菜谱字段的一部分。至少要有:
text 复制代码
recipe_ingredient
- id
- recipe_id
- name
- amount_text
- sort_order

后续从两道菜生成清单时,Repository 可以先按 name 分组,再决定是否合并 amount_text。如果数量暂时不能精确计算,也可以先保留文本,但数据来源一定要留住。

步骤为什么也要独立成表

厨房执行页关心的是"当前做到哪一步",这和详情页展示步骤不一样。
如果步骤只是 Recipe.steps 里的字符串数组,短期可以展示。但后面要支持这些能力时就会吃力:

  • 当前步骤高亮。
  • 下一步提示。
  • 步骤完成状态。
  • 每一步的小贴士。
  • 视频拆菜后生成可编辑步骤草稿。
    所以步骤表至少要有:
text 复制代码
recipe_step
- id
- recipe_id
- content
- tip
- sort_order

这里的关键不是字段多复杂,而是 sort_order 必须稳定。厨房执行页最怕步骤顺序乱,用户照着做菜时会直接出错。

收藏和浏览不要混在 Recipe 里

当前项目里收藏和历史已经有独立模型:

dart 复制代码
class FavoriteItem {
  final String recipeId;
  final String recipeName;
  final String coverImage;
  final DateTime favoritedAt;
  final String category;
  final int cookTimeMinutes;
  final String difficulty;
}
dart 复制代码
class HistoryItem {
  final String recipeId;
  final String recipeName;
  final String coverImage;
  final DateTime viewedAt;
  final int viewCount;
}

这说明项目已经意识到:用户行为和菜谱主体不是一回事。
RDB 里可以把它们收敛到 recipe_action

字段 说明
recipe_id 行为属于哪道菜
action_type favoriteviewsearch_clickshopping_add
count 行为次数
last_at 最近发生时间

后面做推荐排序时,这张表就能发挥作用。比如收藏加 5 分,最近浏览加 2 分,搜索点击加 3 分。用户看到推荐原因时,也能解释成"你最近常看这类菜""你收藏过相似做法",不会像黑盒推荐。

餐桌方案和购物清单怎么接上

当前项目里 TablePlan 已经有餐桌方案:

dart 复制代码
class TablePlan {
  const TablePlan({
    required this.label,
    required this.title,
    required this.duration,
    required this.dishes,
    required this.route,
    required this.tableware,
    this.isLight = false,
  });

  final String label;
  final String title;
  final String duration;
  final List<String> dishes;
  final List<String> route;
  final String tableware;
  final bool isLight;
}

这块后续可以接购物清单。比如用户选了"家宴方案",系统把里面的菜转成食材项,再写入 shopping_item
购物清单表不要只存一个字符串列表,至少要知道:

字段 说明
source_recipe_id 食材来自哪道菜
name 食材名
amount_text 数量文本
checked 是否已买
created_at 加入时间

这样用户勾选"莲藕"以后,状态能恢复;删除某道菜来源时,也能只移除那道菜贡献的食材,不会把别的菜也需要的食材误删。

验证方式:不要只看页面能不能打开

RDB 设计最容易停在"表结构看起来对"。真正要验收,至少做下面几组动作:

操作 预期结果
首次启动应用 默认菜谱能写入或读取,首页不空白
搜索"莲藕" 能查到食材或标签命中的菜谱
打开菜谱详情 能读取食材和步骤,顺序稳定
收藏一道菜 recipe_action 里出现收藏行为
从餐桌方案生成清单 shopping_item 出现对应食材
勾选一个食材 重进页面后仍保持勾选
清空本地数据 页面显示空状态,不崩溃

这里我建议把验证写成表,而不是只贴几张截图。截图只能证明"看起来有页面",验证表才能证明"数据链路是闭环的"。

这套设计的风险

风险 处理方式
一开始拆表太多,开发成本变高 先只做菜谱、食材、步骤、购物清单小切片
旧缓存数据迁移麻烦 保留 Repository 接口,DataSource 内部做迁移
食材数量文本不容易合并 第一版先按名称分组,数量保留文本,后续再做结构化数量
推荐排序过早复杂化 先记录行为,不急着上复杂模型

这也是为什么我不建议一上来就把中式美食改成很重的数据库工程。先把表边界立住,再用一个真实页面验证,比一次性铺开更稳。

本章小结

  • 中式美食当前的 Recipe 模型已经覆盖菜谱主体、食材、步骤、标签、热度等信息,适合展示,但不适合长期承担所有查询和行为记录。
  • 下一阶段可以先用 reciperecipe_ingredientrecipe_steprecipe_actionshopping_item 五张表做 RDB 小切片。
  • 页面不要直接碰 RDB。继续保留 Page、ViewModel、Repository、DataSource 的边界,后续重构成本会小很多。
    如果你也在做菜谱、收藏、购物清单这类本地状态比较多的 App,可以先检查一个问题:你的页面是在展示数据,还是已经开始替数据层做查询、合并和排序了?如果是后者,就该考虑把数据结构往下沉了。
相关推荐
恋猫de小郭1 小时前
Flutter material/cupertino 解耦最新进展,已经在做新样式了
android·前端·flutter
小雨青年1 小时前
【HarmonyOS 7开发者前瞻】06 HarmonyOS 7 AI 能力接入前,先用业务场景判断优先级
人工智能·华为·harmonyos
TrisighT1 小时前
鸿蒙 ArkWeb 混合开发全攻略:JSBridge 双向通信 + 实战登录注入
前端·harmonyos·arkts
翟硕的笔记1 小时前
Flutter 插件发布到 pub.dev 操作手册
flutter
●VON2 小时前
HarmonyKit | 鸿蒙新特性:pasteboard 剪贴板 API 集成与 promptAction 迁移指南
华为·harmonyos
木木子223 小时前
# 星座配对深度解析:Select 长列表选择器、矩阵评分算法与数据驱动建议
java·算法·华为·矩阵·harmonyos
如此风景17 小时前
如何在一个features下面创建多个HSP?
harmonyos
花燃柳卧19 小时前
跨平台路由组件工程源码补充上传
android·flutter·kotlin
如此风景19 小时前
鸿蒙开发中 Model 类的组织方式
harmonyos