跳转至

开发过程记录

这份文件是手写的,和 code/ 下那些由脚本生成的逐文件副本不同 —— 重跑 scripts\gen-wiki.ps1 不会覆盖它。

这个工程是什么

NFarms HUB 分两半:

半边 位置 形态
桌面端 crates/ Rust + egui/eframe,一个可执行文件,菜单三层、页面上百
移动端 apps/ 五个独立 Android App,共用 apps/core/common

移动端五个 App 各自回答一个问题:

App 回答的问题 关键实现
Player 本地加密曲库能不能只靠"点一下"就播出声? Media3 MediaSessionService、整文件 AES-GCM 解密到缓存、LRC 跟随
Store 没网也能点单,联网后不重不漏地上报? 先落盘再支付、幂等键、RSA 账单验签、离线队列
HUB 17 GB 的权重不能进安装包,怎么下才敢用? 清单只带 SHA-256、.part → 校验 → rename、串行任务队列
IOT 固件升级怎么保证"坏包一个字节都不下发"? 验签 → 摘要 → 机型 → 版本,四道闸在传输之前
online 两个人改同一张工单,冲突时能不能不丢东西? 三路合并 + 乐观锁 rev,冲突必须人工裁决

轮次记录

第 1–2 轮 · 菜单重构

把侧栏改成需求给定的三层树(7 个一级 / 若干二级 / 三级可折叠),删掉需求里没有的页面。 途中一度"打开软件还是和以前一样" —— 原因是跑的是旧 dist\nfarms.exe, 重建后一致。教训:改完 Rust 端必须重建 release 产物,否则验证的是历史。

第 3 轮 · 五个 Android App

按"五个 App 全部可安装、本地能跑通主流程"的边界实现。所有外部依赖都做了接口隔离: PaymentGateway / ReportApi / BleTransport / TicketApi / Secrets, 默认给本地/模拟实现,真接的时候只换实现类。

这一轮的产物只能"编译通过 + 打包成功",没有跑过 —— 这为第 4 轮埋了雷。

第 4 轮 · 真机(模拟器)验证与修 bug

这一轮的核心认识:编译通过和单测通过,都不能证明 App 能用。

前一轮的验证只到 aapt2 dump badging(确认 APK 里有可启动的 Activity)。 这一轮把模拟器跑起来之后,发现的第一个事实是:五个 App 里没有一个的界面被人看过一眼。

为了能跑起来,补了三块基础设施:

  1. 下载 Android 16 (API 36) x86_64 系统镜像(1.8 GB)。SHA-1 与仓库清单一致, 但 .NET 的 ZIP 解压报 A local file header is corrupt —— 是解压器不支持该包的特性, 不是文件坏了,改用 tar 才成功。
  2. 环境里没有 avdmanager,手写 ~/.android/avd/nfarms-test.avd/config.ini 建 AVD。
  3. apps/smoke-install.ps1:装 APK → 启动 → 抓 FATAL EXCEPTION / ANR。 判定标准刻意不是"窗口出现了":一个 App 完全可以先画出界面、再在后台协程里崩掉。

修掉的问题

按"为什么单测没抓到"分三类。

(a) 单测抓不到 —— 因为它测不到那层

问题 根因
Store 支付失败后购物车被清空 清空写在支付之前;仓库层依赖 Context,普通 JVM 测试根本构造不出来
密码学整层零测试 Crypto 用 android.util.Base64,在 JVM 单测里是未实现的桩

修法不是"多写几条测试",而是先把可测性拆开:AppFiles 加纯目录构造 AppFiles.at(...);Crypto 换 java.util.Base64(minSdk 26 起可用)。 之后测试数从 35 涨到 57,新增的都是端到端: 整条 48 KB 分片 OTA、模拟设备握手/大帧往返、下单→支付→验签、乐观锁与三路合并。

(b) 只有端到端才暴露 —— 分片边界

问题 根因
IOT OTA 卡在 95%,小命令全正常 模拟设备把每个 MTU 分片单独 Frame.decode();68 字节的 OtaChunk 被切成 4 片,每片都解不出一帧,设备永远不回 ack
偶发"回了 ack 却报超时" 计时窗口套在 write() 外面,分片写耗时吃掉了等待预算

第二处修复中途我自己又引入过一个竞态:先把 waiter 挪到 write() 之后, 结果设备回包比 write() 返回还早,被判成"未等待的帧"丢掉。 最终形态是先立 waiter、write() 完再计时,两个坑都避开。

(c) 静默错误 —— 不报错,只是悄悄不对

问题 表象
Player 上一首/下一首无效 setMediaItem 单曲进队列,队列长度 1
Player 2.0x 按钮消失 Row 放不下会静默裁掉,不报任何错
Player 文件路径截断掉文件名 takeLast 截尾,而文件名恰在尾部
HUB 目录标签指错位置 写死 files/nfarms/media/models,实际是 files/nfarms/models,照界面找文件找不到
online 首次同步 0 张工单 sync() 只遍历本地已有工单,服务端多出来的从没拉下来

还有两处体验级问题:播放页每秒 5 次 copy() 整个 UI 状态导致整棵树重组(掉帧); 清单缺 MediaButtonReceiver,耳机/蓝牙播放键不生效。

验证到什么程度

