news 2026/9/16 9:40:42

Qt/C++超市管理系统开发实战:从数据库设计到事务处理完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt/C++超市管理系统开发实战:从数据库设计到事务处理完整指南

简介:基于QT的C++超市信息管理系统源码与配套数据库打包为zip,面向正在做课程设计、毕业设计或想学习QT界面开发与数据库编程的读者,可直接复用完整项目逻辑,减少从零搭建的工作量。包内共94个文件,压缩包约105.86MB;其中12个cpp与4个h组成核心业务源码,2个ui文件对应登录与主窗口界面,1个sql是数据库初始化脚本,29个dll和3个exe则用于免安装快速运行,另有qm翻译文件、png图标和qrc资源文件等。项目代码完整,下载后无需修改即可直接运行,自带可执行程序和数据库,便于演示效果;同时目录区分debug与release,还保留了Makefile和pro工程文件,适合对照学习QT的信号槽、界面布局、样式表及SQL增删改查等关键点。目前已有559人学习/下载,适合需要快速获取可用课设方案或参考QT项目结构的开发者。

1. 为什么课程设计都爱选“超市信息管理系统”

如果你打开过任意一个课程设计题目库,会发现“超市信息管理系统”几乎是 C++/Qt 方向出现频率最高的题目之一。原因不在于题目本身有多新颖,而在于它恰好覆盖了 GUI 开发、数据库设计、业务逻辑分层、增删改查、权限控制、报表统计这些核心考察点,并且数据模型足够直观——商品、供应商、员工、销售单、进货单,每一张表都能和现实业务对上。用 Qt 做界面层,用 SQLite 或 MySQL 做持久化层,再用 C++ 把两者粘起来,正好构成一个能演示、能答辩、能拿高分的闭环。

这篇博文不会去分析某个现成压缩包里的代码,而是顺着这个标题把一套可靠的实现路径讲清楚。你会看到完整的表结构设计、Qt 5.15 下的数据库连接方式、购物车与结算的核心逻辑、TableView 的绑定技巧,以及那些在答辩时容易被追问的细节:事务、外键、库存一致性、SQL 注入防护。哪怕你是第一次接触 Qt 的数据库编程,按这个顺序写,也能在两周内跑出一个结构完整、能演示、经得起追问的课程设计。

2. 先立框架:Qt 项目结构与分层设计

2.1 为什么不要把所有代码塞进 MainWindow

不少初学者会犯一个典型错误:把数据库查询、界面刷新、业务判断全部写在 MainWindow 的槽函数里。这样做在功能少的时候没问题,一旦加入进货、销售、会员、统计四个模块,MainWindow.cpp 堆到两千行很正常,编译没问题,但调试和答辩都很痛苦。

我一般会按三层来组织代码结构,这也是课程设计答辩时加分的地方:

  • 界面层(Views):每个业务模块一个窗口类,比如SaleWidgetStockWidgetGoodsWidget
  • 数据访问层(DAO):封装所有 SQL 语句,对外只暴露queryGoodsByBarcode()insertSaleOrder()这类接口
  • 业务逻辑层(Service):处理库存扣减、金额计算、流水号生成

这样做最直接的好处是:界面代码里不出现任何INSERTSELECT字符串,数据库结构变更时只需要改 DAO 层。答辩时老师问“如果 SQLite 换成 MySQL 要改什么地方”,你直接回答“只需要改数据库访问层的连接部分”,这就是一个高分的回答点。

2.2 Qt Creator 中创建项目的基本步骤

假设你已经在 Qt 官网上完成了 Qt 5.15.2 与 Qt Creator 的安装,打开 Creator 后按如下顺序创建项目:

# 注意:以下路径和命令仅适用于 Qt 5.15 及以上版本 # 这不是 shell 命令,而是 Qt Creator 中的图形化操作路径 # 1. File -> New File or Project -> Application -> Qt Widgets Application # 2. 填写项目名: SuperMarket(不要用中文项目名,Qt Creator 对中文路径支持不友好) # 3. Build System 选择 qmake 或 CMake,二选一即可 # 4. Kit 选择你安装的编译器,Windows 上建议选择 MinGW 64-bit 或 MSVC 2019 64-bit

