IStarry

一台只跑 Win7 的机器、2148 台设备、一份手工台账

系列总纲:在 Win7 单机、单 exe、无网络、无登录的约束下,用 Go + SQLite + React 做一个设备管理系统——讲清约束清单、系统边界、技术选型的传导链与全景数字。

IStarry

5 min read

一台只跑 Win7 的机器、2148 台设备、一份手工台账

从一张 Excel 到一个 27 MB 的 exe

我接手这件事的时候,手里只有三样东西:一张手工维护了十年的 Excel 台账、一台必须能跑起来的 Win7 机器,和一条"交付物就是一个 exe"的硬要求。

这个系列记录的是接下来两周里发生的事:两次核心设计被推翻、一次差点写进生产库的导入事故、一轮把 134KB 问题清单翻出来的全库审查。九篇文章,具体的事。


0. 一份回答不了基本问题的台账

起点是 设备借出总账.xlsx

单 Sheet,名义范围 A1:U540,实际只用了 A–K 共 11 列;数据行 R5–R466,第 467 行是合计;里面有 174 处合并单元格,最早一笔记录是 2009 年。

它最大的问题不是"数据脏",而是回答不了几个很基本的问题。需求文档把要答的问题列成了 8 条验收锚点:

  1. 设备 1024 现在在哪里?(仓库 / 班组 / 外借方)
  2. 什么时候到当前位置的?(交付时间)
  3. 以前在哪?(完整流转历史)
  4. 哪些设备在库
  5. 哪些设备在某个班组
  6. 哪些设备外借了
  7. 借给谁了?
  8. 借了多久 / 是否逾期

这 8 个问题看着平淡,但对照那张表就知道难度在哪:这张表没有归还列。设备借出去之后,只有极少数编号单元格的括号注释里藏着一句"某月某日入南库"——全文只有 2 例

所以"设备现在在哪",从文件里根本推不出来

这才是项目的真正起点:不是"把 Excel 搬到数据库",而是建立一套能持续回答这 8 个问题的机制


1. 约束清单:项目的真正起点

需求文档里有张部署约束表。我把它放在技术选型之前,因为它比任何选型讨论都重要:

约束具体要求后果
运行环境Win7 SP1(64 位) + Win10/11后端工具链锁定 Go ≤ 1.20(最后支持 Win7 的版本,已 EOL、无安全更新)
交付形态双击 equipment.exe → 起本地服务 → 自动开浏览器前端必须内嵌进 exe(go:embed),不能有独立的静态资源部署
浏览器Win7 上只能用 Chrome 109 / Firefox ESR 115前端按 Chrome 109 能力构建;不支持 IE
数据存储本地 SQLite 单文件无 CGO(否则要带 mingw 运行时 DLL)→ 用纯 Go 驱动
网络单机离线,不依赖互联网、服务器、云没有远程日志、没有埋点、没有 CDN、没有在线字体
用户与角色无登录体系,单机单使用者没有权限模型;操作人靠表单输入
技术边界无 Docker / PostgreSQL / Redis / K8s / 微服务——

注意最后一行。这些"不"是写进治理文件的,不是我事后总结的。它意味着当我想说"用 Docker 打包一下比较方便"的时候,答案是"不行,不在技术栈内"。

约束先于技术选型。 这句话在很多项目里是口号,但在这里它是一条可执行的规则:约束被写进了治理文件,动手之前必须先读一遍,所以它不会被绕过。

顺带说一句代价。Win7 支持不是免费的:

已接受代价:Go ≤ 1.20(EOL、无安全更新)+ 全部依赖版本锁定 + 双环境兼容基线 + 前端按 Chrome 109 / Firefox ESR 115 能力降级。

这条"代价声明"是和决策一起记录下来的。一个不写代价的技术决策,等于没做决策。


2. 不做什么

需求文档第 8 节叫「范围外(第一版不做)」:

  • 新增状态类型 / 新流转动作的自定义(白名单锁死,需代码升级);
  • 页面可视化装修 / 自定义报表;
  • 多用户、权限、登录
  • 多仓库 / 多库存位管理;
  • 网络版 / 多客户端并发。

这一节比"做什么"更能解释这套系统为什么这么简单。

举一个具体例子:没有登录体系,操作人怎么办?

这个问题有两种答法:

  • 答法 A:引入用户表 + 登录页 + 会话。听起来"完整",但在一台不联网的单机、一个使用者的场景下,登录的唯一作用是让人每天多点两次鼠标
  • 答法 B:操作表单上加一个「操作人」输入框 + 最近使用记忆(存在浏览器本地)。操作留痕的目的达到了,成本是零。

我选了 B。

