AIDishRecognition · Implementation Plan

DishManagementActivity 菜谱识别切换同步变更方案

移除当前页面不必要的菜品基础目录强制同步,将“打开/不打开菜谱识别” 调整为可回滚的数据模式切换,保证开关、列表、本地目录与 AI SDK 特征一致。

日期:2026-07-23 状态:代码已实施 自动化验证通过 待指定设备真机回归 证据等级:B

1. 目标

解除不必要前置

菜品管理页进入和点击“同步记录”时,不再强制分页拉取全部菜品基础目录, 菜品目录同步失败也不再阻断学习记录同步。

切换即生效

用户切换模式时立即同步学习记录、更新 AI SDK 特征并刷新列表; 只有完整成功后才保存新配置,失败保持原模式。

一致性目标: 页面开关、列表内容、本地学习目录、AI SDK 有效特征四者始终一致。
  • 不打开:页面和 AI SDK 使用服务端全部学习记录。
  • 打开:页面和 AI SDK 只使用当前餐次菜谱内的学习记录。

2. 当前实现与问题

2.1 当前进入页面的同步链路

  1. 调用 DishCatalogRepository.syncAllAsync(),分页拉取全部菜品基础目录。
  2. 菜品目录成功后才进入学习记录同步。
  3. 根据菜谱识别开关决定是否拉取当前菜谱。
  4. 拉取学习图片、重建 SDK 特征、替换本地学习目录。
  5. 最后刷新页面列表。
菜品基础目录不是学习图片同步和 SDK 特征重建的必要前置条件。 当前设计会导致目录同步失败时,学习记录同步和本地列表展示一起被阻断。

2.2 当前开关切换逻辑

DishMenuStore.setMenuRecognitionEnabled(enabled);

当前只保存配置,不同步本地学习目录和 SDK 特征,也不刷新页面列表。 因此开关显示、页面内容和设备实际识别范围可能不一致。

2.3 名称和价格数据

  • /p/api/getMeal 返回完整 DishBean,包括编码、名称、价格、单位和图片。
  • /api/consume/dishesRecognizeImgList 只返回学习图片 ID、菜品编码、图片地址和特征。
  • 当前 DishMenuStore.saveCurrentMenu() 只保存编码集合,完整菜品信息没有用于管理页展示。

打开模式应保留本次菜谱响应中的完整 DishBean 供页面展示, 不需要为名称和价格再次拉取全部菜品目录。

3. 变更范围

计划修改

  • app/src/main/java/com/cpt/aidishrecognition/activity/DishManagementActivity.java
  • app/src/main/res/values/strings.xml(仅在新增切换提示时修改)

计划复用

  • AiReadinessGuard
  • DishLearningStore.FullSyncSession
  • DishMenuStore
  • DishCatalogRepository 的本地查询能力
  • 现有菜谱、学习记录和 SDK 修正接口

非目标

  • 不修改服务端接口,不删除全局 DishCatalogRepository
  • 不修改首页和识别页的公共菜品目录同步策略。
  • 不修改识别页“未排菜”的判定和支付规则。
  • 不改变打开模式下空菜谱形成空学习数据的现有语义。
  • 不重构整个菜品学习模块。

4. 目标处理逻辑

4.1 页面进入

  1. 初始化页面后立即读取本地学习目录并展示。
  2. 读取已经生效的模式并设置开关初始状态。
  3. AI SDK 就绪后,按当前模式同步学习记录。
  4. 成功后刷新列表;失败时保留进入页面前的数据。
页面进入不再调用 DishCatalogRepository.syncAllAsync()

4.2 点击“同步记录”

按当前已经生效的模式执行学习记录同步。成功刷新列表;失败保留原本地目录、 原 SDK 特征和原开关配置。

4.3 切换为“打开”

请求最新菜谱
生成允许编码集合
下载命中学习图片
SDK 重建并提交目录
保存配置并刷新
  1. 暂时显示目标选项,并禁用重复点击。
  2. 请求当前餐次菜谱,保留完整 DishBean
  3. 分页拉取学习记录,只下载命中当前菜谱的图片。
  4. staging 准备完成后调用 SDK 重建特征。
  5. SDK 和目录提交均成功后,保存“打开”配置。
  6. 页面只显示菜谱内已有学习记录的菜品。
菜谱为空时沿用现有规则:提交空学习目录并让 SDK 按空数据修正。 该行为必须纳入人工验收。