创建完成后,在.pro文件(如果使用 qmake)或CMakeLists.txt(如果使用 CMake)中加上 QT += sql。这一步是很多新手卡住的第一关:明明代码里写了#include <QSqlDatabase>,编译却报找不到头文件,大概率就是这一步漏了。

不同构建系统的写法如下。这是最容易踩的第一个坑,qmake 是 Qt 传统的构建工具,写法和 CMake 完全不同:

# 如果使用 qmake,在 .pro 文件中加入以下内容 QT += core gui sql # 如果涉及网络模块,可以加上 network # QT += core gui sql network
# 如果使用 CMake,在 CMakeLists.txt 中找到 find_package 那行 # 把它修改为: find_package(Qt5 COMPONENTS Core Gui Widgets Sql REQUIRED) # 然后在 target_link_libraries 中加上 Qt5::Sql target_link_libraries(SuperMarket PRIVATE Qt5::Widgets Qt5::Sql )

这个模块声明的作用是告诉编译器和链接器,这个项目需要使用 Qt 的 SQL 相关类库。没有它,代码里所有 QSqlDatabase、QSqlQuery 相关的调用都会因为找不到符号而编译失败。建议先写一个临时窗口验证连接成功再开始建表,避免后期排查范围过大。

3. 数据库层的核心设计:表结构、连接与初始化

3.1 商品表、员工表、销售表的主键与外键设计

数据库是整个系统的心脏。课程设计里用 SQLite 就够了,它不需要单独安装数据库服务端,一个文件搞定,方便拷贝演示。但如果老师要求用 MySQL,连接部分只有连接参数不同,SQL 语句基本可以复用。下面这组表结构是按“能支撑完整业务闭环”的标准设计的,比网上常见的单表 demo 要有说服力得多。在 SQLite 中执行以下建表语句:

-- 超市信息管理系统核心表结构 -- 使用 SQLite3 语法,兼容 MySQL 的 InnoDB 引擎 -- 商品表:存储商品基本信息 CREATE TABLE goods ( goods_id INTEGER PRIMARY KEY AUTOINCREMENT, -- 商品ID,自增主键 barcode VARCHAR(20) UNIQUE NOT NULL, -- 条形码,唯一索引 goods_name VARCHAR(100) NOT NULL, -- 商品名称 category VARCHAR(50), -- 商品分类,如饮料、零食 price DECIMAL(10, 2) NOT NULL DEFAULT 0, -- 销售单价,保留两位小数 stock INTEGER NOT NULL DEFAULT 0, -- 当前库存数量 supplier_id INTEGER, -- 供应商ID,外键 FOREIGN KEY (supplier_id) REFERENCES supplier(supplier_id) ); -- 供应商表:与商品表形成一对多关系 CREATE TABLE supplier ( supplier_id INTEGER PRIMARY KEY AUTOINCREMENT, supplier_name VARCHAR(100) NOT NULL, contact VARCHAR(50), -- 联系人 phone VARCHAR(20) -- 联系电话 ); -- 员工表:用于登录验证 CREATE TABLE employee ( emp_id INTEGER PRIMARY KEY AUTOINCREMENT, emp_no VARCHAR(20) UNIQUE NOT NULL, -- 工号,登录账号 emp_name VARCHAR(50) NOT NULL, -- 姓名 password VARCHAR(64) NOT NULL, -- 密码,存 SHA256 而非明文 role VARCHAR(20) DEFAULT 'cashier' -- 角色:admin / cashier ); -- 销售主表:一次结账对应一条记录 CREATE TABLE sale_order ( order_id INTEGER PRIMARY KEY AUTOINCREMENT, order_no VARCHAR(30) UNIQUE NOT NULL, -- 订单号,如 20250616001 emp_id INTEGER NOT NULL, -- 操作员工ID total_amount DECIMAL(10, 2) NOT NULL, -- 订单总金额 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, -- 下单时间 FOREIGN KEY (emp_id) REFERENCES employee(emp_id) ); -- 销售明细表:一笔销售中的每种商品各占一行 CREATE TABLE sale_detail ( detail_id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, -- 主表 ID goods_id INTEGER NOT NULL, -- 商品 ID quantity INTEGER NOT NULL CHECK(quantity > 0), -- 购买数量 unit_price DECIMAL(10, 2) NOT NULL, -- 成交单价,可能不等于原价 subtotal DECIMAL(10, 2) NOT NULL, -- 小计金额 FOREIGN KEY (order_id) REFERENCES sale_order(order_id), FOREIGN KEY (goods_id) REFERENCES goods(goods_id) ); -- 为常用查询字段建立索引,避免全表扫描 CREATE INDEX idx_goods_barcode ON goods(barcode); CREATE INDEX idx_sale_order_time ON sale_order(create_time);

