消费扣款前失败分类与 PSAM 32 字节续包兼容变更记录

原因确认

目标 F700-HW 设备在执行 PSAM 通道 SELECT 3F00 时使用 32 字节 HID 报告。首段响应后需要发送 0x50 续包请求,但设备的后续响应把 offset 再次返回为 0,原协议实现要求绝对 offset 严格递增,因此在扣款 APDU 发出前抛出 PROTOCOL_ERROR

现场前后读卡数据一致:主钱包余额和操作计数均未变化,日志也明确记录 cardDebitSent=false。原业务层仅以“是否进入消费调用”判断结果,因而把确定的扣款前失败误归类为 DEBIT_UNKNOWN

第二次真机复测确认第一版兼容已经命中:首段 25 字节后接受了 offset=0 的 2 字节续包,业务错误也正确变为 PRE_DEBIT_FAILURE。但首段已经以 9000 结束,理论 CRC 尾字节应为 A4 05,设备的 PSAM 0x50 续包实际返回通用应答 01 01,拼接后触发 CRC_ERROR。因此该 32 字节固件的 PSAM 长响应不能直接沿用普通卡通道续包语义。

第三次真机复测否定了“不返回 FCI”参数方案:32 字节 PSAM 对 SELECT FILE 的 P2=0x0C 明确返回 6A86,日志仍为 cardDebitSent=false。最终方案恢复现场设备支持的 P2=0x00,不再修改 APDU 命令参数。

实施内容

  1. 新增 PRE_DEBIT_FAILURE 错误类型。协议层仅在扣款命令尚未发出时,将 PSAM、卡片准备和通信失败包装为该类型;扣款命令一旦进入发送阶段,仍保持 TRANSACTION_OUTCOME_UNKNOWN,防止重复扣款。
  2. 业务分类器将无已完成扣款的 PRE_DEBIT_FAILURE 判定为确定失败,进入现有 TRANSACTION_FAILED 页面,提示“请取走卡片和餐盘后重新操作”。若已经完成任一钱包扣款,仍归类为结果未知。
  3. 增加 32 字节 PSAM 长响应兼容。只有输入报告为 32 字节、操作为 APDU 0x6F、命中 PSAM 通道、已进入续包,且剩余长度严格递减、数据不重复时,才接受续包 offset=0
  4. 兼容命中后继续核对声明总长度;长度不一致、剩余长度不递减或重复帧仍按协议错误拒绝,并记录 HID_PSAM_RELATIVE_OFFSET_ACCEPTED 诊断事件。
  5. 撤回被现场 PSAM 以 6A86 拒绝的 P2=0x0C 方案,32/64 字节 PSAM 及普通卡通道的 SELECT FILE 均恢复 P2=0x00
  6. 仅对现场已经确认的精确帧形态做状态归一化:输入报告为 32 字节、PSAM 通道、SELECT FILE P1/P2=0x00、首包恰好 25 字节且末尾为 9000、声明仅余 2 字节、续包为 offset=0/remaining=0/0101。由于业务不使用 SELECT 返回的 FCI,兼容层丢弃 FCI,只生成供公共 APDU 校验器验证的最小 9000 + CRC 状态响应,并记录 HID_PSAM_SELECT_STATUS_RECOVERED。非 SELECT 指令仍必须使用设备返回的完整 CRC。

HID 64 影响边界

64 字节 HID 不进入状态归一化分支,仍使用原有绝对 offset 和完整 CRC 严格校验,SELECT P2 保持 0x00。已增加专门回归:标准 64 字节 PSAM 多段响应能够正常拼包;64 字节 PSAM 若续包错误返回 offset=0 仍会被拒绝;32 字节普通卡通道同样保持 P2=0x00 和严格 offset 校验;非 SELECT PSAM 指令即使出现相同 0101 续包也不会被归一化。

源码与测试文件

自动化验证

验证项结果
定向测试业务分类、PSAM 续包和 SELECT 隔离共 15 项测试通过
全量 JVM 单元测试172 tests,0 failures,0 errors,0 skipped
Debug 构建:app:assembleDebug 成功
APKapp/build/outputs/apk/debug/app-debug.apk,66,954,953 bytes
APK SHA-256bebc264a56c6118dc3b18d88c36fbcc4e1c28d2265c1e4f40e9090f844fa0aab
F700-HW 安装2026-08-29 18:28:03;设备与本地 APK 哈希一致

构建仅有项目既有的 Java 8 编译器读取 Android build-tools 30 类版本提示及 deprecated 提示,没有新增编译错误。

尚需真机确认