值得留意的是 B 方案的代价:审计记录里的操作人是自填的,不能作为身份凭证。这是一个明确的取舍——这套系统的目标是"可追溯",不是"防抵赖"。想清楚这一点,很多设计就不纠结了。

再举一个:「白名单锁死,需代码升级」。设备状态机只有 8 个动作(入库、出库、班组转交、外借、归还、送修、维修完成、报废),不可扩展。代价是业务变化时要改代码;收益是状态机可以被完整测试——8 个状态 × 有限动作,穷举是可行的。如果允许用户自定义状态,这个测试就永远做不完。

"不做什么"往往决定了系统的复杂度上限。


3. 技术选型是怎么被逼出来的

最终锁定的技术栈:

选型
后端Go + Gin + GORM + SQLite(纯 Go 驱动,无 CGO)+ go:embed(内嵌前端)
前端React + TypeScript + Vite + Ant Design + ECharts + react-router-dom
交付Windows 单机 equipment.exe;数据 equipment.db;备份 backup/;日志 logs/

但这些名字不是重点,重点是它们是怎么被逼出来的

Win7 SP1 要支持
  └─→ Go ≤ 1.20(最后支持 Win7 的版本)
        └─→ 交付构建必须 CGO_ENABLED=0
              └─→ SQLite 不能用需要 CGO 的驱动 → 锁定纯 Go 驱动
                    └─→ 交付包不需要带 mingw/winpthread 运行时 DLL
Chrome 109 是 Win7 上的浏览器上限
  └─→ 前端构建 target 设为 chrome109
        └─→ 不能用 Next.js(其官方浏览器基线是 Chrome 111+)  ← 第 8 篇的主角

选型链的每一环,源头都是"那台只跑 Win7 的机器"。

这条链上还有一个反直觉的结论,第 8 篇会详细论证:当时的要求是"顺便把界面改成 Next.js"。看起来是"想更好看",但 Next.js 的浏览器基线(Chrome 111+)与 Win7 上限(Chrome 109)硬冲突。所以正确的回答既不是"Next.js 好看所以上",也不是"老项目别折腾",而是把冲突证据逐条摆出来——观感来自设计令牌,不来自框架


4. 交付形态:最终就是一个目录

这是最终交付目录(实测):

equipment/
├── equipment.exe       双击即用(27 MB,前端已内嵌其中)
├── equipment.db        运行生成(勿手工编辑)
├── config.yaml         端口等配置(缺失自动生成)
├── user-guide.md       用户手册
├── upgrade-guide.md    升级说明(已有数据时怎么只换程序)
├── update-app.ps1      升级脚本(只换 exe,保留数据)
├── backup/             备份(启动自动 + 手动,默认保留最近 30 份)
└── logs/               日志

几个数字值得单独说:

  • exe 28,530,688 字节(约 27 MB)。它是自包含的:前端构建产物(HTML/JS/CSS)通过 go:embed 编进了二进制,没有外部资源目录。用户拿到的是一个文件,不是一个文件夹。
  • 交付库 1,503,232 字节(约 1.4 MB),装着 2148 台设备、333 张外借单、2481 条流转记录。
  • 数据文件、程序文件、备份目录三者在同一个目录里,因为程序启动时会把自己的工作目录锚定到 exe 所在目录——这样无论用户从哪里启动(快捷方式、任务计划、管理员运行),数据都落在同一个地方。这条规则是一次事故换来的,第 7 篇细讲。

5. 规模

