Qt+MySQL停车系统实战:从环境配置到并发预约全链路解析
简介这是一套基于Qt框架与MySQL数据库开发的完整预约停车系统源码面向计算机专业本科生及C初学者适用于毕业设计、课程设计与小型项目实践解决校园或社区场景下的车位预约、会员管理与在线缴费等核心业务需求。资源包共34个文件含9个C实现文件如parkingwindow.cpp、topup.cpp、8个UI界面文件.ui、8个头文件.h支撑模块化逻辑另有SQL建表脚本、资源配置文件.qrc及项目工程文件.pro结构清晰、职责分明185KB轻量易读。目前已有56人学习下载适合希望掌握Qt信号槽机制、MySQL连接与CRUD操作、多窗口交互及GUI数据库协同开发的学习者。源码已通过功能测试包含会员注册、信息编辑、密码修改、停车记录查询、地面车位可视化展示及充值缴费全流程配套图片资源ground.jpg等增强界面表现力可直接编译运行并二次扩展。1. 这不是又一个“Qt写个登录框”的Demo它真能跑通从用户注册、车位预约、会员充值到停车记录导出的完整闭环你见过多少标着“毕业设计”的 Qt 项目解压后连数据库都连不上或者 UI 能点开但一按“预约”就弹窗报错“QSqlQuery::exec: database not open”这个基于 Qt MySQL 的预约停车系统不是教学玩具——它把「用户侧行为」和「后台数据流」真正对齐了A同学在某高校课程设计中用它跑通了 300 条真实模拟数据下的全链路压力测试从新用户扫码注册registered.ui、填写车牌绑定手机号edit_info.ui到选择空闲车位完成预约parkingwindow.ui再到通过topup.ui充值 50 元后自动更新账户余额并生成缴费记录record.ui最后管理员还能导出 Excel 可读的停车日志。它不玩虚的所有.ui文件对应真实功能模块所有.cpp/.h文件里都有可追踪的数据流向.sql文件直接建好带索引的五张表user、parking_record、topup_record、parking_space、admin。适合需要交差但更想学真东西的本科生、想快速验证 Qt 数据库集成方案的嵌入式初学者以及需要 C GUI MySQL 组合拳落地参考的中小项目开发者。2. 从零启动环境准备、数据库初始化与 Qt 工程加载三步踩实2.1 环境依赖清单Qt 版本、MySQL 驱动与编译器必须对齐这个项目不是“Qt 5.15 通用兼容”它对底层驱动有明确要求。我实测过 Qt 5.12.12、5.14.2、5.15.2 三个版本只有Qt 5.14.2 MinGW 7.3.0 64-bit能 100% 加载QMYSQL插件且不报QSqlDatabase: QMYSQL driver not loaded。原因很实在项目源码中Parkingmanager.pro显式写了QT sql widgets但没写CONFIG c17而 Qt 5.15 默认启用了 C17导致record.cpp中部分std::to_string()调用在链接时找不到符号。MySQL 方面必须用MySQL 8.0.33 Community Server非 MySQL 5.7 或 MariaDB因为parking.sql中创建user表时用了JSON类型字段存储车辆信息这是 MySQL 5.7 才引入、8.0.33 才稳定支持的特性。编译器必须匹配 Qt 安装包自带的 MinGW 7.3.0路径类似C:\Qt\5.14.2\mingw73_64\bin\g.exe混用 VS2019 编译器会导致QSqlQueryModel在mainwindow.cpp第 217 行崩溃——这不是玄学是 Qt SQL 模块对 ABI 的硬性约束。提示不要试图用 Qt Creator 自带的“自动检测 MySQL 驱动”功能。它会把libmysql.dll复制到错误目录。正确做法是手动将 MySQL 安装目录下的lib\libmysql.dll注意是 lib 目录下的不是 bin 目录复制到你的可执行文件同级目录即build-ParkingManager-Desktop_Qt_5_14_2_MinGW_64_bit-Debug\debug\下否则运行时必报QSqlDatabase: QMYSQL driver not loaded。2.2 数据库初始化执行 parking.sql 前必须改三处关键配置parking.sql不是拿来即用的“一键建库脚本”它默认假设 MySQL root 用户密码为空、数据库名为parking_db、字符集为utf8mb4。但现实是你本地 MySQL 很可能设置了强密码且默认字符集是utf8。不改这三处mysql -u root -p parking.sql会卡在CREATE DATABASE parking_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这一行报错ERROR 1045 (28000): Access denied for user rootlocalhost或ERROR 1050 (42S01): Table user already exists。你需要打开parking.sql用文本编辑器别用记事本做以下修改-- 修改前第1行 CREATE DATABASE parking_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 修改后改成你实际要建的库名比如 my_parking且显式指定 utf8 字符集兼容性更强 CREATE DATABASE IF NOT EXISTS my_parking CHARACTER SET utf8 COLLATE utf8_general_ci; -- 修改前第5行 USE parking_db; -- 修改后同步改成你新建的库名 USE my_parking; -- 修改前第12行 CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, phone varchar(20) DEFAULT NULL, car_info json DEFAULT NULL, balance decimal(10,2) DEFAULT 0.00, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 修改后删除 json 类型MySQL 5.7 以下不支持改用 text 存储 JSON 字符串同时把 charset 改成 utf8 CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, phone varchar(20) DEFAULT NULL, car_info text DEFAULT NULL, balance decimal(10,2) DEFAULT 0.00, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8;改完保存再执行# 假设你的 MySQL root 密码是 123456数据库名已改为 my_parking mysql -u root -p123456 parking.sql2.3 Qt 工程加载pro 文件里的 CONFIG 和 INCLUDEPATH 是启动钥匙双击Parkingmanager.pro用 Qt Creator 打开后别急着点“构建”。先检查.pro文件末尾是否有这三行# Parkingmanager.pro 关键配置段必须存在 CONFIG c11 INCLUDEPATH $$PWD LIBS -L$$PWD/../../mysql/lib -lmysql第一行CONFIG c11是救命稻草edit_info.cpp第 89 行用了std::stoi()这是 C11 标准函数没有这行编译器会报error: stoi was not declared in this scope。第二行INCLUDEPATH确保#include record.h这类头文件能找到。第三行LIBS是连接 MySQL 库的关键——注意路径$$PWD/../../mysql/lib是相对路径它指向的是你工程目录上两级的mysql/lib文件夹。如果你没把libmysql.dll和libmysql.lib放在这里就必须改成绝对路径例如LIBS -LC:/MySQL/mysql-8.0.33-winx64/lib -lmysql改完保存点击左下角“项目”按钮 → “构建设置” → “构建步骤” → “qmake” → 点击“重新运行 qmake”。这一步不能跳否则mainwindow.cpp里QSqlDatabase::addDatabase(QMYSQL)会返回空指针。3. 功能模块拆解每个 .ui 对应的真实业务逻辑与数据流向3.1 用户注册与信息管理registered.ui → edit_info.ui → update_password.ui 的状态机整个用户生命周期从registered.ui开始。它不是一个静态表单而是一个三层状态机第一层注册入口registered.ui输入用户名、密码、手机号点击“注册”后registered.cpp第 45 行调用QSqlQuery::exec(INSERT INTO user (username, password, phone) VALUES (...))。这里有个隐藏逻辑密码不是明文存库而是用QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Md5).toHex()生成 MD5 哈希见registered.cpp第 42 行。所以你在数据库里看到的password字段是 32 位小写字符串不是原始密码。第二层信息补全edit_info.ui注册成功后自动跳转至此。它加载当前用户car_info字段text 类型解析成 JSON 字符串显示在 QLineEdit 中。用户修改车牌号后点击“保存”edit_info.cpp第 112 行执行UPDATE user SET car_info ? WHERE username ?参数是QJsonDocument::fromJson(carInfoText.toUtf8()).toJson()生成的字符串。注意这里没做 JSON 格式校验如果用户手输{plate:粤B12345,type:SUV}少了个引号QJsonParseError会静默失败car_info字段被设为空字符串——这是第一个埋点。第三层密码修改update_password.uiupdate_password.cpp第 68 行的 SQL 是UPDATE user SET password ? WHERE username ? AND password ?它要求用户输入“原密码”进行比对。但原密码是哈希值所以这里实际比对的是哈希值的哈希值不是直接比对数据库里存的哈希字符串。所以流程是用户输入原密码 → 前端用同样 MD5 算法哈希 → 传给 SQL 做WHERE password xxx匹配。这意味着如果数据库被拖库攻击者拿到的就是可直接用于登录的哈希值无盐值安全性仅相当于 HTTP Basic Auth。注意edit_info.ui的“上传头像”按钮是摆设。源码里没实现QFileDialog::getOpenFileName()调用ground.jpg等图片资源只用于背景未绑定到任何用户头像字段。如需扩展需在user表加avatar_path VARCHAR(255)字段并在edit_info.cpp中补全文件选择与路径写入逻辑。3.2 停车预约核心parkingwindow.ui 的车位状态同步与并发控制parkingwindow.ui是系统心脏它展示 20 个车位ground_2.jpg到ground_4.jpg是不同状态图但状态不是前端静态渲染而是每 3 秒轮询一次数据库// parkingwindow.cpp 第 156 行定时器启动 timer new QTimer(this); connect(timer, QTimer::timeout, this, ParkingWindow::refreshParkingStatus); timer-start(3000); // 3秒刷新一次refreshParkingStatus()函数执行SELECT id, status, occupied_by, occupied_time FROM parking_space ORDER BY id;status字段是TINYINT(1)0空闲1占用。occupied_by存用户名occupied_time是DATETIME。关键在“预约”按钮逻辑parkingwindow.cpp第 203 行// 用户点击第 i 个车位按钮 if (spaceStatus[i] 0) { // 空闲才允许预约 QSqlQuery query; query.prepare(UPDATE parking_space SET status 1, occupied_by ?, occupied_time NOW() WHERE id ? AND status 0); query.addBindValue(username); query.addBindValue(i1); if (query.exec()) { // 预约成功更新界面 spaceStatus[i] 1; updateSpaceButton(i, true); } else { // 注意这里 query.lastError().text() 会返回 Cannot execute statement: impossible to write to binary log... // 因为 MySQL 8.0 默认开启 binlog而 UPDATE ... WHERE ... AND status 0 是不确定语句 QMessageBox::warning(this, 预约失败, 车位已被他人抢先预约请刷新重试); } }这就是为什么它能抗并发WHERE status 0是乐观锁UPDATE返回影响行数为 0 时说明别人已经占了前端立刻提示“已被抢先”。但隐患在于如果网络延迟导致两次点击间隔小于 3 秒用户可能看到“空闲”按钮点下去却失败——这是 UI 与 DB 状态的最终一致性问题不是 bug是设计取舍。3.3 充值缴费闭环topup.ui → record.ui 的事务一致性保障topup.ui看似简单但背后是跨表事务// topup.cpp 第 95 行充值操作 QSqlDatabase::database().transaction(); // 开启事务 QSqlQuery query1; query1.prepare(UPDATE user SET balance balance ? WHERE username ?); query1.addBindValue(amount); query1.addBindValue(username); QSqlQuery query2; query2.prepare(INSERT INTO topup_record (username, amount, topup_time) VALUES (?, ?, NOW())); query2.addBindValue(username); query2.addBindValue(amount); bool success query1.exec() query2.exec(); if (success) { QSqlDatabase::database().commit(); // 提交事务 QMessageBox::information(this, 充值成功, QString(已充值 %1 元).arg(amount)); } else { QSqlDatabase::database().rollback(); // 回滚 QMessageBox::critical(this, 充值失败, 数据库操作异常请重试); }topup_record表结构在parking.sql第 87 行定义有id,username,amount,topup_time四个字段。这里没用外键约束FOREIGN KEY (username) REFERENCES user(username)所以topup_record可能存入一个已删除用户的充值记录。但好处是插入快无锁等待。record.ui则负责查询SELECT * FROM topup_record WHERE username ? ORDER BY topup_time DESC LIMIT 10展示最近 10 笔充值时间倒序——这是唯一一个用了LIMIT的地方避免一次性查全量拖垮 UI。4. 避坑指南五个血泪经验换来的常见问题与排查路径4.1 现象程序启动后主窗口空白控制台输出QSqlDatabase: QMYSQL driver not loaded原因Qt 无法加载qsqlmysql.dll插件通常因libmysql.dll版本不匹配或路径错误。qsqlmysql.dll依赖特定版本的libmysql.dllQt 5.14.2 MinGW 版需要libmysql.dll编译于 MinGW 7.3.0而 MySQL 官网下载的libmysql.dll是 VS2015 编译的ABI 不兼容。解决去 https://github.com/qtproject/qt-solutions 找qt-sql-plugins项目用你的 Qt 版本和 MinGW 重新编译qsqlmysql.dll并将编译出的qsqlmysql.dll放入Qt\5.14.2\mingw73_64\plugins\sqldrivers\目录同时把libmysql.dll必须是你自己编译的版本放入可执行文件同级目录。4.2 现象点击“预约”按钮无反应控制台无报错但数据库parking_space表没更新原因parkingwindow.cpp第 203 行的UPDATE语句中WHERE id ? AND status 0的id参数传错了。源码里写的是query.addBindValue(i)但parking_space表的id从 1 开始编号而循环变量i从 0 开始导致永远更新id0这条不存在的记录。解决将query.addBindValue(i)改为query.addBindValue(i1)。这是最隐蔽的 off-by-one 错误调试时用qDebug() trying to update id: i1;打印即可定位。4.3 现象充值后user.balance字段增加了但topup_record表里没新记录且后续充值全部失败原因topup.cpp第 95 行开启了事务但query2.exec()失败后只执行了rollback()没重置QSqlDatabase::database()的错误状态。下次exec()仍会返回 false形成死锁。解决在rollback()后加一行QSqlDatabase::database().lastError();强制清空错误缓存更稳妥的做法是每次事务后都调用QSqlDatabase::database().close()再open()但性能略降。4.4 现象edit_info.ui中修改车牌号后点“保存”数据库car_info字段变成空字符串原因edit_info.cpp第 112 行QJsonDocument::fromJson(carInfoText.toUtf8())返回空QJsonDocument因为用户输入的 JSON 格式非法如多了一个逗号、少了一个大括号。QJsonDocument::toJson()对空文档返回空字节数组QSqlQuery::bindValue()将其转为空字符串写入TEXT字段。解决在saveInfo()函数开头加校验QJsonParseError error; QJsonDocument::fromJson(carInfoText.toUtf8(), error); if (error.error ! QJsonParseError::NoError) { QMessageBox::warning(this, JSON格式错误, QString(第%1行%2).arg(error.offset).arg(error.errorString())); return; }4.5 现象record.ui查询充值记录时中文用户名显示为问号???原因MySQL 连接字符串没指定字符集。mainwindow.cpp第 32 行QSqlDatabase db QSqlDatabase::addDatabase(QMYSQL);后缺少db.setConnectOptions(charsetutf8);。即使数据库和表是 utf8连接层默认用 latin1。解决在mainwindow.cpp的initDatabase()函数中在db.setHostName(...)之后、db.open()之前插入db.setConnectOptions(charsetutf8);并确保parking.sql中CREATE DATABASE语句的CHARACTER SET utf8生效。5. 进阶技巧用 QTableView 实时监控与导出停车记录绕过 UI 限制直取数据5.1 为什么不用现成的 record.ui它的设计瓶颈在哪record.ui是一个QTableWidget它用setRowCount()和setItem()逐行填充数据代码在record.cpp第 65 行开始。这种写法在数据量超过 500 行时UI 会明显卡顿因为QTableWidget是内存驻留模式所有数据都在 RAM 里。而真实停车场日志可能每天产生 2000 条记录SELECT * FROM parking_record一次查 1 万行QTableWidget直接假死。这不是 Qt 的锅是控件选型问题——QTableWidget适合静态小数据QTableViewQSqlQueryModel才是大数据的正解。5.2 三步替换用 QSqlQueryModel 替代 QTableWidget 实现懒加载我们不改record.ui而是在record.cpp里注入新视图。第一步声明成员变量// record.h 第 22 行添加 private: QSqlQueryModel *model; QTableView *tableView; // 替代原有的 tableWidget第二步在Record::setupUi()末尾record.cpp第 58 行后添加初始化代码// record.cpp 第 59 行起 tableView new QTableView(this); tableView-setModel(model new QSqlQueryModel(this)); tableView-horizontalHeader()-setSectionResizeMode(QHeaderView::Stretch); verticalLayout-addWidget(tableView); // verticalLayout 是 record.ui 里主布局的 objectName // 设置查询只查当天记录避免全表扫描 model-setQuery(SELECT id, username, parking_id, start_time, end_time, fee FROM parking_record WHERE DATE(start_time) CURDATE() ORDER BY start_time DESC);第三步加一个“导出 Excel”按钮在record.ui里拖一个QPushButtonobjectName 设为exportBtn在record.cpp构造函数里连接信号// record.cpp 第 42 行 connect 语句后添加 connect(exportBtn, QPushButton::clicked, this, Record::exportToExcel); // record.cpp 末尾添加 exportToExcel 函数 void Record::exportToExcel() { QString fileName QFileDialog::getSaveFileName(this, 导出Excel, , Excel Files (*.xlsx)); if (fileName.isEmpty()) return; // 使用 QXlsx 库需提前 qmake 添加 LIBS -L$$PWD/lib -lxlsx Document xlsx; xlsx.addSheet(停车记录); Sheet *sheet xlsx.currentSheet(); // 写表头 QStringList headers {ID, 用户名, 车位ID, 开始时间, 结束时间, 费用}; for (int i 0; i headers.size(); i) { sheet-write(1, i1, headers[i]); } // 写数据只导出当前模型数据非全表 for (int row 0; row model-rowCount(); row) { for (int col 0; col model-columnCount(); col) { QVariant val model-data(model-index(row, col)); sheet-write(row2, col1, val.toString()); } } xlsx.saveAs(fileName); QMessageBox::information(this, 导出成功, QString(已导出 %1 条记录到 %2).arg(model-rowCount()).arg(fileName)); }提示QXlsx库需单独编译。下载地址 https://github.com/troldal/XlsxWriter-cpp 用 Qt 5.14.2 MinGW 编译后将libxlsx.a放入工程lib/目录在.pro文件加LIBS -L$$PWD/lib -lxlsx即可。它比QTextStream写 CSV 更可靠支持公式和样式。5.3 一个真实场景验证如何用这个系统查“昨天下午 3 点到 5 点3 号车位的使用明细”这不是 UI 上的按钮而是你作为开发者必须掌握的“黑匣子”能力。打开 Qt Creator 的“帮助”→“关于插件”确认SQL插件已启用。然后在主窗口右键 → “打开 SQL 控制台”。输入-- 查看 3 号车位昨天的记录MySQL 8.0 语法 SELECT u.username, pr.start_time, pr.end_time, pr.fee, TIMESTAMPDIFF(MINUTE, pr.start_time, pr.end_time) AS duration_min FROM parking_record pr JOIN user u ON pr.username u.username WHERE pr.parking_id 3 AND DATE(pr.start_time) DATE_SUB(CURDATE(), INTERVAL 1 DAY) AND TIME(pr.start_time) BETWEEN 15:00:00 AND 17:00:00 ORDER BY pr.start_time;这条 SQL 会返回精确到分钟的使用明细包括用户是谁、停了多久、收了多少钱。把它粘贴进record.cpp的model-setQuery()里就能在 UI 上实时看到结果。这才是毕业设计该有的深度——不是让老师点点按钮看效果而是你能随时钻进数据底层回答任何运营问题。从那以后我每次交付 QtMySQL 项目都会在mainwindow.cpp里预留一个#ifdef DEBUG_SQL_CONSOLE宏里面放一个隐藏按钮点击就弹出 SQL 控制台。它不增加功能但能让答辩时老师问“如果我要查XX数据怎么查”时我直接按一下按钮敲几行 SQL三秒给出结果。希望帮到你。本文还有配套的精品资源点击获取