App 跑通的路径
Player 播放 → PLAYING(3) + 音频焦点 + 前台服务 → 歌词跟随 → 杀进程重开,从上次进度续播
Store 选规格 → 下单支付 → 购物车清空 → 已确认 → 服务端收单 1 笔,幂等键不变
HUB 下载 → 校验 100% → 已就绪(显示实际占用与 SHA-256)
IOT 扫描 → 连接 → 固件 1.4.2 → 1.5.0;三种坏包分别被 Digest/Signature/Version 拒绝
online 自动同步拉取 2 张工单

截图在 apps/screenshots/。

仍然没验证的(写在这里,免得下次被当成已验证):

  • 真实 BLE 设备 —— RealBleTransport 是 NotImplemented,只附了交接说明。 没有真机时写出来的只会是"能编过、连不上",那比没有更坏。
  • 真实支付 / 上报 / 工单后端 —— 全部是本地模拟实现。
  • Release 签名 —— 当前五个 APK 用 debug 签名,能装能跑,不能上架。
  • 五个 App 之间不共享登录态,这是设计,不是缺陷。

怎么复现验证

# 1) 编+测(Java 21 是硬要求,AGP 8.7 不支持更高版本)
powershell -ExecutionPolicy Bypass -File .\apps\build-all.ps1

# 2) 起模拟器(镜像与 AVD 已在 ~/.android 与 SDK 下)
& "$env:LOCALAPPDATA\Android\Sdk\emulator\emulator.exe" -avd nfarms-test -no-window -no-audio

# 3) 装机冒烟
powershell -ExecutionPolicy Bypass -File .\apps\smoke-install.ps1

知识库怎么更新

知识库有三个位置,由同一份源码派生,但读者不同:

位置 谁在读 怎么更新
wiki\(仓库) 安装包 —— installer\nfarms.iss 把 wiki\ 打进 {app}\wiki powershell -File .\scripts\gen-wiki.ps1
dist\wiki\(工作副本) 本机跑的 dist\nfarms.exe 的对话检索(nfarms-core/src/kb.rs 读 paths.wiki()) powershell -File .\scripts\gen-wiki.ps1 -OutDir 'dist\wiki'
https://zeroling.nfarms.cn/docs/nfarms/ 线上浏览与搜索 powershell -File .\scripts\deploy-wiki-site.ps1

三处都只重建 code\ 与索引;wiki\ 根下的手写文档不动, dist\wiki\ 里用户自己放的资料目录(nfos\、nfarms hub\、各 *.bak-*)也不会被碰。

线上站点的两点说明:

  1. 不改 nginx。 zeroling 的 vhost 已有 location ^~ /docs/ { root /opt/Cy/nfarms/zeroling; }, 于是 /docs/nfarms/x 天然落在 zeroling/docs/nfarms/x。只往那个目录放文件, 不需要 reload,也就不会波及同 vhost 下的 /tools、/hub 等 7 条路由。
  2. scripts\deploy-wiki-site.ps1 本身不发布。 它含本机私钥路径与服务器账号; 仓库 wiki 收录它,公网站点把它排除(gen-wiki-site.ps1 里的 $deny)。 公网上少一份文档不影响使用,多一条内部路径没有好处。

三个容易踩的点:

  1. gen-wiki.ps1 / gen-wiki-site.ps1 / deploy-wiki-site.ps1 必须保存成 UTF-8 with BOM。 本机没有 pwsh(只有 Windows PowerShell 5.1),而 5.1 读取无 BOM 脚本时按系统 ANSI 代码页(此处 GBK)解码 —— 脚本里的中文会变乱码,并把索引与导航标题一起写坏。 用编辑器或补丁工具改过这三个脚本后要确认 BOM 还在(前 3 字节 EF BB BF)。 这一点是实测踩到的:改完脚本后 BOM 被抹掉,必须补回来才能正确生成。

  2. kb.rs 刻意不读任何索引文件,它扫的是 wiki\ 下所有 .md 正文。 所以"更新了 README.md 索引"≠"更新了知识库" —— 真正被检索的是 code\ 里那些文档。

  3. 站点挂在子路径下,site_url 必须带 /docs/nfarms/。 填错时页面看起来完全正常(静态资源是相对路径),但搜索索引与 sitemap 里的地址会错 —— 所以"能打开"不能作为判断依据,要看 rel="canonical" 和 sitemap。

桌面端的历史记录在哪

本文只覆盖移动端(App 部分)与"真机验证"这一轮。桌面端第 1–19 轮的逐轮记录在 dist\wiki\开发过程记录.md,第 20 轮起在 dist\wiki\开发过程记录-第20轮起.md。 那两份在用户自己的知识库目录里,没有复制进本仓库的 wiki\ —— 刻意不复制,是为了避免出现两套会各自漂移的副本。

这一轮留下的判断

  1. "跑不起来"要先怀疑验证手段,再怀疑代码。 第 3 轮花了很久在编译错误上, 但真正的问题在第 4 轮才出现 —— 因为前者的验证方式根本不可能发现问题。
  2. 静默失败比崩溃危险。 崩溃有日志;被裁掉的按钮、写错路径的下载、没被拉取的工单, 全都安安静静地"成功了"。
  3. 可测性是设计属性,不是测试数量。 仓库层绑死 Context、加解密用 Android 专属 API, 这两件事各自让一整类行为变成"永远测不了"。拆开它们比补一百条断言有用。