4.4 切换为“不打开”

不请求菜谱
拉取全部学习记录
下载全部有效图片
SDK 重建并提交目录
保存配置并刷新

关闭模式重新下载全部学习记录,恢复完整本地学习目录和全部 SDK 特征, 清除当前页面的菜谱展示缓存,最终显示全部已学习菜品。

4.5 切换失败

  • 不保存目标模式配置。
  • 静默恢复原 RadioGroup 选项,避免再次触发监听器。
  • 回滚 staging,正式目录和原 SDK 数据保持不变。
  • 页面继续展示原模式列表,恢复所有控件。
  • SDK 返回 NOT_ACTIVE 时继续交给 AiReadinessGuard

5. 设计决策

成功后提交配置

目标模式只作为同步参数传递。学习数据、SDK 和正式目录全部成功后, 才调用 setMenuRecognitionEnabled()

显式目标模式

同步方法显式接收目标模式,不能在异步过程中重新读取可能变化的全局配置。

展示数据优先级

最新菜谱完整数据 → 公共本地菜品目录 → 菜品编码和“本地已学”。

单任务保护

同步期间禁用开关和同步按钮,避免两个 full-sync staging 会话并行。

建议同步接口

syncLearningDataForMode(
    boolean targetMenuRecognitionEnabled,
    SyncReason reason
);

同步原因至少区分页面初始同步、手动同步和用户切换模式。 三种入口共用数据同步内核,只在配置提交、失败回退和提示语上区分。

6. 建议代码结构

onCreate
  ├─ initViews
  ├─ fetchDishList                 # 立即展示本地数据
  └─ runAfterAiReady
       └─ syncCurrentMode

menu switch listener
  └─ requestModeSwitch(targetMode)
       ├─ disable controls
       ├─ syncLearningDataForMode(targetMode)
       ├─ success: persist + refresh
       └─ failure: restore switch + keep old data

manual sync
  └─ syncLearningDataForMode(appliedMode)

建议增加的 Activity 内部状态:

  • 已经生效的模式 appliedMenuRecognitionEnabled
  • 静默修改开关标记 suppressMenuSwitchCallback
  • 同步中标记 learningSyncInProgress
  • 本次成功菜谱的 Map<String, DishBean>
  • 同步结果回调或内部结果对象

7. 实施任务拆分

1

拆除菜品目录强制前置同步

当前页面不再调用 syncAllAsync(),直接按当前模式进入学习记录同步; 初始化后先绑定已有本地学习记录。

验收标准

  • 当前页面无 DishCatalogRepository.syncAllAsync() 调用。
  • 网络请求完成前能够显示本地学习记录。
  • 菜品目录同步失败不再阻断学习记录同步。

依赖:无 范围:S,1 个文件

2

把学习记录同步改为显式目标模式

重构同步输入与结果回调;关闭模式同步全部,打开模式先拉菜谱后过滤, 任一失败都不替换正式目录。

验收标准

  • 关闭模式不请求菜谱,打开模式必须先请求最新菜谱。
  • 同步结果区分成功、普通失败和 SDK 未激活。
  • 失败路径全部保留正式学习目录。

依赖:Task 1 范围:M,1 个主文件

3

实现开关事务与失败回退

用户切换后立即同步目标模式;成功才保存配置,失败静默恢复原选项, 同步期间禁止重复操作。

验收标准

  • 成功后开关、列表、目录和 SDK 特征一致。
  • 失败后开关和配置恢复原状态。
  • 快速重复点击不能启动并行同步。
  • NOT_ACTIVE 进入统一激活流程。

依赖:Task 2 范围:M,1-2 个文件

4

接入菜谱完整数据并刷新列表

打开模式保留最新菜谱完整 DishBean,展示优先使用该数据; 关闭模式继续复用公共本地菜品目录。

验收标准

  • 打开模式名称和价格来自本次最新菜谱。
  • 搜索可以匹配本次菜谱中的最新名称。
  • 关闭模式显示全部学习菜品。
  • 不新增第二套持久化菜品目录。

依赖:Task 2、3 范围:S,1 个文件

5

提示语、构建和真机验收

补充必要的切换状态提示,完成单元测试、Debug 构建和真机端到端回归。

验证命令

./gradlew :app:testDebugUnitTest
./gradlew :app:assembleDebug

