简介基于Qt与MySQL实现的预约停车系统源码集成了用户注册登录、车位预约、会员办理以及充值缴费等核心功能适合作为毕业设计、课程设计或日常项目开发的参考模板。资源包整体压缩为zip格式共34个文件压缩后约185KB包括C源文件cpp、Qt界面文件ui、头文件h、界面背景图片jpg、数据库脚本sql以及工程配置文件pro、qrc目录结构清晰便于按模块查阅和二次开发。项目代码经过严格测试能稳定跑通主要业务流程其中9个cpp与8个h组织业务逻辑8个ui对应各操作界面sql文件提供了建表与初始数据脚本。目前已有56人学习下载适合希望在较短时间内掌握Qt图形界面和MySQL数据库联动开发的初学者也可作为答辩演示与功能扩展的起点。1. 预约停车系统为什么值得做一个把 Qt 界面和 MySQL 事务真正拧在一起的项目多数课设管理系统只做到“列表增删改查”答辩时被问一句“两个用户同时预约同一个车位怎么办”就接不上话。预约停车系统恰好把这个问题摆上台面车位不能重复预约、余额不能扣成负数、订单状态要按流程流转。用 Qt 写桌面客户端用 MYSQL 存会员和订单数据Qt 负责把人机交互做顺MYSQL 负责用事务和行锁守住并发边界两边的核心知识点都被逼着真用一遍。这是我认为它适合做毕业设计或课程设计的原因。按这个方向做出来的源码也能比较自然地扩展到小程序或 Web 后端接口适合想继续往工程方向走的同学。2. 用 Qt Creator 搭出客户端骨架版本配对、工程文件与登录界面2.1 版本怎么选Qt 5.15.2 MySQL 5.7 是课设最稳组合先定版本再写代码。我的习惯是Windows 上用 Qt Creator Qt 5.15.2 LTS MySQL 5.7。这套组合的驱动最成熟网上能查到的报错案例也基本都被踩过一遍适合第一次做完整项目的人。如果你在 mysql 官网下载安装的是 MySQL 8.0也不是不能用但要注意 8.0 默认的认证插件是 caching_sha2_passwordQt 5.15 自带的 QMYSQL 驱动可能直接不认需要在第 5 章排查里处理一下。Qt Creator 安装时建议把 MSVC 和 MinGW 两套编译器都勾上。MSVC 版本配 MSVC 编译的 MySQL 库MinGW 版本配 MinGW 能识别的 dll位数必须一致Qt 是 64 位MySQL 的 lib 也要用 64 位混搭最常见的结果就是驱动加载失败。如果你是在树莓派或者无桌面的服务器上跑启动时遇到qt.qpa.plugin: could not find the qt platform plugin linuxfb通常是缺 libqt5gui5 的 platform 插件或者可以直接导出QT_QPA_PLATFORMoffscreen做无界面运行后面自动化冒烟测试也会用到这个模式。2.2 工程文件与链接参数pro 文件里把 MySQL 头文件和库指对Qt 项目用 .pro 文件组织依赖。MYSQL 不是 Qt 自带的库必须告诉编译器头文件在哪、链接库在哪。下面是我常用的最小工程配置# parking.pro QT core gui sql greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET ParkingClient TEMPLATE app CONFIG c11 # Windows MySQL 5.7 为例换机器后改成你自己的实际路径 INCLUDEPATH C:/Program Files/MySQL/MySQL Server 5.7/include LIBS -LC:/Program Files/MySQL/MySQL Server 5.7/lib -lmysql SOURCES main.cpp \ mainwindow.cpp \ database/databasemanager.cpp HEADERS mainwindow.h \ database/databasemanager.h这里有几个注意点。MySQL 的 include 目录下要能看到 mysql.hlib 目录下要能看到 libmysql.lib 或对应动态库路径里尽量不要带中文。如果编译阶段报qt 编译 时候 cannot find -lpublic大概率不是 Qt 的问题而是 LIBS 里的路径写错、库名多打了字母或者 MinGW 和 MSVC 的库文件混用了-lmysql在 MinGW 下对应的是 libmysql.dll 的导入库在 MSVC 下对应 mysql.lib两个不要互换。2.3 登录界面和主界面切换QStackedWidget 做页面路由登录是整个系统的入口也顺手把 Qt 的常用控件都用一遍。我的做法是界面上放两个 QLineEdit 和一个 QPushButton点登录时用 QSqlQuery 去 member 表里查账号密码通过后把 QStackedWidget 切到主功能页。// mainwindow.cpp 中的登录槽函数 void MainWindow::onLoginClicked() { QString uid ui-editUser-text().trimmed(); QString pwd ui-editPass-text(); QSqlDatabase db DatabaseManager::instance()-database(); QSqlQuery query(db); query.prepare(SELECT id, real_name, is_admin FROM member WHERE login_name ? AND password ? ); query.addBindValue(uid); query.addBindValue(pwd); // 课设演示用明文生产环境必须换成哈希 if (!query.exec()) { QMessageBox::warning(this, 登录失败, query.lastError().text()); return; } if (query.next()) { m_currentMemberId query.value(0).toInt(); ui-stackedWidget-setCurrentIndex(1); // 切到主功能页 } else { QMessageBox::warning(this, 登录失败, 账号或密码不正确); } }这段代码里最值得说的是prepare和addBindValue。直接用字符串拼接 SQL 在课设里也能跑但遇到账号里带单引号会直接炸用预处理语句把参数和 SQL 模板分开既是好习惯也能在答辩时多一个可讲的点。query.value(0)拿到的是 SELECT 的第一列也就是 member.id把它存成成员变量后面预约、充值都要靠这个 id 定位当前用户。登录之后的界面建议不要开新窗口而是用 QStackedWidget 在一个主窗口里切换页面。这样预约页、充值页、会员管理页互相传数据方便窗口管理也简单。3. 数据模型先行会员、车位、预约三张表与 Qt 连接层封装3.1 三张核心表怎么设计DECIMAL、TINYINT 状态位和唯一索引数据库设计决定了后面所有代码的写法。预约停车系统最核心的就是三张表会员表、车位表、预约订单表。我见过不少课设把金额设计成 float这是典型的坑——浮点数在累加和比较时会出精度问题充值 0.1 元十次变 0.999999。金额一律用 DECIMAL(10,2)能精确到分。车位状态不要直接存“空闲/已预约/已占用”这样的中文字符串用 TINYINT 存 0、1、2、3含义在代码里用注释或者常量说明。原因有两个存储更省更重要的是 UPDATE 判断状态时写WHERE status 0比写WHERE status 空闲可靠得多不会因为全角半角空格匹配不上。预约订单表里要设计一个唯一索引 uk_order_no订单号由代码生成。这个唯一索引是防重复下单的兜底即使代码里漏了判断数据库也会因为订单号冲突而拒绝第二次插入。这个设计在课设答辩里非常加分属于“我从数据层面保证了幂等”。3.2 可执行的建库脚本从 CREATE DATABASE 到三张业务表直接用 MySQL 命令行或者 workbench 执行下面这段脚本就能把整套库表建出来。所有字段注释我都写在 SQL 里方便评审老师直接看。CREATE DATABASE IF NOT EXISTS parking_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE parking_system; -- 会员表既是登录账号也是充值账户 CREATE TABLE member ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, login_name VARCHAR(32) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 课设演示用明文可自行改为sha2, real_name VARCHAR(32) NOT NULL, phone VARCHAR(20) DEFAULT , balance DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 账户余额, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_login_name (login_name) ) ENGINEInnoDB; -- 车位表status 0空闲 1已预约 2占用 3维护 CREATE TABLE parking_space ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, space_no VARCHAR(16) NOT NULL, status TINYINT NOT NULL DEFAULT 0, area VARCHAR(32) DEFAULT , UNIQUE KEY uk_space_no (space_no) ) ENGINEInnoDB; -- 预约订单表status 0待支付 1已预约 2已入场 3已离场 4已取消 CREATE TABLE appointment ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, member_id INT UNSIGNED NOT NULL, space_id INT UNSIGNED NOT NULL, book_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 0, amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, paid_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_member (member_id), KEY idx_space_status (space_id, status) ) ENGINEInnoDB;注意几个细节。字符集用 utf8mb4 而不是 utf8因为 utf8 在 MySQL 里不是真正的四字节 UTF-8会员昵称里万一有 emoji 会存不进去。DEFAULT 0.00和DEFAULT 0分别给金额和状态位设了默认值这样 INSERT 时少写两个字段也不会报错。表引擎全部用 InnoDBMyISAM 不支持事务这是预约扣款能回滚的前提。3.3 Qt 侧连接层单例连接、utf8mb4 与中文乱码代码里不要到处addDatabase否则每次调用都会创建一条新连接用完不关就会“Too many connections”。我一般把连接封装成一个单例整个程序共享同一条连接// database/databasemanager.cpp节选 DatabaseManager* DatabaseManager::instance() { static DatabaseManager mgr; // C11 起线程安全的局部静态变量 return mgr; } bool DatabaseManager::open() { // 每个连接起一个独立的名字避免重复注册 QSqlDatabase db QSqlDatabase::contains(parking_main) ? QSqlDatabase::database(parking_main) : QSqlDatabase::addDatabase(QMYSQL, parking_main); db.setHostName(127.0.0.1); // 用 IP 而不是 localhost避开 socket 文件解析 db.setPort(3306); db.setDatabaseName(parking_system); db.setUserName(root); db.setPassword(123456); db.setConnectOptions(MYSQL_OPT_RECONNECT1); if (!db.open()) { qCritical() database open failed: db.lastError().text(); return false; } QSqlQuery query(db); query.exec(SET NAMES utf8mb4); return true; }有同学会问为什么不用连接池。连接池是给高并发 Web 后端用的QSqlDatabase 本身的连接复用在单客户端场景下已经够用强行引第三方连接池反而增加理解成本。你可以在答辩时说“当前单客户端场景用单例连接足够如果未来改成 C/S 多客户端再把连接层替换成连接池”这句话比真的写一个半吊子连接池更能说明设计意识。SET NAMES utf8mb4是为了让客户端和 MySQL 之间的字符集一致。如果不加这一句Qt 界面显示中文正常但写进数据库的是乱码查出来再显示又变乱码这一类问题全都可以归因于字符集不一致。4. 预约与充值的核心事务状态机、行锁和余额防负4.1 预约状态机五个状态和两条硬约束预约订单的状态不是随便改的需要有一个明确的流转规则。我设计成五个状态0 待支付、1 已预约、2 已入场、3 已离场、4 已取消。正常路径是 0→1→2→3异常路径是 1 或 0 都可以取消。除了状态本身系统里还有两条硬约束要时刻守住。第一条是同一时刻一个车位最多被一个订单预约第二条是会员余额不能因为扣费变成负数。这两条如果只靠界面上“先查一下有没有被占”来实现在两人同时点击时一定会翻车必须由数据库的事务和锁来保证。4.2 原子抢车位与扣费为什么先 UPDATE 再 INSERT预约的核心动作是“占车位 扣余额 生成订单”三件事要么全成功要么全失败必须放在同一个事务里。这里最容易写错的是先 SELECT 判断车位是否空闲再 UPDATE。两个客户端同时 SELECT 都看到空闲然后都 UPDATE就会出现重复预约。正确做法是直接用 UPDATE 带条件去抢// 预约下单整段包在事务里 bool reserveSpace(int memberId, int spaceId, const QDateTime bookTime, double amount, QString errMsg) { QSqlDatabase db DatabaseManager::instance()-database(); db.transaction(); // 1) 原子占位只有 status0 的车位能被抢到 QSqlQuery q(db); q.prepare(UPDATE parking_space SET status 1 WHERE id ? AND status 0); q.addBindValue(spaceId); if (!q.exec() || q.numRowsAffected() ! 1) { db.rollback(); errMsg 车位已被其他人预约请换一个车位; return false; } // 2) 扣减余额余额不足则回滚 q.prepare(UPDATE member SET balance balance - ? WHERE id ? AND balance ?); q.addBindValue(amount); q.addBindValue(memberId); q.addBindValue(amount); if (!q.exec() || q.numRowsAffected() ! 1) { db.rollback(); errMsg 余额不足请先充值; return false; } // 3) 生成订单 QString orderNo QString(P%1%2) .arg(QDateTime::currentDateTime().toString(yyyyMMddhhmmss)) .arg(QRandomGenerator::global()-bounded(1000, 9999)); q.prepare(INSERT INTO appointment (order_no, member_id, space_id, book_time, status, amount) VALUES (?, ?, ?, ?, 1, ?)); q.addBindValue(orderNo); q.addBindValue(memberId); q.addBindValue(spaceId); q.addBindValue(bookTime); q.addBindValue(amount); if (!q.exec()) { db.rollback(); errMsg 订单生成失败: q.lastError().text(); return false; } db.commit(); return true; }这段代码有三个关键点。第一占车位用UPDATE ... WHERE status 0InnoDB 会锁住命中行第二个事务执行同一条 UPDATE 时会阻塞等第一个事务提交后它拿到的是 status1影响行数为 0从而抢不到车位。这个机制比 SELECT 再判断可靠得多。第二扣款条件把balance ?写进 WHERE余额不够时影响行数是 0事务回滚余额永远不会变负数。第三事务中间的每一步失败都要 rollbackcommit 放在最后缺一不可。4.3 会员办理与充值缴费流水表保证每一笔钱都有据可查会员模块实际上由两个功能组成办理新会员和充值缴费。办理会员就是 INSERT 一条 member 记录这个简单。充值缴费则需要“写流水 改余额”两步一起做不然对账的时候不知道钱是怎么变的。// 充值插入流水 更新余额同一事务 bool recharge(int memberId, double amount, QString errMsg) { QSqlDatabase db DatabaseManager::instance()-database(); db.transaction(); QSqlQuery q(db); q.prepare(INSERT INTO recharge_log (order_no, member_id, amount) VALUES (?, ?, ?)); q.addBindValue(QString(R%1%2) .arg(QDateTime::currentDateTime().toString(yyyyMMddhhmmss)) .arg(QRandomGenerator::global()-bounded(1000, 9999))); q.addBindValue(memberId); q.addBindValue(amount); if (!q.exec()) { db.rollback(); errMsg 流水写入失败: q.lastError().text(); return false; } q.prepare(UPDATE member SET balance balance ? WHERE id ?); q.addBindValue(amount); q.addBindValue(memberId); if (!q.exec()) { db.rollback(); errMsg 余额更新失败: q.lastError().text(); return false; } db.commit(); return true; }为什么有了 member.balance 还要单独一张流水表因为余额只是一个结果流水是产生结果的过程。两笔充值先后到账如果只更新余额某天余额对不上时你完全无法判断是哪笔出了问题。有了充值流水再加上预约订单里的扣费记录就能用 SQL 把账重新算一遍这就是第 6 章要讲的对账方法。流水表的订单号同样要加唯一约束防止同一次充值被重复提交。4.4 状态流转入场、离场和取消的边界条件预约完成之后还需要提供入场、离场、取消三个操作。入场是把预约订单和车位状态一起更新离场是计算费用并完成扣款取消则是把订单置为已取消并释放车位。我一般会把每个状态流转写成一个独立的方法而不是用一个巨大的 switch 包揽。比如入场操作bool enterParking(int appointmentId, int spaceId, QString errMsg) { QSqlDatabase db DatabaseManager::instance()-database(); db.transaction(); QSqlQuery q(db); // 只有“已预约”状态的订单才能入场 q.prepare(UPDATE appointment SET status 2 WHERE id ? AND status 1); q.addBindValue(appointmentId); if (!q.exec() || q.numRowsAffected() ! 1) { db.rollback(); errMsg 订单状态不允许入场; return false; } // 车位同步置为占用 q.prepare(UPDATE parking_space SET status 2 WHERE id ?); q.addBindValue(spaceId); if (!q.exec()) { db.rollback(); errMsg 车位状态更新失败; return false; } db.commit(); return true; }这里用的是同一套逻辑每次状态更新都带上当前状态作为 WHERE 条件影响行数为 0 就说明状态已经变了直接报错。离场时除了把订单改成 3、车位改成 0还要再把该订单的 amount 作为扣费记录处理。整个系统的核心思想就是一句话条件写在 UPDATE 里判断交给影响行数而不是先查出来再判断。5. Qt MySQL 常见问题排查驱动、插件版本、socket 与影响行数5.1 QSqlDatabase: QMYSQL driver not loaded不是缺驱动是缺 dll现象程序一启动QSqlDatabase 报QSqlDatabase: QMYSQL driver not loaded或者Driver not loaded Driver not loaded。原因Qt 的 QMYSQL 插件是一个动态库它本身还需要依赖 MySQL 的客户端库 libmysql.dll。很多时候 Qt 装好了但 libmysql.dll 不在 Qt 的搜索路径里插件加载就失败。另外位数不一致也会导致加载失败Qt 是 64 位而 dll 是 32 位时同样报这个错。解决从 MySQL 安装目录的 lib 文件夹里找到 libmysql.dll复制到 Qt 的 bin 目录下和 Qt5Sql.dll 同级或者复制到 exe 同目录。如果还不行用 Dependency Walker 看 qsqlmysql.dll 缺什么依赖。课设环境里不要动 Qt 源码重新编译驱动耗时太长。5.2 cannot mix incompatible qt library (version ex50601) with this librar现象程序运行时弹窗报cannot mix incompatible qt library (version ex50601) with this library或者在编译链接阶段直接失败。版本号 ex50601 表示 Qt 5.6.1但这只是表面版本实际问题是 Qt 的二进制库混了。原因最常见的是 PATH 环境变量里残留了另一个版本的 Qt或者程序里同时链接了不同版本的 Qt 库。比如你用 Qt 5.15 编译出来的 exe运行时 PATH 里先找到了 Qt 5.6 的 Qt5Core.dll就会报版本不兼容。解决打开系统环境变量把 PATH 里的 Qt 目录调整到当前版本的最前面在 Qt Creator 里确认当前构建套件只有一个 qmake删除 build 目录重新 qmake。如果换过 Qt 版本一定要清理一遍编译缓存这个报错会像幽灵一样反复出现。5.3 error 2002 (HY000): cant connect to local mysql server through socket /tmp/mysql.sock现象Qt 程序连接 MySQL 时报错error 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock编译好的程序在自己电脑上好好的换个电脑就跑不起来。原因MySQL 服务没有启动或者 Qt 在 Unix 系统上把 localhost 解析成了 socket 连接而不是 TCP 连接。SOCKET 文件路径不对也会导致这个报错。解决连接参数里把主机名写成127.0.0.1而不是localhost这样会强制走 TCP启动 MySQL 服务Windows 下用net start mysql或者mysqld --consoleLinux 下用systemctl start mysqld确认端口 3306 没被防火墙拦。做完这三步90% 的 2002 都能解决。5.4 MySQL 8.0 的 caching_sha2_password 认证插件Qt 5.15 驱动直接不认现象Qt 程序连 MySQL 8.0 时报错Authentication plugin caching_sha2_password cannot be loaded或者驱动加载成功但连接被拒绝。原因MySQL 8.0 默认的认证插件是 caching_sha2_password而 Qt 5.15 自带的 QMYSQL 驱动是为 mysql_native_password 设计的不支持这个新插件。和 5.1 的 dll 缺失不是同一个问题。解决把 MySQL 的 root 账号认证方式改回旧插件在 MySQL 命令行执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456;然后FLUSH PRIVILEGES;。如果你后续要把系统部署到生产环境建议反过来升级 Qt 驱动而不是降低 MySQL 安全等级但课设答辩用这个办法最省时间。5.5 预约扣款的 numRowsAffected 判空翻车MySQL 默认统计的是“变更行”而不是“匹配行”现象预约代码在本地测试没问题但某人充值 0 元或者重复点击时明明 UPDATE 执行成功了numRowsAffected()返回 0导致判断成“余额不足”。原因MySQL 默认返回的是“变更行数”不是“匹配行数”。比如UPDATE member SET balance balance - 0 WHERE id 1 AND balance 0匹配到了行但因为余额没变化MySQL 认为影响行数是 0。Qt 的numRowsAffected()拿到的就是这个值。解决第一种办法是业务上禁止 0 金额预约在下单前判断amount 0第二种办法是在连接参数里加上CLIENT_FOUND_ROWS让 UPDATE 返回匹配行数而不是变更行数。注意setConnectOptions未必能直接传这个 flag最稳的做法是业务层判断不依赖这个返回值判断唯一结果。每次扣款金额都是正数时变更行数等于匹配行数这个坑就不会触发。6. 验收前的最后打磨并发模拟、对账 SQL 和 QtTest 冒烟先把最简单也最重要的验证方法说清楚打开两个客户端实例用两个不同账号同时预约同一个车位观察是否只有一个能成功。你会发现按照第 4 章的写法第二个客户端会在“车位已被其他人预约”处停下来这就是行锁生效的直接证据。这个操作只需要两个窗口不需要写任何并发测试代码但答辩演示效果比口述强得多。对账是体现系统可靠性的另一个亮点。所有充值和消费都有流水那我就能写一条 SQL 校验余额对不对把“理论余额”和“实际余额”不一致的会员查出来SELECT m.id, m.login_name, m.balance, (SELECT COALESCE(SUM(r.amount),0) FROM recharge_log r WHERE r.member_id m.id) - (SELECT COALESCE(SUM(a.amount),0) FROM appointment a WHERE a.member_id m.id AND a.status IN (2,3)) AS calc_balance FROM member m HAVING m.balance calc_balance;查询结果为空说明每一笔充值和扣费都正确地反映到了余额上。这条 SQL 可以直接演示给评审老师看它会证明你的数据不只是“能存进去”而是“存进去之后依然可核对”。最后一个技巧是用 QtTest 模拟鼠标点击事件做冒烟测试。登录、预约、充值这几个主流程写成一个自动化脚本每次改完代码跑一遍能省下大量重复手工点击的时间#include QtTest/QTest void smokeTest() { QTest::mouseClick(loginWindow.btnLogin, Qt::LeftButton); QTest::qWait(200); QTest::mouseClick(mainWindow.btnReserve, Qt::LeftButton); QTest::qWait(200); QTest::mouseClick(mainWindow.btnConfirm, Qt::LeftButton); }QTest 的 mouseClick 会真的往控件上发送鼠标消息模拟的是用户点击行为不是直接调用业务函数所以能验证信号槽链路是否完整。我个人的习惯是每次大改之后先跑一遍冒烟再手工走一遍完整流程最后把对账 SQL 跑一遍。三件事全过这个系统才敢提交。希望这套思路能帮你在课设或毕设里少走弯路把时间花在真正有技术含量的地方。本文还有配套的精品资源点击获取