从一碗米线到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秒出单》