
简介这是一套基于Qt框架开发的轻量级超市管理系统实战项目面向C与GUI编程初学者及课程设计学习者聚焦零售业务核心场景覆盖员工登录、商品管理、库存监控、采购流程、销售收银及历史数据查询等完整功能模块。资源包共45个文件含13个CPP源码文件实现业务逻辑与事件响应、12个H头文件定义类接口与数据结构、9个UI界面文件构建登录、收银、库存、员工管理等可视化窗口以及5个JSON配置与数据文件如inventory.json、sales.json等用于轻量级数据持久化整体仅49KB结构清晰、开箱即用。已有705人学习下载提供从登录验证、权限分级员工/管理员到JSON读写、QSS样式美化、QRC资源集成等Qt典型开发实践代码注释规范模块职责分明是理解事件驱动编程与商业系统分层设计的优质入门范例。 先说结论用Qt做一套超市管理系统完全能落地而且比想象中顺手。这个项目我从需求梳理、界面搭建、数据库设计做到最后打包发布前前后后花了三周左右核心功能都跑通了包括商品管理、收银结算、库存预警、进货入库、销售统计这些。如果你正准备做一个Qt项目实战练手或者公司/课程里需要一个能真正演示的桌面管理系统这篇内容可以直接当参考。我用的是Qt 5.15.2 Qt Creator 4.15编译器选的MSVC2015 64位数据库用的SQLite图表用的Qt自带的Qt Charts模块。整套东西没有引入第三方控件库所有界面都是Qt Widgets手写布局没有用QML。为什么这么选后面我会详细说。先把这个系统到底做什么、怎么拆、哪些地方容易踩坑一条条讲清楚。1. 项目概述与整体设计思路1.1 这套超市管理系统到底能做什么在动手写代码之前我最先做的一件事不是建工程而是把“超市管理系统”这六个字拆成具体功能点。超市管理系统的核心不是界面多漂亮而是数据能不能闭环商品进来、卖出去、库存变少、需要补货、卖了多少、赚了多少。整套系统要能回答这些问题。我这个版本最终实现了以下功能模块登录模块区分管理员和收银员两种角色管理员能看所有菜单收银员只能看到收银、商品查询、个人修改密码这几个页面。商品管理商品的新增、编辑、删除、上下架支持按商品编码、名称、分类模糊查询。分类管理维护商品分类树形结构一级分类对应方便面、饮料、日用品这种大类的层级。库存管理每个商品有安全库存阈值低于阈值列表直接标红入库时自动更新库存。采购入库录入供应商、进货数量、进货单价保存后自动增加库存并记录一条入库流水。收银结算购物车模式扫码或搜索添加商品支持数量修改、整单折扣、抹零结账后扣减库存并生成销售单。销售统计按日/按月统计销售额和订单量用柱状图和折线图展示支持导出CSV报表。基础档案供应商管理、操作用户管理、操作日志查看。这些功能看起来多但拆开之后每个都不复杂。难点在于模块之间的数据联动比如收银要扣库存、退货要恢复库存、入库单要记录操作人这些关系理清楚了代码写起来就顺了。1.2 技术选型为什么是Qt Widgets而不是QML先说技术选型。这套系统是典型的“管理信息系统”场景核心是表单、表格、菜单、弹窗交互逻辑相对固定不需要复杂的触摸手势和动画效果。这种场景下Qt Widgets是比QML更合适的选择。原因有三个。第一Qt Widgets对键盘操作和焦点控制的默认行为更成熟。超市收银员用的电脑一般没有触摸屏主要靠键盘和鼠标。Widgets的QLineEdit、QTableWidget这些控件在键盘上下移动、回车确认、Tab切换焦点方面天然好用QML要实现同样效果需要自己写很多焦点控制逻辑。第二开发效率高。管理系统里大量出现“一个表单 一个表格”的页面结构Widgets用代码创建控件非常直接信号槽写起来也直观。QML虽然声明式语法更现代但业务逻辑还是要回到C两套语言来回切换中小型项目反而容易乱。第三打包体积和依赖管理更可控。Widgets程序用windeployqt一跑依赖基本都齐了QML程序还需要额外带上QML运行时和各个模块的qml文件稍不注意就漏这个漏那个。Qt版本我选了5.15.2。这个版本是Qt 5系列的长期支持版本中比较稳定的网上资料最多遇到编译问题基本一搜就能找到答案。新版Qt 6我也试过信号槽语法更现代化但一些老第三方库的兼容性还没完全跟上比如一些串口设备和打印组件所以稳妥起见还是选了Qt 5。1.3 数据库选型SQLite为什么够用系统涉及到商品、订单、库存、用户这些数据肯定要上数据库。我对比过MySQL和SQLite最终选了SQLite。主要原因是部署成本。超市管理系统通常是一台收银主机直接运行不需要多台机器同时写同一份数据。SQLite是嵌入式数据库整个数据库就是一个文件程序启动时直接打开不需要单独装服务、配账号密码、开端口。对一套桌面端的超市管理系统来说SQLite的性能完全够用——几千种商品、几万条订单记录的查询都是毫秒级。当然SQLite也有它的短板最明显的是并发写能力弱。多线程同时写同一个数据库文件时可能会出现database is locked错误。解决方案我在后面第5章会专门讲核心思路是所有写操作集中在同一个数据库连接上执行避免多个连接同时写。2. 需求拆解与数据库设计2.1 用户角色与权限模型权限控制是管理系统逃不开的需求。我定义了两张表用户表保存账号密码和角色用户的权限直接通过角色字符串判断没有做成细粒度权限表。用户表结构如下CREATE TABLE t_user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, real_name TEXT, role TEXT NOT NULL DEFAULT cashier, -- admin / cashier status INTEGER NOT NULL DEFAULT 1, -- 1启用 0禁用 create_time TEXT DEFAULT (datetime(now, localtime)) );密码字段我没有做明文存储而是用QCryptographicHash做了一次SHA-256加盐哈希。具体逻辑是注册用户时生成一个随机盐值存在单独字段里密码存的是盐值密码拼接后的SHA-256结果。登录验证时从数据库取出盐值和哈希值用同样的方式计算后对比。这样即使数据库文件泄露也不至于直接暴露明文密码。角色区分对界面的影响我是通过菜单栏控显实现的。管理员登录后菜单栏里“系统管理”这一项可见收银员登录后这项直接隐藏。收银页面固定为启动后的默认页面管理员可以在菜单栏里自由切换模块。这种实现比在每个页面里反复判断角色权限要简单得多也够用。2.2 核心数据表设计除了用户表整个系统最核心的是商品表、库存流水表、销售单主表和销售单明细表。商品表CREATE TABLE t_goods ( id INTEGER PRIMARY KEY AUTOINCREMENT, barcode TEXT UNIQUE, -- 条码 name TEXT NOT NULL, -- 商品名称 category_id INTEGER, -- 分类ID spec TEXT, -- 规格如“500ml/瓶” unit TEXT DEFAULT 件, -- 单位 purchase_price REAL DEFAULT 0, -- 进货价 sale_price REAL DEFAULT 0, -- 售价 stock INTEGER DEFAULT 0, -- 当前库存 safety_stock INTEGER DEFAULT 10, -- 安全库存阈值 status INTEGER DEFAULT 1, -- 1上架 0下架 create_time TEXT DEFAULT (datetime(now, localtime)) );销售单主表和明细表CREATE TABLE t_order ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_no TEXT NOT NULL UNIQUE, -- 单号日期随机数 total_amount REAL DEFAULT 0, -- 应收总额 discount REAL DEFAULT 1, -- 折扣0.95表示95折 final_amount REAL DEFAULT 0, -- 实收金额 cashier_id INTEGER, -- 收银员ID create_time TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE t_order_detail ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, goods_id INTEGER NOT NULL, goods_name TEXT, sale_price REAL, qty INTEGER, subtotal REAL );库存流水表CREATE TABLE t_stock_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, goods_id INTEGER NOT NULL, change_type TEXT NOT NULL, -- in入库 out销售出库 refund退货 change_qty INTEGER NOT NULL, before_stock INTEGER, after_stock INTEGER, operator_id INTEGER, remark TEXT, create_time TEXT DEFAULT (datetime(now, localtime)) );设计这几个表的时候我踩了一个坑想特别提醒一下商品表里直接冗余了stock库存字段但同时保留库存流水表。为什么不只查流水表统计库存呢因为每次展示商品列表都要SUM求和数据量大了会慢而且容易算错。保留冗余字段每次入库/销售时同步更新查询就非常快代价是需要保证“流水记录”和“库存加减”的一致性。这个我在第3章里用事务解决了。2.3 为什么不建议一开始就上MySQL很多人一看到“管理系统”就觉得得上MySQL或者SQL Server我认为这是惯性思维。对于单机桌面应用引入独立数据库服务会让部署复杂度提升至少一个档次用户得先装数据库配账号密码程序要管连接池出问题了还得排查服务有没有启动。而SQLite只需要拷一个文件过去程序打开就能用。换个场景如果这套系统是多台收银机同时连一个中心数据库那确实该考虑MySQL或者更合理的方案是“本地SQLite 定时同步中心数据库”这样断网时收银机也能独立工作。我这个项目按单机部署来设计SQLite是性价比最高的选择。3. 核心功能模块实现与关键代码3.1 登录界面与权限落地登录界面我用了最简单的QWidget方案中间竖排一个Logo区域、两个输入框账号/密码、一个登录按钮。整体用QSS做了圆角和渐变背景看起来不至于太“程序员审美”。关键点是登录之后的全局状态管理。我定义了一个单例类UserSession登录成功后把当前用户信息写进去其他任何模块要判断当前操作员或者取用户ID都从UserSession取不自己乱传参数。class UserSession { public: static UserSession* instance(); void setCurrentUser(const UserInfo user) { m_current user; } UserInfo currentUser() const { return m_current; } bool isAdmin() const { return m_current.role admin; } private: UserInfo m_current; static UserSession* m_inst; };登录时对密码做校验的代码也很简单核心是SHA-256哈希比对QString hashPassword(const QString pwd, const QString salt) { QByteArray data (salt pwd).toUtf8(); return QCryptographicHash::hash(data, QCryptographicHash::Sha256).toHex(); }这里有个细节盐值一定要拼在密码前面还是后面自己定一个统一规则就行但前后端逻辑必须一致。我习惯写成salt pwd简单直接。3.2 商品管理模块表格模型与库存预警商品列表展示我用了QTableView QSqlQueryModel而不是QTableWidget。原因是商品数据量可能上千条QTableWidget要先把所有数据加载成Item内存开销大、刷新慢QSqlQueryModel直接绑定SQL查询结果数据是懒加载的滚动时性能表现更好。但QSqlQueryModel有一个短板默认是只读的不能直接编辑。我的处理思路是“详情编辑不直接在表格里改点按钮弹出编辑对话框”。表格只负责展示和选择这样反而避免了很多误操作。库存预警是这个模块最有价值的点。我重写了QSqlQueryModel的data方法在Qt::BackgroundRole中返回QBrush库存低于安全库存的行直接显示淡红色背景。QVariant data(const QModelIndex index, int role) const override { if (role Qt::BackgroundRole) { int stockIdx record().indexOf(stock); int safetyIdx record().indexOf(safety_stock); int stock record().value(stockIdx).toInt(); int safety record().value(safetyIdx).toInt(); if (stock safety) { return QColor(255, 200, 200); } } return QSqlQueryModel::data(index, role); }这样做的好处是预警逻辑集中在数据层新增一种查询只要复用同一个model所有列表都会自动变色不用每个查询重新写一遍判断。实战中我还把“低于安全库存”的商品数量统计放到了主界面的状态栏登录管理员后默认显示“当前有N种商品库存偏低”点一下直接跳到商品列表的筛选结果非常实用。3.3 收银结算模块不是单纯“加购物车”收银模块是整套系统里最容易出错的地方因为它涉及多表联动购物车要有临时状态、结算要生成订单、要扣库存、要写流水。而且任何一个环节失败都不能让库存出现负数或者订单缺失。购物车我用了列表控件 一张HashMap存储商品信息。界面上是左侧商品搜索区支持条码、名称模糊查询右侧是当前购物车表格。每次扫码或者点击添加先把商品加到map里再刷新表格。结算按钮点击后执行的是带事务的完整流程bool MainWindow::checkout() { QSqlDatabase db QSqlDatabase::database(); db.transaction(); // 1. 生成销售单主表记录 QString orderNo generateOrderNo(); // 2. 插入主表获取orderId // 3. 循环购物车明细插入明细表 // 4. 更新库存UPDATE t_goods SET stock stock - ? WHERE id ? AND stock ? // 5. 写入库存流水 // 6. 全部成功则提交否则回滚 if (success) { db.commit(); QMessageBox::information(this, 支付成功, 找零: ...); } else { db.rollback(); QMessageBox::critical(this, 支付失败, 库存不足或数据异常); } }这里最值得注意的SQL是扣库存这步。我用的是带条件的UPDATEUPDATE t_goods SET stock stock - :qty WHERE id :goodsId AND stock :qty这个语句利用数据库的行锁保证扣减操作的原子性同时检查库存是否足够。如果affected row count为0说明库存不足后面的订单就不能继续。这种写法比“先SELECT查库存再UPDATE扣减”更安全因为两条语句之间如果有其他线程抢着把库存改了就会出问题。找零计算我放在了UI层实收金额减去应收金额支持支付宝、微信、现金三种支付方式。现金支付需要弹窗输入“收到金额”计算找零扫码支付直接显示收款码弹窗模拟确认收款。3.4 采购入库与库存流水采购入库模块的逻辑和收银结算正好相反增加库存、记录入库流水、写采购单。我复用了同一个事务模式但增加了一个采购单表记录供货商、采购日期、经办人方便后续对账。入库页面是一个表单页顶部选供货商下拉框中间是商品选择区支持按条码或名称搜索底部填进货数量、进货单价点击“确认入库”后事务提交入库时要同时更新两个地方商品表的进货价和库存。如果这次进货价和之前不同我会在流水里记一条备注同时把商品表的purchase_price更新为最新进货价。这样做的好处是下次销售利润统计时用的都是最新成本比较合理。3.5 销售统计与Qt Charts图表销售统计页用到了Qt Charts模块。这里要注意Qt Charts在Qt 5中是单独一个模块安装Qt时要确保勾选了否则编译会报“unknown module in qt: charts”之类的错。我一开始就吃过这个亏重新打开了安装器补装模块才解决。统计页的布局是上半部分两个卡片显示今日销售额和今日订单量中间是日期筛选默认最近7天下面放两个图表柱状图展示每天销售额折线图展示订单量变化趋势。生成柱状图的简化逻辑QBarSet *set new QBarSet(销售额); for (auto day : weekDays) { double total queryTotalByDate(day); *set total; } QBarSeries *series new QBarSeries(); series-append(set); QChart *chart new QChart(); chart-addSeries(series); chart-setTitle(近7日销售额); chart-createDefaultAxes();这里有个实操小经验QChart默认的坐标轴标题格式经常显示不完整尤其是中文字体需要手动设置一下字体和标签角度。我写完之后发现横轴日期挤成一团后来设置了axisX-setLabelsAngle(-30); axisX-setTitleFont(QFont(Microsoft YaHei, 9));整体就好看很多。另外图表数据查询尽量不要在UI线程同步执行如果统计时间跨度大界面会卡顿。我后面把统计查询丢到了QtConcurrent::run里查询完通过信号回传UI线程这个问题就解决了。3.6 操作日志与数据安全管理员需要知道谁在什么时候做了什么操作比如“哪个用户删除了商品”“哪个用户改过价格”。我写了一个LogHelper类所有关键操作后调用统一接口写入日志表。void LogHelper::write(const QString action, const QString detail) { QSqlQuery q; q.prepare(INSERT INTO t_log(user_id, user_name, action, detail) VALUES(?, ?, ?, ?)); q.addBindValue(UserSession::instance()-currentUser().id); q.addBindValue(UserSession::instance()-currentUser().username); q.addBindValue(action); q.addBindValue(detail); q.exec(); }这个模块看起来不起眼但真到系统上线后特别有价值。有一次我测试时发现库存莫名少了查操作日志发现是测试时反复点“删除商品再新增”导致的逻辑问题。没有日志的话这个问题基本没法定位。4. 关键技术点拆解与经验总结4.1 数据库连接与模型/视图架构整个程序所有数据库操作我统一使用一个全局的QSqlDatabase连接在main函数里初始化一次其他模块调用QSqlDatabase::database()获取同一个连接。这样做的好处是规避了SQLite多连接并发写的冲突问题。关于模型/视图架构我的建议是简单查询用QSqlQueryModel需要编辑、排序、自定义显示格式时换成QSqlTableModel。这个项目的商品列表因为要自定义库存预警颜色所以继承QSqlQueryModel重写了data方法订单历史列表因为要格式化金额显示也重写了data。重写data方法时有几个常见坑需要注意调用基类方法前一定要先判断index是否有效否则刷新时可能崩溃。record()方法每次调用会从当前查询结果里取一行数据成本不低。如果列表滚动频繁建议在model里缓存数据只刷新需要变色的行。表格表头字段名不要用中文用英文字段名中文标题通过setHeaderData设置。这样避免编码问题也好维护。4.2 多线程处理的正确做法系统启动时我会让程序在后台线程预加载基础数据比如商品分类、供应商列表、系统配置等。这步用QtConcurrent::run就能实现不用手动创建QThread。真正需要认真处理线程的是报表导出。用户点击“导出CSV”后如果直接在UI线程里写几万条数据到文件界面果断卡死。我的方案是导出任务放到子线程进度条用信号槽更新完成时弹提示框。void MainWindow::exportReport() { QFutureWatcherbool *watcher new QFutureWatcherbool(this); connect(watcher, QFutureWatcherbool::finished, this, []() { if (watcher-result()) { QMessageBox::information(this, 提示, 导出成功); } watcher-deleteLater(); }); QFuturebool future QtConcurrent::run([]() { return exportToCsv(m_startDate, m_endDate, m_filePath); }); watcher-setFuture(future); }这里需要注意的是exportToCsv函数里不能再操作UI控件只能操作数据文件。如果需要每个阶段的进度反馈可以再传递一个std::function回调对象在子线程里发射信号。4.3 用QSS把界面做得像个正经软件Qt Widgets默认的界面确实有点“朴素”但给管理系统换个皮肤并不难。我用了一套QSS样式表整体主题是浅灰色背景、蓝色主按钮、卡片式布局代码就几百行效果立竿见影。QSS的几个实用技巧QLineEdit的focus边框用border: 1px solid #409EFF聚焦后边框变蓝交互反馈很明确。QPushButton普通状态和hover状态分别设置background-color用户知道这个按钮可以点。QTableWidget/QTableView去掉默认的网格线改成隔行变色阅读性大幅提升。QTableWidget { background-color: #FFFFFF; alternate-background-color: #F5F7FA; gridline-color: transparent; selection-background-color: #409EFF; selection-color: #FFFFFF; } QTableWidget::item { padding: 4px; }4.4 windeployqt打包发布开发完成后的打包往往是新手最痛苦的一步。吃过几次“换台电脑就报缺少DLL”的亏之后我总结了一套固定流程把编译好的exe放到一个干净的发布目录。打开Qt自带的命令行工具Qt 5.15.2 for DesktopCD到发布目录。执行windeployqt程序名.exe --release。检查输出确认所有Qt依赖都已拷贝。如果使用了Qt Charts要额外确认charts相关DLL和platform插件都齐了。我的项目里有一个容易漏掉的依赖Qt Charts模块。windeployqt在早期版本对charts的识别不一定完整所以我手动检查过Charts相关dll是否存在缺了就手工从Qt安装目录拷贝。另外SQLite插件也要确认sqldrivers目录下需要包含qsqlite.dll否则程序在别人电脑上会报“driver not loaded”。这个错误非常经典几乎每个Qt新手都会遇到一次。注意打包之前记得把编译模式切成ReleaseQt Creator里分Debug和Release两套目录发错成Debug版会连带一堆调试DLL不仅体积大而且通常不能在干净环境里正常启动。5. 常见问题与排查技巧实录5.1 中文显示乱码这是我遇到的第一个坑。Qt 5的默认编码是UTF-8但Windows控制台和某些老旧系统环境下中文从数据库读出后显示成乱码。排查思路确认代码源文件编码是UTF-8。数据库连接后执行SET NAMES UTF-8对SQLite不需要但对MySQL必要。如果界面显示乱码优先检查QTextCodec的全局设置。Qt 5.15以上版本基本不需要手动设置QTextCodec了但如果你用的是Qt 5.12及以下建议在main函数开头加QTextCodec::setCodecForLocale(QTextCodec::codecForName(UTF-8));这个坑的来源很多时候是Windows的区域语言设置。如果系统区域是中文但是数据库里的字符串是UTF-8编码QSqlQueryModel读出来后直接用QString显示一般是没问题的。真正容易出问题的是文件读取和网络传输我后来统一规定项目里所有字符串保存都用UTF-8直接治好了这个毛病。5.2 表格刷新后数据丢失或者选中行错乱商品列表在每次增删改之后需要重新查询Model。我一开始的做法是model-setQuery(SELECT ...);这行代码会让表格切回到第一行用户刚编辑完一条数据列表瞬间跳走体验很差。改进方案是记住当前选中的商品ID刷新完成后用QModelIndex恢复到原行QModelIndexList selected ui-tableView-selectionModel()-selectedRows(); int currentRow selected.isEmpty() ? 0 : selected.first().row(); int goodsId model-record(currentRow).value(id).toInt(); model-setQuery(SELECT ...); for (int row 0; row model-rowCount(); row) { if (model-record(row).value(id).toInt() goodsId) { ui-tableView-selectRow(row); break; } }这样改完刷新后表格仍停留在用户刚才操作的记录附近体验好了很多。5.3 “database is locked”错误这个错误我是在测试多线程导出时遇到的。原因是SQLite允许同一时间多个连接读但只允许一个连接写事务如果写的时候有其他连接占着写锁新来的写操作会报错。解决思路所有写操作集中到同一个数据库连接。写操作使用QSqlDatabase::database()获取默认连接禁止在子线程里new一个QSqlDatabase连接。如果需要跨线程写库用信号槽把写请求抛回UI线程或者使用连接池但保证同一时间只有一条连接执行写事务。我最后选择的是“信号槽回主线程”的方案简单可靠虽然有一定串行等待但系统本身写操作频率不高完全够用。5.4 Qt Charts编译报错unknown module in qt: charts这个是典型的Qt组件缺失问题。安装Qt时默认只装基础组件Qt Charts属于Additional Libraries需要在安装器里勾选。如果已经装完了才发现不用卸载重装重新运行Qt的MaintenanceTool选择“添加或移除组件”找到“Qt Charts”勾上更新一下就行。注意更新组件之后一定要在Qt Creator中重新运行qmake让工程重新生成Makefile否则编译器还是不认这个模块。5.5 SQLite数据库文件被占用无法备份用户在使用过程中我想直接复制数据库文件做备份发现文件被占用复制不出来。原因不是SQLite文件本身被锁而是我的程序一直在保持打开状态。解决方案是在文件菜单里加了一个“安全退出”按钮退出前把所有查询模型清空、关闭数据库连接model-clear(); ui-tableView-setModel(nullptr); QSqlDatabase::database().close();这样用户备份前先执行一次安全退出数据文件就能正常复制了。这也提醒了我程序异常退出时数据库可能处于未知状态所以我在启动时加了自动完整性检查PRAGMA integrity_check;如果返回值不是ok直接弹窗提示数据库可能损坏并停止加载。6. 项目结构梳理与后续扩展方向6.1 源码目录结构参考这个项目写了二十多个类目录结构如下SupermarketMS/ ├── main.cpp ├── mainwindow.cpp/h ├── database/ │ ├── dbmanager.cpp/h // 数据库初始化、迁移、连接管理 │ └── ini.sql // 建表脚本 ├── models/ │ ├── goodsmodel.cpp/h // 商品表模型库存预警颜色 │ ├── ordermodel.cpp/h │ └── stocklogmodel.cpp/h ├── dialogs/ │ ├── logindialog.cpp/h │ ├── goodseditdialog.cpp/h │ ├── supplierdialog.cpp/h │ └── checkoutdialog.cpp/h ├── widgets/ │ ├── goodswidget.cpp/h // 商品管理页面 │ ├── stockwidget.cpp/h // 入库管理页面 │ ├── poswidget.cpp/h // 收银页面 │ ├── reportwidget.cpp/h // 统计报表页面 │ ├── supplierwidget.cpp/h │ └── logwidget.cpp/h ├── utils/ │ ├── usersession.cpp/h // 当前登录用户 │ ├── loghelper.cpp/h // 操作日志 │ └── common.cpp/h // 通用工具函数 └── resources/ ├── style.qss └── images/这种“一个页面一个Widget类”的组织方式在Qt前端项目里非常实用。每个页面只关心自己的业务逻辑页面之间通过MainWindow的信号槽通信互不干扰。后续要加新页面只要再写一个Widget类塞到主窗口里就行。6.2 还能往哪些方向扩展这个系统如果继续往生产级方向做我建议优先考虑以下几个点条码扫描枪支持。现在收银页面已经是从输入框获取条码了接扫描枪其实就是加一个USB设备扫描枪会模拟键盘输入直接在输入框里回车就行。如果要做得更严谨可以监听键盘事件判断是否在极短时间内连续输入了13位数字并触发回车。会员管理。在订单表里加member_id字段增加积分累计和折扣判断收银页再加一个“积分抵扣”的入口。数据备份与恢复。做一个定时备份功能每天自动把SQLite文件复制到指定目录保留最近7天的备份。多店连锁方向。本地SQLite 定时同步中心数据库这个我之前已经说过是单机系统走向网络化的常见路径。难点在于冲突处理比如同一件商品在两个门店同时卖库存怎么同步这个需要设计好同步策略。打印小票。如果目标是要实际投入使用小票打印是刚需。可以用Qt的QPrinter生成简单的小票内容或者对接Windows的POS打印机驱动。6.3 给同样在做Qt项目的人一些实在建议这个项目做完我最大的体会是Qt做管理系统类桌面应用选型非常正确。开发效率比用C# WinForms高不少跨平台能力也是WinForms不具备的。如果哪天这个系统要往Mac或Linux上迁移Qt的代码迁移成本比C#低很多基本上重新编译一遍再改几个平台相关的API就行。还有一个建议是产品原型先画出来再写代码。我一开始直接开写结果很多东西边写边改比如下单时到底要不要支持一次买多个商品、库存不足时是弹窗还是直接禁用按钮这些决定如果提前定好后期就能少走很多弯路。另外建议多用Qt的示例代码和官方文档。Qt的官方文档是我见过做得最详细的框架文档之一老版本的类也都有完整的API参考。遇到不懂的控件先看官方示例再查Stack Overflow基本都能解决。这套系统的大部分疑难问题我都是这么解决的。本文还有配套的精品资源点击获取