数值
Go 代码17,955 行 / 100 文件(其中测试 7,639 行 / 47 文件 → 生产代码 10,316 行
前端代码5,355 行 / 30 文件web/src
提交 / 版本标签85 commits / 14 tags(截至 commit 0d1f30c,随提交增长)
项目文档docs/ 12 份 392 KB(另有本系列 6 份,约 137 KB)
真实数据规模2148 台设备(在库 1815 / 外借 333)· 333 张外借单 · 2481 条流转记录
交付物exe 28,530,688 B · 交付库 1,503,232 B
技术边界无 Docker / 无云 / 无网络依赖 / 无登录体系

一个比例值得注意:测试代码 7,639 行,占 Go 代码的 43%。这个数字不说明"测试写得好",但它说明了另一件事——在这套做法里,测试是与实现同等的一等公民,每一处修复都要有对应的用例(第 5、9 篇会反复出现"先红后绿"这个手法)。


6. 这套系统做了什么

抛开技术,它是个具体的东西:

模块能力
设备台账搜索/筛选/分页、详情页含完整流转时间线;新增、编辑(编号不可改)
设备流转8 个动作:入库、出库给班组、班组转交、外借、归还、送修、维修完成、报废
外借管理独立外借单(一设备一单)、归还、延期、逾期标红
班组 / 内部单位班组设备三级视图(班组 → 类别 → 设备)
字典类别字典、外借方字典(均可页面维护,受控删除)
Excel 导入解析 → 预览 → 逐项审阅 → 确认 → 对账(五步,带门禁)
Excel 导出设备台账导出;流转情况导出(三工作表)
数据备份 / 恢复启动自动备份 + 手动备份 + 恢复(二次确认,恢复前再备份当前库)
操作审计只读页:受限更正、备份恢复、清空重导、导入、导出流转的留痕
流转记录只读事件流(谁在何时把什么从哪挪到哪),默认隐藏导入建档记录

有一条贯穿全部模块的硬规则,它是这套系统的地基:

任何状态变化 = 更新设备当前状态 + 新增一条流转记录,二者在同一个事务里完成

配套两条:流转记录禁止删除;设备禁止物理删除(只能流转到报废)。

这三条合起来保证了一件事:设备现在的状态,永远能被它的历史解释。 反过来,任何"只改状态不写历史"的实现都是 bug——哪怕它看起来跑通了。


7. 为什么我让每个阶段都停下来

最后说方法。它是这个项目里最不寻常的部分,也是第 2、3 篇的主题。

我没有一开始就写代码。我先写了三份文档:主章程、AGENTS.md(治理约定)、两个内嵌 Skill(工程师工作人格 + 领域开发流程)。它们规定了:

  • 不跨阶段:严格按 Phase 0 → 1 → 2 → … 推进,禁止一次性铺开整个项目;
  • 每个阶段:分析 → 实现 → 测试 → 修复 → 更新文档 → commit(+tag)→ 阶段报告 → 停下等验收
  • 上一阶段未验收,不进入下一阶段——即使下一阶段很容易;
  • 不猜测:字段含义或业务规则无法确定时,标记「待确认」交回业务方,绝不擅自定夺。

同时有一份决策基线,共 23 条。每条都写成「结论 / 理由 / 代价」——写代价,是因为没有代价声明的决策无法被复核。

这套东西的产出是:14 个 tag,每个是一个可回退的阶段点;85 次提交,每次都能对上一份阶段报告。

代价也很实在:。每个阶段都要停下来等人确认。但换来了两样东西——约束不会被绕过,以及错误能在阶段边界上被发现,而不是在发布之后。


8. 还没做完的事

结尾必须写"未完成",否则读者会以为这是一个已完结的完美项目。

仍然没做完的(实测状态):

  • Win7 SP1 实机回归尚未执行——它被列为目标与约束,但真机验证仍在待办清单上;
  • Chrome 109 离线安装包尚未放入 release/equipment/install/(该目录当前为空),也就是说"交付物随附 Chrome 109"这条目前只有设计没有产物
  • 界面改版的吸顶表头只有静态计数与约定脚本证据,没有浏览器截图证据
  • 交付目录里还有几处措辞与注释待收口。

另外,这个项目的公开仓库仍含真实业务名称(往来单位名出现在治理文件与测试文件里)。这是已知的残留暴露,尚未处理——本系列所有文章都已做脱敏改写,但仓库本身不是干净的。

把这些写出来不是自我批评,而是这套做法的一部分:阶段报告里"未做"清单和"已做"清单同等重要


9. 系列导航

九篇,三幕:

#篇名讲什么
方法与治理
1本文(总纲)约束、边界、交付形态、规模
2先写规范,再写代码AGENTS.md、两个内嵌 Skill、23 条决策基线
3为什么每个阶段都要停下来验收「不猜测」与 Phase-STOP
数据与领域
4十年手工台账的脏数据全景174 处合并单元格、85 行"无编号"、三种口径的对账
55,693,048 台设备一次差点写进生产库的导入事故复盘
6编号不是身份删掉亲手建的唯一索引,以及它引发的两处 bug
交付与自证
7Win7 如何一路锁死技术选型约束传导链 + 一个 131072 字节的空库事故
8不换框架的界面改版拒绝 Next.js 的论证、设计令牌、被实测推翻的 1.6:1
9一次全库只读审查134KB 报告、P0×4 / P1×16,以及审查方自己的 4 处更正

如果你只想看一篇,看 第 5 篇——它是一次真实事故的完整复盘,也是这个项目里最能说明"结构如何救命、缺失的校验如何让用户看到荒唐数字"的故事。

如果你是技术负责人,从 第 2 篇开始:那里讲了为什么我坚持先写规范再写代码,以及这套规范的实际成本。

下一篇:一张预览截图上的 5,693,048 台设备。


Related Articles

Knowledge Relations