开发过程记录¶
这份文件是手写的,和
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 里没有一个的界面被人看过一眼。
为了能跑起来,补了三块基础设施:
- 下载 Android 16 (API 36) x86_64 系统镜像(1.8 GB)。SHA-1 与仓库清单一致,
但 .NET 的 ZIP 解压报
A local file header is corrupt—— 是解压器不支持该包的特性, 不是文件坏了,改用tar才成功。 - 环境里没有
avdmanager,手写~/.android/avd/nfarms-test.avd/config.ini建 AVD。 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-*)也不会被碰。
线上站点的两点说明:
- 不改 nginx。 zeroling 的 vhost 已有
location ^~ /docs/ { root /opt/Cy/nfarms/zeroling; }, 于是/docs/nfarms/x天然落在zeroling/docs/nfarms/x。只往那个目录放文件, 不需要 reload,也就不会波及同 vhost 下的/tools、/hub等 7 条路由。 scripts\deploy-wiki-site.ps1本身不发布。 它含本机私钥路径与服务器账号; 仓库 wiki 收录它,公网站点把它排除(gen-wiki-site.ps1里的$deny)。 公网上少一份文档不影响使用,多一条内部路径没有好处。
三个容易踩的点:
-
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 被抹掉,必须补回来才能正确生成。 -
kb.rs刻意不读任何索引文件,它扫的是wiki\下所有.md正文。 所以"更新了README.md索引"≠"更新了知识库" —— 真正被检索的是code\里那些文档。 -
站点挂在子路径下,
site_url必须带/docs/nfarms/。 填错时页面看起来完全正常(静态资源是相对路径),但搜索索引与 sitemap 里的地址会错 —— 所以"能打开"不能作为判断依据,要看rel="canonical"和 sitemap。
桌面端的历史记录在哪¶
本文只覆盖移动端(App 部分)与"真机验证"这一轮。桌面端第 1–19 轮的逐轮记录在
dist\wiki\开发过程记录.md,第 20 轮起在 dist\wiki\开发过程记录-第20轮起.md。
那两份在用户自己的知识库目录里,没有复制进本仓库的 wiki\ ——
刻意不复制,是为了避免出现两套会各自漂移的副本。
这一轮留下的判断¶
- "跑不起来"要先怀疑验证手段,再怀疑代码。 第 3 轮花了很久在编译错误上, 但真正的问题在第 4 轮才出现 —— 因为前者的验证方式根本不可能发现问题。
- 静默失败比崩溃危险。 崩溃有日志;被裁掉的按钮、写错路径的下载、没被拉取的工单, 全都安安静静地"成功了"。
- 可测性是设计属性,不是测试数量。 仓库层绑死
Context、加解密用 Android 专属 API, 这两件事各自让一整类行为变成"永远测不了"。拆开它们比补一百条断言有用。