news 2026/6/5 13:46:04

基于Qt的饮料售货机双端系统:含图形界面、SQLite本地数据库与完整业务流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Qt的饮料售货机双端系统:含图形界面、SQLite本地数据库与完整业务流程

本文还有配套的精品资源,点击获取

简介:这个Qt桌面应用实现了完整的饮料自动售货模拟,包含可独立运行的服务端和客户端两个工程。服务端负责数据管理与后台逻辑,客户端提供用户交互界面,全部使用C++和Qt框架开发。系统内置SQLite数据库(UserDatabase.db),统一存储用户信息、商品列表和订单记录,所有数据库操作由dbhandler.cpp封装,支持增删改查及事务处理。界面通过Qt Designer设计,覆盖登录验证、用户注册/编辑、商品浏览与选购、菜单配置、广告展示等核心场景,对应UI文件如login.ui、shop.ui、user_reg.ui、good_edit.ui等均已集成。代码结构清晰,table.cpp和goodtable.h支撑商品表格展示,media.qrc集中管理图片与音效资源,.pro工程文件明确区分双端构建目标。项目附带README.md说明文档,开箱即用,无需网络或远程服务器依赖,适合教学演示、课程设计或Qt GUI开发入门实践。

1. 项目概述:为什么一个“饮料售货机”值得用Qt双端架构重做一遍?

你可能见过不少Qt练手小项目:计算器、记事本、简易画板……但真正能让你把Qt的信号槽、模型视图、资源管理、数据库集成、多线程基础、工程组织这七八样核心能力串起来的,少之又少。这个“基于Qt的饮料售货机双端系统”,表面看是个模拟卖可乐雪碧的小玩具,实际上是一套高度凝练的Qt工业级GUI开发范式训练套件——它不依赖网络、不调用云服务、不连远程服务器,所有逻辑跑在本地,却完整复现了真实商业软件中“前后端分离”的思想内核。

我带过三届毕业设计,每年都有学生卡在“不知道Qt项目怎么分层”“UI和逻辑总搅在一起改得崩溃”“数据库一加事务就崩”这些坑里。而这个项目,从第一天打开.pro文件就能看到清晰意图:VendingSystem_server.pro里没有一行UI代码,只有dbhandler.cpptable.cpp这类纯数据与业务逻辑模块;VendingSystem_client.pro里则全是login.cppshop.cpp这类界面控制器,通过信号与服务端模块解耦通信。这不是为了炫技,而是因为——真正的桌面应用,从来不是“把所有代码塞进main.cpp”就能跑通的

关键词里的“Qt售货机”,本质是场景载体;“SQLite本地库”,是轻量可靠的数据中枢;而“Qt双端架构”,才是这个项目最硬核的价值点。它用最朴素的方式回答了一个关键问题:当你的程序既要响应用户点击(比如点“购买冰红茶”),又要保证库存扣减+订单生成+余额更新三步原子执行(不能扣了库存但没生成订单),还要让UI实时刷新(比如商品列表立刻变灰表示售罄),该怎么组织代码?答案就藏在dbhandler.hbeginTransaction()/commitTransaction()封装里,藏在shop.cpp里对GoodTableModeldataChanged信号监听中,更藏在serverclient两个.pro工程之间那条看不见却极其严格的职责边界上。

它适合谁?如果你正在写本科毕设,担心答辩时被问“你的项目和网上教程有什么区别”,这个双端结构就是你的底气;如果你刚学完Qt Designer,正发愁怎么把拖出来的按钮和后端逻辑连起来,login.cppon_loginButton_clicked()调用DbHandler::instance()->verifyUser()的写法,就是教科书级示范;如果你已经会写单文件Demo,但团队协作时总被吐槽“你的代码没法给别人维护”,看看media.qrc如何统一管理27张广告图、goodtable.h如何把QTableView的列宽/排序/编辑策略全部封装成可复用类——这些细节,才是工业实践和课堂作业的分水岭。

别小看这个“卖饮料”的壳子。它背后是一整套经过验证的Qt桌面应用开发心法:数据归数据,界面归界面,资源归资源,工程归工程。接下来,我们就一层层剥开它的实现肌理。

2. 整体架构设计:双端分离不是噱头,而是为了解耦复杂度

2.1 双端划分的底层逻辑:为什么必须拆成两个.pro工程?

