首页 / 资讯中心 / 文章详情

Qt+MySQL预约停车系统开发实战:会员充值与车位预约核心实现

Qt+MySQL预约停车系统开发实战:会员充值与车位预约核心实现 ★ FEATURED ARTICLE
简介这是一套基于Qt框架与MySQL数据库开发的完整预约停车系统源码面向计算机专业本科生及C初学者适用于毕业设计、课程设计与小型项目实践解决校园或社区场景下的车位预约、会员管理与在线缴费等核心业务需求。资源包共34个文件含9个C实现文件如parkingwindow.cpp、topup.cpp、8个UI界面设计文件.ui、8个头文件.h支撑模块化逻辑另含1个SQL建表脚本、1个资源文件.qrc及6张界面配图整体结构清晰、分层合理便于理解MVC模式在Qt中的落地。压缩包仅185KB轻量易部署已通过功能测试可直接编译运行。目前已有56人学习下载读者将获得可运行的全功能客户端、完整的数据库初始化方案、会员注册/信息修改/密码更新/停车记录查询/充值缴费等全流程代码实现以及典型Qt信号槽交互与MySQL连接封装范例。1. 为什么一个“预约停车会员充值”的QtMySQL系统成了课程设计里最常被选、也最容易翻车的项目很多同学在开题时看到“基于QtMYSQL的预约停车系统会员办理充值缴费功能”这个标题第一反应是界面有、数据库有、功能点全、答辩能讲三分钟——稳了。但真正动手后才发现Qt控件联动像黑匣子MySQL事务一写就丢数据会员余额扣减和预约状态同步总差那么一拍导出报表时日期格式崩成乱码……这不是功能堆砌而是一套典型的「轻量级业务系统」最小闭环用户身份会员、资金流充值/扣费、资源调度车位预约三者必须实时咬合。它不追求高并发但要求逻辑零歧义不用微服务但数据库设计稍有疏漏第二天测试就集体报错。适合计算机、软件工程类本科生做课程设计或毕业设计——前提是你愿意花3天啃透Qt信号槽与MySQL事务边界而不是只抄UI代码交差。本文就从真实开发节奏出发带你用最简路径跑通这个系统避开90%人踩过的坑。2. 用Qt Creator MySQL 8.0 搭建可运行环境从零配置到连接成功2.1 环境版本选择与安装验证为什么必须用 Qt 5.15.2 而不是 6.xQt 6.x 默认移除了 QtWidgets 模块的隐式链接支持而本项目大量依赖 QTableWidget、QDateEdit、QSqlTableModel 等经典控件强行迁移需重写数据绑定逻辑。Qt 5.15.2 是最后一个长期支持LTS版本兼容性好、文档全、社区示例多且与 MySQL 8.0 的 Connector/C 8.0.33 完全匹配。安装步骤如下下载 Qt Online Installer官方源勾选Qt 5.15.2 → MinGW 7.3 64-bitWindows或Clang 12.0 64-bitmacOS单独下载 MySQL Community Server 8.0.33非最新版8.0.34 默认启用 caching_sha2_password 插件Qt 5.15.2 原生不支持安装 MySQL 后用命令行执行mysql -u root -p ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY your_password; FLUSH PRIVILEGES;提示这一步是玄学关键——不改认证插件Qt 连接时会静默失败无任何错误日志只返回空 QSqlDatabase 对象。2.2 创建 parking_db 数据库并初始化核心表结构本系统只需 4 张表拒绝过度设计。使用 utf8mb4 字符集避免微信昵称等 emoji 存储异常CREATE DATABASE IF NOT EXISTS parking_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE parking_db; -- 会员表含余额、注册时间、状态正常/冻结 CREATE TABLE members ( id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) UNIQUE NOT NULL COMMENT 会员卡号, name VARCHAR(50) NOT NULL, phone VARCHAR(15) NOT NULL, balance DECIMAL(10,2) DEFAULT 0.00 COMMENT 余额单位元, reg_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1 COMMENT 1-正常, 0-冻结 ); -- 车位表区分类型固定/临时、状态空闲/占用/维护 CREATE TABLE parking_spaces ( id INT PRIMARY KEY AUTO_INCREMENT, space_code VARCHAR(10) UNIQUE NOT NULL COMMENT 车位编号如A01,B12, space_type ENUM(fixed,temporary) DEFAULT temporary, status ENUM(free,occupied,maintenance) DEFAULT free, remark VARCHAR(100) DEFAULT ); -- 预约记录表关联会员与车位含开始/结束时间、状态待生效/已入场/已离场/已取消 CREATE TABLE reservations ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id INT NOT NULL, space_id INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status ENUM(pending,checked_in,checked_out,cancelled) DEFAULT pending, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (member_id) REFERENCES members(id) ON DELETE CASCADE, FOREIGN KEY (space_id) REFERENCES parking_spaces(id) ON DELETE RESTRICT ); -- 充值流水表记录每一笔充值动作不可删改 CREATE TABLE recharge_logs ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL COMMENT 充值金额, operator VARCHAR(20) DEFAULT system COMMENT 操作员前台可填, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (member_id) REFERENCES members(id) ON DELETE CASCADE );参数说明ON DELETE CASCADE保证会员删除时自动清理其预约和充值记录ON DELETE RESTRICT防止误删车位导致预约记录悬空DATETIME类型比TIMESTAMP更稳定避免时区转换引发的显示错乱。2.3 Qt 项目中配置 MySQL 驱动并测试连接在.pro文件末尾添加QT sql widgets LIBS -lmysqlclient INCLUDEPATH C:/Program Files/MySQL/MySQL Server 8.0/include LIBS -LC:/Program Files/MySQL/MySQL Server 8.0/libmacOS 路径为/usr/local/mysql/include和/usr/local/mysql/lib主程序入口main.cpp中加入连接验证逻辑#include QApplication #include QSqlDatabase #include QSqlQuery #include QDebug bool initDatabase() { QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL); db.setHostName(127.0.0.1); db.setDatabaseName(parking_db); db.setUserName(root); db.setPassword(your_password); // 实际应从配置文件读取 db.setPort(3306); if (!db.open()) { qCritical() 数据库连接失败 db.lastError().text(); return false; } // 测试查询检查 members 表是否存在且可读 QSqlQuery query(db); if (!query.exec(SELECT COUNT(*) FROM members)) { qCritical() members 表访问失败 query.lastError().text(); db.close(); return false; } qDebug() 数据库连接成功members 表记录数 query.value(0).toInt(); return true; } int main(int argc, char *argv[]) { QApplication a(argc, argv); if (!initDatabase()) return -1; // 连接失败直接退出 // ... 启动主窗口 }逻辑说明此段代码不是“能连上就行”而是强制验证members表可读——因为后续所有业务都依赖该表。若此处失败后续所有QSqlTableModel绑定都会静默崩溃。参数setPort(3306)必须显式指定Qt 5.15.2 在 Windows 下对默认端口识别不稳定。3. 用 QSqlTableModel 实现会员管理模块增删改查与实时校验3.1 构建可编辑的会员列表视图QTableView QSqlTableModel会员管理是系统基石必须支持✅ 卡号唯一性前端拦截✅ 余额修改需二次确认✅ 手机号格式实时校验11位数字✅ 冻结/解冻状态一键切换核心代码在MemberManagerWidget.cpp中// 初始化模型 model new QSqlTableModel(this, db); model-setTable(members); model-setEditStrategy(QSqlTableModel::OnManualSubmit); // 关键禁用自动提交便于事务控制 model-select(); // 设置列头显示名 model-setHeaderData(1, Qt::Horizontal, QObject::tr(卡号)); model-setHeaderData(2, Qt::Horizontal, QObject::tr(姓名)); model-setHeaderData(3, Qt::Horizontal, QObject::tr(手机号)); model-setHeaderData(4, Qt::Horizontal, QObject::tr(余额(元))); model-setHeaderData(6, Qt::Horizontal, QObject::tr(状态)); // 绑定到视图 ui-tableView-setModel(model); ui-tableView-setSelectionBehavior(QAbstractItemView::SelectRows); ui-tableView-setSelectionMode(QAbstractItemView::SingleSelection);参数说明OnManualSubmit是血泪经验——若用OnFieldChange用户每输一个字就触发一次 SQL UPDATE不仅慢且在余额字段修改时无法做“当前余额充值额”原子计算OnRowChange也不够因单行内多字段修改需统一校验。3.2 自定义委托实现手机号格式校验与状态开关直接复用QStyledItemDelegate重写createEditor和setModelDataclass PhoneDelegate : public QStyledItemDelegate { public: QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem option, const QModelIndex index) const override { QLineEdit* editor new QLineEdit(parent); QRegExp rx(^1[3-9]\\d{9}$); // 严格匹配11位手机号 editor-setValidator(new QRegExpValidator(rx, editor)); return editor; } void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex index) const override { QLineEdit* lineEdit static_castQLineEdit*(editor); QString value lineEdit-text(); if (value.isEmpty()) { model-setData(index, QVariant(), Qt::EditRole); } else { model-setData(index, value, Qt::EditRole); } } }; // 在初始化处注册委托 ui-tableView-setItemDelegateForColumn(3, new PhoneDelegate(ui-tableView)); // 第3列手机号状态列status用QCheckBox委托但注意数据库存的是TINYINT而QCheckBox传Qt::CheckState需转换class StatusDelegate : public QStyledItemDelegate { public: void paint(QPainter* painter, const QStyleOptionViewItem option, const QModelIndex index) const override { bool isActive index.data(Qt::DisplayRole).toInt() 1; QStyleOptionButton btnOpt; btnOpt.rect option.rect.adjusted(10, 0, -10, 0); btnOpt.state isActive ? QStyle::State_On : QStyle::State_Off; QApplication::style()-drawControl(QStyle::CE_CheckBox, btnOpt, painter); } QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem, const QModelIndex) const override { QCheckBox* cb new QCheckBox(parent); cb-setTristate(false); return cb; } void setEditorData(QWidget* editor, const QModelIndex index) const override { QCheckBox* cb static_castQCheckBox*(editor); cb-setChecked(index.data(Qt::DisplayRole).toInt() 1); } void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex index) const override { QCheckBox* cb static_castQCheckBox*(editor); int val cb-isChecked() ? 1 : 0; model-setData(index, val, Qt::EditRole); } };逻辑说明paint()方法手动绘制 checkbox避免QSqlRelationalDelegate的复杂依赖setModelData()将布尔值转为0/1写入数据库与表定义完全对齐。3.3 会员充值功能余额更新必须走事务且禁止负数充值不是简单UPDATE members SET balance balance ?必须满足① 充值金额 0② 更新余额与插入流水必须同时成功或同时失败③ 前端显示余额实时刷新不依赖model-select()全量重刷void MemberManagerWidget::on_btnRecharge_clicked() { bool ok; double amount ui-lineEditAmount-text().toDouble(ok); if (!ok || amount 0) { QMessageBox::warning(this, 输入错误, 请输入有效充值金额); return; } int memberId getCurrentMemberId(); // 从选中行获取 QSqlDatabase db QSqlDatabase::database(); db.transaction(); // 开启事务 QSqlQuery query(db); // 步骤1更新余额带条件防止超扣 query.prepare(UPDATE members SET balance balance ? WHERE id ? AND balance 0); query.addBindValue(amount); query.addBindValue(memberId); if (!query.exec()) { db.rollback(); QMessageBox::critical(this, 充值失败, 余额更新异常 query.lastError().text()); return; } // 步骤2插入充值流水 query.prepare(INSERT INTO recharge_logs (member_id, amount, operator) VALUES (?, ?, ?)); query.addBindValue(memberId); query.addBindValue(amount); query.addBindValue(ui-lineEditOperator-text().isEmpty() ? system : ui-lineEditOperator-text()); if (!query.exec()) { db.rollback(); QMessageBox::critical(this, 充值失败, 流水记录失败 query.lastError().text()); return; } db.commit(); // 仅当两步都成功才提交 // 步骤3局部刷新余额显示不重刷整表 QModelIndex idx model-index(model-rowCount()-1, 4, QModelIndex()); // 假设余额在第4列 // 实际应通过 member_id 查找对应行此处简化示意 model-setData(idx, model-data(idx).toDouble() amount, Qt::EditRole); model-submitAll(); // 提交本次修改 }参数说明WHERE id ? AND balance 0是安全冗余——虽业务逻辑已控但数据库层再加一层保险db.transaction()必须在QSqlQuery使用同一QSqlDatabase对象否则事务无效model-submitAll()触发模型将缓存变更写入数据库与前面OnManualSubmit策略呼应。4. 预约停车模块的核心逻辑时间冲突检测与状态机驱动4.1 预约前必做的三重时间校验为什么不能只查“车位是否空闲”车位空闲 ≠ 可预约。必须同时满足① 用户未在同时间段有其他有效预约防一人多约② 目标车位在预约时段内无任何pending/checked_in状态记录③ 预约起止时间合法开始 结束且不早于当前时间bool ReservationManager::isTimeSlotAvailable(int memberId, int spaceId, const QDateTime start, const QDateTime end) { QSqlQuery query; // 校验1用户自身冲突 query.prepare(SELECT COUNT(*) FROM reservations WHERE member_id ? AND status IN (pending,checked_in) AND ((? BETWEEN start_time AND end_time) OR (? BETWEEN start_time AND end_time) OR (start_time BETWEEN ? AND ?) OR (end_time BETWEEN ? AND ?))); query.addBindValue(memberId); query.addBindValue(start); query.addBindValue(end); query.addBindValue(start); query.addBindValue(end); query.addBindValue(start); query.addBindValue(end); query.exec(); if (query.next() query.value(0).toInt() 0) return false; // 校验2车位冲突排除已离场和已取消的记录 query.prepare(SELECT COUNT(*) FROM reservations WHERE space_id ? AND status IN (pending,checked_in) AND ((? BETWEEN start_time AND end_time) OR (? BETWEEN start_time AND end_time) OR (start_time BETWEEN ? AND ?) OR (end_time BETWEEN ? AND ?))); query.addBindValue(spaceId); query.addBindValue(start); query.addBindValue(end); query.addBindValue(start); query.addBindValue(end); query.addBindValue(start); query.addBindValue(end); query.exec(); if (query.next() query.value(0).toInt() 0) return false; return true; }逻辑说明SQL 中的四段BETWEEN覆盖所有时间重叠场景A包含B、B包含A、A左交B、A右交Bstatus IN (pending,checked_in)过滤掉已失效记录避免误判函数返回bool而非int语义清晰调用方无需解析数值。4.2 预约状态机用枚举SQL约束驱动业务流转定义状态流转规则不允许跳变pending→checked_in扫码入场checked_in→checked_out扫码离场pending→cancelled用户取消checked_in不允许直接cancelled必须先离场在数据库层面加 CHECK 约束MySQL 8.0 支持ALTER TABLE reservations ADD CONSTRAINT chk_status_transition CHECK ( (status pending AND (status_old IS NULL OR status_old IN (pending))) OR (status checked_in AND status_old pending) OR (status checked_out AND status_old IN (checked_in, pending)) OR (status cancelled AND status_old IN (pending, checked_in)) );注意MySQL 原生不支持status_old列此处为示意。实际项目中我们用触发器模拟状态机DELIMITER $$ CREATE TRIGGER tr_reservation_status_check BEFORE UPDATE ON reservations FOR EACH ROW BEGIN IF NEW.status checked_in AND OLD.status ! pending THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 入场只能从待生效状态发起; END IF; IF NEW.status checked_out AND OLD.status NOT IN (checked_in, pending) THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 离场只能从入场或待生效状态发起; END IF; END$$ DELIMITER ;4.3 场景化预约流程从选车位到生成二维码含时间精度处理用户预约时界面提供 日历控件选日期 下拉框选时段30分钟粒度08:00-08:30, 08:30-09:00… 自动计算结束时间起始2小时可手动改关键细节MySQLDATETIME存储精度为秒但 QtQDateTime默认带毫秒。若不截断QDateTime::toString(yyyy-MM-dd hh:mm:ss)与数据库值比对可能失败// 正确做法入库前统一截断毫秒 QDateTime start ui-dateTimeEditStart-dateTime(); QDateTime end ui-dateTimeEditEnd-dateTime(); start start.addMSecs(-start.time().msec()); // 清零毫秒 end end.addMSecs(-end.time().msec()); QSqlQuery query; query.prepare(INSERT INTO reservations (member_id, space_id, start_time, end_time, status) VALUES (?, ?, ?, ?, pending)); query.addBindValue(memberId); query.addBindValue(spaceId); query.addBindValue(start); // 此时已无毫秒 query.addBindValue(end); query.exec();参数说明addMSecs(-msec())是 Qt 官方推荐的毫秒清零法比toString(...).toDateTime()更可靠status固定为pending由后续操作触发状态变更避免前端传参污染状态机。5. 避坑指南QtMySQL预约系统里最常被忽略的5个致命细节5.1 现象QSqlTableModel 修改后调用 submitAll() 返回 false但 lastError() 为空原因模型未设置主键或主键列在setTable()后被隐藏如model-removeColumn(0)导致 Qt 无法构造 WHERE 条件。解决在setTable()后立即调用model-setPrimaryKey(id)Qt 5.15.2 需手动指定或确保主键列未被移除调试时打印model-filter()和model-lastError().text()。5.2 现象会员充值成功但 TableView 中余额列显示未更新需手动点一下才刷新原因QSqlTableModel默认不监听底层数据库变更如其他进程插入充值记录且submitAll()仅提交模型缓存不触发视图重绘。解决在submitAll()后调用model-select()强制重载或更优方案——重写data()函数在余额列返回model-query().value(balance)动态值。5.3 现象预约时间跨日如23:00-01:00时查询冲突失败但实际无重叠原因SQL 中BETWEEN对跨日时间计算错误23:00 BETWEEN 22:00 AND 01:00返回 false因 01:00 22:00。解决改用NOT (end_time ? OR start_time ?)逻辑即“不满足‘早于开始’或‘晚于结束’则重叠”SELECT COUNT(*) FROM reservations WHERE space_id ? AND status IN (pending,checked_in) AND NOT (end_time ? OR start_time ?)5.4 现象Qt 程序退出时 MySQL 连接未关闭再次启动报 “QSqlDatabasePrivate::addDatabase: duplicate connection name ‘qt_sql_default_connection’”原因QSqlDatabase::addDatabase()未指定连接名多次调用注册同名连接。解决在main()中首次连接时指定唯一名称QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL, parking_connection); // 后续所有操作均用 db而非 QSqlDatabase::database()5.5 现象导出预约报表为 Excel 时中文列名和数据全变成问号原因Qt 的QTextStream默认编码为Local8BitWindows 系统下为 GBK而 MySQL 连接字符集为utf8mb4中间未转码。解决导出前强制设置流编码QFile file(report.csv); file.open(QIODevice::WriteOnly); QTextStream out(file); out.setCodec(UTF-8); // 关键 out 卡号,姓名,车位,开始时间,结束时间,状态\n; // ... 写入数据 file.close();6. 进阶技巧用 QThread QTimer 实现后台车位状态自动刷新与超时预警6.1 为什么不能用 QTimer 在主线程轮询——界面卡死的根源课程设计常犯错误在主窗口中放一个QTimer每5秒执行model-select()刷新所有预约。结果是——界面假死、鼠标响应延迟、Qt 报QEventDispatcherWin32::interrupt警告。根本原因是QSqlQuery::exec()是阻塞调用而 MySQL 查询哪怕毫秒级积少成多也会拖垮事件循环。正确解法用QThread将数据库操作移出主线程通过信号传递结果class RefreshWorker : public QObject { Q_OBJECT public slots: void doRefresh() { QSqlDatabase db QSqlDatabase::database(parking_connection); if (!db.isOpen()) return; QSqlQuery query(db); query.exec(SELECT id, space_id, status, end_time FROM reservations WHERE status checked_in); QListQVariantMap data; while (query.next()) { QVariantMap row; row[id] query.value(id); row[space_id] query.value(space_id); row[status] query.value(status); row[end_time] query.value(end_time); data.append(row); } emit refreshFinished(data); } signals: void refreshFinished(const QListQVariantMap data); }; // 在主窗口中启动线程 QThread* thread new QThread; RefreshWorker* worker new RefreshWorker; worker-moveToThread(thread); connect(thread, QThread::started, worker, RefreshWorker::doRefresh); connect(worker, RefreshWorker::refreshFinished, this, MainWindow::onRefreshDone); connect(worker, RefreshWorker::refreshFinished, thread, QThread::quit); connect(thread, QThread::finished, worker, QObject::deleteLater); connect(thread, QThread::finished, thread, QObject::deleteLater); thread-start();6.2 超时预警用 QTimer 精确计算“已入场超2小时”并弹窗提醒checked_in状态的预约若end_time已过期但未checked_out需人工干预。用QTimer单次触发比轮询更省资源void MainWindow::startTimeoutCheck() { // 每分钟检查一次找出超时记录 QTimer* timer new QTimer(this); connect(timer, QTimer::timeout, []() { QSqlQuery query; query.exec(SELECT id, member_id, space_id FROM reservations WHERE status checked_in AND end_time NOW()); while (query.next()) { int id query.value(id).toInt(); int memberId query.value(member_id).toInt(); int spaceId query.value(space_id).toInt(); // 弹窗提醒仅首次 if (!timeoutAlerted.contains(id)) { QMessageBox::warning(this, 超时预警, QString(预约ID %1 的用户已超时\n车位%2会员ID%3) .arg(id).arg(spaceId).arg(memberId)); timeoutAlerted.insert(id); } } }); timer-start(60000); // 60秒 }参数说明timeoutAlerted是QSetint成员变量用于去重避免同一记录反复弹窗NOW()是 MySQL 函数比 Qt 获取本地时间更可靠规避客户端时钟误差。6.3 最后一条血泪经验把所有 SQL 字符串提取为 const QString 变量不要在代码中写query.exec(UPDATE members SET balance ...)。全部提取const QString SQL_UPDATE_BALANCE UPDATE members SET balance balance ? WHERE id ?; const QString SQL_INSERT_RESERVATION INSERT INTO reservations (member_id, space_id, start_time, end_time, status) VALUES (?, ?, ?, ?, pending);理由有三① 方便全局搜索替换如某天要加审计字段改一处全更新② 避免拼写错误导致静默失败IDE 能语法高亮字符串里写错reservations变reservatons不报错③ 为未来迁移到预编译语句QSqlQuery::prepare铺路提升性能。我带过的某高校课程设计小组7 个组里有 5 个在答辩前两天还在 debugQSqlQuery拼写错误。把 SQL 提出来不是为了炫技是给自己的代码加一道后悔药。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站