资讯动态

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

发布时间:2026/9/16 9:40:51 来源:尧图企业网站定制
简介基于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每个业务模块一个窗口类比如SaleWidget、StockWidget、GoodsWidget数据访问层DAO封装所有 SQL 语句对外只暴露queryGoodsByBarcode()、insertSaleOrder()这类接口业务逻辑层Service处理库存扣减、金额计算、流水号生成这样做最直接的好处是界面代码里不出现任何INSERT或SELECT字符串数据库结构变更时只需要改 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 对应QSQLITEMySQL 对应QMYSQL。第二参数是连接名在多个连接并存时用来区分。setDatabaseName对于 SQLite 是数据库文件的路径对于 MySQL 是数据库名。如果切换 MySQL需要额外调用setHostName、setPort、setUserName、setPassword四个方法。查询代码中的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(); // 有结果则登录成功 }这段代码的精华在prepare加addBindValue组合。如果写成QString sql SELECT ... WHERE emp_no empNo 用户输入 OR 11就能在不知道密码的情况下登录系统。参数化绑定让数据库引擎把用户输入当作单纯的数值处理不会解析成 SQL 语句的一部分这是所有数据库编程必须养成的习惯。登录成功后把emp_id、emp_name、role保存到全局单例或主窗口成员变量中后续所有的销售记录、进货记录都会用到当前操作员工的信息。这里不建议把密码哈希也存下来只需要保留 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_castQSqlTableModel*(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. 遍历购物车逐条插入明细并扣减库存 QMapIteratorint, 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:ss。DATE(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 注入答所有用户输入都用参数化绑定而不是字符串拼接。prepareaddBindValue是 Qt 提供的安全方式。第四问库存不足的处理策略是什么答使用事务先查询再检查不满足quantity 0就回滚。更严格的做法是用UPDATE goods SET stock stock - ? WHERE stock ?由数据库保证原子性。最后一个实用技巧为了防止演示时数据库里的测试数据影响展示效果在 main 函数中可以加一个--demo命令行参数检测到后自动清空商品表和销售表并插入若干条演示数据。这个特性在答辩前特别有用可以让老师在评分时看到完整、干净的数据展示。可以把它放在程序启动时可选也可以做在菜单栏的“初始化数据”功能中。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价