这里有 4 个值得在答辩时主动讲解的设计点。第一,商品表与供应商表通过supplier_id外键关联,避免了在商品表里重复存供应商名称,这叫范式化。第二,销售主表和销售明细表分开,是为了保证一次购买多件商品时只生成一个订单号,总金额放在主表,明细放在子表,通过order_id关联。第三,CHECK(quantity > 0)约束防止负数和零数量的异常订单。第四,password只存哈希值而不是明文,这是安全意识的体现。

3.2 在 Qt 中用 QSqlDatabase 连接数据库

表结构确定后,接着写数据库连接代码。SQLite 和 MySQL 的连接区别其实很小,这里把两者都写出来,方便你对比。数据库连接的核心代码在 Qt 中写法如下,我们需要创建一个独立的连接管理类。Qt 的 QSqlDatabase 是一个连接池管理器,不是单一连接对象,这一点和很多传统数据库 API 不一样。addDatabase相当于注册一种数据库类型,需要传入连接名作为唯一标识:

// dbmanager.h #ifndef DBMANAGER_H #define DBMANAGER_H #include <QSqlDatabase> #include <QSqlQuery> #include <QSqlError> #include <QVariant> #include <QCryptographicHash> class DbManager { public: static DbManager& instance(); // 单例模式,全局只保留一个数据库连接 // 初始化数据库连接并建表,在 main() 中调用一次 bool initDatabase(const QString& dbPath); // 验证登录,返回 true 表示账号密码正确 bool login(const QString& empNo, const QString& password); // 按条形码查询商品,返回查询到的行数 QSqlQuery queryGoodsByBarcode(const QString& barcode); // 获取当前库存量 int getStock(int goodsId); private: DbManager() = default; DbManager(const DbManager&) = delete; DbManager& operator=(const DbManager&) = delete; }; #endif // DBMANAGER_H

这里使用单例模式的原因:整个项目只需要一个数据库连接,避免多处创建连接导致 SQLite 文件被多个连接同时打开而出现“database is locked”错误。实际的原生数据库连接代码在 Qt 中写法如下,这是连接 SQLite 的标准姿势,QSqlDatabase::addDatabase每次调用都会注册一个新的连接名,同样名字不能重复注册两次:

// dbmanager.cpp #include "dbmanager.h" #include <QDebug> #include <QFile> bool DbManager::initDatabase(const QString& dbPath) { // 1. 检查数据库文件是否存在,不存在则后续代码会自动创建 bool isNew = !QFile::exists(dbPath); // 2. 添加 SQLite 数据库连接,连接名为 "main_connection" // 如果连接名已存在,先删除再创建 if (QSqlDatabase::contains("main_connection")) { QSqlDatabase::removeDatabase("main_connection"); } QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "main_connection"); db.setDatabaseName(dbPath); if (!db.open()) { qDebug() << "数据库打开失败:" << db.lastError().text(); return false; } // 3. 如果是新建文件,先创建表结构并插入默认管理员账号 if (isNew) { QSqlQuery query(db); // 指定在 main_connection 上执行 query.exec("CREATE TABLE ..."); // 建表语句,完整 DDL 见 3.1 节 query.exec("INSERT INTO employee (emp_no, emp_name, password, role) VALUES ('admin', '系统管理员', ?, 'admin')"); // 由于 SQLite 不支持参数化语句中直接绑定密码哈希,先绑定再执行 query.addBindValue(QString(QCryptographicHash::hash("123456", QCryptographicHash::Sha256).toHex())); query.exec(); } return true; }

