菜品与菜谱分类统一设计
1. 目标
在菜品列表、菜谱列表及菜品识别页的两个替换入口中,统一使用以下分类顺序。业务分类只定义一次,但两个接口各自保留真实的服务端编号契约:
| 展示顺序 | 显示标签 | 菜品列表 /api/consume/dishes |
菜谱 /p/api/getMeal 分组 key |
|---|---|---|---|
| 0 | 全部 | 空值 | 无独立分组 |
| 1 | 主食 | 1 |
1 |
| 2 | 荤菜 | 3 |
3 |
| 3 | 素菜 | 2 |
2 |
| 4 | 水果 | 5 |
5 |
| 5 | 蛋奶坚果 | 4 |
4 |
| 6 | 汤饮 | 6 |
6 |
分类顺序、服务端编码、显示资源和服务端名称别名只能在公共分类模型中定义一次。页面不得再维护分类字符串数组、数字转换 switch 或 dishItemList1 至 dishItemList6 的手工对应关系。
2. 当前问题
2.1 分类契约散落在页面中
NewDishLearningActivity、DishMenuManagementActivity 和 DishRecognitionActivity 分别保存了分类标签、分类顺序、请求编码转换或菜单响应分组逻辑。同一规则存在多份副本,修改任意一份时都可能遗漏其他入口。
2.2 菜品和菜谱使用不同的解释路径
- 菜品列表把页面标签转换为
category=1~6后请求/api/consume/dishes,响应回来后又根据tag.name修正分类名称。 - 菜谱列表请求
/p/api/getMeal,直接把响应对象的键"1"~"6"手工分配到六个页面列表。
两条路径的业务分类和当前数字值一致,现场最终确认均为 2=素菜、3=荤菜、4=蛋奶坚果、5=水果。模型仍分别保存菜品列表请求值和菜谱响应 key,使调用者明确选择接口边界,避免未来协议变化或页面自行换位。
2.3 异常分类被静默归入主食
菜品列表在“全部”条件下遇到无法识别的 tag.name 时会回退到“主食”。这会隐藏服务端数据异常并造成错误分类。
2.4 响应模型暴露无语义字段
GetDishMenuResponse 对外暴露 dishItemList1 至 dishItemList6。所有调用者都需要再次知道数字与业务分类的关系,导致硬对应扩散到页面、缓存和同步策略。
3. 统一设计
3.1 公共分类模型
新增 DishCategory 分类模型,职责是定义客户端认可的分类语义及两个接口各自的编码契约,并提供安全解析能力。该类需要有明确的类级注释,说明它只负责分类编码、顺序、显示资源和服务端名称归一化,不负责网络请求和界面渲染。
每个分类包含:
- 菜品列表接口请求编码;
- 菜谱接口响应分组编码;
strings.xml中的显示文案资源 ID;- 是否参与标准分类标签展示;
- 服务端可能返回的名称别名;
- 固定的展示顺序。
公共方法至少覆盖:
- 获取“全部 + 六个标准分类”的有序列表;
- 获取六个标准分类的有序列表;
- 根据菜品列表请求编码查找分类;
- 根据菜谱响应分组编码查找分类;
- 根据服务端名称或别名查找分类;
- 获取指定接口所需的分类编码。
全部 是查询范围,不是实际菜品类别。未知分类也不是 全部 或 主食,解析时必须明确区分。
3.2 菜谱响应按编码索引
GetDishMenuResponse 改为按菜谱接口原始分组编码保存菜品列表,并通过分类模型中的菜谱分组映射提供以下能力:
- 获取某个标准分类的菜品;
- 按标准分类顺序遍历所有已知分类;
- 获取全部菜品;
- 保留服务端返回但客户端尚不认识的分类分组。
页面和其他业务类不再访问 dishItemList1 至 dishItemList6。
菜单响应解析应遍历服务端响应对象的实际 key,通过 DishCategory.fromMenuGroupCode() 解析已知编码,而不是套用菜品列表请求编码,也不在转换器中逐项写死六个字段。
3.3 页面统一使用分类对象
菜品学习页
NewDishLearningActivity 使用公共有序分类列表创建标签。请求时读取所选分类的菜品列表接口编码,响应中的 tag.name 使用公共名称解析方法处理。
删除页面内的:
CATEGORY_ALL;CATEGORY_LIST;mapCategoryToApiValue();normalizeCategoryLabel()中的分类别名硬编码。
菜谱管理页
DishMenuManagementActivity 使用公共有序分类列表创建标签,并使用按分类索引的数据结构保存页面数据。页面通过分类对象从 GetDishMenuResponse 读取列表,由响应模型统一把业务分类转换为菜谱分组 key,不再维护六个具名列表,也不再手工绑定响应字段。
“全部”列表必须按照主食、荤菜、素菜、水果、蛋奶坚果、汤饮的顺序合并。
菜品识别页
DishRecognitionActivity 的“替换菜谱”和“替换后学习”两个入口都使用同一个公共分类模型:
- 替换菜谱按分类对象读取菜单响应;
- 替换后学习直接使用分类对象生成菜品列表请求参数;
- 删除页面内重复的分类数组、编码转换、名称归一化和六个菜谱列表。
3.4 非展示消费者统一遍历
DishMenuStore 和 DishManagementSyncPolicy 虽然不显示分类,但也不再逐字段读取六组菜单。两者使用 GetDishMenuResponse 提供的统一遍历或全部菜品接口,避免继续依赖旧响应结构。
DishCatalogRepository 只负责全量同步菜品目录,不解释分类标签,因此不需要修改。
4. 异常处理
- 未知服务端编码:保留在菜谱响应的未知分组中,只在“全部”范围展示,不归入六个标准分类,并记录可定位的警告日志。
- 未知
tag.name:在明确选择某个分类请求时,可使用当前请求分类作为数据对象的回退分类;在“全部”请求时保持未知,不再回退到“主食”。 - 空
tag:处理规则与未知tag.name相同。 - 服务端漏掉某个标准分组:该分类显示空列表,不改变其他分类顺序。
5. 布局和资源
现有三个布局中的分类区域都是动态容器,不包含分类标签或分类顺序,因此不修改 XML 布局:
activity_new_dish_learning.xml;activity_dish_menu_management.xml;activity_dish_recognition.xml。
在 strings.xml 中新增或复用“全部、主食、荤菜、素菜、水果、蛋奶坚果、汤饮”的字符串资源。页面只通过分类模型持有的资源 ID 获取显示文案。
6. 测试设计
新增公共分类模型单元测试,至少验证:
- 标签顺序固定为全部、主食、荤菜、素菜、水果、蛋奶坚果、汤饮;
- 菜品列表接口编码
1~6分别对应主食、素菜、荤菜、蛋奶坚果、水果、汤饮; - 菜谱接口分组
1~6分别对应主食、素菜、荤菜、蛋奶坚果、水果、汤饮; - “肉类、菜类、汤类”等服务端名称别名归一化正确;
- 未知编码和未知名称不会归入主食;
- 菜谱响应按标准分类顺序聚合;
- 未知菜谱分组只出现在“全部”范围;
- 服务端缺少部分分组时返回空列表且不崩溃。
更新现有 DishManagementSyncPolicyTest,改用新的菜单响应写入和遍历接口。完成后运行:
./gradlew :app:testDebugUnitTest :app:assembleDebug
git diff --check
7. 影响范围
计划修改:
- 新增公共
DishCategory分类模型; GetDishMenuResponse;- 菜单响应解析相关转换器;
NewDishLearningActivity;DishMenuManagementActivity;DishRecognitionActivity;DishMenuStore;DishManagementSyncPolicy;strings.xml;- 分类模型、菜单响应和同步策略相关单元测试。
明确不修改:
- 三个页面的 XML 布局结构和样式;
- 服务端接口路径和字段名称;
- 菜品支付、识别算法、学习图片和订单逻辑;
- Git 分支、暂存区和提交记录。
8. 验收标准
- 四个分类入口的标签顺序完全一致;
- 菜品列表请求和菜谱响应使用同一份业务分类模型,并分别采用各自真实的数字协议;
- 三个页面中不存在独立的分类标签数组和数字映射方法;
- 菜谱页面中不存在
dishItemList1~6到标签的手工对应; - 未知分类不会显示为主食;
- 自动化测试和 Debug 构建通过;
- 业务代码变更后,在提示词工程
change_records/中生成同名 Markdown 与 HTML 变更记录; - 代码仓库中不产生新的文档目录或文档产物。