先看效果: 点击观看软件实际演示视频
视频里展示的是:
- 树莓派5做的后端
- 内存客户端的内存使用率和响应速度
- 页面切换0.5毫秒,秒开秒切
这套客户端正在80人的工厂里每天使用。不是什么大厂作品,也不是demo,是我一个人写的生产系统。
一、模块清单(共21个)
往来业务类(9个)
| 模块 | 做的事 |
|---|---|
| 入库 | 入库单录入、保存、查询 |
| 出库 | 出库单录入、验证、打印、作废 |
| 下单 | 客户下单、产品选择 |
| 采购 | 采购订单 |
| 工单 | 工单管理、BOM、排产、完结 |
| 打包单 | 库存管理、库位编辑、标签打印 |
| 领用 | 内部领料 |
| 报工 | 生产报工、计件统计 |
| 盘点 | 库存盘点 |
基础资料类(2个)
| 模块 | 做的事 |
|---|---|
| 产品信息 | 产品增删改查 |
| 字典 | 客户、规格、工艺等字典项 |
硬件设备类(7个)
| 模块 | 做的事 | 接入的硬件/协议 |
|---|---|---|
| 地磅 | 过磅称重、拍照、车牌识别 | 串口地磅、3路RTSP摄像头、MQTT |
| 视觉检测 | 纹理分割、图像对比 | 网络摄像头、AI服务器 |
| 看板 | 实时产量图表、设备状态 | MQTT |
| 报工扫码枪 | 扫码录入 | USB/串口扫码枪 |
| 地图 | 位置标记 | 高德API |
| 终端 | 产线工单排入 | MQTT |
| AI助手 | DeepSeek对话 | DeepSeek API(本地部署) |
系统类(3个)
| 模块 | 做的事 |
|---|---|
| 脑图导航 | 可视化流程导航 |
| 权限配置 | IP级权限控制 |
| 导航管理 | 菜单树、页面切换 |
二、设计理念:极简至上,谁都能用
这套软件的核心原则是:把复杂留给自己,把简单留给用户。
工厂里的工人年龄偏大、文化程度不高、很多人没用过电脑。你给他一个复杂的软件,他学不会,也不用。
所以每个页面要做到:不用教,点就完事。
工单页面:把一堆功能塞到一起
传统ERP:工单管理一个页面、排产一个页面、BOM一个页面、原料分析一个页面。
这里:一个工单页面,用tab切换,全搞定。
ui->tabWidget->addTab(tab1, "工单");
ui->tabWidget->addTab(tab2, "BOM清单");
ui->tabWidget->addTab(tab3, "工单/BOM明细");
ui->tabWidget->addTab(tab4, "采购单");
ui->tabWidget->addTab(tab5, "排产单");
ui->tabWidget->addTab(tab6, "BOM库存表");
工人打开“工单”,从上往下看,该排产的排产,该采购的采购,该完结的完结。
页面虽少,但该有的功能全有。 工人培训10分钟就能上手。
三、架构核心:每个模块完全独立
每个业务模块都是一个独立的QWidget,有自己的UI、网络请求、表格逻辑。
// 打包单模块
class dabaodan : public QWidget {
NetworkManager* m_networkManager; // 自己的网络
Ui::dabaodan *ui; // 自己的UI
void fetchData(...); // 自己的数据加载
};
// 出库模块
class chuku : public QWidget {
NetworkManager* m_networkManager; // 自己的网络
Ui::chuku *ui; // 自己的UI
// 完全不共享代码
};
模块之间不知道彼此存在
入库模块保存成功后,只做一件事:回到列表页。没有任何代码通知其他模块。
if(head == "入库成功") {
ui->stackedWidget->setCurrentIndex(0);
// 没有任何通知代码
}
打包单怎么知道有新数据?用户点开的时候重新从服务器拉。
生产环境验证的效果
- 改打包单,出库单不会崩(实际改过多次,没出过问题)
- 地磅模块接了三路摄像头,不影响报工模块的扫码枪
- 视觉检测的AI服务器超时了,工单排产照常运行
- 新加一个模块,不需要改任何旧代码
四、数据同步:本地SQLite + MQTT实时推送
基础数据都缓存在本地,不每次发HTTP请求。
void ProductSelectionDialog::refreshProductList() {
// 直接读本地SQLite,不是HTTP请求
QSqlQuery query;
query.exec("SELECT name FROM products WHERE type = :type");
}
服务器数据变了,通过MQTT实时更新本地库。
if(topic == "directory_updating") {
if(leiXing == "产品") {
// 更新本地products表
}
if(leiXing == "字典") {
// 更新本地dictionary表
}
}
三种同步场景
| 场景 | 处理方式 |
|---|---|
| 软件开启中,服务器数据变了 | MQTT推送 → 更新本地库 |
| 软件关闭时,服务器数据变了 | 下次打开,点开页面时先拉一次最新数据 |
| 正常运行中,用到基础数据 | 直接读本地SQLite |
效果: 产品下拉框一点就开,不用等。服务器压力很小,树莓派带80人没问题。
五、后端只做数据转发,计算在客户端
后端的角色很简单:接收请求,从数据库取数据,原样返回。不做任何复杂运算。
# 后端只做数据转发
@app.route('/get-gongdan', methods=['POST'])
def get_gongdan():
result = db.query("SELECT * FROM gongdan WHERE ...")
return jsonify(result) # 原样返回,不加工
所有复杂计算都在Qt客户端:排产计算、库存分析、工资汇总……
// 所有计算都在客户端做
SalarySummarizer summarizer;
QJsonArray result = summarizer.processData(jsonArray);
为什么? 树莓派性能本来就不强,让它只做数据转发,把计算丢给客户端,是更合理的分工。
生产环境验证: 80人同时用,树莓派CPU占用不到30%。如果让后端做计算,早就卡死了。
六、每个模块独立接入硬件
地磅模块自己管三路摄像头和串口:
class dibang : public QWidget {
QMediaPlayer m_player; // 自己的摄像头1
QMediaPlayer m_player1; // 自己的摄像头2
QMediaPlayer m_player2; // 自己的摄像头3
QSerialPort* serialPort; // 自己的串口地磅
};
报工模块自己管扫码枪:
class baogong : public QWidget {
QSerialPort* serialPort; // 自己的串口扫码枪
QTimer* scanTimer; // 自己的扫描定时器
};
生产环境验证: 地磅的摄像头坏过一个,报工扫码枪完全不受影响。
七、原生机器码:没有虚拟机开销
Qt C++编译出来是直接的机器码,不是Java/C#那种跑在虚拟机上的字节码。
| 对比 | Java/C#(虚拟机) | Qt C++(机器码) |
|---|---|---|
| 启动速度 | 慢(要加载虚拟机) | 快(直接执行) |
| 运行效率 | 有JIT/GC开销 | 无中间层 |
| 内存占用 | 虚拟机本身占几十MB | 只有程序自己的开销 |
| 响应延迟 | 毫米级但有波动 | 稳定在微秒级 |
实际表现:
- 扫码枪扫完码,界面立刻跳转,没有“转圈”
- 表格刷几千条数据,瞬间渲染完
- 软件开一天,内存不会慢慢涨
- 相比工厂以前用的某Java ERP(点一下等2秒),体验是天壤之别
八、内存管理:主动清理,防止内存累积
Qt程序长时间运行,内存会慢慢涨。哪怕代码写得再好,Qt内部的一些缓存、QWidget的残留、QNetworkAccessManager的连接池……都会让内存只升不降。
在终端模块(shengchan.cpp)里加了一个主动清理机制:
// 每50次MQTT消息处理,主动清理一次内存
if (callCount % 50 == 0) {
SetProcessWorkingSetSize(GetCurrentProcess(), (SIZE_T)-1, (SIZE_T)-1);
}
SetProcessWorkingSetSize是Windows API,作用是强制操作系统回收进程的物理内存。
效果:
- 平时内存慢慢涨到80-100MB
- 触发清理后,直接掉回10-20MB
- 软件开一个月也不会卡
九、安全性:局域网+加密,数据出不去
1. 物理隔离:服务器在本地
服务器(树莓派)部署在工厂内部局域网,没有公网IP,也不做内网穿透。
// 所有的API都是内网IP
static QString api_ui_get = "http://192.168.0.134:8000/get-xxx";
效果: 软件拷到外面,打开就是白屏,连不上任何服务器。别说用,连数据都看不到。
2. 所有网络请求都加密
不是明文HTTP,而是先AES加密再Base64编码:
QAESEncryption encryption(...);
QByteArray enBA = encryption.encode(jsonData, ENCRY_KEY.toUtf8());
data["data"] = QString::fromLatin1(enBA.toBase64());
效果: 就算有人在局域网里抓包,抓到的也是密文,解不开。
3. 权限控制:IP级认证
class PermissionManager {
bool hasPagePermission(int pageIndex) const;
bool hasButtonPermission(int pageIndex, const QString& buttonId) const;
};
每个IP可以配置不同的权限。不是这个车间的人,打不开对应的页面。
十、主窗口只是一个容器
主窗口只干三件事:持有所有模块的指针、提供公共接口(MQTT、设备ID)、切换页面。
class TreeDemo : public QMainWindow {
private:
Ui::TreeDemoClass* ui;
// ui->widget_6 是打包单
// ui->widget_2 是出库
// ui->gongdan_w 是工单
// ui->widget_14 是地磅
// ...
};
不关心模块内部怎么搞。
十一、代码层面的几个亮点
1. SignalRouter:MQTT消息解耦
class SignalRouter : public QObject {
void routeData(const QByteArray &data, int targetWindowId = -1);
void registerWindow(int windowId, QObject* window);
};
MQTT消息来了,不知道谁要听,就丢给SignalRouter广播。各个模块自己决定是否订阅。这是“中介者模式”的典型应用。
2. 脑图组件:自己画的拖拽式流程图
不是用第三方库,是自己用QFrame+QPainter画的。节点拖拽、连线、缩放、全屏、遮罩、对齐……功能完整。工厂的生产流程可以拖拽画出来,比文字描述直观。
3. 工资汇总器:写一次,到处用
class SalarySummarizer : public QObject {
QJsonArray processData(const QJsonArray &inputData);
};
名字叫“工资汇总”,但其实是一个通用的数据聚合工具。按“ob”字段分组,把“shu”字段累加。出库汇总、采购汇总都在用它。
4. 全屏/画布模式:展示场景下的UI简化
脑图组件里做了工具栏折叠,不需要看工具栏的时候,一键全屏只显示画布。老板或者客户来看系统的时候,可以只展示内容,不被复杂的工具栏干扰。
十二、核心原则总结
| 原则 | 做法 | 效果 |
|---|---|---|
| 极简页面 | 一个页面干完所有事 | 工人10分钟上手 |
| 模块隔离 | 每个模块独立,不共享代码 | 改A没崩过B |
| 硬件独立 | 每个模块自己管自己的硬件 | 摄像头坏了不影响扫码枪 |
| 本地缓存 | SQLite + MQTT实时更新 | 下拉框秒开 |
| 客户端计算 | Qt做复杂运算,后端只转发 | 树莓派CPU不到30% |
| 原生机器码 | C++直接编译 | 启动快、响应快 |
| 主动内存清理 | 定期调用Windows API回收内存 | 内存稳定在10-20MB |
| 安全隔离 | 局域网+加密+IP权限 | 软件拿出去用不了 |
十三、技术栈
| 项目 | 技术 |
|---|---|
| 框架 | Qt 6 (C++17) |
| 构建 | qmake |
| 协议 | HTTP、MQTT |
| 硬件接口 | 串口(QSerialPort)、RTSP(QMediaPlayer) |
| 数据库 | SQLite(本地)+ MySQL(后端) |
| 加密 | AES |
| 第三方 | 高德地图、DeepSeek(本地部署) |
| 图表 | Qt Charts |
| Web | Qt WebEngine、Qt WebChannel |
十四、后续计划
- 三维数字孪生:用Qt 3D做车间三维可视化
- 配置中心:把硬编码的IP、端口抽到配置文件
- 更多硬件接入:温湿度传感器、电表、PLC
十五、一个人能写出来的原因
这套系统不是“设计”出来的,是“长”出来的。
一开始只有几个模块,后面慢慢加。因为每个模块都是独立的,新加一个模块不需要改旧代码,所以可以一个人慢慢堆。
而且它不是demo,是每天在生产线上跑的真实系统。
如果一开始就要设计一个“完美架构”,可能到现在还没写出来。