
一台只跑 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 条验收锚点:
- 设备 1024 现在在哪里?(仓库 / 班组 / 外借方)
- 它什么时候到当前位置的?(交付时间)
- 它以前在哪?(完整流转历史)
- 哪些设备在库?
- 哪些设备在某个班组?
- 哪些设备外借了?
- 借给谁了?
- 借了多久 / 是否逾期?
这 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 行"无编号"、三种口径的对账 |
| 5 | 5,693,048 台设备 | 一次差点写进生产库的导入事故复盘 |
| 6 | 编号不是身份 | 删掉亲手建的唯一索引,以及它引发的两处 bug |
| 交付与自证 | ||
| 7 | Win7 如何一路锁死技术选型 | 约束传导链 + 一个 131072 字节的空库事故 |
| 8 | 不换框架的界面改版 | 拒绝 Next.js 的论证、设计令牌、被实测推翻的 1.6:1 |
| 9 | 一次全库只读审查 | 134KB 报告、P0×4 / P1×16,以及审查方自己的 4 处更正 |
如果你只想看一篇,看 第 5 篇——它是一次真实事故的完整复盘,也是这个项目里最能说明"结构如何救命、缺失的校验如何让用户看到荒唐数字"的故事。
如果你是技术负责人,从 第 2 篇开始:那里讲了为什么我坚持先写规范再写代码,以及这套规范的实际成本。
下一篇:一张预览截图上的 5,693,048 台设备。
Related Articles
编号不是身份:我删掉了亲手建的那个唯一索引
项目早期给设备编号加了唯一索引,后来因为一个 670 台的设备块被整块拦下而删掉它。记录这次建模推翻,以及它连锁引发的两处真实 bug。
一次全库只读审查:134KB 报告、P0×4 / P1×16,以及审查方自己的 4 处更正
v1.5.0 发布后我做了一次全库只读审查:不许改代码、只在副本上实测、逐条可核实,交回 134KB 报告。记录 4 个 P0、16 个 P1、两个治理机制(生产库保护铁律、已知欠账豁免)、审查方自己的 4 处更正,以及一份诚实的未完成清单。
不换框架的界面改版:拒绝 Next.js、设计令牌、和被实测推翻的 1.6:1
需求方要求“优化界面,加上 Next.js”。本文记录我为什么拒绝它、为什么只引入一个依赖、设计令牌如何落地,以及一次口算被 WCAG 实测推翻(1.6:1 实为 1.16:1)后我对问题的重判。