很多初学者看到“双端”第一反应是:“不就是两个窗口吗?在一个工程里用QMainWindow切换不就行了?” 这恰恰是本项目最值得深挖的设计起点。我们来算一笔账:假设把服务端逻辑(数据库操作、库存校验、订单生成)和客户端逻辑(登录界面、商品展示、按钮点击响应)全塞进一个工程,会发生什么?

  • 编译耦合:每次改一个UI按钮样式,整个包含数据库连接池的庞大工程都要重新编译,耗时从3秒变成47秒;
  • 测试灾难:想单独测试“用户余额不足时购买失败”这个逻辑,必须先启动登录窗口、输入账号密码、再点进商品页——根本没法写单元测试;
  • 职责混乱shop.cpp里既要处理QMouseEvent,又要调用QSqlQuery执行UPDATE语句,还要播放QSound::play("beep.wav"),一个文件承担三重角色,出Bug时连日志都分不清是UI线程还是数据库线程崩的。

而本项目用两个独立.pro文件强行划清界限:
-VendingSystem_server.pro:只含.cpp/.h源码,无.ui文件,不链接Qt5Widgets(只链Qt5SqlQt5Core)。编译产物是静态库libvending_server.a或动态库vending_server.dll,对外只暴露DbHandler单例和GoodTableModel接口。
-VendingSystem_client.pro:只含.cpp/.h/.ui文件,链接Qt5WidgetsQt5Sql,但不直接操作SQL语句——所有数据库访问必须通过DbHandler::instance()调用。它像一个“前台服务员”,只负责把用户指令(“我要买瓶可乐”)翻译成标准请求,再把服务端返回的结果(“已扣款,库存-1”)渲染成界面反馈。

提示:这种设计直接受益于Qt的元对象系统(MOC)。DbHandler类声明了Q_OBJECT,其userLoginSuccess()信号可被login.cpp中的connect()捕获,而无需任何头文件依赖循环。这是Qt原生支持的松耦合,不是靠“约定俗成”的伪解耦。

2.2 数据流全景图:从点击按钮到数据库落盘的七步闭环

以用户点击“购买冰红茶”为例,完整流程如下(全程无跨进程通信,纯内存调用):

  1. UI触发shop.cppon_buyButton_clicked()槽函数被激活;
  2. 参数组装:获取当前选中商品ID、用户ID、当前时间戳,构造成PurchaseRequest结构体;
  3. 服务端委托:调用DbHandler::instance()->processPurchase(request)
  4. 事务开启dbhandler.cppbeginTransaction()启动SQLite事务;
  5. 三重校验
    a) 查询users表确认余额 ≥ 商品价格;
    b) 查询goods表确认库存 > 0;
    c) 检查orders表中是否存在未完成订单(防重复提交);
  6. 原子写入
    a)UPDATE goods SET stock = stock - 1 WHERE id = ?
    b)INSERT INTO orders (user_id, good_id, amount, time) VALUES (?, ?, ?, ?)
    c)UPDATE users SET balance = balance - ? WHERE id = ?
  7. 结果广播:事务commit()成功后,发射purchaseCompleted(QVariantMap result)信号,shop.cpp监听该信号并刷新商品列表、更新用户余额显示。

这个流程里最关键的不是SQL语句本身,而是第4步事务控制与第7步信号驱动UI更新。SQLite的BEGIN IMMEDIATEBEGIN DEFERRED更适合售货场景——它在事务开始时就加锁,避免高并发下出现“查库存有10瓶,扣减时只剩5瓶”的超卖问题。而信号机制确保了:哪怕你在dbhandler.cpp里新增一个“购买成功发送短信”功能,shop.cpp也完全不用改一行代码,只需监听新信号即可。

2.3 SQLite本地库的精妙取舍:为什么不用MySQL或JSON文件?

有人会质疑:“SQLite不是只能单写?售货机万一多人同时买怎么办?” 这恰恰暴露了对嵌入式数据库的误解。本项目选择SQLite,是经过三重权衡后的最优解:

维度SQLite方案MySQL方案JSON文件方案
部署复杂度零配置,UserDatabase.db随程序分发即用需预装MySQL服务,配置用户权限,防火墙放行3306端口无服务依赖,但需手动解析/序列化
事务可靠性ACID完备,ROLLBACK可回滚至任意保存点ACID完备,但本地部署易因权限问题失效无事务概念,写入中断即数据损坏
读写性能单线程写入延迟<0.5ms(实测i5-8250U)网络往返+服务端解析,延迟>15ms解析1MB JSON需80ms,且无法局部更新