依赖:Task 1-4 范围:S,1-2 个文件

8. 验收场景

A · 打开模式

菜谱 A、B;学习记录 A、B、C。

预期:目录、SDK 和列表只包含 A、B;名称价格来自最新菜谱;配置为打开。

B · 关闭模式

使用同一组数据。

预期:目录、SDK 和列表包含 A、B、C;配置为不打开。

C · 菜谱接口失败

预期:不提交新目录;原 SDK、原目录和原配置不变;开关恢复。

D · 图片下载失败

预期:staging 回滚;正式目录和 SDK 原数据不变。

E · SDK 未激活

预期:不提交、不保存目标模式,进入统一 AI 激活提示。

F · 空菜谱

预期:打开模式过滤全部记录,SDK 按空数据修正,页面空列表。

G · 断网进入页面

预期:立即展示原本地列表,提示网络不可用,不清空任何有效数据。

9. 风险与控制

风险 影响 控制措施
切换时先保存配置 配置与 SDK 数据不一致 成功后提交配置,失败恢复原选项
多次点击并发同步 staging 目录互相覆盖 同步中禁用控件并设置单任务标记
菜谱为空 清空有效学习数据 保留现规则,但列为人工确认项
图片下载中途失败 新模式数据不完整 staging 全量成功后才调用 SDK 和提交
SDK 成功但目录提交失败 SDK 与磁盘不一致 沿用正式目录重新修正 SDK 的恢复逻辑
关闭模式依赖公共菜品目录 极端情况下展示降级 明确应用初始化同步契约,不在当前页恢复阻塞式全量同步

回退方案

  1. 恢复开关只保存配置的旧监听逻辑。
  2. 恢复 syncDishList() 中原有同步入口。
  3. 保留 staging、commit、rollback 的原子目录保护。
  4. 回退后重新执行一次完整学习记录同步,确保 SDK 与正式目录一致。
不建议只回退开关 UI 而保留新的配置提交时序,避免形成半套逻辑。

10. 人工评审清单

  • 同意移除当前页面的全部菜品基础目录强制同步。
  • 同意页面进入后先展示本地数据。
  • 同意切换成功后才保存配置。
  • 同意切换失败自动恢复原选项。
  • 同意打开模式直接使用最新菜谱中的名称和价格。
  • 空菜谱沿用现有业务规则:打开模式下形成空学习数据。
  • 切换过程不增加额外二次确认弹窗。
  • 已进入代码实施并完成自动化验证。
  • 待指定目标设备后完成打开、关闭、失败回退和空菜谱真机验证。

人工评审状态:用户已确认并授权实施。

接受的经验:模式配置必须在数据与 SDK 提交成功后再写入。

拒绝的反模式:只切换开关配置,不同步页面列表和实际识别数据。

待决问题:指定真机回归目标。

11. 结论

已按方案实施。

本方案移除当前页面不必要的菜品基础目录强制同步,把“菜谱识别开关” 从单纯配置项改为受控的数据模式切换。通过“目标模式同步 → SDK 与目录提交 → 保存配置 → 刷新列表”的顺序,保证用户看到的开关、页面列表和设备真实 识别能力一致;失败时完整保留原模式和原数据。

12. 实施结果

已完成

  • 页面进入后先绑定现有本地学习记录。
  • 移除当前页 DishCatalogRepository.syncAllAsync() 强制前置同步。
  • 打开模式先拉取最新菜谱并按编码过滤;关闭模式恢复全部学习记录。
  • 模式切换成功后才写入配置,失败时静默恢复原选项。
  • 同步期间禁用开关和同步入口,避免重复 full-sync。
  • 打开模式列表优先使用本次菜谱返回的完整 DishBean
  • SDK 未激活时保留目标模式请求,激活成功后可继续原同步流程。
  • 新增纯逻辑策略和单元测试,覆盖全量、过滤、空菜谱和菜品映射。

自动化验证

验证项 结果
聚焦策略测试 DishManagementSyncPolicyTest 通过
完整单元测试 29 项,0 失败,0 错误
Debug 构建 :app:assembleDebug 通过
APK app/build/outputs/apk/debug/app-debug.apk
当前连接 3 台设备,但本次未指定安装和回归目标,因此没有擅自向设备安装 APK。 待指定设备后验证打开、关闭、断网失败回退、SDK 未激活和空菜谱场景。