熟食称重“先放后选”实施计划

项目:SellWeight日期:2026-09-20状态:Task 1–7 及自动化验证已完成,待真机验收分支:refactor_cooked_model

目标:实现“用户归零并稳定 → 放商品并稳定 → 点击商品直接入车 → 取走商品并由用户归零”的单一闭环,同一轮只入车一次。

架构:串口完整帧组装 → 不可变 WeightSnapshot → 纯 Java CookedFoodWeighingStateMachine → Activity 编排 → 购物车原子更新与支付门禁。

技术栈:Android Java 8、SDK 30、AGP 4.2.1、Gradle 6.7.1、JUnit 4.13.2、现有支付会话控制器。

执行规则:使用 executing-plans 逐任务实施;默认不执行暂存、提交、推送、分支切换或工作树创建。

1. 文件职责

文件动作职责
ScaleFrameAssembler.java新增从任意串口读取块提取完整 16 字节帧
WeightSnapshot.java新增规范化整数克、协议状态与采集时间
WeightSerialPortManager.java修改接入组帧器,逐帧解析并分发断开事件
CookedFoodWeighingStateMachine.java新增管理称重周期、权限和单轮防重复
CookedFoodCartUpdater.java新增原子新增/合并并返回本次新增金额
CookedFoodCartLine.java修改提供不修改原对象的称重行复制能力
CookedFoodActivity.java修改编排主线程事件、商品点击、用户归零、门禁与支付
activity_cooked_food.xml / strings.xml修改隐藏旧入口并补齐状态和提示文案
ScaleTestFrameFactory.java 与四组测试新增复用协议帧构造,覆盖组帧、快照、状态机和购物车

2. 固定实现约束

3. 分步实施

Task 1:串口完整帧组装

文件:ScaleFrameAssembler.java、WeightSerialPortManager.java、ScaleFrameAssemblerTest.java、ScaleTestFrameFactory.java

  1. 先写失败测试:完整帧、半帧、粘包、前置噪声、错误标记后恢复。
  2. 实现 16 字节缓存组帧器,使用协议常量校验索引 0/1/13/14;保留半帧,错误起点逐字节丢弃。
  3. 串口读线程只对完整帧解析和回调;命令 future 不能再由任意读取块完成。
  4. 读线程退出前清空残留缓冲并通知断开。
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash gradlew :app:testDebugUnitTest \
  --tests 'com.cpt.cusumption.devices.weight.ScaleFrameAssemblerTest' --console=plain

通过标准:半帧不提前输出,粘包输出两帧,噪声后仍能恢复;全部单元测试成功。

Task 2:不可变重量快照与输入契约

文件:WeightSnapshot.java、WeightSnapshotTest.java

  1. 先写 kg 转克、零位、过期、空结果、解析失败、BCC、负号、溢出、去皮、非 kg、非法数字和零位冲突测试。
  2. 实现 WeightSnapshot.from(result, receivedAtMs),保存整数克、稳定、零位、采集时间和失败原因。
  3. 转换固定使用 BigDecimal.movePointRight(3);稳定零位同时满足协议零位标志与规范化 0g。
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash gradlew :app:testDebugUnitTest \
  --tests 'com.cpt.cusumption.devices.weight.WeightSnapshotTest' --console=plain

通过标准:无效输入有明确原因,过期快照不可消费,UI 文本不参与计算。

Task 3:称重周期状态机

文件:CookedFoodWeighingStateMachine.java、CookedFoodWeighingStateMachineTest.java

状态固定为:WAITING_SCALE、WAITING_ZERO_STABLE、READY_TO_PLACE、WEIGHT_CHANGING、READY_TO_SELECT、WAITING_REMOVE_ZERO、SCALE_ERROR。

  1. 测试稳定零位到稳定正重,只能成功预留一次;提交成功后必须等待新的稳定零位。
  2. 测试首次非零、未稳定、异常输入、快照过期、断线恢复、等待取走时重复点击。
  3. 测试无活动周期故障时定值流程可用,活动周期断线时定值和结算锁定。
  4. reserveSelection() 原子预留 cycleId;购物车失败只能回滚同一周期。
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash gradlew :app:testDebugUnitTest \
  --tests 'com.cpt.cusumption.product.CookedFoodWeighingStateMachineTest' --console=plain

通过标准:同一周期只入车一次;故障时权限符合“活动周期锁定、无活动周期降级”。

Task 4:购物车原子更新与本次金额

