简介:基于Qt与MySQL开发的预约停车管理系统源码,完整包含停车位预约、会员办理和充值缴费三大核心功能,适合作为毕业设计、课程设计及项目开发参考。面向计算机相关专业学生与初级开发者,既能用于课程作业的完整交付,也能帮助学习C++/Qt界面编程与MySQL数据库联动开发。压缩包共34个文件,涵盖9个C++源文件、8个Qt界面文件、8个头文件以及1个SQL数据库脚本,另附界面背景图与Qt资源文件,整体仅185KB,结构简洁,导入Qt工程后即可快速浏览并编译运行。目前已有56人学习下载,代码经过严格测试,可放心运行验证。借助这套源码,可以快速理清会员管理、充值缴费、车位预约等业务的数据表设计与界面交互逻辑,节省从零搭建项目的时间,对课程答辩或项目演示也具有较强的参考价值。
1. 为什么把「预约停车+会员办理+充值缴费」做成一个闭环节点?
很多人拿到这个题目,第一反应是“Qt做界面不难,MySQL建表更不难”,真正动手才发现:会员办卡时余额没写进流水、预约车位时两个窗口同时抢同一个车位、缴费后订单状态和余额对不上。这些不是界面问题,而是数据模型与事务边界问题。这个题目最值得做的点,是把预约停车、会员办理、充值缴费串成一条完整业务链路,而不是三个独立的CRUD界面。适合正在做毕业设计/课程设计的学生,也适合想把课程作业扩展成可演示demo的开发者。下文按“建表→连接→界面→业务→排错”的顺序,给出一套能直接复现的Qt5 + MySQL8方案。
2. 先把数据模型立住:六张表加一个Qt最小连接代码
2.1 预约停车、会员充值缴费最少需要哪几张表
别急着写UI,先想数据模型。预约停车系统做毕业设计至少要六张表:用户表管登录,会员表管余额和身份,车位表管占用状态,预约订单表管预约时段,充值记录表管钱怎么进来,缴费记录表管钱怎么扣掉。再加一张业务日志表,后面排查问题会省很多时间。
我见过不少同学把余额直接写成用户表的一个字段,还把预约记录和缴费记录合成一张表。短期看省事,等你做“今日充值总额”“某会员消费明细”这些统计时,会发现少一张流水表,数据怎么都对不上。原因很简单:余额是一个冗余汇总值,充值/缴费才是事实日志。保留收入流水,才能在账目不平的时候找到错在哪里。
下面的建表SQL可以直接在MySQL Workbench或命令行执行。注意所有业务表都用InnoDB引擎,因为MyISAM不支持事务,预约抢车位的场景必须靠事务兜底。
CREATE DATABASE IF NOT EXISTS parking_db DEFAULT CHARACTER SET utf8mb4; USE parking_db; -- 用户表(登录用) CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password_md5 VARCHAR(64) NOT NULL, role TINYINT NOT NULL DEFAULT 0, -- 0普通用户,1管理员 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 会员表:余额是汇总字段,流水表才是事实 CREATE TABLE t_member ( member_id VARCHAR(20) PRIMARY KEY, user_id INT NOT NULL, real_name VARCHAR(30) NOT NULL, phone VARCHAR(20) NOT NULL, balance DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 1, -- 1有效,0冻结 FOREIGN KEY (user_id) REFERENCES t_user(id) ) ENGINE=InnoDB; -- 车位表 CREATE TABLE t_space ( space_id INT PRIMARY KEY AUTO_INCREMENT, space_no VARCHAR(20) NOT NULL UNIQUE, area VARCHAR(50), status TINYINT NOT NULL DEFAULT 0, -- 0空闲,1已预约,2长租 updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 预约订单表 CREATE TABLE t_reserve ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, member_id VARCHAR(20), space_id INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, amount DECIMAL(10,2) DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0, -- 0预约中,1已入场,2已完成,3已取消 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (member_id) REFERENCES t_member(member_id), FOREIGN KEY (space_id) REFERENCES t_space(space_id) ) ENGINE=InnoDB; -- 充值记录表 CREATE TABLE t_recharge ( id INT PRIMARY KEY AUTO_INCREMENT, member_id VARCHAR(20), amount DECIMAL(10,2) NOT NULL, pay_type TINYINT DEFAULT 1, -- 1现金,2在线 operator VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (member_id) REFERENCES t_member(member_id) ) ENGINE=InnoDB; -- 缴费记录表 CREATE TABLE t_payment ( id INT PRIMARY KEY AUTO_INCREMENT, reserve_id INT NOT NULL, member_id VARCHAR(20), amount DECIMAL(10,2) NOT NULL, payment_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (reserve_id) REFERENCES t_reserve(id) ) ENGINE=InnoDB;这套表结构里最容易被忽视的是t_recharge和t_payment。很多同学觉得充值和缴费就是改一下余额字段,但课程设计答辩时,老师大概率会问“你如何统计今天收入多少”“你如何证明余额没算错”。没有流水表,这两个问题是答不圆的。另外,金额字段用了DECIMAL(10,2),这是有意的:FLOAT/DOUBLE在计算时会丢掉精度,导致充100元显示成99.99999,这种问题极难排查。
2.2 用QMYSQL驱动连接MySQL:最小可用连接代码
Qt连接MySQL最常用的是QSqlDatabase的QMYSQL驱动,而不是通过ODBC中转。ODBC也能连,但多一层配置,换机器容易断。先在.pro文件里加上QT += sql。
#include <QSqlDatabase> #include <QSqlQuery> #include <QSqlError> #include <QDebug> bool initDatabase() { QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL"); db.setHostName("127.0.0.1"); // 避免用localhost,Windows下容易走socket导致连接失败 db.setPort(3306); db.setDatabaseName("parking_db"); db.setUserName("parking_user"); db.setPassword("your_password"); if (!db.open()) { qDebug() << "open failed:" << db.lastError().text(); return false; } // 关键:让连接按utf8mb4传输,避免Qt端中文乱码 QSqlQuery query(db); query.exec("SET NAMES utf8mb4"); return true; }这段代码有两点值得说明。第一,setHostName应使用127.0.0.1而不是localhost,因为某些MySQL客户端配置下,localhost会尝试走Unix socket,Qt的MySQL驱动对socket支持并不友好,报错信息通常是Can't connect to local MySQL server through socket。第二,SET NAMES utf8mb4必须在open()成功之后执行,它告诉MySQL“这条连接后面传过来的字符都是UTF-8”,否则中文会在写入和读取时变成问号。
推荐先用MySQL Workbench或命令行客户端建好库和表,再写Qt代码。如果Qt程序第一次启动就报driver not loaded,说明Qt自带的MySQL驱动插件没装好,这个坑在避坑章节专门讲。
2.3 车位状态字段的边界设计
status字段的0/1/2含义必须固定。有的系统把车位状态写成字符串,导致预约判断时还要拼“空闲/预约/长租”,容易拼错。推荐用TINYINT,0空闲、1已预约、2长租,这样更新时可以直接用status=0作为条件,性能好且不会产生歧义。
预约订单的状态同样用数字:0预约中、1已入场、2已完成、3已取消。注意增加字段时不要用MySQL保留字,比如order是保留字,所以订单表里用了order_no而不是order,这是一个常见新手坑。
另一个边界是车位号space_no的唯一性。如果车场有A区、B区,很多同学直接写A001、B001,然后忘记加UNIQUE,结果两条重复车位记录让界面列表错乱。建表SQL中已经用UNIQUE约束兜住。字段长度也要留够:member_id建议用20位字符串,前面留出前缀,比如M202506150001,比纯数字好看也方便区分。
3. 从登录到会员办理:Qt界面层与业务逻辑接口要分离
3.1 登录窗口的校验流程
很多课程设计的登录代码是直接在按钮槽里拼SQL字符串:
QString sql = "SELECT * FROM user WHERE name='" + ui->lineEdit->text() + "'";这样写,别说不安全,单是用户名输入引号就能让程序报错退出。正确做法是用QSqlQuery的prepare+addBindValue。
先设计接口:登录按钮点击后,调用一个独立的checkUser函数,只负责查数据库,不碰界面。界面层根据返回值决定跳到主窗口还是弹错误框。这样以后想换密码算法或加验证码,只改checkUser即可。
#include <QCryptographicHash> QString hashMd5(const QString &plain) { return QCryptographicHash::hash(plain.toUtf8(), QCryptographicHash::Md5).toHex(); } bool checkUser(const QString &username, const QString &passwordMd5, QString &role) { QSqlQuery query; query.prepare("SELECT id, role FROM t_user WHERE username = ? AND password_md5 = ?"); query.addBindValue(username); query.addBindValue(passwordMd5); if (!query.exec()) { qDebug() << "login query error:" << query.lastError().text(); return false; } if (query.next()) { role = query.value(1).toString(); return true; } return false; }参数绑定是防止SQL注入的最基本手段。SQL里两个?占位符,分别绑定用户名和密码摘要,即使用户名输入' OR 1=1 --,也只会当作普通字符串处理。密码不要存明文,哪怕课程设计也要展示“不是明文存储”。hashMd5里把输入QString先转成UTF-8再取摘要,是因为MySQL端密码字段也是VARCHAR(64)的十六进制,两者必须一致。
登录界面代码简化为这样:
void LoginDialog::on_btnLogin_clicked() { QString username = ui->editUsername->text().trimmed(); QString password = ui->editPassword->text(); QString role; bool ok = checkUser(username, hashMd5(password), role); if (!ok) { QMessageBox::warning(this, "登录失败", "用户名或密码错误"); return; } MainWindow *w = new MainWindow(role); w->show(); this->close(); }注意username做了trimmed(),但密码没有。如果密码首尾有空格,trim掉会让用户输入的密码失配,这个问题我踩过,所以密码只取原始值,不做trim。
3.2 会员办理时“一张会员卡+一条充值流水”必须同时落库
会员办理功能不只是INSERT一条会员记录。很多需求要求开卡时预充值,比如充100送20,或者最少充值50。如果只插一条会员记录,不写充值记录,那么“今日充值总额”统计就会漏掉开卡这部分钱。所以在createMember里把两件事打包成一个事务。
bool createMember(const QString &memberId, int userId, const QString &name, const QString &phone, double initialBalance) { QSqlDatabase db = QSqlDatabase::database(); db.transaction(); // 1. 插入会员主记录 QSqlQuery q1(db); q1.prepare("INSERT INTO t_member (member_id, user_id, real_name, phone, balance) " "VALUES (?, ?, ?, ?, ?)"); q1.addBindValue(memberId); q1.addBindValue(userId); q1.addBindValue(name); q1.addBindValue(phone); q1.addBindValue(initialBalance); if (!q1.exec()) { db.rollback(); qDebug() << "create member failed:" << q1.lastError().text(); return false; } // 2. 如果开卡时有初始充值,同时写充值流水 if (initialBalance > 0) { QSqlQuery q2(db); q2.prepare("INSERT INTO t_recharge (member_id, amount, operator) " "VALUES (?, ?, ?)"); q2.addBindValue(memberId); q2.addBindValue(initialBalance); q2.addBindValue(QString("ADMIN")); if (!q2.exec()) { db.rollback(); return false; } } db.commit(); return true; }这段代码的重点是事务不要漏掉第二张流水表。我见过不少实现里会员表和充值表分开两个函数调用,界面层先后调用createMember()和addRecharge(),结果第一个成功第二个失败,数据库里留下一个没有初始余额的会员。事务里的两步要么都成功,要么都回滚,界面层不需要关心顺序。调用时传入的initialBalance是double,但SQL绑定后MySQL会按DECIMAL精度处理,不会出现浮点尾差。
3.3 会员余额刷新:查询而不是靠界面本地累加
会员充值或缴费后,主界面显示的余额经常迟迟不变。常见做法是拿本地double变量加减,然后刷新label。这个做法在单机演示没问题,但一旦两个人同时在两个窗口操作同一会员,本地变量就是错的。
正确姿势是每次进入会员详情、办理充值/缴费成功后,都重新查一次数据库里的balance并setText。写一个loadMemberBalance成员函数:
QString loadMemberBalance(const QString &memberId) { QSqlQuery query; query.prepare("SELECT balance FROM t_member WHERE member_id = ?"); query.addBindValue(memberId); if (query.exec() && query.next()) { return query.value(0).toString(); } return QString("0.00"); }这里故意不做toDouble()再转字符串,而是直接取QVariant的字符串形式。DECIMAL字段在Qt里转字符串会保留两位小数,比如100.00;如果中间做了double转换,再QString::number,可能因为格式设置输出成100,导致界面上看起来余额缺失小数位。这个函数放在会员详情页、充值成功弹窗关闭后、缴费成功页面刷新时各调用一次,保证界面永远和数据一致。
4. 预约停车与充值缴费:两个必须靠事务兜底的联动场景
4.1 预约车位的“先查后插”会让并发翻车
预约停车最怕同时两个人选到同一车位。大部分学生的第一版代码是:
SELECT * FROM t_space WHERE space_id=? AND status=0 if (有结果) { INSERT INTO t_reserve ...; UPDATE t_space SET status=1 ...; }这个流程在单机点击时没问题,但一旦部署成多人访问(或者课程答辩时两台电脑同时操作),两次查询都可能读到status=0,结果两个人都insert成功,车位状态变成1,一个订单完全丢失。
解决办法不是加锁(那会把界面锁死),而是把“抢占车位”变成一个原子更新:直接执行UPDATE t_space SET status=1 WHERE space_id=? AND status=0,然后看numRowsAffected()是不是1。如果是1,说明这一行车位的状态是从0变成1的,抢成功;如果是0,说明别人已经改成1,抢失败。这一步在任何数据库事务里都安全,因为UPDATE语句本身自带行锁。
QString generateOrderNo() { return QString("R%1%2").arg(QDateTime::currentDateTime().toString("yyyyMMddHHmmsszzz")) .arg(qrand() % 1000); } bool reserveSpace(int spaceId, const QString &memberId, const QDateTime &start, const QDateTime &end) { QSqlDatabase db = QSqlDatabase::database(); db.transaction(); // 1. 条件更新车位状态:只有当status=0空闲时才能抢到 QSqlQuery q1(db); q1.prepare("UPDATE t_space SET status=1 WHERE space_id=? AND status=0"); q1.addBindValue(spaceId); if (!q1.exec()) { db.rollback(); return false; } if (q1.numRowsAffected() != 1) { // 没更新到一行,说明车位已经被占用 db.rollback(); return false; } // 2. 生成预约订单 QSqlQuery q2(db); q2.prepare("INSERT INTO t_reserve (order_no, member_id, space_id, start_time, end_time, status) " "VALUES (?, ?, ?, ?, ?, 0)"); q2.addBindValue(generateOrderNo()); q2.addBindValue(memberId); q2.addBindValue(spaceId); q2.addBindValue(start.toString("yyyy-MM-dd HH:mm:ss")); q2.addBindValue(end.toString("yyyy-MM-dd HH:mm:ss")); if (!q2.exec()) { db.rollback(); return false; } db.commit(); return true; }这段代码里generateOrderNo用了当前时间到毫秒再加随机数,课程设计足够用。不要用id字段当订单号,因为预约单要展示给用户,递增数字容易被猜到业务量。numRowsAffected()是判断抢占成功的唯一依据,必须在同一个db对象上执行,不能新建QSqlQuery query;而不指定连接,否则可能走另一个默认连接,事务隔离就失效了。
4.2 充值缴费必须用事务:流水和余额不能分开落库
充值功能看起来像一次UPDATE余额。但如果你只UPDATE余额,不写充值表,后面每天的“充值总额”统计就会变成拍脑袋数据。反过来,只写流水不更新余额,会员余额是欠的。
把“插入充值流水”和“更新余额”放进同一个事务。常见错误是事务里第一条语句执行失败后没有rollback,Qt会一直顶着这个失败事务,后续查询全部报错。所以写一个辅助函数判断每一步是否成功,失败立即rollback并返回false。
bool recharge(const QString &memberId, double amount) { QSqlDatabase db = QSqlDatabase::database(); db.transaction(); // 1. 写充值流水 QSqlQuery q1(db); q1.prepare("INSERT INTO t_recharge (member_id, amount, operator) " "VALUES (?, ?, ?)"); q1.addBindValue(memberId); q1.addBindValue(amount); q1.addBindValue(QString("OPERATOR")); if (!q1.exec()) { db.rollback(); qDebug() << "insert recharge failed:" << q1.lastError().text(); return false; } // 2. 更新余额:在原值上累加,而不是先读出再更新 QSqlQuery q2(db); q2.prepare("UPDATE t_member SET balance = balance + ? WHERE member_id = ?"); q2.addBindValue(amount); q2.addBindValue(memberId); if (!q2.exec() || q2.numRowsAffected() != 1) { db.rollback(); return false; } db.commit(); return true; }为什么不用“先SELECT余额,再UPDATE余额”?因为这两条语句之间有时间差,另一个窗口可能刚刚也完成了一笔充值。如果在SELECT之后、UPDATE之前,另一笔充值已经提交,当前UPDATE会用旧余额覆盖新余额,导致充值丢失。直接用balance + ?是在数据库行内做原子累加,天然避免并发覆盖。numRowsAffected() != 1用来判断会员是否存在,避免插入了一条没有主表对应的孤儿流水。
4.3 缴费时余额扣减用“条件更新”防负余额
会员缴费扣费是第二个并发集中区。如果用户故意同时提交两个缴费请求,或者界面卡顿后连点两次,余额会被扣成负数。解决办法是在UPDATE余额时把余额够不够作为条件。
bool payForReserve(int reserveId, double amount) { QSqlDatabase db = QSqlDatabase::database(); db.transaction(); // 先查出这笔预约对应的会员 QSqlQuery q0(db); q0.prepare("SELECT member_id, space_id FROM t_reserve WHERE id = ? AND status IN (0,1)"); q0.addBindValue(reserveId); if (!q0.exec() || !q0.next()) { db.rollback(); return false; } QString memberId = q0.value(0).toString(); int spaceId = q0.value(1).toInt(); // 扣余额:只有balance >= amount时才扣,避免负余额 QSqlQuery q1(db); q1.prepare("UPDATE t_member SET balance = balance - ? " "WHERE member_id = ? AND balance >= ?"); q1.addBindValue(amount); q1.addBindValue(memberId); q1.addBindValue(amount); if (!q1.exec() || q1.numRowsAffected() != 1) { db.rollback(); return false; // 余额不足或会员被冻结 } // 更新预约订单状态为已完成 QSqlQuery q2(db); q2.prepare("UPDATE t_reserve SET status=2 WHERE id=?"); q2.addBindValue(reserveId); if (!q2.exec()) { db.rollback(); return false; } // 写缴费流水 QSqlQuery q3(db); q3.prepare("INSERT INTO t_payment (reserve_id, member_id, amount) " "VALUES (?, ?, ?)"); q3.addBindValue(reserveId); q3.addBindValue(memberId); q3.addBindValue(amount); if (!q3.exec()) { db.rollback(); return false; } // 释放车位:缴费完成,车位回到空闲 QSqlQuery q4(db); q4.prepare("UPDATE t_space SET status=0 WHERE space_id=?"); q4.addBindValue(spaceId); if (!q4.exec()) { db.rollback(); return false; } db.commit(); return true; }这里最好解释一下UPDATE ... WHERE member_id=? AND balance>=?的作用。它不只是“检查余额够不够”,而是把检查动作和扣款动作合并成一个原子步骤。任何并发请求到达数据库时,同一行会员记录会被行锁串行处理,后到的扣款会看到更新后的余额,从而避免两个请求都判定“余额够”然后扣成负数。这在MySQL的InnoDB引擎下是可靠行为,也是事务内写操作自带的行锁。注意扣款成功后,车位释放和订单更新必须在同一个事务里,否则会出现“钱扣了,车位还占着”的脏数据。
4.4 缴费后还要释放车位
我见过不少系统缴费后忘了把车位状态改回0,结果一个车位预约一次就永远不能再用。所以在事务里更新订单状态为completed的同时,要顺带UPDATE t_space SET status=0 WHERE space_id=?。这个动作与扣款同事务,避免扣款成功但车位没释放,或车位释放了但扣款失败。
状态机的流转要统一规划:预约时车位status=1;缴费完成后车位status=0。如果系统以后要支持“超时取消预约”,需要把订单从status=0改成status=3,同时把车位释放。建议把状态流转集中写在同一个Dao层函数里,不要在Qt界面上用多条独立SQL拼逻辑。比如你写一个finishReserve(int reserveId),里面只做扣费、状态更新、车位释放,界面层永远不要直接在槽函数里写三条UPDATE,那样一旦中途失败,很难定位是哪个环节出了问题。
5. 避坑:Qt连MySQL的5个常见问题与排查顺序
5.1 程序启动报“QSqlDatabase: QMYSQL driver not loaded”
现象:程序能编译,但运行时db.open()失败,QSqlDatabase::drivers()里看不到QMYSQL。
原因:Qt安装包默认不带MySQL驱动插件,或者MySQL客户端的libmysql.dll不在PATH里。Qt的数据库驱动是插件机制,光有QSqlDatabase没有qsqlmysql.dll一样白搭。
解决:先确认Qt安装目录的plugins/sqldrivers下有没有qsqlmysql.dll。没有的话,需要用Qt对应MSVC版本的编译工具链编译mysql插件,或者下载与你Qt版本完全一致的预编译插件。Windows上最简单做法:把MySQL安装目录里lib/libmysql.dll拷贝到exe同一目录,并用windeployqt把Qt插件带上。如果是MinGW版Qt和MSVC版MySQL混用,驱动加载会失败。经验是不要直接拿别人的qsqlmysql.dll拷到自己Qt目录,必须与Qt版本、编译器完全匹配,否则会报cannot mix incompatible Qt library的错误。
5.2 MySQL8默认认证插件与Qt旧版驱动冲突
现象:连接时提示Authentication plugin 'caching_sha2_password' cannot be loaded。
原因:MySQL8默认创建用户使用caching_sha2_password,而Qt5早期版本自带驱动只支持mysql_native_password。
解决:创建应用专用账号时指定插件。在MySQL命令行执行:
CREATE USER 'parking_user'@'%' IDENTIFIED WITH mysql_native_password BY '密码'; GRANT ALL PRIVILEGES ON parking_db.* TO 'parking_user'@'%'; FLUSH PRIVILEGES;如果服务器不允许换插件,可以升级Qt到6.2以上,新版Qt MySQL驱动已经支持caching_sha2_password。做课程设计时为了减少折腾,直接用mysql_native_password是最省时间的。注意parking_user不要用root连接应用,root账号权限太宽,万一项目被扫描到,风险很大。
5.3 中文乱码:界面显示正常但表里是问号,或倒过来
现象:Qt界面输入中文,MySQL表里存成???;或者数据库里中文正常,Qt查出来乱码。
原因:客户端和服务的字符集不一致。MySQL8默认字符集是utf8mb4,但Qt连接对应的会话字符集可能仍是latin1,导致写入时中文被转成问号。
解决:在open()成功后立刻执行SET NAMES utf8mb4;建库时指定DEFAULT CHARACTER SET utf8mb4;表中的VARCHAR字段也统一utf8mb4。Qt侧保存QString,绑定参数时Qt驱动会自动转码,但前提是连接字符集设置正确。另外,用MySQL Workbench手动插入数据时,也要把连接字符集设为utf8mb4,否则数据本身入库前已经变成乱码,Qt这边怎么修都修不回来。
5.4 事务提交不生效:明明commit了,数据还是没写进去
现象:调用transaction()后执行多条SQL,最后commit(),重新查询发现部分数据没有落库。
原因:第一,表引擎是MyISAM,只有InnoDB才支持事务;第二,在Qt里,查询和写库没有使用同一个QSqlDatabase连接对象,比如某条SQL用默认连接,另一条用新建连接;第三,某条语句失败时没有rollback,事务一直处于失败状态,commit被MySQL拒绝。
解决:建表时明确写ENGINE=InnoDB;所有业务SQL都通过QSqlDatabase::database()获取同一连接对象来创建QSqlQuery;每条关键语句检查返回值,失败立即rollback。另外,如果你在事务里执行了SELECT,MySQL默认是快照读,不会加锁;但写操作会加行锁。所以并发场景下的安全不能靠SELECT保证,要用条件更新或FOR UPDATE。
5.5 两个窗口同时抢同一个车位,订单重复
现象:测试时开两个Qt窗口同时点“预约”,两个都提示成功,订单表多了两条相同车位和重叠时段的记录。
原因:你写的“先SELECT再INSERT”存在竞态窗口。Qt单线程并不能自动保证数据库并发安全,它只是加速了请求到达数据库,真正的问题还是业务SQL没有原子性。
解决:采用4.1的条件更新抢占车位,检查numRowsAffected()。要更稳妥,可以给t_reserve加一个唯一索引(space_id, start_time, end_time, status),让数据库在订单层面兜底。不过要注意,订单状态变成已完成之后,如果同一车位同一时段再来一条,唯一索引会挡住。所以这个索引只适合“同一时段同一车位不允许重复预约”的需求。简单业务下,条件更新已经足够,额外加索引会让学生答辩时多一个亮点。
6. 从“课程设计”变成“可交付系统”:日志、对账与打包验证
课程设计做完能跑只是第一步,能不能让老师相信“这个系统逻辑严谨”,需要再加三样东西:业务日志、对账SQL、打包回归清单。
先加一张业务日志表,把所有关键写操作统一记录。设计如下:
CREATE TABLE t_biz_log ( id INT PRIMARY KEY AUTO_INCREMENT, log_type VARCHAR(20) NOT NULL, -- RECHARGE / PAY / RESERVE ref_no VARCHAR(32) NOT NULL, -- 订单号或会员ID before_value VARCHAR(64), -- 操作前余额或状态 after_value VARCHAR(64), -- 操作后余额或状态 created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB;在recharge()和payForReserve()的commit之后各插一条日志。注意日志写入不要放进主事务里,否则日志表失败会让主业务回滚;正确做法是主事务提交后,再单独insert日志,即使日志插入失败也不影响业务。
当日对账是证明系统没算错钱的最直接方式。用一条SQL汇总当日充值、缴费和当前总余额:
SELECT (SELECT IFNULL(SUM(amount),0) FROM t_recharge WHERE DATE(created_at)=CURDATE()) AS today_recharge, (SELECT IFNULL(SUM(amount),0) FROM t_payment WHERE DATE(payment_time)=CURDATE()) AS today_payment, (SELECT IFNULL(SUM(balance),0) FROM t_member) AS total_balance;验证逻辑:如果系统从昨天到今天的会员余额变化量,不等于today_recharge - today_payment,说明有笔流水没落库或者余额更新错误。这个SQL可以写成一个Qt按钮“每日对账”,点击后把三个数字显示在窗口上。答辩时,老师会认为你不是只做了界面,而是真的考虑过资金安全。
发布前用windeployqt打包exe时,记得手动把libmysql.dll放到exe目录。U盘拷贝到没有Qt环境的机器上跑通以下回归就够了:
| 步骤 | 操作 | 预期 |
|---|---|---|
| 1 | 注册新用户并开卡充值100元 | 会员表出现记录,余额显示100.00 |
| 2 | 预约A001车位一小时 | 车位状态变为已预约,订单状态预约中 |
| 3 | 进入订单缴费,余额支付 | 余额扣除成功,车位释放,订单已完成 |
| 4 | 再次预约同一车位 | 第二笔预约成功,车位状态正常 |
| 5 | 点击每日对账 | 当日充值=100,缴费=停车费,总余额=100-停车费 |
我做这类系统时吃过没开事务的亏:早期版本缴费是先扣费再释放车位,结果扣费成功但车位释放SQL失败,后台一堆脏车位,后来强制所有写操作走同一入口,每笔流水都写日志,再没出现过这种问题。这套路对Qt+MySQL同样适用,希望帮到你。
本文还有配套的精品资源,点击获取