更重要的是,售货机本质是单点终端设备。所谓“多人同时购买”,实际是同一台机器上不同用户账号的串行操作(A买完退出,B再登录),SQLite的写锁完全够用。而JSON方案看似简单,一旦需要“查询余额大于50元的所有用户”,你就得把整个用户表加载进内存再遍历——这在10万条记录时直接OOM。SQLite一句SELECT * FROM users WHERE balance > 50,毫秒级返回结果。

UserDatabase.db的表结构设计也暗藏巧思:
-users(id INTEGER PRIMARY KEY, username TEXT UNIQUE, password_hash TEXT, balance REAL)
-goods(id INTEGER PRIMARY KEY, name TEXT, price REAL, stock INTEGER, image_path TEXT)
-orders(id INTEGER PRIMARY KEY, user_id INTEGER, good_id INTEGER, amount REAL, time TEXT, status INTEGER DEFAULT 0)
-advertisements(id INTEGER PRIMARY KEY, title TEXT, content TEXT, image_path TEXT, duration INTEGER)

注意image_path字段存的是相对路径(如:/images/coke.jpg),而非绝对路径。这使得所有图片资源通过media.qrc注册后,无论程序安装在C:\还是/home/user/,都能用QPixmap(":/images/coke.jpg")统一加载——这才是Qt资源系统的正确打开方式。

3. 核心模块深度解析:从UI绑定到数据库封装的实战细节

3.1 Qt Designer界面工程化实践:不只是拖控件,更是定义交互契约

很多人把.ui文件当成“画布”,其实它是UI与逻辑的契约接口。以login.ui为例,表面看只是两个QLineEdit和一个QPushButton,但其内部属性设置决定了后续开发效率:

  • usernameEditobjectName设为"usernameEdit"(非默认lineEdit),确保login.cppfindChild<QLineEdit*>("usernameEdit")能精准定位;
  • passwordEditechoMode设为PasswordinputMethodHints勾选ImhNoPredictiveText,禁用输入法联想,防止密码泄露;
  • loginButtondefault属性设为true,使用户按Enter键自动触发点击事件;
  • 整个窗口的sizePolicy设为Preferred/PreferredminimumSize设为400x300,避免缩放时控件挤压变形。

最关键的,是login.ui未放置任何布局管理器(Layout)。这并非疏忽,而是刻意为之——因为login.cpp继承自QDialog,在showEvent()中动态添加QVBoxLayout,并插入一个QLabel显示动态广告(从advertisements表随机读取)。这种“UI静态定义 + 逻辑动态注入”的模式,让广告内容变更无需重新编译.ui文件,只需更新数据库即可生效。

再看shop.ui的商品列表区域:这里没有用QTableWidget(易内存泄漏),而是放置一个空QWidget容器,命名为goodsContainershop.cpp在构造函数中创建GoodTableModel实例,并用QTableView关联该模型,最后将QTableView设为goodsContainer的子部件。这样做的好处是——表格列宽、排序、右键菜单等行为全部封装在goodtable.h中,shop.ui只负责提供容器位置,彻底解耦表现层与数据层

注意:goodtable.h中重写了headerData()函数,将数据库字段名price映射为中文“价格”,stock映射为“库存余量”。这种映射关系写死在代码里,而非UI中,确保国际化时只需修改此处字符串,无需触碰Designer。

3.2 dbhandler.cpp:SQLite封装的艺术,远不止增删改查

dbhandler.cpp是整个系统的数据心脏,其设计体现了Qt数据库开发的精髓。我们逐段解析核心代码逻辑:

// dbhandler.cpp 第42行:单例模式确保全局唯一数据库连接 DbHandler* DbHandler::instance() { static QMutex mutex; static DbHandler* _instance = nullptr; if (!_instance) { QMutexLocker locker(&mutex); if (!_instance) { _instance = new DbHandler; } } return _instance; }

这里用双重检查锁定(Double-Checked Locking)避免多线程下重复初始化,比简单static DbHandler instance更安全——因为DbHandler构造函数中会调用QSqlDatabase::addDatabase(),而Qt文档明确警告:addDatabase()不是线程安全的。

再看事务处理的关键函数:

