news 2026/9/16 12:28:16

基于C++Qt的超市管理系统:从数据库建模到完整项目交付

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于C++Qt的超市管理系统:从数据库建模到完整项目交付

简介:基于C++与QT开发的超市管理系统完整源码包,面向高校C++课程设计、期末大作业及毕业设计场景,功能覆盖商品资料、销售管理、日常管理等多个业务模块,界面采用QT绘制,美观且交互友好,代码结构清晰并附有注释,便于学习者理解面向对象设计思想与Qt布局技巧。资源共44个文件,以14个cpp源码、13个h头文件、3个ui界面文件为主体,另含PDF/PPT格式的文档说明、演示gif图片及应用图标等辅助资源,压缩包整体仅15.17MB,轻量易部署,下载后按说明简单配置即可运行。系统从源码结构看模块划分合理、耦合度低,适合作为二次开发或课设改进的基础框架。文档说明涵盖系统设计、模块划分和关键代码讲解,帮助读者从界面布局到业务逻辑快速打通完整实现路径;演示动图则直观呈现系统运行效果。已有149人学习下载,无论是期末大作业还是课程设计,均能提供有力支撑,特别适合需要冲刺高分课程设计的同学借鉴参考。

1. 基于C++&QT的超市管理系统:不是把代码跑通,而是把“课程设计”四个字做完整

课程设计截止前一晚还在补功能的场景,不少人都经历过。超市管理系统是C++&Qt课设里出现频率极高的选题,但大多数人交上去的版本只是“能增删改查”,答辩被问两句就问住了。这个题目真正要解决的不只是“写代码”,而是“怎么把源码、文档说明和演示路径配成一套能拿高分的东西”。本文围绕C++&Qt的Widgets框架,从表结构设计、数据库访问封装、界面逻辑拆分,一直讲到文档说明书的写法与发布验证,把手上的半成品补齐成能站住场的完整项目。

适合两类人看:正在做C++&Qt课程设计的学生,以及想快速搭一个桌面进销存骨架的开发者。前置条件不需要多深,Qt能编译即可,数据库用SQLite起步最省事。先说结论:高分项目的分水岭往往不在界面多花哨,而在表设计是否有冗余、代码分层是否清楚、文档是否讲得明白。

2. 先定数据库与Qt工程骨架:超市管理系统的地基是表,不是按钮

2.1 商品表为什么要带 supplier_id 外键,而不是直接存供应商名称

常见的课程设计写法是把商品表设计成“名称、价格、库存、供应商”四个字段,供应商直接存字符串。看起来方便,但答辩时被问到“按供应商统计采购金额怎么做”,就卡住了——因为字符串没法做可靠的关联聚合。规范的进销存建模会把供应商独立成表,商品表里只保存外键。

下面是SQLite的建表脚本,覆盖了登录、供应商、商品、订单四个维度,足够撑起一整份文档说明:

-- 用户表:区分操作员与管理员 CREATE TABLE IF NOT EXISTS user_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role INTEGER NOT NULL DEFAULT 0 -- 0: 操作员 1: 管理员 ); -- 供应商表 CREATE TABLE IF NOT EXISTS supplier ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL UNIQUE, name TEXT NOT NULL ); -- 商品表 CREATE TABLE IF NOT EXISTS goods ( id INTEGER PRIMARY KEY AUTOINCREMENT, barcode TEXT UNIQUE, -- 条码,可为空 name TEXT NOT NULL, spec TEXT, -- 规格:瓶/包/箱 price REAL NOT NULL DEFAULT 0, -- 售价 stock INTEGER NOT NULL DEFAULT 0, -- 库存数量 supplier_id INTEGER, -- 外键 FOREIGN KEY (supplier_id) REFERENCES supplier(id) ); -- 销售订单表与明细表 CREATE TABLE IF NOT EXISTS sale_order ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_no TEXT UNIQUE, -- 订单号 created_at TEXT DEFAULT (datetime('now','localtime')), total_price REAL NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS sale_item ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER, goods_id INTEGER, goods_name TEXT, -- 冗余名称,避免连表查询 goods_price REAL, qty INTEGER NOT NULL, FOREIGN KEY (order_id) REFERENCES sale_order(id), FOREIGN KEY (goods_id) REFERENCES goods(id) );

