从一碗米线到12MB的exe:龙讯POS单机版架构设计
从一碗米线到12MB的exe:龙讯POS单机版架构设计
老张开了家米线店,4张桌子,1个收银台。
朋友推荐了三套收银系统:一套要装数据库,一套要租云服务器,一套要买专用硬件。
老张说:“我就想收个钱,能不能简单点?”
01 老张的烦恼
米线店不大,但收银的麻烦不少:
手写单子——顾客点"肉酱米线加豌豆尖加鸡枞菌",收银员写一遍、厨房看一遍、结账再算一遍。写错一个字,要么上错菜,要么算错钱。
价格记不住——米线15块,加豌豆尖5块,加鸡枞菌5块,牛肉米线18块……新来的收银员光背价格就要3天。
算账总出错——128单/天,每单平均5个菜品,手抖输错一个价格,一天亏50,一年亏18000。
老张试过几套收银系统,但都太复杂:
- 要装MySQL数据库?他店里只有一台Windows电脑
- 要租云服务器?他连公网IP都没有
- 要买专用收银机?预算只有一台普通电脑的钱
“我就想要一个东西——双击就能用。”
02 一个exe的诞生
龙讯POS v1,就是为老张这种店做的。
整个系统就一个文件:longxunpos-server.exe,12MB。
双击运行,3秒启动。不需要装数据库,不需要联网,不需要任何配置。
它是怎么做到的?
2.1 数据库?自带了
系统内置了SQLite数据库——一个文件就能存所有数据。你的菜品、订单、支付记录,全在一个 .db 文件里。每天300单?完全撑得住。
import "github.com/glebarez/sqlite" // 纯Go实现,无需CGO
func initDB(cfg *config.DatabaseConfig) *gorm.DB {
db, err := gorm.Open(sqlite.Open(cfg.SQLite.Path), &gorm.Config{})
db.Exec("PRAGMA journal_mode=WAL")
db.Exec("PRAGMA busy_timeout=5000")
}
关键选择:glebarez/sqlite(纯Go实现)而非 mattn/go-sqlite3(需要CGO)。零CGO意味着交叉编译一条命令搞定,Windows构建不需要MinGW。
| 维度 | glebarez/sqlite | mattn/go-sqlite3 |
|---|---|---|
| CGO | 不需要 | 必须 |
| 交叉编译 | CGO_ENABLED=0 |
需要C工具链 |
| Windows构建 | 简单 | 需要MinGW |
2.2 管理后台?嵌进去了
Web管理后台在编译时通过Go的 embed 机制打包进exe:
// web/embed.go
package web
import "embed"
//go:embed dist
var DistFS embed.FS
浏览器打开 http://127.0.0.1:8080/admin/ 就能用,不需要Nginx,不需要Node.js。
React Router的客户端路由通过 spaHandler 回退到 index.html:
fileServer := http.FileServer(http.FS(web.DistFS))
r.Handle("/admin/*", http.StripPrefix("/admin", spaHandler{fileServer}))
2.3 客户端?另一个exe
C# Avalonia客户端也是独立exe,155MB(自包含发布,包含.NET运行时)。双击启动,自动全屏,触屏操作。
C:\LongxunPOS\
├── Server\
│ ├── longxunpos-server.exe ← 双击启动
│ └── data\longxunpos.db ← 自动创建
└── Client\
└── LongxunPOS.Client.exe ← 双击启动
03 老张的一天
上午10:00,老张打开电脑,双击服务端exe,双击客户端exe。
中午12:00,高峰来了。
张阿姨走到收银台:“肉酱米线一碗,加豌豆尖,加鸡枞菌。”
收银员在触屏上点了几下——
肉酱米线 × 1 ¥15.00
+ 豌豆尖 × 1 ¥5.00
+ 鸡枞菌 × 1 ¥5.00
─────────────────────
小计:¥25.00
3秒点完,价格自动算好。
张阿姨掏出手机扫码支付——3秒到账,语音播报"微信收款25元",小票自动打印,副屏显示"付款成功 ¥25.00"。
整个流程:点单→支付→打印→播报→客显,一气呵成。
晚上10:00,打烊。老张打开浏览器看经营数据——每一分钱的来龙去脉,清清楚楚。
04 三个关键架构决策
决策一:纯Go SQLite驱动
零部署、零CGO、简单交叉编译。代价是单机无法水平扩展——但单店场景本来就不需要。
决策二:go:embed 编译时嵌入
单exe部署、无需Web服务器。代价是前端更新需重新编译——但收银系统更新频率低,可以接受。
决策三:C# Avalonia 跨平台客户端
低内存(~80MB vs Electron ~300MB)、原生硬件集成、触屏支持。代价是学习曲线和包体积。
| 决策 | 解决的问题 | 代价 |
|---|---|---|
| 纯Go SQLite | 零部署、零CGO | 单机无法水平扩展 |
| go:embed | 单exe部署 | 前端更新需重新编译 |
| Avalonia | 低内存、原生硬件 | 学习曲线、包体积 |
05 分层架构
服务端严格三层架构,依赖注入通过构造函数实现:
Handler(HTTP层)→ Service(业务层)→ Repository(数据层)
| 层 | 职责 | 关键约束 |
|---|---|---|
| Handler | HTTP请求解析、参数校验、响应格式化 | 不包含业务逻辑 |
| Service | 业务逻辑、事务管理、跨repo协调 | 不直接操作http.Request |
| Repository | GORM数据访问,纯CRUD | 不包含业务判断 |
双API路由体系:/api/v1/*(收银端,admin+staff)和 /admin/api/v1/*(管理后台,仅admin),复用相同的Handler和Service实例。
06 金额精度与并发控制
Decimal精度:自定义 Decimal 类型通过字符串中转实现精确存储,业务层使用 pkgdecimal 包避免浮点误差。
订单级互斥锁:每个订单ID对应一个 sync.Mutex,防止同一订单的并发操作冲突。日序号也有独立锁,保证订单号唯一。
07 适合什么样的店?
- 1家店,不需要多门店管理
- 1台电脑,收银+管理都在这台电脑上
- 日均300单以内,SQLite完全够用
- 有触屏,触控操作比鼠标快3倍
- 有热敏打印机,58mm或80mm都行
- 有副屏,给顾客看金额
不适合:多家店统一管理、小程序点餐、日订单量超1000单——这些需要v2 SaaS多租户版本。
08 写在最后
12MB的exe,包含了完整的REST API、Web管理后台、SQLite数据库、JWT认证、支付集成、核销集成。
这不是"简陋",而是"刚好够用"。
老张现在每天打烊后,打开浏览器看看今天赚了多少,然后关机回家。没有数据库要维护,没有服务器要续费,没有升级要操心。
下期预告:《触屏收银的交互密码:从44px到3秒出单》