文件:CookedFoodCartLine.java、CookedFoodCartUpdater.java、CookedFoodCartUpdaterTest.java、CookedFoodCartLineTest.java

  1. 先测新建行、同商品合并、不同商品分行、合并后置顶、非法克数拒绝和原对象不被就地修改。
  2. 增加 copyWithAdditionalWeight(),再由更新器一次性替换列表行。
  3. AddResult 返回更新后行和 addedAmount;250g + 250g 的第二次提示金额仍是 250g 对应金额。
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash gradlew :app:testDebugUnitTest \
  --tests 'com.cpt.cusumption.product.CookedFoodCart*Test' --console=plain

通过标准:购物车累计正确,成功提示使用本次新增金额。

Task 5:页面接入“点击即入车”

文件:CookedFoodActivity.java、activity_cooked_food.xml、strings.xml

  1. 重量回调只生成带时间的快照并投递主线程;每帧刷新数据超时任务,销毁页面时移除。
  2. 称重商品点击调用状态机预留,成功后用 CookedFoodCartUpdater 直接入车并提交周期。
  3. 成功提示固定为“某商品 250g 已加入,¥xx.xx;请取走商品并手动归零”。
  4. 删除预选和二次加入业务入口,隐藏 cooked_food_add_view;无预选时实时金额保持 0.00 元。
  5. 加入失败回滚同一周期并允许重试;动态成功详情只做文字提示,不新增 TTS。
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash gradlew :app:compileDebugJavaWithJavac --console=plain

通过标准:正重稳定后点击一次直接入车,第二次点击不能重复加入。

Task 6:用户归零与操作门禁

文件:CookedFoodActivity.java、strings.xml

  1. 增加 zeroRequestInFlight;只有归零按钮点击链路可调用 WeightSerialPortManager.zero()。
  2. 归零命令返回成功只解除请求锁,不改变称重状态;必须等待新稳定零位帧。
  3. 称重周期活动时,定值商品、页签切换、手动金额打开和确认、数量编辑统一拦截。
  4. 删除购物车行和清空购物车仍可纠错,但不能解除 WAITING_REMOVE_ZERO。
  5. 无活动周期且秤未就绪时,定值和手动金额保持可用。
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash gradlew :app:testDebugUnitTest \
  --tests 'com.cpt.cusumption.product.CookedFood*Test' --console=plain

硬约束:入车、超时、重连、生命周期和状态转换均不得自动发送归零命令。

Task 7:结算二次校验与支付边界

文件:CookedFoodActivity.java、strings.xml

  1. 抽取 ensureCheckoutAllowed(),同时检查支付会话与称重周期。
  2. 在 showBillCheckPop() 创建订单快照前检查一次。
  3. 在账单确认回调第一行、关闭弹窗和发起支付前再检查一次。
  4. 第二次失败时关闭确认、保留购物车并提示完成称重,不发起刷卡、扫码或人脸支付。
  5. 支付成功不自动归零,下一轮仍以真实秤状态为准。
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash gradlew :app:compileDebugJavaWithJavac --console=plain

通过标准:账单弹窗打开后再放商品,支付确认被二次门禁拦截。

Task 8:全量验证、真机验收与交付记录

文档:实施后创建同名 Markdown/HTML 变更记录,并将设计状态如实更新为已实施、已验收或记录未完成项。

自动化验证

JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash gradlew :app:testDebugUnitTest :app:assembleDebug --console=plain

git diff --check
rg -n 'WeightSerialPortManager\.getInstance\(\)\.zero\(\)' app/src/main/java
rg -n 'Math\.max\(0.*weight|selectedProduct|cooked_food_add_view' \
  app/src/main/java/com/cpt/cusumption/activity/CookedFoodActivity.java

真机验收

  1. 启动空秤、启动压秤、用户归零失败后重试。
  2. 慢放、快放、重量抖动、稳定瞬间连续点击两个商品。
  3. 同商品连续两个完整周期,核对本次提示金额与累计金额。
  4. 入车后不取走、取走一半、取走不归零、页面归零和实体键归零。
  5. 半帧、粘包、拔线、重连,核对有无活动周期的权限差异。
  6. 秤故障且无活动周期时完成定值商品、手动金额和已完成记录结算。
  7. 账单打开后放商品,刷卡、扫码、人脸确认都应被拦截。
  8. 支付成功、失败和离线记账后核对购物车、秤状态与下一轮。

通过标准:自动化和 Debug 构建成功;真机记录包含设备/固件、原始帧、超时验证、风险和回滚方法。

4. 完成定义

代码实施与自动化验证已完成,真实秤体和支付设备验收尚未执行。实施结束仍由用户决定是否执行任何 Git 写操作。