审计日期:2026-09-09。只读审计,未修改业务仓库,未连接设备、生产或网络业务接口。
1. 版本与适用链
- 真实仓库:
/Users/jack/code/010-cpt/008-zhct/zhctproject/android/ZhctWeightingTableYoukate。 - 工作树:
master,HEAD2f759f9415b46a7166215fa670d5b4fafeef615e,审计前后git status --short均为空。 - 当前 Gradle 版本为
versionName 2.6.0 / versionCode 21,不是 README 所写的 2.0.0 / 5(app/build.gradle:11)。README 仅用作导航,判断以源码为准。 - 已比对
origin/zymy_ongoing21a5d3d956f0a2dabec036a947e315e957a86997,版本 2.6.2 / 23。主代理已通过在线ls-remote确认此两个引用。两个快照的 HomeActivity、YoukateHardwareManager、ScanCodeUtils、离盘配置与策略完全相同;2.6.2 有营养展示 Fragment/UI 差异。因此本报告关于扫码接入、离盘判断的结论适用于两个快照,不代表验证了现场 APK。 - 实际依赖
app/build.gradle:65:app/libs/DeviceSDK-80.1.10.12.20260129.aar。
真实首页链:DeviceSDK.PC681ScaleManager → YoukateHardwareManager.scaleDataCallback → cleanScanCode(scaleData.rfidData) → HomeActivity.onWeightData → 先 handleWeightData 后 handleYoukateScanCode → handleEmptyScanCode / handleScanResult → Fragment 和取餐记录。
关键入口:hardware/YoukateHardwareManager.java:35,58,108,268,288,316;activity/HomeActivity.java:982,996,1013,1473。下文路径均相对 app/src/main/java/com/cpt/zhct/weighting/。
ScanCodeUtils 不在首页业务链。 它调用 SerialScanCodeImpl(hardware/ScanCodeUtils.java:13,22,26),使用者是 ScanCodeActivity 与 DebugScannerPop。不能把其单独扫码测试结果直接解释为首页 PC681 数据正常/异常。字段名 rfidData 也不能单凭命名认定现场使用 RFID 硬件。
2. 与会议原话逐项对照
| 会议说法 | 源码事实 | 判断 |
|---|---|---|
| 连续 3 次空码认为拿走 | HomeActivity:1473-1492 每次空码回调递增,达到 N 调 scanCodeResult(2);当前已有配置 |
基础计数思路已实现,N 不一定为 3 |
| 中间有普通有效码就不算连续空码 | HomeActivity:1037 在相同/不同普通有效码分支之前清零 |
已实现;不是累计若干非连续空码 |
| 设置范围 3-10,默认 7 | Constants:92-97;SetActivity:815-860;HomeActivity:276,1875-1883 |
范围与回首页即时生效已实现 |
| 没有设置时默认 7;显式设置 3 才保持 3 | 缺配置时按安装历史分流:升级设备是 3,新装才是 7;读取安装状态失败也按升级处理 | 会议摘要遗漏关键迁移分支,现场升级后仍可能是 3 |
| 每 100ms 做一次判断 | 每次成功进入业务的 PC681 回调计一次,未按时钟判断;配置支持 50/100/150/200/250ms | 仅在实际回调严格 100ms 时才能近似换算 |
| 厂商建议 900-1200ms | 代码只有 3-10 帧阈值,没有独立毫秒超时;100ms 下默认 7 帧并不等于 900-1200ms,10 帧也不覆盖整个 1200ms 上限 | 未实现厂商最新建议的时间契约;建议本身尚待证据确认 |
| 一次空就是硬件没读到/盘拿走 | 应用将任何长度非 6 的 SDK 非空码直接变为空;SDK 代理亦确认有协议长度异常变空路径 | 空码语义混合,不能据此证明物理离盘 |
| 程序按称体稳定帧计数 | HomeActivity:996-998 无条件执行扫码;handleWeightData:1953 任意状态即置初始数据已就绪 |
离盘未受秤稳定标志过滤;进取餐另有重量窗口检查 |
| 测试覆盖九十几 | 仓库现有测试包含阈值配置/辅助策略,没有实际 HomeActivity 连续帧链路测试 | 未发现可回读的覆盖率分母、场景集或现场证据,不能认可覆盖百分比 |
会议文件是待审查证据;纪要中的“已排除外部干扰/硬件故障”不是本次审计已证实结论。
3. 主要发现(按与本次问题的相关性排序)
APP-01:异常码和真实无盘合并为同一种离盘输入(P1,机制已确认,现场触发待证)
YoukateHardwareManager:316-323 先要求原始 code.length()==6,否则返回空字符串;不记录原始码长度/原因。该空字符串在 HomeActivity:1032-1034,1473-1492 与真实无盘完全相同。
确定性实际方法测试:当前盘为 123456,阈值 3,连续收到三个非空 12345,应用在第三次执行离盘。SDK 独立审计也已验证合法校验、rfidLen=5 的帧确实可返回 12345。所以“有数据但码异常”可以沿真实接入链触发“扫码结束”。这证明可发生路径,不证明现场已经出现该串口帧。
还有两个边界:六位码追加 CRLF 会先因长度 8 被丢弃,不过 PC681 当前 parser 已先 trim 首尾,正常首页路径通常不会遇到这个具体例子;不能把 CRLF 例子冒充现场根因。六字符内嵌控制字符清理后不足六位则进入 HomeActivity 的非法长度告警分支(:770-777)。
建议业务输入区分 有效码 / 明确无盘 / 非法码或协议错误 / 无新数据,记录异常原因,不由一个空字符串包办。
APP-02:升级安装缺配置仍取 3,默认 7 的理解不成立(P1,代码及实际类测试确认)
antiescape/TrayRemovalConfirmationCountConfig:18-22,41-48 读取安装时间;TrayRemovalConfirmationCountPolicy:22-26 对升级安装返回 3,新装返回 7。已有合法值保留,非法值修复为 7。
实际编译此 Config/Policy 类并以可控 PackageInfo/MMKV 边界验证:升级无键=3、新装无键=7、已保存3=3、非法11=7。此次不是说迁移保留行为必然设计错误,而是现场不能仅凭“默认7”假设所有设备已扩大窗口。应采集实际配置与安装升级时间;是否把升级设备也改为7需先明确业务迁移决定。
APP-03:帧数并非独立扫码次数,更不是固定毫秒窗口(P1,计数行为已确认)
HomeActivity:1473-1492 只有 ++ 与阈值比较。mFirstEmptyScanElapsedMs 仅在 PerformanceTrace 开启时记性能日志,不参与判断(:1477-1487)。实际方法无延时连调三次,数微秒内即完成离盘。该测试证明代码没有最短持续时间,未模拟物理设备。
SDK 代理确认串口解析后通过主线程 Handler 投递,ScaleData 未附设备采集时间/序号;主线程阻塞恢复时排队回调的处理间距可能短于物理间隔。SDK 还存在单次 read 只解析一个包的问题,陈旧缓存会改变数据新鲜度。应用当前不能识别陈旧/重复观测。
名义 T=100ms 时,从第一个空回调到 N 个空回调是 (N-1)×T:N=3/7/10 分别 200/600/900ms;从最后有效回调量约是 300/700/1000ms。物理离盘到最终判定还要叠加扫码采样相位、缓存、串口与主线程排队,不能用 N×100 直接当真实离盘耗时。YoukateHardwareManager:179-229 又允许称体间隔变更,N不随T转换。
APP-04:抬高空帧阈值后,快速换盘会走直接替换旧会话的支路(P1,源码链确认,需业务回归)
触发:A 盘已经进入取餐,A→B 之间空帧不足 N,随后读到合法 B。HomeActivity:1037-1040 直接识别B;:787-800 覆写 mCurrentCode/mLastValidCode;:856-861 调 switchToOngoingFragment;:456-480 没有已在取餐的拒绝/收尾判断,直接重置起始重量并替换 Fragment。正常保存旧单的路径在 scanCodeResult(2)(:611-660),异码替换没有先执行该路径。
因此提高 N 能降低短暂空码误离盘,同时加大两盘快速交接时旧会话尚未收尾的机会。精确抽取的实际路由方法已验证 B 会触发新识别而没有离盘回调;旧重量是否丢失/错归需包含 Fragment/数据库的完整回归。这里并非当前物理中断的已证实根因,但属于同一改阈值方案必须验证的业务边界。
APP-05:现有日志不能闭合“串口→解析→过滤→计数→订单结束”的证据链(P1,审计确认)
YoukateHardwareManager:288只把清洗后的字符串传给业务,不携带原码长度、帧有效性、设备时间、主机接收时间、序号。HomeActivity:1014原始到业务的扫码打印使用LogUtils.e,其LogUtils:45-50仅在 debug 输出;而且已经是清洗后的值。HomeActivity:1002-1004解析错误只打印 debug log,没有与此次离盘计数关联。- 通常本地日志在有效新盘或结束时写入(
:798-800,665-667),不会记录完整空码序列。 - PerformanceTrace 默认关闭(
utils/PerformanceTrace:19-20);开启后tray_removed只有 emptyFrames 和局部耗时(HomeActivity:1484-1487),没有本次阈值、采样间隔、原始/净化码和结束原因分型。 LocalFileRecorder:35-41异步写;:83-85在落盘时追加时间,不能作为精确的采集时间。
建议带有限环形缓冲保存故障前后帧:单调时间、raw frame摘要/长度/校验、SDK解析状态、raw/normalized码、配置N/T、计数前后值、Activity/会话ID、结束原因;餐盘码按项目隐私边界脱敏。没有这些现场证据,不能下“扫码头硬件故障”或“仅改N解决”的结论。
APP-06:普通有效码确实归零,但解除卡是显式例外(P2,实际方法确认)
HomeActivity:1021-1029 对 888880-888899 解除卡在归零前直接 return,UnlockCodePolicy:29-42 明确把它消费掉。输入 空、空、888880、空,N=3时会结束,因此“任何非空帧都会清零”不准确;“普通餐盘有效码清零”准确。这可能是业务有意忽略管理卡,应确认连续性应以“无普通餐盘码”还是“任何无扫码数据”定义。报警支路又有单独规则(HomeActivity:1044-1112),应分别覆盖,不能用普通支路测试代表全部报警模式。
4. 与持续运行、页面切换和资源有关的补充风险
这些是源码中的独立风险/缺口,不能自动解释本次连续空码中断。
- 回首页的状态不一致(P1,静态链):
HomeActivity:352-356每次 onResume 强制切打餐前,但 mCurrentCode 只在 onCreate 或离盘时清空;onStop:572-592仅解绑回调。如果餐盘未拿走而页面退回,收到同盘码会在:1038因相等跳过重新进入取餐,页面/会话可能不一致。暂停前的普通空帧计数也没有无条件清零,配置没变时:1878-1881保持原值。需要“放盘中进设置/回来/同盘持续/再移盘”的真实生命周期测试。 - 异步查询回包过期(P1,静态链):
HomeActivity:902-974的人员查询结果在验证result.equals(mCurrentCode)之前就改全局 mCurrentStaffInfo(:913,941,962),最后无条件发UI事件(:972)。该相等检查仅保护部分告警。快速换盘/误离盘后的迟到回包可能覆盖当前人,需要会话ID/请求代次验证。 - 稳定等待的线程和遍历问题(P2,静态风险):
HomeActivity:814-849在工作线程 while 等10条重量数据稳定,未持有可在 onStop/onDestroy 取消的 Disposable/句柄;mCurrentCode不是 volatile(:126),且生命周期不必修改它。DataProcessor:22,29-35,43-55使用 synchronizedList,但 Collections.max/min 的复合遍历没有外围同步,主线程 add/remove 与工作线程遍历可能发生并发异常。ThreadUtils:60-65没有显式 onError 恢复。尚未做Android竞争复现,不声称现场因此崩溃。 - 存储低于3GB时日志循环缺退出(P2,静态确定条件):
LocalFileRecorder:51-57一直删最旧日志直到可用空间>=3GB;若没有可删 local_log_ 文件或删除失败,循环没有失败/无进展退出。写日志任务是IO线程异步投递,持续调用可能积累消耗。未访问设备磁盘,不认为这是本次已发生情况。 - 运行期无回调恢复未实现(P2,范围缺口):HardwareManager startListening 仅依据内部 listening 布尔(
:104-109),没有最后回调时间监测;Home onError仅debug log。初始化有10s兜底,失败后2s退出(HomeActivity:2046-2060),不等于运行中扫码停止会自动恢复。原始串口无回调与有效帧空码是两个不同故障,前者不会递增当前空帧计数。 - SDK release 隔离存在但调试链不同:
YoukateHardwareManager:146-171绕过 SDK native close;ScanCodeUtils:37-40仍直接调用 SerialScanCodeImpl.release。后者只能作为单独调试关闭风险,不能当首页问题根因。
5. 历史“SDK接收缓存竞态修复”是否绕过解析
已检索当前 app/src/main 全部源码:没有 receiveCache、parseCacheData、反射修改 PC681 字段;仅 NewCrashHandler 反射 Build 元数据,且与串口无关。git log -S receiveCache 与 hardware 目录 git log -S setAccessible 没有匹配提交。
266709fecdfe27586d505bc9782a242447db84da(2026-08-01、VWCG-1147)真实修改:hardwareListener 加 volatile;pause 从 isListening=false / mReadThread=null / mCacheLen=0 / listening=false 改为只 hardwareListener=null,现于 YoukateHardwareManager:120-123。这减少切页与SDK接收线程缓存状态竞态,但不更换SDK parser、不排空剩余帧、不验证rfid长度。当前 SDK 解析缺陷仍在首页实际调用路径上,是否现场触发需实帧证据。
6. 本轮确定性验证与局限
产物 /tmp/scan-interruption-audit-20260909/app-logic-tests/:
- 实际 Config/Policy/Constants/UnlockCodePolicy/AlarmType 源文件复制编译,Android PackageInfo/MMKV使用可控边界替身;没有改写策略实现。
- 以源码签名+括号配对精确抽取 HomeActivity 的
handleYoukateScanCode、handleEmptyScanCode和 HardwareManager 的cleanScanCode;UI、日志、硬件和告警入口替身不在验证范围,源文件 SHA-256 记录于app-verification/extraction-provenance.txt。 - 11项断言全部通过,见
app-verification/results.txt。覆盖迁移、新装、保留/非法配置、连续7空、普通有效码清零、短码转空、CRLF边界、管理卡例外、无时间门槛、异码新识别。 - “PASS”表示如实复现当前实现;其中多项PASS是确认问题,不是验收实现正确。
- 未运行整套 Gradle/Android测试,未模拟真机串口、UI生命周期、网络和数据库。未给覆盖率百分比,未宣布现场根因或修复完成。
7. 建议形成的任务目标与问题清单
- 冻结现场实际 APK/SHA、SDK哈希、设备型号/固件、称体实际频率、扫码实际频率、N值及安装升级来源,解决“讨论的版本/默认值是否就是现场值”。
- 统一输入与离盘语义,明确有效盘、明确无盘、协议异常、未知/无新帧、过期重复帧,解决“空字符串不能等同真实离盘”。
- 为SDK解析确认的问题建立串口字节级回放与厂商修复闭环;分别验完整帧、半包、粘包、长度异常、校验/帧尾冲突、idle残包,解决“数据先被SDK损坏/滞留还是应用误判”。
- 用真实时间轴验证N=3/7/9/10及50-250ms秤频率,量化误离盘率、离盘P95/P99耗时、恢复耗时,决定帧数/持续时间/新鲜度策略;不能先把增加N当最终修复。
- 回归完整取餐会话:同盘短丢码、换盘不足N空帧、异码跳变、报警/解除卡、带盘切页、迟到查人回包、断读恢复;确保无重复单、无漏单、重量与人一致。
- 建立默认可用且容量受控的故障证据链;用现场日志、录屏/实物动作时间共同证明至少一个中断事件的责任层,再列修复验收。
持久化复跑入口:python3 app-verification/run_audit.py /absolute/path/to/ZhctWeightingTableYoukate;脚本从目标源码动态提取方法,写入自建临时目录,结束即清理。说明见 应用验证说明。