核心思路是“主表 + 明细表”拆开。头部存订单号、总价,明细每条对应一种商品。这样查“某天卖了多少件可口可乐”只需要在明细表里做一次条件统计;如果只建一张流水大表,数据量一大查询就越来越慢,而且回滚和修改都不方便。

字段类型上,价格要用REAL或DECIMAL,千万别用INTEGER存“分”之外再手工换算,Qt侧显示会很不直观。库存字段加默认值0,取出来永远是有效整数。

2.2 封装一个 DbManager 类,避免每个页面都写 QSqlQuery 连接逻辑

课程设计代码最容易被扣分的地方是每个窗口类里重复写QSqlDatabase::addDatabaseopen。更好的做法是做一个单例数据库管理类,把连接初始化、查询执行、事务提交收拢到一处。

// dbmanager.h #pragma once #include <QSqlDatabase> #include <QSqlQuery> #include <QVariant> class DbManager { public: static DbManager& instance(); bool init(const QString& dbPath); // 连接SQLite QSqlQuery exec(const QString& sql); bool beginTrans(); bool commitTrans(); bool rollbackTrans(); private: DbManager() = default; QSqlDatabase db; }; // dbmanager.cpp DbManager& DbManager::instance() { static DbManager mgr; return mgr; } bool DbManager::init(const QString& dbPath) { db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName(dbPath); if (!db.open()) { return false; } QSqlQuery query; query.exec("PRAGMA foreign_keys = ON"); // 关键:SQLite外键默认关闭 return true; } QSqlQuery DbManager::exec(const QString& sql) { QSqlQuery q(db); q.exec(sql); return q; // 注意:返回后立即检查q.lastError() }

使用时的固定写法是:先init("supermarket.db"),再执行业务SQL。注意PRAGMA foreign_keys = ON必须放在每次新建连接之后,SQLite默认是关闭外键约束的,如果漏了,前文外键设计就形同虚设,删供应商时商品表里残留的supplier_id不会报错。

2.3 Qt工程用 qmake 还是 CMake:课程设计够用就选 qmake

Qt 6 官方默认推荐CMake,但如果你的课设环境是Qt 5.15,qmake的.pro文件明显更短、更不容易写错。课程设计的核心目标是快速出活儿,没有必要引入额外的CMake语法成本。

QT += core gui sql greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = SuperMarket TEMPLATE = app SOURCES += main.cpp \ mainwindow.cpp \ dbmanager.cpp \ logindialog.cpp \ goodsmanager.cpp HEADERS += mainwindow.h \ dbmanager.h \ logindialog.h \ goodsmanager.h

.pro里加了sql模块,这是连接SQLite的前提。如果编译时报"unknown module(s) in QT: sql",多半是安装Qt时没有勾选Qt SQL驱动,重装勾选即可,不用改代码。

3. 核心链路实现:登录、商品管理、收银入库扣减怎么串起来

3.1 登录鉴权:用role字段分流操作员与管理员入口

超市系统的登录很简单:打开程序弹登录框,输入用户名密码后查user_info表。密码在课程设计里可以不加密,但为了答辩有亮点,用QCryptographicHash存SHA-256摘要比明文好得多。

// logindialog.cpp 核心校验函数 bool LoginDialog::verifyLogin(const QString& user, const QString& pass, int& role) { QByteArray hash = QCryptographicHash::hash(pass.toUtf8(), QCryptographicHash::Sha256).toHex(); QSqlQuery q; q.prepare("SELECT role FROM user_info WHERE username = ? AND password_hash = ?"); q.addBindValue(user); q.addBindValue(QString(hash)); if (!q.exec() || !q.next()) { return false; } role = q.value("role").toInt(); return true; }

参数说明:prepare配合addBindValue是为了防止SQL注入,哪怕课设系统不对外网开放,这也是答辩老师最容易追问的点。role返回值用于控制主窗口的菜单显隐——管理员可见“商品价格修改”,操作员只留“收银”。这样做权限分级,比只做一张用户表更有说服力。

3.2 商品管理页:QTableView + QStandardItemModel 动态刷新

