1. 目标
解除不必要前置
菜品管理页进入和点击“同步记录”时,不再强制分页拉取全部菜品基础目录, 菜品目录同步失败也不再阻断学习记录同步。
切换即生效
用户切换模式时立即同步学习记录、更新 AI SDK 特征并刷新列表; 只有完整成功后才保存新配置,失败保持原模式。
- 不打开:页面和 AI SDK 使用服务端全部学习记录。
- 打开:页面和 AI SDK 只使用当前餐次菜谱内的学习记录。
2. 当前实现与问题
2.1 当前进入页面的同步链路
- 调用
DishCatalogRepository.syncAllAsync(),分页拉取全部菜品基础目录。 - 菜品目录成功后才进入学习记录同步。
- 根据菜谱识别开关决定是否拉取当前菜谱。
- 拉取学习图片、重建 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.javaapp/src/main/res/values/strings.xml(仅在新增切换提示时修改)
计划复用
AiReadinessGuardDishLearningStore.FullSyncSessionDishMenuStoreDishCatalogRepository的本地查询能力- 现有菜谱、学习记录和 SDK 修正接口
非目标
- 不修改服务端接口,不删除全局
DishCatalogRepository。 - 不修改首页和识别页的公共菜品目录同步策略。
- 不修改识别页“未排菜”的判定和支付规则。
- 不改变打开模式下空菜谱形成空学习数据的现有语义。
- 不重构整个菜品学习模块。
4. 目标处理逻辑
4.1 页面进入
- 初始化页面后立即读取本地学习目录并展示。
- 读取已经生效的模式并设置开关初始状态。
- AI SDK 就绪后,按当前模式同步学习记录。
- 成功后刷新列表;失败时保留进入页面前的数据。
DishCatalogRepository.syncAllAsync()。
4.2 点击“同步记录”
按当前已经生效的模式执行学习记录同步。成功刷新列表;失败保留原本地目录、 原 SDK 特征和原开关配置。
4.3 切换为“打开”
- 暂时显示目标选项,并禁用重复点击。
- 请求当前餐次菜谱,保留完整
DishBean。 - 分页拉取学习记录,只下载命中当前菜谱的图片。
- staging 准备完成后调用 SDK 重建特征。
- SDK 和目录提交均成功后,保存“打开”配置。
- 页面只显示菜谱内已有学习记录的菜品。
4.4 切换为“不打开”
关闭模式重新下载全部学习记录,恢复完整本地学习目录和全部 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. 实施任务拆分
拆除菜品目录强制前置同步
当前页面不再调用 syncAllAsync(),直接按当前模式进入学习记录同步;
初始化后先绑定已有本地学习记录。
验收标准
- 当前页面无
DishCatalogRepository.syncAllAsync()调用。 - 网络请求完成前能够显示本地学习记录。
- 菜品目录同步失败不再阻断学习记录同步。
依赖:无 范围:S,1 个文件
把学习记录同步改为显式目标模式
重构同步输入与结果回调;关闭模式同步全部,打开模式先拉菜谱后过滤, 任一失败都不替换正式目录。
验收标准
- 关闭模式不请求菜谱,打开模式必须先请求最新菜谱。
- 同步结果区分成功、普通失败和 SDK 未激活。
- 失败路径全部保留正式学习目录。
依赖:Task 1 范围:M,1 个主文件
实现开关事务与失败回退
用户切换后立即同步目标模式;成功才保存配置,失败静默恢复原选项, 同步期间禁止重复操作。
验收标准
- 成功后开关、列表、目录和 SDK 特征一致。
- 失败后开关和配置恢复原状态。
- 快速重复点击不能启动并行同步。
NOT_ACTIVE进入统一激活流程。
依赖:Task 2 范围:M,1-2 个文件
接入菜谱完整数据并刷新列表
打开模式保留最新菜谱完整 DishBean,展示优先使用该数据;
关闭模式继续复用公共本地菜品目录。
验收标准
- 打开模式名称和价格来自本次最新菜谱。
- 搜索可以匹配本次菜谱中的最新名称。
- 关闭模式显示全部学习菜品。
- 不新增第二套持久化菜品目录。
依赖:Task 2、3 范围:S,1 个文件
提示语、构建和真机验收
补充必要的切换状态提示,完成单元测试、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 的恢复逻辑 |
| 关闭模式依赖公共菜品目录 | 极端情况下展示降级 | 明确应用初始化同步契约,不在当前页恢复阻塞式全量同步 |
回退方案
- 恢复开关只保存配置的旧监听逻辑。
- 恢复
syncDishList()中原有同步入口。 - 保留 staging、commit、rollback 的原子目录保护。
- 回退后重新执行一次完整学习记录同步,确保 SDK 与正式目录一致。
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 |