菜品与菜谱分类统一 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:
Create:
app/src/main/java/com/cpt/aidishrecognition/model/DishCategory.javaCreate:
app/src/test/java/com/cpt/aidishrecognition/model/DishCategoryTest.javaModify:
app/src/main/res/values/strings.xmlStep 1: 写分类顺序、编码和别名测试
测试断言展示顺序为 ALL, STAPLE, MEAT, VEGETABLE, FRUIT, EGG_MILK_NUT, SOUP_DRINK;菜品列表和菜谱分组均按 1主食、2素菜、3荤菜、4蛋奶坚果、5水果、6汤饮 解析,但通过不同字段访问;未知值为 UNKNOWN。
- Step 2: 运行分类测试并确认失败
./gradlew :app:testDebugUnitTest --tests com.cpt.aidishrecognition.model.DishCategoryTest
预期:因 DishCategory 尚不存在而编译失败。
- Step 3: 实现公共枚举和字符串资源
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。
- Step 4: 运行分类测试并确认通过
./gradlew :app:testDebugUnitTest --tests com.cpt.aidishrecognition.model.DishCategoryTest
预期:测试通过。
Task 2: 菜谱响应改为按编码索引
Files:
Create:
app/src/main/java/com/cpt/aidishrecognition/network/converter/GetDishMenuResponseJsonDeserializer.javaCreate:
app/src/test/java/com/cpt/aidishrecognition/network/response/GetDishMenuResponseTest.javaModify:
app/src/main/java/com/cpt/aidishrecognition/network/response/GetDishMenuResponse.javaModify:
app/src/main/java/com/cpt/aidishrecognition/network/converter/MyGsonConverterFactory.javaModify:
app/src/main/java/com/cpt/aidishrecognition/network/converter/MyGsonResponseBodyConverter.javaStep 1: 写菜单分组解析测试
测试用 Gson 解析包含键 1~6、9 的菜单对象,断言 2=素菜、3=荤菜、4=蛋奶坚果、5=水果,全部按标准展示顺序后追加未知分组,缺失分组返回空列表,未知编码不进入任一标准分类。
- Step 2: 运行响应测试并确认失败
./gradlew :app:testDebugUnitTest --tests com.cpt.aidishrecognition.network.response.GetDishMenuResponseTest
预期:新响应接口和反序列化器尚不存在。
- Step 3: 实现按编码索引的响应对象
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();
返回值使用副本或只读视图保护内部集合。“全部”先按六个标准分类顺序聚合,再追加未知编码分组。
- Step 4: 实现动态反序列化和通用补丁遍历
GetDishMenuResponseJsonDeserializer 遍历 JSON 对象实际 key 并调用 putDishes()。在 MyGsonConverterFactory 注册该反序列化器;MyGsonResponseBodyConverter 根据 response.getCategoryCodes() 遍历原始数组,不再逐项引用 dishItemList1~6。
- Step 5: 运行响应测试并确认通过
./gradlew :app:testDebugUnitTest --tests com.cpt.aidishrecognition.network.response.GetDishMenuResponseTest
预期:测试通过。
Task 3: 迁移菜谱缓存与同步规则
Files:
Modify:
app/src/main/java/com/cpt/aidishrecognition/utils/DishMenuStore.javaModify:
app/src/main/java/com/cpt/aidishrecognition/utils/DishManagementSyncPolicy.javaModify:
app/src/test/java/com/cpt/aidishrecognition/utils/DishManagementSyncPolicyTest.javaStep 1: 更新同步策略测试数据构造
使用 response.putDishes("1", ...) 和 response.putDishes("4", ...) 构造菜单响应,并新增未知分组仍参与“全部菜品编码”聚合的断言。
- Step 2: 迁移两个消费者
DishMenuStore.saveCurrentMenu() 和 DishManagementSyncPolicy.buildMenuDishMap() 只遍历 response.getAllDishes(),删除六次逐字段读取。
- Step 3: 运行同步策略测试
./gradlew :app:testDebugUnitTest --tests com.cpt.aidishrecognition.utils.DishManagementSyncPolicyTest
预期:全部通过。
Task 4: 迁移三个页面的四个分类入口
Files:
Modify:
app/src/main/java/com/cpt/aidishrecognition/activity/NewDishLearningActivity.javaModify:
app/src/main/java/com/cpt/aidishrecognition/activity/DishMenuManagementActivity.javaModify:
app/src/main/java/com/cpt/aidishrecognition/activity/DishRecognitionActivity.javaStep 1: 迁移菜品学习页
将 selectedCategory 改为 DishCategory,标签通过 DishCategory.displayCategories() 创建并把分类对象写入 View tag。请求参数使用 selectedCategory.getDishListApiValue();服务端名称通过 DishCategory.fromServerName() 解析,未知名称在“全部”请求时显示“未分类”,不再回退到主食。
- Step 2: 迁移菜谱管理页
使用 EnumMap<DishCategory, List<DishLearningDish>> 代替六个具名列表。按 DishCategory.standardCategories() 从响应取数并聚合;未知分组最后追加到“全部”。分类切换直接按分类对象查表。
- Step 3: 迁移菜品识别页两个入口
“替换菜谱”复用与菜谱管理页相同的 EnumMap 分组;“替换后学习”直接使用分类对象生成请求参数和响应标签。删除页面中的 CATEGORY_ALL、CATEGORY_LIST、mapCategoryToApiValue()、分类名称 switch 和六个菜谱列表。
- Step 4: 静态检查页面不再自定义分类契约
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:
Create:
work_android/AIDishRecognition/change_records/2026-09-03-dish-category-unification.mdCreate:
work_android/AIDishRecognition/change_records/2026-09-03-dish-category-unification.htmlStep 1: 运行完整单元测试和 Debug 构建
./gradlew :app:testDebugUnitTest :app:assembleDebug
预期:BUILD SUCCESSFUL。
- Step 2: 检查差异和硬编码
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
预期:没有空白错误、页面独立分类映射或无说明的新增协议字面量。
- Step 3: 生成成对变更记录
变更记录包括分类契约、修改文件、未知分类处理、自动化验证结果和真机回归项;Markdown 与 HTML 内容一致。
- Step 4: 保持 Git 状态不变更
不执行 git add、git commit、git push、切换分支或其他 Git 写操作,只报告本次修改和工作区既有修改。
Task 6: 根据现场菜谱数据纠正接口边界
Files:
Modify:
app/src/main/java/com/cpt/aidishrecognition/model/DishCategory.javaModify:
app/src/main/java/com/cpt/aidishrecognition/network/response/GetDishMenuResponse.javaModify:
app/src/main/java/com/cpt/aidishrecognition/network/converter/MyGsonResponseBodyConverter.javaModify: 分类与菜谱响应单元测试
Step 1: 用现场现象补充失败测试
断言菜谱分组 2=素菜、3=荤菜、4=蛋奶坚果、5=水果,与菜品列表请求编号分别解析。修改前因公共模型只有一套编号而编译失败。
- Step 2: 拆分两套接口编码
在 DishCategory 内新增 dishListApiValue 和 menuGroupCode,菜品查询只读前者,菜谱响应只读后者。页面仍只持有分类对象,不出现交换下标或页面级数字映射。
- Step 3: 全量验证
执行 ./gradlew :app:testDebugUnitTest :app:assembleDebug 和 git diff --check,结果为 BUILD SUCCESSFUL,61 个单元测试全部通过。
Task 7: 根据菜品学习页现场结果修正请求编码
Files:
Modify:
app/src/main/java/com/cpt/aidishrecognition/model/DishCategory.javaModify:
app/src/test/java/com/cpt/aidishrecognition/model/DishCategoryTest.javaStep 1: 建立学习页请求映射失败测试
断言菜品列表接口请求值为 2=素菜、3=荤菜、4=蛋奶坚果、5=水果。旧实现的荤菜断言首先失败,能够复现学习页标签与返回列表互换的问题。
- Step 2: 只修正菜品列表协议字段
将 dishListApiValue 修正为现场值;菜谱侧滑继续读取独立的 menuGroupCode,不修改已经正确的菜谱取数路径。
- Step 3: 回归验证
分类模型和菜谱响应针对性测试通过;完整单元测试、Debug 构建及差异空白检查通过。