连接参数说明:QSqlDatabase::addDatabase第一个参数是驱动名,SQLite 对应QSQLITE,MySQL 对应QMYSQL。第二参数是连接名,在多个连接并存时用来区分。setDatabaseName对于 SQLite 是数据库文件的路径,对于 MySQL 是数据库名。如果切换 MySQL,需要额外调用setHostNamesetPortsetUserNamesetPassword四个方法。查询代码中的addBindValue是参数化绑定,这是防止 SQL 注入的标准做法。query.exec()在此之前已经把预编译语句准备完毕,绑定值再执行。

一个常见问题:为什么密码要用QCryptographicHash::hash而不是直接存明文?因为数据库文件在演示时会拷贝给老师,如果里面有明文密码,任何拿到文件的人都能看到所有员工的账号密码。哈希加密后,即使数据库文件泄露,密码也无法还原。

3.3 登录验证的完整实现与 SQL 注入防护

登录功能是系统运行时的第一个交互界面。实现思路:用户在登录窗口输入工号和密码,程序把密码做 SHA256 哈希后,与数据库中的哈希值比对。验证的核心 SQL 语句要用参数化查询,这是课程设计中必考的一个安全点。以下是登录验证的完成解法,注意和普通字符串拼接的区别:

bool DbManager::login(const QString& empNo, const QString& password) { // 1. 将输入的密码进行哈希,与数据库存的哈希值比较 QByteArray hash = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256); QString hashedPwd = QString(hash.toHex()); // 2. 从连接池中取出已建立的连接 QSqlDatabase db = QSqlDatabase::database("main_connection"); QSqlQuery query(db); // 3. 使用预编译语句 + 占位符,禁止拼 SQL 字符串 query.prepare("SELECT emp_id, emp_name, role FROM employee " "WHERE emp_no = ? AND password = ?"); query.addBindValue(empNo); query.addBindValue(hashedPwd); if (!query.exec()) { qDebug() << "登录查询执行失败:" << query.lastError().text(); return false; } return query.next(); // 有结果则登录成功 }

这段代码的精华在prepareaddBindValue组合。如果写成QString sql = "SELECT ... WHERE emp_no = '" + empNo + "'",用户输入' OR '1'='1就能在不知道密码的情况下登录系统。参数化绑定让数据库引擎把用户输入当作单纯的数值处理,不会解析成 SQL 语句的一部分,这是所有数据库编程必须养成的习惯。

登录成功后,把emp_idemp_namerole保存到全局单例或主窗口成员变量中,后续所有的销售记录、进货记录都会用到当前操作员工的信息。这里不建议把密码哈希也存下来,只需要保留 ID 和姓名。

4. 核心业务编码:从商品查询到购物车结算

4.1 用 QTableView + QSqlQueryModel 展示商品列表

商品管理是最基础的模块,也最容易写难看。你需要一个表格展示所有商品,支持按名称或条形码搜索,双击某一行可以把商品加入进货单或销售购物车。Qt 提供了 QTableView 作为表格视图,QSqlQueryModel 作为数据模型,两者通过 setModel 绑定。使用 QSqlQueryModel 的好处是它直接执行 SQL 并把结果映射到表格,不需要手动逐行 setItem,写法像这样:

// goods_widget.cpp 的关键部分,展示了 QSqlQueryModel 的用法 #include <QSqlQueryModel> #include <QTableView> // ... 在构造函数中初始化的完整流程 QSqlTableModel* model = new QSqlTableModel(this, QSqlDatabase::database("main_connection")); model->setTable("goods"); // 指定操作 goods 表 model->setEditStrategy(QSqlTableModel::OnManualSubmit); // 手动提交,防误操作 model->select(); // 执行 SELECT * FROM goods // 设置表头中文显示,这里用横向表头索引 model->setHeaderData(0, Qt::Horizontal, "商品ID"); model->setHeaderData(1, Qt::Horizontal, "条形码"); model->setHeaderData(2, Qt::Horizontal, "商品名称"); model->setHeaderData(3, Qt::Horizontal, "分类"); model->setHeaderData(4, Qt::Horizontal, "单价"); model->setHeaderData(5, Qt::Horizontal, "库存"); // 将模型绑定到界面的 QTableView 控件 ui->tableView->setModel(model); ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows); // 整行选中 ui->tableView->setSelectionMode(QAbstractItemView::SingleSelection); // 单选 ui->tableView->horizontalHeader()->setStretchLastSection(true); // 最后一列自动拉伸 ui->tableView->setAlternatingRowColors(true); // 隔行变色,提升可读性

关键参数说明:setTable("goods")告诉模型要操作哪张表;setEditStrategy(QSqlTableModel::OnManualSubmit)表示修改只在内存中生效,必须调用submitAll()才写回数据库,避免误改;select()才是真正执行查询的时刻。setSelectionBehavior(SelectRows)让用户单击任意单元格时选中的是整行,这在后续获取当前选中行商品数据时会省很多事。

如果要按关键字过滤,在select()之前调用model->setFilter("goods_name LIKE '%" + keyword + "%'")。注意这里的数据来自用户输入,不能做参数化绑定,需要先用QRegularExpression过滤特殊字符,只保留中英文和数字。

4.2 购物车的数据结构与数量加减逻辑

销售模块是整个系统中最能体现设计功力的部分。每次扫描一个商品条形码,本质上就是往购物车容器中添加一条记录。购物车用 QMap 管理比较合适,键是商品 ID,值是封装了商品信息和购买数量的购物车条目结构体。这里定义结构体和购物车操作的代码如下:

// cart_item.h // 购物车条目结构体 struct CartItem { int goodsId; QString barcode; QString goodsName; double price; int quantity; }; // 购物车槽函数:点击“加购”按钮时的核心逻辑 void SaleWidget::addToCart(int currentRow) { // 1. 确认当前表格中选中了有效行 if (currentRow < 0) { QMessageBox::warning(this, "提示", "请先在商品列表中选择一个商品"); return; } // 2. 从 QSqlTableModel 中取出该行的数据 QSqlTableModel* model = qobject_cast<QSqlTableModel*>(ui->goodsTable->model()); int goodsId = model->record(currentRow).value("goods_id").toInt(); QString barcode = model->record(currentRow).value("barcode").toString(); QString name = model->record(currentRow).value("goods_name").toString(); double price = model->record(currentRow).value("price").toDouble(); // 3. 若购物车中已有相同商品,数量加一;否则新增条目 if (m_cartMap.contains(goodsId)) { m_cartMap[goodsId].quantity += 1; } else { CartItem item; item.goodsId = goodsId; item.barcode = barcode; item.goodsName = name; item.price = price; item.quantity = 1; m_cartMap.insert(goodsId, item); } // 4. 刷新右侧购物车表格并更新总金额 refreshCartView(); updateTotalAmount(); }

购物车的显示用 QTableWidget 而不是 QSqlQueryModel,因为购物车是国家在内存中临时组装的数据,没有对应的数据库表。refreshCartView需要清空表格再逐行插入,这里注意setRowCount(0)后要重新setRowCount(m_cartMap.size())。总金额的更新要在每次数量变动后执行,核心就是把整个购物车遍历一遍累加price * quantity

4.3 结账的事务处理与库存扣减

结账是整个系统最容易出现数据不一致的环节。想象一个场景:一个订单包含三件商品,第一件扣库存成功,第二件库存不足,此时如果不做任何处理,数据库里就会出现“销售明细已生成但库存只扣了一半”的问题。解决方法是使用数据库事务,要么全部成功,要么全部回滚。

Qt 的 QSqlDatabase 提供了transaction()commit()/rollback()方法。结账的完整代码如下,这段代码是课程设计的核心,也是答辩时大概率会被要求现场解释的部分:

bool SaleWidget::checkout() { QSqlDatabase db = QSqlDatabase::database("main_connection"); // 1. 开启事务 if (!db.transaction()) { QMessageBox::critical(this, "错误", "无法开启事务"); return false; } QSqlQuery query(db); // 2. 生成订单号: 时间戳 + 随机数 QString orderNo = QDateTime::currentDateTime().toString("yyyyMMddhhmmss"); orderNo += QString::number(QRandomGenerator::global()->bounded(100, 999)); // 3. 插入销售主表 query.prepare("INSERT INTO sale_order (order_no, emp_id, total_amount) " "VALUES (?, ?, ?)"); query.addBindValue(orderNo); query.addBindValue(m_currentEmpId); query.addBindValue(m_totalAmount); if (!query.exec()) { db.rollback(); return false; } // 4. 获取刚插入订单的自增 ID int orderId = query.lastInsertId().toInt(); // 5. 遍历购物车,逐条插入明细并扣减库存 QMapIterator<int, CartItem> it(m_cartMap); while (it.hasNext()) { it.next(); const CartItem& item = it.value(); // 检查库存,不足则直接回滚 int stock = getStock(item.goodsId); if (stock < item.quantity) { QMessageBox::warning(this, "库存不足", "商品 [" + item.goodsName + "] 库存不足"); db.rollback(); // 回滚到刚开启事务时的状态 return false; } // 插入销售明细 query.prepare("INSERT INTO sale_detail (order_id, goods_id, quantity, unit_price, subtotal) " "VALUES (?, ?, ?, ?, ?)"); query.addBindValue(orderId); query.addBindValue(item.goodsId); query.addBindValue(item.quantity); query.addBindValue(item.price); query.addBindValue(item.price * item.quantity); if (!query.exec()) { db.rollback(); return false; } // 扣减库存 query.prepare("UPDATE goods SET stock = stock - ? WHERE goods_id = ?"); query.addBindValue(item.quantity); query.addBindValue(item.goodsId); if (!query.exec()) { db.rollback(); return false; } } // 6. 所有操作成功,提交事务 if (!db.commit()) { db.rollback(); return false; } m_cartMap.clear(); refreshCartView(); QMessageBox::information(this, "成功", "结算完成,订单号: " + orderNo); return true; }

事务是这段代码的灵魂。如果没有第 1 步的transaction()和第 6 步的commit(),当第二步插入明细失败时,前面的主表记录和库存扣减已经永久生效,就会留下脏数据。第 5 步循环中的每一次query.exec()失败后都必须db.rollback(),把整个事务恢复到开启前的状态。注意query.lastInsertId()获取的是上次 INSERT 自增 ID,在 SQLite 和 MySQL 中语法一致。

库存检查是一个典型的“先查后改”逻辑,在并发环境下会有竞态问题。课程设计阶段不需要引入行锁和乐观锁,但要在答辩时能提一句:如果使用 MySQL,可以在 UPDATE 语句中加上WHERE stock >= ?条件来判断影响行数,由数据库原子地完成检查与扣减,这是更严谨的写法。

5. 进货模块与统计报表:补全系统闭环

5.1 进货单的设计与库存增加

销售模块处理出库,进货模块处理入库,两者合在一起才算闭环。进货模块的表结构和销售类似,也需要主表 + 明细表的结构。由于篇幅有限,这里给出进货主表和明细表的简化 DDL,你需要根据业务需求自行补全字段。SQL 语句同样遵循参数化原则,后续的 Qt 代码与销售结算几乎一样,区别只在于 UPDATE 语句的方向:

-- 进货主表 CREATE TABLE stock_in_order ( order_id INTEGER PRIMARY KEY AUTOINCREMENT, order_no VARCHAR(30) UNIQUE NOT NULL, supplier_id INTEGER, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (supplier_id) REFERENCES supplier(supplier_id) ); -- 进货明细表 CREATE TABLE stock_in_detail ( detail_id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER, goods_id INTEGER, quantity INTEGER, purchase_price DECIMAL(10, 2), FOREIGN KEY (order_id) REFERENCES stock_in_order(order_id), FOREIGN KEY (goods_id) REFERENCES goods(goods_id) );

进货模块的 UI 可以复刻销售模块:左边是商品列表,中间是进货数量输入框,右边是本次进货清单。提交时也要用事务,但不需要检查库存(进货只会增加库存),只需要注意进货价和销售价的分离:商品表中的price是销售价,进货明细表中额外记录purchase_price进货价。这样后续统计利润时,才有足够的数据支撑。

由于进货模块和销售模块共享商品表,界面上需要加一个权限控制:只有 admin 角色能进入进货页面。在 Qt 中实现权限控制最简单的方法是创建不同的 QWidget 页面,在showEvent中判断角色字段。菜单栏中直接屏蔽非管理员入口也可以,但仅屏蔽界面不够,更重要的是在调用进货相关的 DAO 方法入口处再校验一次角色,防止通过调试工具直接调用函数绕过去。

5.2 按日期统计销售额的三种写法与参数说明

统计报表是拿高分的另一块内容。最基础的需求是:输入起止日期,显示这段时间的总销售额,并按商品分类汇总。这个查询主要涉及三张表的联合查询,SQL 的书写顺序有讲究:先做连接缩小数据范围,再做分组聚合。报表相关的核心 SQL 如下,可以直接放进 QSqlQuery 的 prepare 中执行:

-- 按商品分类统计某时间段内的销售额 -- 注意: 这里使用了 strftime 函数,SQLite 专用;MySQL 中用 DATE() 替代 SELECT g.category, -- 商品分类 SUM(sd.subtotal) AS total_sales, -- 销售额总和 COUNT(DISTINCT so.order_no) AS order_count -- 订单笔数 FROM sale_detail sd JOIN sale_order so ON sd.order_id = so.order_id JOIN goods g ON sd.goods_id = g.goods_id WHERE so.create_time >= ? AND so.create_time <= ? GROUP BY g.category ORDER BY total_sales DESC;

WHERE 条件中的两个问号需要在 Qt 代码里用QDateTime类型绑定,注意 SQLite 中日期时间比较用的是字符串比较,格式必须统一为yyyy-MM-dd HH:mm:ssDATE(so.create_time)是 SQLite 的一个日期处理函数,它会把2026-06-16 09:30:00截断为2026-06-16,这样就能只按天来分组统计数据。

报表展示用 QTableView 绑定 QSqlQueryModel 即可。要注意 QSqlQueryModel 是只读的,不能编辑,这符合报表页面的定位。为了提升观感,可以在 TableView 中设置表格为只读模式。但要注意,QSqlQueryModel 的数据是执行完 select() 后一次性加载到内存的,数据量大的时候会卡顿;上万条数据时用 QSqlTableModel 配合 setFilter 会更流畅。合理使用范围:课程设计数据量几百条记录,QSqlQueryModel 足够。

6. 最终打包与答辩准备:Release 构建、SQLite 文件管理与防坑

6.1 三种常见的数据库路径处理方式

开发时数据库文件放在项目目录下,但是发布时需要解决路径问题。SQLite 路径写死为./supermarket.db的问题在于,发布版和开发版的工作目录不同,经常出现“数据库文件不存在被自动重建”的诡异现象。我一般处理方式有三种,你可以根据交付场景任选:

第一种是固定路径。把数据库文件放在程序可执行文件所在目录的 data 子目录下。获取方式是用QCoreApplication::applicationDirPath()获取可执行文件目录,然后拼接data/supermarket.db。这种方式最稳妥,也最便于查库调试。

第二种是用户数据目录。用QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)获取系统为用户准备的配置目录,这样数据库不会因为程序被放到 Program Files 而缺失写权限。缺点是用户直接查看数据库文件比较麻烦。

第三种是嵌入资源。把建表 DDL 写进代码里,每次启动时检查数据库是否存在,不存在就去创建一个。对课程设计而言,推荐第三种方式配合QFile::exists判断初始化和初始化完成后写入默认数据。因为老师拷贝你的整个项目文件夹后运行,不需要手动建库,体验最好。

6.2 Qt 程序发布时的依赖库拷贝

从 Qt Creator 的 Release 模式构建成功后,生成的 exe 不能直接拷贝到别的电脑运行,它依赖 Qt 的多个 DLL。手动拷贝容易遗漏,Qt 官方提供了 windeployqt 工具来自动部署。在命令行中运行:

# 进入 Release 构建目录,确认 SuperMarket.exe 存在 cd build-SuperMarket-Desktop_Qt_5_15_2_MinGW_64_bit-Release # 执行部署工具,把依赖的 Qt DLL 拷贝到 exe 所在目录 windeployqt SuperMarket.exe

运行完之后,release 目录下会多出一堆 Qt 相关的 DLL 和 platforms 文件夹。此时再把数据库文件(如果没被自动创建)拷贝到 exe 同目录即可。判断部署是否完整的唯一标准:不带 Qt 环境依赖的电脑直接双击 exe 能正常运行且能打开数据库写入数据。如果报错Qt5Core.dll 找不到,说明 windeployqt 没有成功执行或路径不对;如果报错Driver not loaded,说明 SQLite 驱动插件qsqlite.dll没有复制过来。检查sqldrivers目录,如果缺少该子目录,也可以手动把 Qt 安装目录下的plugins/sqldrivers整个目录复制到 exe 同目录。

6.3 答辩时老师最常追问的 4 个技术细节

答辩十分钟,大概率围绕“这个系统是怎么实现的”展开。四个高频追问点基本都落在本文写过的内容上,这里提前把答法列出来:

第一问:为什么数据库要用外键?答:外键保证了引用完整性,防止插入销售明细时引用一个不存在的商品 ID,同时通过外键可以方便的 JOIN 查询统计报表。第二问:如果两张表的数据不一致,比如 sale_detail 中有一条记录对应的 goods 被删除了,怎么办?答:需要先删明细或设置外键的ON DELETE CASCADE约束,但删除商品涉及历史订单,一般建议逻辑删除(加一个is_deleted字段),不做物理删除。第三问:如何防止 SQL 注入?答:所有用户输入都用参数化绑定,而不是字符串拼接。prepare+addBindValue是 Qt 提供的安全方式。第四问:库存不足的处理策略是什么?答:使用事务,先查询再检查,不满足quantity > 0就回滚。更严格的做法是用UPDATE goods SET stock = stock - ? WHERE stock >= ?由数据库保证原子性。

最后一个实用技巧:为了防止演示时数据库里的测试数据影响展示效果,在 main 函数中可以加一个--demo命令行参数,检测到后自动清空商品表和销售表并插入若干条演示数据。这个特性在答辩前特别有用,可以让老师在评分时看到完整、干净的数据展示。可以把它放在程序启动时可选,也可以做在菜单栏的“初始化数据”功能中。

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

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

免费AI图片转视频实战指南:运动建模与工程化落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 9:40:02

数据中心动力环境监控系统与智能控制终端:监测与控制一

数据中心运行依赖大量设备&#xff0c;供配电、制冷、温湿度、漏水、安防等环节相互关联&#xff0c;任何一个环节异常都可能影响业务稳定。动力环境监控系统的职责&#xff0c;是把这些设备的运行状态和环境参数统一采集、集中展示&#xff0c;发现异常及时告警&#xff1b;但…

作者头像 李华
网站建设 2026/9/16 9:38:28

校园外卖系统架构设计与智能配送算法解析

1. 校园外卖软件的市场需求分析校园外卖软件作为近年来快速崛起的细分领域&#xff0c;其核心价值在于解决了高校师生群体的"最后一公里"餐饮配送问题。根据我参与多个高校外卖平台开发的经验&#xff0c;这类软件需要同时满足三个刚性需求&#xff1a;时效性要求&am…

作者头像 李华
网站建设 2026/9/16 9:38:27

51单片机SPI模式读写SD卡:从协议到Proteus仿真实践

简介&#xff1a;一份面向51单片机学习者和嵌入式开发初学者的SD卡读卡器仿真设计资料包&#xff0c;定位在教学实验、课程设计与入门项目场景&#xff0c;解决SD卡通信协议、SPI接口配置及软硬件协同调试中的常见问题。压缩包共19个文件&#xff0c;约203KB&#xff0c;核心内…

作者头像 李华
网站建设 2026/9/16 9:38:00

Lumen:基于FaceMesh与STM32U585的实时人脸光照方向感知系统

1. 项目概述&#xff1a;Lumen不是光通量单位&#xff0c;而是一个跨平台实时人脸光照感知系统“Lumen”这个词在光学里是光通量单位&#xff0c;但在当前嵌入式AI开发圈子里&#xff0c;它正迅速成为一个代号——特指一套基于Arduino UNO Q主控、融合MediaPipe FaceMesh算法轻…

作者头像 李华
网站建设 2026/9/16 9:37:38

硬盘加密密码遗忘的五大解决方案与预防措施

1. 硬盘加密密码遗忘的常见场景与风险分析硬盘加密技术&#xff08;如BitLocker、VeraCrypt&#xff09;在保护数据安全的同时&#xff0c;也带来了密码遗忘的潜在风险。根据实际案例统计&#xff0c;约23%的用户曾因密码问题导致加密数据无法访问。这种情况通常发生在以下几种…

作者头像 李华