ARTICLE DETAIL

资讯详情

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

Qt5+MySQL8预约停车与会员充值系统:事务设计与并发控制实战

Qt5+MySQL8预约停车与会员充值系统:事务设计与并发控制实战 简介基于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 ) ENGINEInnoDB; -- 会员表余额是汇总字段流水表才是事实 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) ) ENGINEInnoDB; -- 车位表 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 ) ENGINEInnoDB; -- 预约订单表 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) ) ENGINEInnoDB; -- 充值记录表 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) ) ENGINEInnoDB; -- 缴费记录表 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) ) ENGINEInnoDB;这套表结构里最容易被忽视的是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); // 避免用localhostWindows下容易走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 socketQt的MySQL驱动对socket支持并不友好报错信息通常是Cant 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含义必须固定。有的系统把车位状态写成字符串导致预约判断时还要拼“空闲/预约/长租”容易拼错。推荐用TINYINT0空闲、1已预约、2长租这样更新时可以直接用status0作为条件性能好且不会产生歧义。预约订单的状态同样用数字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的prepareaddBindValue。先设计接口登录按钮点击后调用一个独立的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 11 --也只会当作普通字符串处理。密码不要存明文哪怕课程设计也要展示“不是明文存储”。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 status0 if (有结果) { INSERT INTO t_reserve ...; UPDATE t_space SET status1 ...; }这个流程在单机点击时没问题但一旦部署成多人访问或者课程答辩时两台电脑同时操作两次查询都可能读到status0结果两个人都insert成功车位状态变成1一个订单完全丢失。解决办法不是加锁那会把界面锁死而是把“抢占车位”变成一个原子更新直接执行UPDATE t_space SET status1 WHERE space_id? AND status0然后看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. 条件更新车位状态只有当status0空闲时才能抢到 QSqlQuery q1(db); q1.prepare(UPDATE t_space SET status1 WHERE space_id? AND status0); 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余额不写充值表后面每天的“充值总额”统计就会变成拍脑袋数据。反过来只写流水不更新余额会员余额是欠的。把“插入充值流水”和“更新余额”放进同一个事务。常见错误是事务里第一条语句执行失败后没有rollbackQt会一直顶着这个失败事务后续查询全部报错。所以写一个辅助函数判断每一步是否成功失败立即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 status2 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 status0 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 status0 WHERE space_id?。这个动作与扣款同事务避免扣款成功但车位没释放或车位释放了但扣款失败。状态机的流转要统一规划预约时车位status1缴费完成后车位status0。如果系统以后要支持“超时取消预约”需要把订单从status0改成status3同时把车位释放。建议把状态流转集中写在同一个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拒绝。解决建表时明确写ENGINEInnoDB所有业务SQL都通过QSqlDatabase::database()获取同一连接对象来创建QSqlQuery每条关键语句检查返回值失败立即rollback。另外如果你在事务里执行了SELECTMySQL默认是快照读不会加锁但写操作会加行锁。所以并发场景下的安全不能靠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 ) ENGINEInnoDB;在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.002预约A001车位一小时车位状态变为已预约订单状态预约中3进入订单缴费余额支付余额扣除成功车位释放订单已完成4再次预约同一车位第二笔预约成功车位状态正常5点击每日对账当日充值100缴费停车费总余额100-停车费我做这类系统时吃过没开事务的亏早期版本缴费是先扣费再释放车位结果扣费成功但车位释放SQL失败后台一堆脏车位后来强制所有写操作走同一入口每笔流水都写日志再没出现过这种问题。这套路对QtMySQL同样适用希望帮到你。本文还有配套的精品资源点击获取
返回列表