菜品与菜谱分类统一 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 用单一分类模型统一菜品列表和菜谱列表的展示顺序、业务语义、名称别名与异常分类处理,同时独立管理两个接口的协议字段。

Architecture: 新增纯枚举 DishCategory 作为唯一业务分类契约,在枚举内分别保存菜品列表请求编码和菜谱响应分组编码。菜单响应按原始分组 key 保存数据,页面统一遍历分类对象并通过 EnumMap 管理分组。网络转换器按响应实际 key 解析,未知分类只进入“全部”范围,不再落入“主食”。

Tech Stack: Android Java 8、AndroidX、Retrofit 2、Gson、JUnit 4、Gradle。


Task 1: 建立公共分类契约

Files:

测试断言展示顺序为 ALL, STAPLE, MEAT, VEGETABLE, FRUIT, EGG_MILK_NUT, SOUP_DRINK;菜品列表和菜谱分组均按 1主食、2素菜、3荤菜、4蛋奶坚果、5水果、6汤饮 解析,但通过不同字段访问;未知值为 UNKNOWN

./gradlew :app:testDebugUnitTest --tests com.cpt.aidishrecognition.model.DishCategoryTest

预期:因 DishCategory 尚不存在而编译失败。

DishCategory 提供以下稳定接口:

public enum DishCategory {
    ALL("", null, R.string.dish_category_all, "全部"),
    STAPLE("1", "1", R.string.dish_category_staple, "主食"),
    MEAT("3", "3", R.string.dish_category_meat, "荤菜", "肉类"),
    VEGETABLE("2", "2", R.string.dish_category_vegetable, "素菜", "菜类"),
    FRUIT("5", "5", R.string.dish_category_fruit, "水果"),
    EGG_MILK_NUT("4", "4", R.string.dish_category_egg_milk_nut, "蛋奶坚果"),
    SOUP_DRINK("6", "6", R.string.dish_category_soup_drink, "汤饮", "汤类"),
    UNKNOWN(null, null, R.string.dish_category_unknown);

    public static List<DishCategory> displayCategories();
    public static List<DishCategory> standardCategories();
    public static DishCategory fromDishListApiValue(String apiValue);
    public static DishCategory fromMenuGroupCode(String menuGroupCode);
    public static DishCategory fromServerName(String serverName);
    public String getDishListApiValue();
    public String getMenuGroupCode();
    public int getLabelResId();
    public boolean isStandardCategory();
}

类级注释说明该枚举只维护分类契约,不负责网络和 UI。

./gradlew :app:testDebugUnitTest --tests com.cpt.aidishrecognition.model.DishCategoryTest

预期:测试通过。

Task 2: 菜谱响应改为按编码索引

Files:

测试用 Gson 解析包含键 1~69 的菜单对象,断言 2=素菜、3=荤菜、4=蛋奶坚果、5=水果,全部按标准展示顺序后追加未知分组,缺失分组返回空列表,未知编码不进入任一标准分类。

./gradlew :app:testDebugUnitTest --tests com.cpt.aidishrecognition.network.response.GetDishMenuResponseTest

预期:新响应接口和反序列化器尚不存在。

GetDishMenuResponse 使用 LinkedHashMap<String, List<DishBean>> 保存服务端分组,并提供:

public void putDishes(String categoryCode, List<DishBean> dishes);
public List<DishBean> getDishes(DishCategory category);
public List<DishBean> getDishesByCode(String categoryCode);
public Set<String> getCategoryCodes();
public List<DishBean> getAllDishes();
public List<DishBean> getUnknownDishes();

返回值使用副本或只读视图保护内部集合。“全部”先按六个标准分类顺序聚合,再追加未知编码分组。

GetDishMenuResponseJsonDeserializer 遍历 JSON 对象实际 key 并调用 putDishes()。在 MyGsonConverterFactory 注册该反序列化器;MyGsonResponseBodyConverter 根据 response.getCategoryCodes() 遍历原始数组,不再逐项引用 dishItemList1~6

./gradlew :app:testDebugUnitTest --tests com.cpt.aidishrecognition.network.response.GetDishMenuResponseTest

预期:测试通过。

Task 3: 迁移菜谱缓存与同步规则

Files:

使用 response.putDishes("1", ...)response.putDishes("4", ...) 构造菜单响应,并新增未知分组仍参与“全部菜品编码”聚合的断言。

DishMenuStore.saveCurrentMenu()DishManagementSyncPolicy.buildMenuDishMap() 只遍历 response.getAllDishes(),删除六次逐字段读取。

./gradlew :app:testDebugUnitTest --tests com.cpt.aidishrecognition.utils.DishManagementSyncPolicyTest

预期:全部通过。

Task 4: 迁移三个页面的四个分类入口

Files:

selectedCategory 改为 DishCategory,标签通过 DishCategory.displayCategories() 创建并把分类对象写入 View tag。请求参数使用 selectedCategory.getDishListApiValue();服务端名称通过 DishCategory.fromServerName() 解析,未知名称在“全部”请求时显示“未分类”,不再回退到主食。

使用 EnumMap<DishCategory, List<DishLearningDish>> 代替六个具名列表。按 DishCategory.standardCategories() 从响应取数并聚合;未知分组最后追加到“全部”。分类切换直接按分类对象查表。

“替换菜谱”复用与菜谱管理页相同的 EnumMap 分组;“替换后学习”直接使用分类对象生成请求参数和响应标签。删除页面中的 CATEGORY_ALLCATEGORY_LISTmapCategoryToApiValue()、分类名称 switch 和六个菜谱列表。

rg -n 'CATEGORY_LIST|CATEGORY_ALL|mapCategoryToApiValue|dishItemList[1-6]|case "主食"|case "荤菜"|case "素菜"' \
  app/src/main/java/com/cpt/aidishrecognition/activity \
  app/src/main/java/com/cpt/aidishrecognition/utils \
  app/src/main/java/com/cpt/aidishrecognition/network

预期:相关页面和消费者无命中;协议值仅存在于公共分类模型或必要的测试数据中。

Task 5: 全量验证与变更归档

Files:

./gradlew :app:testDebugUnitTest :app:assembleDebug

预期:BUILD SUCCESSFUL

git diff --check
git diff -- app/src/main/java/com/cpt/aidishrecognition/model/DishCategory.java \
  app/src/main/java/com/cpt/aidishrecognition/network \
  app/src/main/java/com/cpt/aidishrecognition/activity/NewDishLearningActivity.java \
  app/src/main/java/com/cpt/aidishrecognition/activity/DishMenuManagementActivity.java \
  app/src/main/java/com/cpt/aidishrecognition/activity/DishRecognitionActivity.java \
  app/src/main/java/com/cpt/aidishrecognition/utils/DishMenuStore.java \
  app/src/main/java/com/cpt/aidishrecognition/utils/DishManagementSyncPolicy.java \
  app/src/main/res/values/strings.xml app/src/test

预期:没有空白错误、页面独立分类映射或无说明的新增协议字面量。

变更记录包括分类契约、修改文件、未知分类处理、自动化验证结果和真机回归项;Markdown 与 HTML 内容一致。

不执行 git addgit commitgit push、切换分支或其他 Git 写操作,只报告本次修改和工作区既有修改。

Task 6: 根据现场菜谱数据纠正接口边界

Files:

断言菜谱分组 2=素菜、3=荤菜、4=蛋奶坚果、5=水果,与菜品列表请求编号分别解析。修改前因公共模型只有一套编号而编译失败。

DishCategory 内新增 dishListApiValuemenuGroupCode,菜品查询只读前者,菜谱响应只读后者。页面仍只持有分类对象,不出现交换下标或页面级数字映射。

执行 ./gradlew :app:testDebugUnitTest :app:assembleDebuggit diff --check,结果为 BUILD SUCCESSFUL,61 个单元测试全部通过。

Task 7: 根据菜品学习页现场结果修正请求编码

Files:

断言菜品列表接口请求值为 2=素菜、3=荤菜、4=蛋奶坚果、5=水果。旧实现的荤菜断言首先失败,能够复现学习页标签与返回列表互换的问题。

dishListApiValue 修正为现场值;菜谱侧滑继续读取独立的 menuGroupCode,不修改已经正确的菜谱取数路径。

分类模型和菜谱响应针对性测试通过;完整单元测试、Debug 构建及差异空白检查通过。