熟食称重“先放后选”实施计划
目标:实现“用户归零并稳定 → 放商品并稳定 → 点击商品直接入车 → 取走商品并由用户归零”的单一闭环,同一轮只入车一次。
架构:串口完整帧组装 → 不可变 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. 固定实现约束
- 所有状态机输入和 UI 更新在主线程串行执行;页面展示值不反向参与零位判定。
- 只接受
kg,用BigDecimal转整数克,默认拒绝去皮;初始快照有效期常量为3000L,真机复核。 - 系统任何自动路径都不得调用归零;页面归零命令只能由用户明确点击触发,命令成功也不能直接解锁。
- 同商品合并时,提示金额是本轮新增金额,不是合并后累计行金额。
- 无活动称重周期时秤故障不阻断定值、手动金额和已完成记录结算;活动周期中断则保持锁定,直至重连并收到稳定零位。
- 打开账单和用户确认支付时各检查一次称重门禁。
3. 分步实施
Task 1:串口完整帧组装
文件:ScaleFrameAssembler.java、WeightSerialPortManager.java、ScaleFrameAssemblerTest.java、ScaleTestFrameFactory.java
- 先写失败测试:完整帧、半帧、粘包、前置噪声、错误标记后恢复。
- 实现 16 字节缓存组帧器,使用协议常量校验索引 0/1/13/14;保留半帧,错误起点逐字节丢弃。
- 串口读线程只对完整帧解析和回调;命令
future不能再由任意读取块完成。 - 读线程退出前清空残留缓冲并通知断开。
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
- 先写 kg 转克、零位、过期、空结果、解析失败、BCC、负号、溢出、去皮、非 kg、非法数字和零位冲突测试。
- 实现
WeightSnapshot.from(result, receivedAtMs),保存整数克、稳定、零位、采集时间和失败原因。 - 转换固定使用
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。
- 测试稳定零位到稳定正重,只能成功预留一次;提交成功后必须等待新的稳定零位。
- 测试首次非零、未稳定、异常输入、快照过期、断线恢复、等待取走时重复点击。
- 测试无活动周期故障时定值流程可用,活动周期断线时定值和结算锁定。
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
- 先测新建行、同商品合并、不同商品分行、合并后置顶、非法克数拒绝和原对象不被就地修改。
- 增加
copyWithAdditionalWeight(),再由更新器一次性替换列表行。 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
- 重量回调只生成带时间的快照并投递主线程;每帧刷新数据超时任务,销毁页面时移除。
- 称重商品点击调用状态机预留,成功后用
CookedFoodCartUpdater直接入车并提交周期。 - 成功提示固定为“某商品 250g 已加入,¥xx.xx;请取走商品并手动归零”。
- 删除预选和二次加入业务入口,隐藏
cooked_food_add_view;无预选时实时金额保持 0.00 元。 - 加入失败回滚同一周期并允许重试;动态成功详情只做文字提示,不新增 TTS。
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash gradlew :app:compileDebugJavaWithJavac --console=plain
通过标准:正重稳定后点击一次直接入车,第二次点击不能重复加入。
Task 6:用户归零与操作门禁
文件:CookedFoodActivity.java、strings.xml
- 增加
zeroRequestInFlight;只有归零按钮点击链路可调用WeightSerialPortManager.zero()。 - 归零命令返回成功只解除请求锁,不改变称重状态;必须等待新稳定零位帧。
- 称重周期活动时,定值商品、页签切换、手动金额打开和确认、数量编辑统一拦截。
- 删除购物车行和清空购物车仍可纠错,但不能解除
WAITING_REMOVE_ZERO。 - 无活动周期且秤未就绪时,定值和手动金额保持可用。
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
- 抽取
ensureCheckoutAllowed(),同时检查支付会话与称重周期。 - 在
showBillCheckPop()创建订单快照前检查一次。 - 在账单确认回调第一行、关闭弹窗和发起支付前再检查一次。
- 第二次失败时关闭确认、保留购物车并提示完成称重,不发起刷卡、扫码或人脸支付。
- 支付成功不自动归零,下一轮仍以真实秤状态为准。
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
真机验收
- 启动空秤、启动压秤、用户归零失败后重试。
- 慢放、快放、重量抖动、稳定瞬间连续点击两个商品。
- 同商品连续两个完整周期,核对本次提示金额与累计金额。
- 入车后不取走、取走一半、取走不归零、页面归零和实体键归零。
- 半帧、粘包、拔线、重连,核对有无活动周期的权限差异。
- 秤故障且无活动周期时完成定值商品、手动金额和已完成记录结算。
- 账单打开后放商品,刷卡、扫码、人脸确认都应被拦截。
- 支付成功、失败和离线记账后核对购物车、秤状态与下一轮。
通过标准:自动化和 Debug 构建成功;真机记录包含设备/固件、原始帧、超时验证、风险和回滚方法。
4. 完成定义
- 稳定零位前不能称重入车,稳定正重点击商品立即入车,同一周期只能成功一次。
- 成功提示包含商品、重量、本次新增金额,并提示取走商品和手动归零。
- 系统任何自动路径都不会发送归零命令;只有新稳定零位帧解锁下一轮。
- 活动周期异常时定值、手动金额和结算锁定;无活动周期秤故障时定值流程可降级。
- 账单弹窗打开后新增秤上商品,支付确认被二次检查拦截。
- 原购物车结构、订单 JSON、支付协议和上传方式不变。
- 全量测试、Debug 构建、真机验收和 Markdown/HTML 变更记录完整。
代码实施与自动化验证已完成,真实秤体和支付设备验收尚未执行。实施结束仍由用户决定是否执行任何 Git 写操作。