// dbhandler.cpp 第189行:带错误回滚的购买流程 bool DbHandler::processPurchase(const PurchaseRequest& req) { if (!beginTransaction()) return false; // 1. 开启事务 QSqlQuery query(db); // 2. 复用连接,非新建 // 3. 三重校验(省略具体SQL) if (!checkBalance(req.userId, req.price)) goto rollback; if (!checkStock(req.goodId)) goto rollback; // 4. 原子写入(三步SQL) if (!query.exec(QString("UPDATE goods SET stock = stock - 1 WHERE id = %1").arg(req.goodId))) goto rollback; if (!query.exec(QString("INSERT INTO orders (user_id, good_id, amount, time) VALUES (%1, %2, %3, '%4')") .arg(req.userId).arg(req.goodId).arg(req.price).arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")))) goto rollback; if (!query.exec(QString("UPDATE users SET balance = balance - %1 WHERE id = %2").arg(req.price).arg(req.userId))) goto rollback; commitTransaction(); // 5. 提交 emit purchaseCompleted(resultMap); // 6. 广播结果 return true; rollback: rollbackTransaction(); // 7. 回滚 emit purchaseFailed(errorMsg); return false; }

这段代码的精妙之处在于:
- 使用goto rollback而非嵌套if,大幅提升可读性(实测比五层嵌套if少37%认知负荷);
- 所有SQL语句用QString::arg()拼接,杜绝SQL注入风险(对比"WHERE id = " + QString::number(req.goodId)的危险写法);
-emit信号在事务提交后才触发,确保监听者收到的永远是已持久化的数据状态。

3.3 table.cpp与goodtable.h:模型视图架构(MVC)的落地样板

Qt的QTableView+QAbstractTableModel组合,常被初学者视为“过度设计”。但本项目用table.cpp证明了它的不可替代性:

// goodtable.h 第28行:自定义模型类 class GoodTableModel : public QAbstractTableModel { Q_OBJECT public: explicit GoodTableModel(QObject *parent = nullptr); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; signals: void dataUpdated(); // 当库存变化时通知UI刷新 private: QList<GoodItem> m_goods; // 内存缓存,避免频繁查库 };

关键创新点在于m_goods内存缓存。GoodTableModel::refreshFromDb()函数在构造时从SQLite加载全部商品,后续所有data()调用均从内存读取,将UI渲染帧率从12fps提升至60fps(实测i5-8250U)。而库存变更时,dbhandler.cpp在事务提交后调用GoodTableModel::updateStock(goodId, newStock),直接修改内存并emit dataUpdated()shop.cpp监听此信号后调用tableView->viewport()->update()强制重绘——整个过程不触发任何数据库查询。

goodtable.h还封装了实用功能:
-getColumnWidths()返回QMap<int, int>,存储各列推荐宽度(如“商品名称”列宽200,“价格”列宽80),避免每次启动都手动调整;
-getSortRole()返回Qt::UserRole,使QTableView::sortByColumn()能按价格数值排序,而非字符串排序(否则“10元”会排在“2元”前面);
-contextMenuEvent()重写,右键弹出“编辑商品”菜单,直接跳转到good_edit.ui

这些细节,正是工业级代码与Demo代码的分水岭。

4. 实操全流程:从零构建可运行系统的七步手把手指南

4.1 环境准备与依赖安装(避坑版)

本项目要求Qt 5.15.2或更高版本(因使用了QSqlDatabase::setConnectOptions()设置QSQLITE_ENABLE_SHARED_CACHE选项)。以下是Windows/macOS/Linux三平台通用步骤:

Windows(推荐Qt Online Installer):
1. 下载Qt Online Installer,安装时勾选:
-Qt 5.15.2MinGW 8.1 64-bit
-Developer and Designer ToolsQt Creator 4.15.2
-Additional LibrariesQt SQL Drivers(确保包含qsqlite.dll
2. 安装完成后,在Qt Creator中打开VendingSystem_server.pro,点击左下角ProjectsBuild & RunKits,确认CompilerMinGW 8.1Qt versionQt 5.15.2
3.关键避坑:若编译报错LNK2019: unresolved external symbol _sqlite3_open,说明未链接SQLite库。在VendingSystem_server.pro末尾添加:
pro win32: LIBS += -L$$[QT_INSTALL_LIBS]/../plugins/sqldrivers/ -lqsqlite

macOS(Homebrew + Qt):

# 1. 安装Qt(推荐用aqtinstall避免官网下载慢) pip3 install aqtinstall aqt install --outputdir ~/Qt 5.15.2 mac desktop # 2. 设置环境变量(加入~/.zshrc) export PATH="$HOME/Qt/5.15.2/clang_64/bin:$PATH" export QT_QPA_PLATFORM_PLUGIN_PATH="$HOME/Qt/5.15.2/clang_64/plugins/platforms" # 3. 编译前修复macOS特有问题:在VendingSystem_client.pro中添加 macx: QMAKE_LFLAGS += -Wl,-rpath,@executable_path/../Frameworks

Linux(Ubuntu 22.04):

sudo apt update && sudo apt install qt5-default qttools5-dev-tools libsqlite3-dev # 验证SQLite驱动:ls /usr/lib/x86_64-linux-gnu/qt5/plugins/sqldrivers/libqsqlite.so

提示:所有平台首次运行前,务必确认UserDatabase.db文件存在且可写。若程序启动报错“Cannot open database”,请检查:
- Windows:UserDatabase.db是否在VendingSystem_client.exe同目录;
- macOS:UserDatabase.db是否在.app/Contents/MacOS/目录;
- Linux:UserDatabase.db是否在可执行文件所在目录,且用户有写权限(chmod 644 UserDatabase.db)。

4.2 双端编译与调试技巧(附常见错误速查)

编译顺序必须严格遵守:
1. 先编译VendingSystem_server.pro(生成libvending_server.a);
2. 再编译VendingSystem_client.pro(链接上述静态库)。

Qt Creator调试技巧:
- 在login.cppon_loginButton_clicked()第一行打断点,按F5启动调试;
- 在Debug模式下,右键Locals and Expressions窗口 →Add Expression→ 输入DbHandler::instance()->getUserCount(),实时查看数据库用户总数;
- 若UI卡死,点击Threads窗口,检查是否QThread阻塞在QSqlQuery::exec()——这通常意味着SQLite被其他进程占用(如另一个实例正在运行)。

高频错误速查表:

错误现象根本原因解决方案
启动报错QSqlDatabase: QSQLITE driver not loadedSQLite插件未找到Windows:复制Qt/5.15.2/MinGW81_64/plugins/sqldrivers/qsqlite.dllVendingSystem_client.exe同目录;macOS:在Info.plist中添加<key>QSqldriver</key><string>qsqlite</string>
登录后商品列表空白GoodTableModel::refreshFromDb()未触发shop.cpp构造函数末尾添加model->refreshFromDb(),并确认DbHandler::instance()已初始化
点击购买无反应信号未正确连接shop.cppsetupConnections()中,检查connect(DbHandler::instance(), &DbHandler::purchaseCompleted, this, &Shop::onPurchaseSuccess)是否执行
广告图片不显示media.qrc未正确编译右键media.qrcRebuild,确认Resources节点下有:/images/前缀的资源

4.3 数据库初始化与测试数据注入(含SQL脚本)

UserDatabase.db初始为空,需手动注入测试数据。项目附带init_db.sql脚本(位于resources/目录),内容如下:

-- 创建用户表 CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, balance REAL DEFAULT 100.0 ); -- 创建商品表 CREATE TABLE IF NOT EXISTS goods ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL NOT NULL, stock INTEGER NOT NULL DEFAULT 10, image_path TEXT DEFAULT ':/images/default.jpg' ); -- 插入测试用户(密码明文仅用于演示,实际应bcrypt加密) INSERT OR IGNORE INTO users (username, password_hash, balance) VALUES ('admin', '21232f297a57a5a743894a0e4a801fc3', 500.0), ('user1', 'ad0234829205b9033196ba818f7a872b', 100.0); -- 插入测试商品 INSERT OR IGNORE INTO goods (name, price, stock, image_path) VALUES ('可口可乐', 3.0, 20, ':/images/coke.jpg'), ('百事可乐', 3.0, 15, ':/images/pepsi.jpg'), ('冰红茶', 4.5, 12, ':/images/tea.jpg'), ('矿泉水', 2.0, 50, ':/images/water.jpg'); -- 创建广告表 CREATE TABLE IF NOT EXISTS advertisements ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, image_path TEXT, duration INTEGER DEFAULT 5000 ); INSERT OR IGNORE INTO advertisements (title, content, image_path, duration) VALUES ('夏日特惠', '全场饮料第二瓶半价!', ':/images/ad_summer.jpg', 3000), ('新品上市', '芒果味气泡水,清爽上市!', ':/images/ad_mango.jpg', 4000);

执行方法(三平台通用):

# 1. 安装sqlite3命令行工具 # Windows: 从 https://www.sqlite.org/download.html 下载 sqlite-tools-win32-x86-*.zip # macOS: brew install sqlite3 # Linux: sudo apt install sqlite3 # 2. 执行初始化 sqlite3 UserDatabase.db < init_db.sql

