十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于C++与Qt的二手交易平台开发:从数据库设计到GUI实现

基于C++与Qt的二手交易平台开发:从数据库设计到GUI实现 简介基于C的二手物品交易平台设计与实现项目实例面向熟悉C与Qt框架的研发人员及对电商系统开发感兴趣的开发者重在展示高性能、高并发的二手交易系统完整落地过程。资源包共1个文件为docx格式文档压缩包仅102KB包含项目背景、目标意义、挑战与解决方案、特点创新等完整章节并配有代码实现、数据库设计、GUI设计及系统部署优化说明。目前已有62人学习下载。文档从多用户并发处理、数据安全与隐私保护、商品分类检索、交易状态管理、用户信誉体系到支付接口集成均有细致拆解可帮助读者掌握智能推荐、多维度搜索、多支付方式集成等关键技术的实现思路同时结合校园、社区、专业市场等实际场景提供了容器化部署、自动化运维、实时数据处理方面的最佳实践参考适合边读边动手调试深度提升项目开发与架构设计能力。1. 用 C 建一个二手交易平台的正确打开方式当多数人把电商系统跟 Java、Go 绑在一起时一个用 C 开发、以 Qt 做界面、以 MySQL 做存储的二手物品交易平台反而能让业务逻辑、数据库、GUI 三者之间的边界变得具体。这类项目的常见定位是课程设计或综合实训程序要能注册登录、发布商品、检索列表、下单购买并且数据要能持久化。选 C 不是为了追求吞吐量而是它能利用 C 基础语法和指针用法把数据库增删改查、对象生命周期这些零散知识点放进一个看得见、点得动的窗口程序里。本文以 Qt Widgets MySQL 为组合完整走一遍从建表、界面搭建到业务代码的实现路径适合 C 入门之后想由语法练习跨入完整应用的人。2. 数据库设计用户表、商品表、订单表的建表与查询2.1 三张核心表的字段设计与建表SQL二手交易平台最小可用版本只需要三张表user 表保存账号和联系方式item 表保存商品挂售信息orders 表记录一次买卖行为的归属。很多课程设计项目一上来就画 ER 图但 ER 图最终还是要落到具体字段上与其做六张表再把外键线画得密密麻麻不如先把这三个实体的边界定清楚。字段设计做到每个字段不可再分、非主键字段完全依赖主键、不存在传递依赖对课程设计和后续迭代都够用。建表语句以 MySQL 语法写CREATE DATABASE IF NOT EXISTS secondhand_db DEFAULT CHARACTER SET utf8mb4; USE secondhand_db; CREATE TABLE user ( user_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash CHAR(64) NOT NULL, email VARCHAR(100), phone VARCHAR(20), reg_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE item ( item_id INT AUTO_INCREMENT PRIMARY KEY, seller_id INT NOT NULL, title VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL, category VARCHAR(30), status TINYINT NOT NULL DEFAULT 0 COMMENT 0在售 1已预订 2已售出, image_path VARCHAR(255), publish_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_item_seller FOREIGN KEY (seller_id) REFERENCES user(user_id) ) ENGINEInnoDB; CREATE TABLE orders ( order_id INT AUTO_INCREMENT PRIMARY KEY, item_id INT NOT NULL, buyer_id INT NOT NULL, price DECIMAL(10, 2) NOT NULL, order_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待付款 1已付款 2已完成 3已取消, CONSTRAINT fk_order_item FOREIGN KEY (item_id) REFERENCES item(item_id), CONSTRAINT fk_order_buyer FOREIGN KEY (buyer_id) REFERENCES user(user_id) ) ENGINEInnoDB;字段设计上有一个地方值得多解释几句密码不能存明文password_hash 字段用 CHAR(64) 保存 SHA-256 的十六进制结果注册时在 C 里算好哈希再写入登录时把用户输入重新哈希后和库里值比对。这是最基础的安全写法虽然还没有引入 salt但足以避开数据库一旦泄露账号就全部暴露的常见事故。价格字段没有用 float 或 double而是 DECIMAL(10,2)原因很直接float 在表示 0.1、0.2 这类小数时有二进制误差商品价格涉及多次累加和交易对账用浮点类型会在分账和退款时出现尾差。表名用 orders 而不是 order因为 order 是 SQL 的排序保留字item 表的状态用 TINYINT 而不是字符串状态机在代码里用整数判断更高效也方便扩展。所有表都用 InnoDB 引擎外键约束才能生效事务也才有实际意义。2.2 常用检索与增删改查JOIN 查询与预处理语句商品列表页最常出现的需求是“在售商品 卖家昵称”一起展示。如果分开两次查询还需要在 C 代码里做循环拼装不如一次 JOIN 直接得到视图层需要的数据结构。SELECT i.item_id, i.title, i.price, i.category, u.username AS seller_name FROM item i JOIN user u ON i.seller_id u.user_id WHERE i.status 0 AND i.category 数码 ORDER BY i.publish_time DESC LIMIT 20;这条 SQL 的参数含义JOIN 把 item.seller_id 对齐到 user.user_id取出 username 作为卖家名status 0 过滤掉已预订和已售出商品category 做分类筛选publish_time DESC 让新发布的内容排在最前LIMIT 20 限制单页返回行数。写 GUI 界面时可以直接把结果集映射到 QTableView界面层不再需要知道卖家是谁。当订单详情页需要同时展示买家、卖家、商品三项时三表 JOIN 完成SELECT o.order_id, o.status, u1.username AS buyer_name, u2.username AS seller_name, i.title, o.price, o.order_time FROM orders o JOIN user u1 ON o.buyer_id u1.user_id JOIN item i ON o.item_id i.item_id JOIN user u2 ON i.seller_id u2.user_id WHERE o.order_id 1001;在 C 侧执行这类查询时使用 QSqlQuery 的预处理绑定QSqlQuery query(db); query.prepare(SELECT title, price FROM item WHERE item_id ?); query.addBindValue(itemId); if (query.exec() query.next()) { QString title query.value(0).toString(); double price query.value(1).toDouble(); }参数绑定有两个实际收益。第一避免把用户输入用字符串拼接进 SQL单引号、反斜杠这类字符不再破坏语句结构第二MySQL 会复用预处理计划同一条语句频繁执行时省去重复解析开销。C 学习阶段很容易用 QString::arg 直接拼 SQL 并成功运行但遇到中文引号或特殊符号时会偶发抖动或报错正确做法是从一开始就统一走 addBindValue。2.3 索引、外键与事务边界页面浏览、分类检索、订单查询这几类高频操作会让数据库在读取上反复扫描数据。对过滤和排序使用频率最高的列建立索引是这一阶段最有效的性能手段之一。ALTER TABLE item ADD INDEX idx_item_status (status); ALTER TABLE item ADD INDEX idx_item_category (category); ALTER TABLE orders ADD INDEX idx_order_buyer (buyer_id); ALTER TABLE orders ADD INDEX idx_order_item (item_id);索引列服务场景说明item.status在售列表过滤状态值只有 0/1/2区分度低但能覆盖过滤路径item.category分类检索配合 status 使用时性能提升更明显orders.buyer_id我的订单查询高区分度收益最明显orders.item_id订单与商品关联外键列 JOIN 时依赖索引索引不是建得越多越好写入时每多一个索引就多一次 B 树维护状态这种低区分度列索引主要帮助优化器减少扫描范围不能期望有数量级提升。业务上的事务边界放在“下单”这个动作上。一次下单要同时插入订单记录并把商品从在售改为已预订两步要么都成功要么都回滚。事务保证数据一致性但不解决两个人同时抢同一件商品的问题后者需要在查询商品状态时使用 SELECT ... FOR UPDATE 对行加锁这部分放在第 4 章的业务代码里展开。3. 基于 Qt 的 GUI 设计登录、商品列表和信号槽通信3.1 登录窗口布局与密码校验Qt Widgets 做桌面窗口时我习惯用纯代码而不是 .ui 文件搭建界面。.ui 文件在拖拽布局时效率高但讲代码和答辩时很难把界面长什么样和数据怎么绑定放在同一屏说清楚。登录窗口用一个 QDialog垂直排列两个输入框和两组按钮即可。LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent) { QLabel *userLabel new QLabel(QStringLiteral(用户名), this); QLabel *pwdLabel new QLabel(QStringLiteral(密码), this); m_userEdit new QLineEdit(this); m_pwdEdit new QLineEdit(this); m_pwdEdit-setEchoMode(QLineEdit::Password); QPushButton *loginBtn new QPushButton(QStringLiteral(登录), this); QPushButton *registerBtn new QPushButton(QStringLiteral(注册), this); QGridLayout *layout new QGridLayout(this); layout-addWidget(userLabel, 0, 0); layout-addWidget(m_userEdit, 0, 1); layout-addWidget(pwdLabel, 1, 0); layout-addWidget(m_pwdEdit, 1, 1); layout-addWidget(loginBtn, 2, 0); layout-addWidget(registerBtn, 2, 1); setWindowTitle(QStringLiteral(二手物品交易平台登录)); }布局用 QGridLayout 的原因在于它能把标签和输入框对齐到同一纵列窗口拉伸时控件保持相对位置。setEchoMode 把密码框设为掩码输入密码不会明文显示在屏幕上。点击登录按钮后槽函数不直接查库而是把用户名和密码交给独立的 AuthService 处理界面层和数据库层由此分开。校验逻辑如下bool AuthService::checkLogin(const QString username, const QString password, int userId) { QSqlDatabase db QSqlDatabase::database(main_conn); QSqlQuery query(db); query.prepare(SELECT user_id, password_hash FROM user WHERE username ?); query.addBindValue(username); if (!query.exec() || !query.next()) { return false; } QByteArray inputHash QCryptographicHash::hash( password.toUtf8(), QCryptographicHash::Sha256).toHex(); if (inputHash ! query.value(1).toByteArray()) { return false; } userId query.value(0).toInt(); return true; }这里值得注意的点是先把用户输入的密码做 SHA-256再与库中保存的哈希比对数据库里永远不出现明文。QCryptographicHash 是 Qt 自带的密码学哈希接口不需要额外引入第三方库课程设计里使用成本很低。QSqlQuery 的 addBindValue 已经在前一章说明登录场景同样是参数绑定替代字符串拼接。3.2 商品列表的动态刷新与搜索过滤主窗口的列表区域用 QTableView数据模型直接用 QSqlTableModel 绑定 item 表。它的好处是不用自己维护 QStandardItemModel 的每一行setFilter 自动生成 WHERE 条件select() 重新执行查询后视图自动刷新。void MainWindow::initItemTable() { m_itemModel new QSqlTableModel(this); m_itemModel-setTable(item); m_itemModel-setEditStrategy(QSqlTableModel::OnManualSubmit); m_itemModel-setSort(0, Qt::DescendingOrder); m_itemModel-setHeaderData(2, Qt::Horizontal, QStringLiteral(商品名称)); m_itemModel-setHeaderData(3, Qt::Horizontal, QStringLiteral(描述)); m_itemModel-setHeaderData(4, Qt::Horizontal, QStringLiteral(价格)); m_itemModel-setHeaderData(5, Qt::Horizontal, QStringLiteral(分类)); ui-tableView-setModel(m_itemModel); ui-tableView-setColumnHidden(0, true); // item_id ui-tableView-setColumnHidden(1, true); // seller_id ui-tableView-setColumnHidden(7, true); // image_path }setHeaderData 把默认列名替换成中文显示名隐藏 item_id、seller_id 和 image_path 三列用户不需要看到内部主键和文件路径。setEditStrategy 设为 OnManualSubmit 后任何数据改动都要显式提交可以防止用户在表格上双击修改价格。搜索栏由输入框、分类下拉框和搜索按钮组成void MainWindow::setupSearchBar() { m_searchEdit new QLineEdit(this); m_searchEdit-setPlaceholderText(QStringLiteral(输入商品名称或关键字)); m_categoryCombo new QComboBox(this); m_categoryCombo-addItems({QStringLiteral(全部), QStringLiteral(书籍), QStringLiteral(数码), QStringLiteral(家具), QStringLiteral(服饰)}); QPushButton *searchBtn new QPushButton(QStringLiteral(搜索), this); connect(searchBtn, QPushButton::clicked, this, MainWindow::refreshItemList); QHBoxLayout *searchLayout new QHBoxLayout; searchLayout-addWidget(m_searchEdit, 1); searchLayout-addWidget(m_categoryCombo, 0); searchLayout-addWidget(searchBtn, 0); }QHBoxLayout 的 addWidget 第二参数是伸缩因子输入框设置为 1窗口拉宽时输入框占据新增空间下拉框和按钮保持固定宽度。connect 把 searchBtn 的 clicked 信号接到 refreshItemList 槽这是 Qt 最基础的信号槽用法。refreshItemList 实现搜索条件拼装void MainWindow::refreshItemList() { QString keyword m_searchEdit-text().trimmed(); QString category m_categoryCombo-currentText(); QString filter status 0; if (!keyword.isEmpty()) { QString safeKeyword keyword; safeKeyword.replace(, ); filter QStringLiteral( AND (title LIKE %%1% OR description LIKE %%1%)) .arg(safeKeyword); } if (category ! QStringLiteral(全部)) { filter QStringLiteral( AND category %1).arg(category); } m_itemModel-setFilter(filter); m_itemModel-select(); }filter 字符串中的 status 0 表示只展示在售状态。分类列来自后台预设的下拉项不涉及用户自由输入所以直接用 QString::arg 拼接是安全的关键字是用户输入先把单引号替换成两个单引号避免破坏 SQL 语法。LIKE 的 % 是通配符前后各放一个表示包含匹配。提示QSqlTableModel::setFilter 只能传字符串没有参数绑定接口所以关键字过滤必须自己做转义。更复杂的场景建议改用 QSqlQuery 直接查出记录再填充到自定义模型中但课程设计阶段用 setFilter 完全够用。3.3 信号槽的边界界面和业务之间的解耦信号槽让界面层不需要持有业务对象的指针。发布商品对话框只需要在发布成功后发出 itemPublished 信号主窗口关心这个信号就去刷新列表发布对话框甚至不关心监听着是谁。信号发出者信号接收者槽searchBtnclickedMainWindow::refreshItemListPublishDialogitemPublishedMainWindow::refreshItemListloginBtnclickedLoginDialog::onLoginClicked主窗口在构造时连接connect(publishDialog, PublishDialog::itemPublished, this, MainWindow::refreshItemList);这种设计的直接收益在后续维护中体现把 PublishDialog 换成 QML 页面或者后端 API 调用时只要信号名不变主窗口代码不用改。信号槽默认在接收者所属线程执行界面上的耗时操作要考虑阻塞问题课程设计场景下单条 SQL 查询都在毫秒级可以同步执行如果将来上传图片或批量导入再考虑用 QtConcurrent 把耗时任务移到线程池。4. 代码详解发布、下单与订单状态机的实现4.1 发布商品的业务校验与写入逻辑发布商品表单提供标题、描述、价格、分类四个输入项可能再加上图片路径。业务层 ItemService::publishItem 把校验和写库收在一个函数里返回值配合 errorMsg 输出失败原因。struct ItemInfo { int sellerId; QString title; QString description; double price; QString category; QString imagePath; }; bool ItemService::publishItem(const ItemInfo info, QString errorMsg) { if (info.title.trimmed().isEmpty()) { errorMsg QStringLiteral(商品名称不能为空); return false; } if (info.price 0.0) { errorMsg QStringLiteral(价格必须大于 0); return false; } QSqlDatabase db QSqlDatabase::database(main_conn); if (!db.isOpen()) { errorMsg QStringLiteral(数据库连接已断开); return false; } QSqlQuery query(db); query.prepare(INSERT INTO item (seller_id, title, description, price, category, image_path) VALUES (?, ?, ?, ?, ?, ?)); query.addBindValue(info.sellerId); query.addBindValue(info.title.trimmed()); query.addBindValue(info.description.trimmed()); query.addBindValue(info.price); query.addBindValue(info.category); query.addBindValue(info.imagePath); if (!query.exec()) { errorMsg query.lastError().text(); return false; } return true; }可以看到校验规则直接写在 C 层而不是依赖数据库约束。原因有两个数据库报错信息是英文且偏底层界面直接展示给用户不够友好校验前置可以在网络请求发生之前过滤掉明显非法数据缩短无效交互。INSERT 语句用六个 ? 占位符对应六列addBindValue 按顺序绑定执行失败时 errorMsg 拿到的是 MySQL 的原始错误文本方便后续排错。4.2 创建订单的事务边界一次下单跨两张表orders 表新增一条记录item 表把对应商品从在售改为已预订。两步要是一个整体就必须放在同一个数据库事务里。代码如下bool OrderService::createOrder(int itemId, int buyerId, double price, QString errorMsg) { QSqlDatabase db QSqlDatabase::database(main_conn); db.transaction(); QSqlQuery query(db); query.prepare(SELECT status, seller_id FROM item WHERE item_id ? FOR UPDATE); query.addBindValue(itemId); if (!query.exec() || !query.next()) { db.rollback(); errorMsg QStringLiteral(商品不存在); return false; } int currentStatus query.value(0).toInt(); int sellerId query.value(1).toInt(); if (sellerId buyerId) { db.rollback(); errorMsg QStringLiteral(不能购买自己发布的商品); return false; } if (currentStatus ! 0) { db.rollback(); errorMsg QStringLiteral(商品已不处于在售状态); return false; } query.prepare(INSERT INTO orders (item_id, buyer_id, price, order_time, status) VALUES (?, ?, ?, NOW(), 0)); query.addBindValue(itemId); query.addBindValue(buyerId); query.addBindValue(price); if (!query.exec()) { db.rollback(); errorMsg QStringLiteral(订单创建失败); return false; } query.prepare(UPDATE item SET status 1 WHERE item_id ?); query.addBindValue(itemId); if (!query.exec()) { db.rollback(); errorMsg QStringLiteral(商品状态更新失败); return false; } return db.commit(); }SELECT ... FOR UPDATE 对这行商品数据加排他锁另一个买家的并发下单会阻塞在这句查询上直到当前事务提交或回滚。这样即使两个请求同时读到 status 0也只有一个能走到 INSERT 并提交成功另一个在锁释放后读到已更新状态并回滚。transaction 开启后必须保证每个 return 前都有 commit 或 rollback否则连接会被悬挂事务占住。事务里所有 query 用同一个 db 变量确保它们属于同一个数据库会话。4.3 订单状态机的约束订单状态不能随意更改。状态机的作用是限定合法流转方向当前状态允许操作目标状态0 待付款取消订单3 已取消0 待付款付款1 已付款1 已付款确认收货2 已完成2 已完成无-3 已取消无-对应实现bool OrderService::changeOrderStatus(int orderId, int newStatus, QString errorMsg) { static const QMapint, QVectorint allowTransitions { {0, {1, 3}}, {1, {2}}, {2, {}}, {3, {}} }; QSqlDatabase db QSqlDatabase::database(main_conn); QSqlQuery query(db); query.prepare(SELECT status FROM orders WHERE order_id ?); query.addBindValue(orderId); if (!query.exec() || !query.next()) { errorMsg QStringLiteral(订单不存在); return false; } int current query.value(0).toInt(); if (!allowTransitions.value(current).contains(newStatus)) { errorMsg QStringLiteral(订单状态无法从 %1 变更为 %2) .arg(current).arg(newStatus); return false; } query.prepare(UPDATE orders SET status ? WHERE order_id ?); query.addBindValue(newStatus); query.addBindValue(orderId); if (!query.exec()) { errorMsg query.lastError().text(); return false; } return true; }allowTransitions 用静态 QMap 在进程内只初始化一次。当前状态的合法目标全都列在 value 数组里非法流转直接返回 false 并附上错误说明界面可以把 errorMsg 弹给用户。这套写法把状态管理集中在一个函数里后续加退款、申诉状态时只改映射表不需要散落到各处业务代码。4.4 Qt 对象生命周期与指针用法Qt 项目里一个容易被新手写崩的地方是 QObject 生命周期。像 LoginDialog、MainWindow 这类继承 QObject 的类可以在构造时指定 parent父对象析构时由 Qt 统一 delete 子对象不需要手动 new 和 delete 配对。指针用法 C 课程里强调的谁 new 谁释放在 Qt 里变成谁指定 parent 谁托管。PublishDialog *dlg new PublishDialog(m_userId, this); connect(dlg, PublishDialog::itemPublished, this, MainWindow::refreshItemList); dlg-exec();这里的 this 作为 parent 传入对话框关闭时不会被 delete因为 MainWindow 还持有它MainWindow 销毁时Qt 会沿着对象树自动清理。如果去掉 thisnew 出来的对象就裸露在堆上必须自己在析构函数里 delete。初次接触 Qt 的人最容易混淆 exec() 和 show()exec() 阻塞进入模态循环对话框关闭后内存对象仍然存在这也是上面代码能直接 connect 的原因。5. 构建、竞态排查与交付前验证5.1 CMake 配置 Qt 与 MySQL 链接项目使用 CMake 组织构建关键是开启 AUTOMOC。Qt 的信号槽依赖元对象编译器CMake 的 AUTOMOC 会在编译前自动对含 Q_OBJECT 的头文件执行 moc遗漏这一步会看到大量连接错误。cmake_minimum_required(VERSION 3.16) project(SecondHandTrade) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) find_package(Qt5 COMPONENTS Widgets Sql REQUIRED) add_executable(secondhand_app main.cpp MainWindow.cpp LoginDialog.cpp PublishDialog.cpp services/AuthService.cpp services/ItemService.cpp services/OrderService.cpp ) target_link_libraries(secondhand_app PRIVATE Qt5::Widgets Qt5::Sql )find_package 通过 Qt5Config.cmake 定位安装目录头文件和动态库链接都由 target_link_libraries 带入。若报找不到 Qt5先检查 Qt 安装是否完整以及 CMAKE_PREFIX_PATH 是否指向 Qt 的 lib/cmake 上层目录。Windows 环境下运行时要把 Qt 的 bin 目录和 MySQL 客户端库放在能搜索到的路径里。5.2 数据库连接的复用配置Qt 提供了按名称区分的连接机制。DbManager 里建立连接业务层通过 database(main_conn) 获取同一个会话。bool DbManager::initConnection() { if (QSqlDatabase::contains(main_conn)) { return QSqlDatabase::database(main_conn).isOpen(); } QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL, main_conn); db.setHostName(127.0.0.1); db.setPort(3306); db.setDatabaseName(secondhand_db); db.setUserName(root); db.setPassword(your_password); return db.open(); }连接名称是 main_conn所有 service 类都从这条连接上创建 QSqlQuery同一时刻只有一个连接实例不会出现多处配置不一致。课程设计阶段不需要连接池MySQL 默认并发连接数对本地单用户完全够用。5.3 启动阶段常见的三个问题现象原因处理方式报错 driver not loadedQt SQL 插件缺失确认 Qt 安装包含 QtSql 的 MySQL 驱动部署时用 windeployqt 复制插件目录界面中文乱码数据库与客户端字符集不一致库表和连接参数全部统一为 utf8mb4连接选项设置 charset数据库连接被拒绝MySQL 服务未启动或认证失败检查 3306 端口监听状态确认用户名密码和认证插件交付前验证顺序启动程序后先走一遍注册、登录、发布、下单、确认收货的完整链路用 Navicat 或命令行查看 item.status 的变化确认订单在事务提交后从 0 变成 1再确认商品列表不再显示已预订商品。验证时如果遇到订单状态已更新但商品列表未变化优先检查 refreshItemList 里的 m_itemModel-select() 是否真的执行了QSqlTableModel 在重新 select 之前不会自动感知数据库变化这是信号槽接对了但界面不刷新的最常见原因。本文还有配套的精品资源点击获取
返回列表