商品列表页用QTableView加QStandardItemModel,而不是直接把控件铺出来。核心原因是数据行数动态变化,model/view架构天然适配。

void GoodsManager::refreshGoods() { model->clear(); model->setHorizontalHeaderLabels({"商品ID", "名称", "规格", "售价", "库存", "供应商"}); QSqlQuery q = DbManager::instance().exec( "SELECT g.id, g.name, g.spec, g.price, g.stock, s.name " "FROM goods g LEFT JOIN supplier s ON g.supplier_id = s.id" ); int row = 0; while (q.next()) { for (int col = 0; col < 6; ++col) { QStandardItem* item = new QStandardItem(q.value(col).toString()); if (col == 3 || col == 4) { item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); } model->setItem(row, col, item); // 设置第row行第col列的单元格 } row++; } ui->tableView->setModel(model); }

这段的关键点在于LEFT JOIN:如果某个商品没挂供应商,左连接仍然会返回该商品,只是供应商字段为空。如果写成INNER JOIN,无供应商的商品会从列表消失,这在库存管理里是硬伤——不能因为资料不全就不让这个商品被管理。

model刷新时直接调用refreshGoods()即可,无需setModel两次。新增商品时要记得重新拉一次列表,保持界面和数据库一致。

3.3 收银台:一次事务性下单的三个必写步骤

收银是整个系统里最容易写崩的业务。如果只做“插入一条订单记录”和“更新商品库存”两个独立SQL,中途任何一条失败都会造成库存和流水对不上。正确写法是用事务包起来。

bool MainWindow::createOrder(const QVector<CartItem>& items) { if (items.isEmpty()) return false; DbManager& db = DbManager::instance(); if (!db.beginTrans()) return false; // 第一步:生成订单头,拿到自增id QString orderNo = "SO" + QString::number(QDateTime::currentMSecsSinceEpoch()); QSqlQuery q; q.prepare("INSERT INTO sale_order(order_no, total_price) VALUES(?, 0)"); q.addBindValue(orderNo); if (!q.exec()) { db.rollbackTrans(); return false; } int orderId = q.lastInsertId().toInt(); double total = 0.0; for (const CartItem& it : items) { // 第二步:写明细 QSqlQuery qItem; qItem.prepare("INSERT INTO sale_item(order_id, goods_id, goods_name, goods_price, qty) " "VALUES(?, ?, ?, ?, ?)"); qItem.addBindValue(orderId); qItem.addBindValue(it.goodsId); qItem.addBindValue(it.name); qItem.addBindValue(it.price); qItem.addBindValue(it.qty); if (!qItem.exec()) { db.rollbackTrans(); return false; } // 第三步:扣库存,WHERE里带stock >= 本次数量做乐观锁 QSqlQuery qStock; qStock.prepare("UPDATE goods SET stock = stock - ? WHERE id = ? AND stock >= ?"); qStock.addBindValue(it.qty); qStock.addBindValue(it.goodsId); qStock.addBindValue(it.qty); if (!qStock.exec() || qStock.numRowsAffected() != 1) { db.rollbackTrans(); return false; // 库存不足或商品不存在 } total += it.price * it.qty; } // 回写总价 QSqlQuery qTotal; qTotal.prepare("UPDATE sale_order SET total_price = ? WHERE id = ?"); qTotal.addBindValue(total); qTotal.addBindValue(orderId); if (!qTotal.exec()) { db.rollbackTrans(); return false; } return db.commitTrans(); }

逻辑说明:三步环环相扣,任何一步返回false都立刻rollbackTrans。第13行的stock >= ?是乐观锁式的更新条件,防止两人同时收银导致超卖——课程设计虽是单机版,但这个写法体现的是并发意识。

numRowsAffected() == 1是必须的判断。SQLite的UPDATE即使值没变化也会返回成功,但numRowsAffected在没有匹配行时是0,靠它判断是否真的扣到了货。

4. 高分界面与交互细节:信号槽刷新、卡顿规避和Qt发布坑

4.1 跨窗口刷新库存的三种信号槽姿势

超市系统里,收银窗口开了单,商品管理窗口的库存必须同步变。课程设计常见的做法是“重新打开窗口时加载”,但更顺滑的是自定义信号槽通知刷新。

// goodsmanager.h signals: void stockChanged(); // mainwindow.cpp 收银成功处 emit goodsManager->stockChanged();

emit触发后,在GoodsManager构造函数里connect(this, &MainWindow::stockChanged, this, &GoodsManager::refreshGoods);,一个信号只连一个槽,开销极小。Qt 5的信号槽是类型安全的,编译期就能发现连接错误,比用宏的老式写法更值得在代码里出现。

4.2 两个实战级坑:界面卡死和重复点击

第一个坑是大量数据下的QSqlQuery直接跑在UI线程里。商品几千条时还能忍,但如果文档里写了“支持按日期范围查询销售流水”,全表循环拼接QString就会让界面白屏好几秒。解法是分页查询加LIMIT 500 OFFSET ?,或者查询前弹QProgressDialog,把真正的阻塞模型放到答辩演示里讲清楚。

第二个坑是收银按钮的重复点击。订单提交有事务保护,但双击就一定触发两次commit,生成两笔一模一样的订单。最稳妥的写法是在槽开头设一个bool标志,或者直接在demo演示时避免双击。代码上更习惯的做法是:

pushButton_Checkout->setEnabled(false); if (!createOrder(items)) { ... } pushButton_Checkout->setEnabled(true);

在串行执行中先禁用自己,比防抖计时器可靠得多。

4.3 界面数字格式:显示层和存储层分离

价格和库存从数据库读出来是doubleint,直接toString()会输出"12.3000000001"这种脏数据。必须在显示层统一格式化:

ui->label_TotalPrice->setText(QString::number(total, 'f', 2));

'f', 2代表固定两位小数。库存则先转int再显示。这样存储和显示互不污染,审计时才算有据可查。

5. 文档说明与答辩路线:源码之外的拿分主力

5.1 演示脚本:三分钟讲完却不翻车的路径

一份高分项目,必须有一份“照着点就能演示完”的文档说明。最常见的翻车是演示时才想下一步点哪里。系统交付时把演示脚本写进README或说明书里,路径固定为:

  1. 启动系统,用管理员身份登录,打开“商品管理”,新增两个商品并各入库10件。
  2. 切到收银台,用操作员账号登录,选择商品下单,立刻回商品管理页确认库存已减。
  3. 回到管理员页面,按订单号查询刚才那单,点开明细核对数量金额。

这套流程覆盖了登录鉴权、数据初始化、收银、库存联动和订单查询五个评分点。文档里还要附上截图占位标记:每个页面一张截图,说明“数据表关联”、“刷新原理”和“事务保护”。答辩老师翻文档时,不会只看代码注释,截图配合文字说明是最直观的。

5.2 运维可见性一小节:让文档说明更像工程交付物

课设文档的高分点,在于写了一些“没人要求但显得专业”的内容。比如“数据库文件被删了怎么办”,给一段恢复思路:window环境下SQLite文件就是supermarket.db,先看路径下是否存在;再写清“初始化账号admin/123456,首次登录请修改密码”这样实际可操作的信息。

数据库备份也可以作为亮点:用QDir把supermarket.db复制成带时间戳的文件,放一个“备份”按钮在设置页。这个功能代码不到10行,但文档里写“系统支持手动备份,备份文件存于backup目录”,答辩观感会完全不同。

6. 发布与验证:Qt程序换机运行和库存一致性自查

6.1 用 windeployqt 跑依赖,三步让程序在别的电脑上跑起来

答辩通常要拷到教室电脑上演示,Qt程序不能只拿exe过去——没有开发库跑不起来。用Qt自带的windeployqt工具打包依赖是最常规的路径。命令行进入exe所在目录:

cd /d D:\build-SuperMarket-Desktop_Qt_5_15_2_MinGW_32_bit-Release\release D:\Qt\5.15.2\mingw81_32\bin\windeployqt.exe SuperMarket.exe

参数说明:第一个路径是编译输出目录,第二个是windeployqt的真实路径,版本和编译器必须和构建套件一致。跑完之后目录里多出Qt5Core.dll、Qt5Widgets.dll、Qt5Sql.dll以及platforms目录,再额外把sqldrivers目录里的qsqlite.dll复制到exe同级目录,因为默认发布流程偶尔会漏掉SQL驱动,导致程序在其他机器上提示driver not loaded。一个快速验证是:到release目录把exe双击运行,再试着登录进货,走通就说明发布成功。

6.2 库存一致性自查:一条SQL把账算平

验证系统是否正确,比看界面更可靠的是查数据库。在SQLite里执行下面这条对账查询,看“商品表库存”和“累计销量”之和是否等于初始库存:

SELECT g.name, g.stock AS current_stock, IFNULL(SUM(si.qty), 0) AS sold_qty, g.stock + IFNULL(SUM(si.qty), 0) AS total_check FROM goods g LEFT JOIN sale_item si ON g.id = si.goods_id GROUP BY g.id;

如果total_check不等于初始库存,说明要么初始化数据就是乱的,要么扣减逻辑有漏洞。对课程设计而言,把这条SQL的查询结果截图放进文档“系统测试”一节,比空写“测试通过”四个字扎实得多。排错时如果某些商品总显示负数,检查点回到第3.3节的“UPDATE goods SET stock = stock - ? WHERE id = ? AND stock >= ?”,看条件里的stock >= ?是否真的拦住了超卖——这是整个库存模块最关键的一行代码。

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

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

Unity AI对话系统源码剖析:从状态机到语音交互的工程化实践

简介&#xff1a;一份面向Unity开发者的AI机器人对话系统源码包&#xff0c;适合在Unity3D中实现NPC对话、虚拟角色交互或模拟现实对话场景的开发者参考学习。源码围绕行为树、状态机、C#脚本与对话管理器组织AI逻辑&#xff0c;覆盖从用户输入处理、对话文本解析到回应生成的关…

作者头像 李华
网站建设 2026/9/16 12:22:26

磁位置感应精度实战:AS5134与定制磁芯R7KA8D2KFLCAC协同设计

1. 项目概述&#xff1a;为什么磁位置感应正在从“能用”走向“必须精准可靠”我做工业传感器集成项目快十二年了&#xff0c;从早期用霍尔开关测个大概位置&#xff0c;到现在客户开口就要“0.3角精度、MTBF超10万小时、-40℃到125℃全程无漂移”&#xff0c;中间踩过的坑、换…

作者头像 李华
网站建设 2026/9/16 12:21:01

React Native开发鸿蒙版推箱子游戏实战

1. 项目背景与目标最近在探索React Native的跨平台能力时&#xff0c;我萌生了一个有趣的想法&#xff1a;能否用React Native开发一个能在鸿蒙系统上运行的推箱子游戏&#xff1f;这个经典游戏看似简单&#xff0c;但涉及玩家移动、碰撞检测、状态判断等多个核心机制&#xff…

作者头像 李华
网站建设 2026/9/16 12:20:53

无标题文档整理术:从空白页面到高效知识库的创作方法论

我是那种新建文档之后&#xff0c;能对着“无标题”三个字发呆十分钟的人。时间长了你会发现&#xff0c;“无标题”并不是一个待办事项&#xff0c;也不是一个失职的标记&#xff0c;它是所有创作最诚实、最原始的样子——想法还没成型&#xff0c;方向还没确定&#xff0c;价…

作者头像 李华
网站建设 2026/9/16 12:18:55

MOSS-TTS 纯CPU推理完全实践:无GPU边缘设备部署完整指南

MOSS-TTS 纯CPU推理完全实践&#xff1a;无GPU边缘设备部署完整指南 【免费下载链接】MOSS-TTS An open-source model family for long-form speech, dialogue synthesis, voice design, sound effects, and real-time streaming TTS 项目地址: https://gitcode.com/GitHub_T…

作者头像 李华
网站建设 2026/9/16 12:17:29

西门子828D与840Dsl数控系统PLC编程实战

1. 西门子828D与840Dsl数控系统概述作为一名在数控领域摸爬滚打多年的工程师&#xff0c;我深知西门子828D和840Dsl这两款数控系统在工业自动化领域的重要地位。这两款系统是西门子公司针对中高端机床控制需求开发的数控解决方案&#xff0c;广泛应用于车削、铣削、磨削等各种金…

作者头像 李华