执行后,用sqlite3 UserDatabase.db ".tables"验证表创建成功,再用sqlite3 UserDatabase.db "SELECT * FROM users;"确认测试数据已注入。

4.4 功能验证清单(确保每个模块可独立运行)

按以下顺序逐项验证,每步成功后再进行下一步:

  1. 服务端基础验证
    运行VendingSystem_server(无界面),在Qt Creator的Application Output窗口查看输出:
    ✅ 应显示[INFO] Database connected successfully
    ✅ 应显示[INFO] Server initialized with 2 users, 4 goods

  2. 客户端登录验证
    运行VendingSystem_client→ 输入admin/admin→ 点击登录
    ✅ 应跳转至shop.ui,顶部显示欢迎,admin!余额:500.00元
    ✅ 商品列表应显示4种饮料,库存数字正确

  3. 购买流程验证
    在商品列表选中“可口可乐” → 点击“购买”按钮
    ✅ 弹出提示框购买成功!已扣除3.00元
    ✅ 商品列表中“可口可乐”库存从20变为19
    ✅ 用户余额从500.00变为497.00

  4. 管理功能验证
    点击右上角管理员菜单商品管理新增商品
    ✅ 打开good_add.ui,填写“橙汁”、“5.0”、“30” → 点击保存
    ✅ 返回商品列表,应新增“橙汁”且库存为30

  5. 广告轮播验证
    观察shop.ui底部广告栏(advertisement.ui嵌入区域)
    ✅ 应每5秒切换一张广告图,图片清晰无拉伸
    ✅ 广告标题与内容文字正确显示

完成全部5项,即证明系统已具备完整业务闭环能力。

5. 进阶优化与扩展建议:让毕业设计脱颖而出的五个方向

5.1 性能优化:从“能跑”到“丝滑”的关键改造

当前版本在1000+商品时,GoodTableModel::refreshFromDb()全量加载会导致UI卡顿(实测约1.2秒)。升级方案如下:

方案A:分页懒加载(推荐)
修改GoodTableModel,增加loadPage(int page, int pageSize)函数:

void GoodTableModel::loadPage(int page, int pageSize) { QString sql = QString("SELECT * FROM goods LIMIT %1 OFFSET %2") .arg(pageSize).arg(page * pageSize); QSqlQuery query(db); query.exec(sql); beginResetModel(); m_goods.clear(); while (query.next()) { m_goods.append(GoodItem(query.value("id").toInt(), query.value("name").toString(), query.value("price").toDouble(), query.value("stock").toInt())); } endResetModel(); }

shop.cpp中,监听tableView->verticalScrollBar()->valueChanged(),滚动到底部时自动加载下一页。

方案B:SQLite FTS5全文检索(适配搜索框)
goods表添加FTS5虚拟表:

CREATE VIRTUAL TABLE goods_fts USING fts5(name, content='goods', content_rowid='id'); INSERT INTO goods_fts(goods_fts, rowid, name) SELECT 'rank', id, name FROM goods;

搜索时用SELECT * FROM goods JOIN goods_fts ON goods.id = goods_fts.rowid WHERE goods_fts MATCH '可乐*',响应速度从800ms降至15ms。

5.2 安全加固:毕业答辩时的加分项

当前密码以MD5明文存储(admin的hash是21232f297a57a5a743894a0e4a801fc3),虽满足教学需求,但答辩时若被问及安全性,可展示以下改进:

  • 密码哈希升级:在dbhandler.cpp中引入QCryptographicHash,用PBKDF2-SHA256替代MD5:
    cpp QByteArray salt = QCryptographicHash::hash(username.toUtf8(), QCryptographicHash::Md5); QString hash = QString(QCryptographicHash::hash( username.toUtf8() + salt + password.toUtf8(), QCryptographicHash::Sha256).toHex());
  • SQL注入防御:所有用户输入(如用户名、商品名)在拼接SQL前,用QSqlQuery::addBindValue()参数化:
    cpp query.prepare("INSERT INTO users (username, password_hash) VALUES (?, ?)"); query.addBindValue(username); query.addBindValue(hash); query.exec();
  • 敏感操作日志:在orders表增加operator_id字段,记录每次购买的操作员(登录用户),便于审计。

5.3 界面现代化:用QSS和动画提升专业感

Qt Designer生成的界面偏“Windows 98风”,可通过QSS(Qt Style Sheets)快速美化:

/* 在main.cpp中加载 */ QFile qss(":/styles/dark.qss"); qss.open(QFile::ReadOnly); qApp->setStyleSheet(qss.readAll());

dark.qss示例:

QMainWindow { background-color: #2d2d2d; } QPushButton { background-color: #4CAF50; border: none; color: white; padding: 8px 16px; text-align: center; text-decoration: none; display: inline-block; font-size: 14px; margin: 4px 2px; cursor: pointer; border-radius: 4px; } QPushButton:hover { background-color: #45a049; } QTableView::item:selected { background-color: #3a3a3a; }

再为购买成功添加动画效果:

// shop.cpp 中 onPurchaseSuccess() QPropertyAnimation *anim = new QPropertyAnimation(ui->balanceLabel, "geometry"); anim->setDuration(500); anim->setStartValue(ui->balanceLabel->geometry()); anim->setEndValue(ui->balanceLabel->geometry().adjusted(0, -20, 0, -20)); anim->start(QAbstractAnimation::DeleteWhenStopped);

5.4 扩展硬件交互:对接真实售货机(毕业设计升华点)

若学校实验室有串口售货机模块,可扩展以下功能:

  • 串口通信模块:新增SerialController类,用QSerialPort发送OPEN_DOOR_CMD指令;
  • 状态监控:售货机返回STOCK_LEVEL:COKE=19时,自动调用GoodTableModel::updateStock()同步库存;
  • 故障报警:监听串口超时,弹出QMessageBox::critical()提示“货道堵塞,请联系管理员”。

此扩展能让项目从“软件模拟”跃升为“软硬协同”,极大提升答辩竞争力。

5.5 代码质量提升:为团队协作铺路

当前代码缺乏单元测试,可引入Qt Test框架:

// test_dbhandler.cpp void TestDbHandler::testPurchaseWithInsufficientBalance() { DbHandler *handler = DbHandler::instance(); PurchaseRequest req{1, 2, 100.0}; // 用户1买商品2,价格100元 QVERIFY(!handler->processPurchase(req)); // 应返回false QCOMPARE(handler->getUserBalance(1), 500.0); // 余额不变 }

.pro文件中添加:

QT += testlib CONFIG += c++11 HEADERS += test_dbhandler.h SOURCES += test_dbhandler.cpp

运行./VendingSystem_client -test即可执行测试。覆盖率达80%以上,是优秀毕业设计的硬指标。

6. 实战心得与避坑指南:那些文档不会写的血泪经验

6.1 关于SQLite线程安全的真相

官方文档说“SQLite是线程安全的”,但这是有条件的。我在调试时遇到过诡异问题:服务端在QThread中执行processPurchase(),客户端UI线程同时调用getUserCount(),偶尔触发QSqlQuery::exec(): database not open。排查三天才发现——Qt的QSqlDatabase对象不是线程安全的,但QSqlQuery。解决方案是:

  • 每个线程必须调用QSqlDatabase::database("connectionName")获取专属连接;
  • 或更简单:所有数据库操作强制在主线程执行(用QMetaObject::invokeMethod()投递到主线程)。

最终我选择了后者,因为售货机本质是单点设备,没必要为理论上的并发牺牲可维护性。

6.2 Qt Designer的致命陷阱:绝对不要用“Promoted Widgets”

很多教程教你在Designer里把QWidget提升为QTableView,再设置GoodTableModel。这看似方便,实则埋雷:QTableViewmodel()属性在UI加载时被重置为nullptr,导致setModel()失效。正确做法是——所有QTableView保持原生类型,在shop.cppnew QTableView(this)并手动setModel()。虽然多写两行代码,但避免了90%的UI绑定失败问题。

6.3 资源文件(.qrc)的隐藏规则

media.qrc里路径写成images/coke.jpg还是:/images/coke.jpg?答案是:.qrc文件内写相对路径(images/coke.jpg),代码中用:/前缀引用(:/images/coke.jpg。曾因在.qrc里误写:/images/coke.jpg,导致打包后图片全黑——因为Qt把:/当作根目录,实际去:/:/images/coke.jpg找了。

6.4 调试信号槽的终极技巧

connect()看似成功但槽函数不执行,别急着重写。打开Qt Creator的DebuggerThreadsBreakpoints,右键Add BreakpointCatch Signal,输入purchaseCompleted。这样信号一发出就中断,你能清晰看到调用栈,确认是信号未发出,还是槽函数未连接,或是连接到了错误的对象。

6.5 毕业答辩的黄金话术

答辩时老师常问:“这个项目和网上开源项目有什么区别?” 别说“我改了UI”,要说:
“我重构了数据流架构:将传统单体Qt应用拆分为服务端(纯业务逻辑)与客户端(纯UI交互)两个工程,通过信号槽实现松耦合通信。这使得数据库事务处理、库存校验等核心逻辑可独立测试,UI界面可自由更换主题而不影响业务,符合现代桌面应用分层设计规范。”

一句话点出技术深度,比演示十次购买流程更有说服力。

最后分享个小技巧:在README.md里加一行git log --oneline -n 5的输出,展示你真实的迭代过程(如a1b2c3d fix: ad rotation crash on empty db)。这比任何文字描述都更能证明——你真的亲手敲过每一行代码。

本文还有配套的精品资源,点击获取

简介:这个Qt桌面应用实现了完整的饮料自动售货模拟,包含可独立运行的服务端和客户端两个工程。服务端负责数据管理与后台逻辑,客户端提供用户交互界面,全部使用C++和Qt框架开发。系统内置SQLite数据库(UserDatabase.db),统一存储用户信息、商品列表和订单记录,所有数据库操作由dbhandler.cpp封装,支持增删改查及事务处理。界面通过Qt Designer设计,覆盖登录验证、用户注册/编辑、商品浏览与选购、菜单配置、广告展示等核心场景,对应UI文件如login.ui、shop.ui、user_reg.ui、good_edit.ui等均已集成。代码结构清晰,table.cpp和goodtable.h支撑商品表格展示,media.qrc集中管理图片与音效资源,.pro工程文件明确区分双端构建目标。项目附带README.md说明文档,开箱即用,无需网络或远程服务器依赖,适合教学演示、课程设计或Qt GUI开发入门实践。


本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/5 13:45:11

Navicat无限试用终极方案:macOS版14天限制一键解决完整指南

Navicat无限试用终极方案&#xff1a;macOS版14天限制一键解决完整指南 【免费下载链接】navicat_reset_mac navicat mac版无限重置试用期脚本 Navicat Mac Version Unlimited Trial Reset Script 项目地址: https://gitcode.com/gh_mirrors/na/navicat_reset_mac 还在为…

作者头像 李华
网站建设 2026/6/5 13:41:18

LangChain 工具调用机制:从工具定义到完整调用闭环

一:定义工具 1.tool 简单定义 from langchain_core.tools import tooltool def add(a: int, b: int) -> int:"""两数相加。Args:a: 第一个整数b: 第二个整数"""return a bresult add.invoke({"a":1,"b":2})print(resul…

作者头像 李华
网站建设 2026/6/5 13:37:04

Mac音乐格式解密指南:3分钟解锁QQ音乐加密文件播放限制

Mac音乐格式解密指南&#xff1a;3分钟解锁QQ音乐加密文件播放限制 【免费下载链接】QMCDecode QQ音乐QMC格式转换为普通格式(qmcflac转flac&#xff0c;qmc0,qmc3转mp3, mflac,mflac0等转flac)&#xff0c;仅支持macOS&#xff0c;可自动识别到QQ音乐下载目录&#xff0c;默认…

作者头像 李华
网站建设 2026/6/5 13:34:57

ModelSim仿真信号消失?-voptargs=+acc解决generate块内部信号可见性问题

1. 问题背景与核心痛点在FPGA或ASIC设计验证中&#xff0c;ModelSim/QuestaSim这类仿真器是我们工程师的“老伙计”。它速度快&#xff0c;功能全&#xff0c;但有时候也像一位固执的老师傅&#xff0c;总想帮你“优化”掉一些它认为不必要的东西&#xff0c;结果反而给我们调试…

作者头像 李华
网站建设 2026/6/5 13:33:41

告别喜马拉雅VIP音频无法下载的烦恼:XMly-Downloader-Qt5使用全攻略

告别喜马拉雅VIP音频无法下载的烦恼&#xff1a;XMly-Downloader-Qt5使用全攻略 【免费下载链接】xmly-downloader-qt5 喜马拉雅FM专辑下载器. 支持VIP与付费专辑. 使用GoQt5编写(Not Qt Binding). 项目地址: https://gitcode.com/gh_mirrors/xm/xmly-downloader-qt5 你…

作者头像 李华