☰
基于QT与SQLite的停车场管理系统实现与避坑指南
2026/10/22 19:56:53 网站建设 项目流程

简介:这是一份基于QT框架与C++实现的小型停车场管理系统毕业设计项目,面向计算机相关专业学生,适合作为课程设计、毕业设计或桌面应用与数据库整合开发的入门参考。压缩包共44个文件,包含11个.h头文件、11个.cpp源文件、3个.ui界面定义、可执行exe以及项目工程与说明文档,整体仅1.81MB,目录结构清晰,便于按模块阅读与二次开发。系统涉及车辆进出登记、车位状态监控与费用计算等核心逻辑,可重点参考QT界面的控件布局、SQLite或MySQL表结构设计以及队列处理流程,帮助理解从需求分析、界面设计到编码实现的完整软件工程过程。已有732人浏览学习。

1. 基于QT实现的简单的停车场管理系统zip:不是作业,是一个能落地的本地工具

基于QT实现的简单的停车场管理系统.zip,名字像课程作业包,对应的需求却很真实:小区出入口、单位内部停车场、园区临时点,几十个车位,不需要云平台,只需要一台能开机的旧电脑和一套不算错账的本地软件。

常见做法是 QT + SQLite 桌面方案:QT管界面交互,SQLite管数据落地,不用装数据库服务,拷贝即用。进场登记、出场结算、计费、历史查询,几百行代码就能覆盖,界面和数据都在本地,后续加功能不用动底层。

有C++基础或懂信号槽的人按本文能完整做出来;只会拖控件,照步骤也能半天跑通。下面把拆解路径、核心代码、参数调法和踩坑记录讲清楚。

2. 动手前先定三件事:QT版本、SQLite表结构、项目骨架

拿到一个 .zip 源码包,第一步先看里面有没有 .pro 文件。有 .pro 说明是 qmake 工程,Qt Creator 直接打开;没有就找 CMakeLists.txt。这个判断决定了你不会在打开工程这一步反复试错。

我一般推荐这种小体量项目直接用 qmake,一个 .pro 文件写清楚源文件、头文件和依赖模块就够了,调试输出路径也直观。CMake 适合条件编译和打包发布的大工程,对停车管理这个体量是过度设计。

2.1 编译器和QT版本怎么选:这不是玄学,是后续踩坑的分水岭

常见的稳定组合是 Qt 5.15 LTS + MinGW 64-bit。5.15 是最后一个长期支持免费版本,社区资料最全,第三方控件兼容性也最成熟。Qt 6 新项目可以选,但如果是照着旧教程或旧代码改,6.2 以下版本里部分 API 变化会把你卡在编译期。我的建议:先跑通再说,版本能稳定就行。

编译器方面,MinGW 和 MSVC 都能用,差别在中文字符串上。MSVC 默认按本地代码页 GBK 读取源码文件,而 Qt 5 里const char*字符串默认按 UTF-8 解析,两边对不上就乱码。如果你用的是 MSVC,在公共头文件顶部加一行:

#if defined(_MSC_VER) #pragma execution_character_set("utf-8") #endif

这段宏的作用是把 MSVC 的字符串字面量字符集强制改为 UTF-8,从根源上解决界面中文乱码。MinGW 不存在这个问题,它默认就是 UTF-8 输入输出。

环境装好后,先建一个空项目跑最小验证,看当前构建里有没有 SQLite 驱动,避免写完才发现驱动没带上:

#include <QDebug> #include <QSqlDatabase> int main(int argc, char *argv[]) { QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); qDebug() << "available drivers:" << QSqlDatabase::drivers(); return 0; }

逻辑说明:QSqlDatabase::drivers()返回当前构建里所有可用的数据库驱动名。如果在输出里看不到 QSQLITE,说明安装 Qt 时没有把 SQL 模块勾上,回到安装器补选 Qt SQL 组件即可。这一步验证花两分钟,能避免你写完几百行代码才发现环境缺东西。

2.2 表结构设计:一张表支撑进出场和计费

停车管理的数据模型不需要复杂设计,核心就一张记录表。进出场是一对一的闭环:进场插入一条status=1的记录,出场把同一条记录更新成status=0并写入出场时间和费用。这里的关键是不要为"在场车辆"单独建表,那会引入数据同步问题。

CREATE TABLE IF NOT EXISTS parking_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, plate_number TEXT NOT NULL, parking_space TEXT, entry_time TEXT NOT NULL, exit_time TEXT, fee REAL DEFAULT 0, status INTEGER DEFAULT 1 ); CREATE INDEX IF NOT EXISTS idx_plate ON parking_records(plate_number); CREATE INDEX IF NOT EXISTS idx_status ON parking_records(status);

字段说明:

字段类型说明
plate_numberTEXT车牌号,进出场都靠它关联
parking_spaceTEXT车位编号,格式如 A01
entry_timeTEXT进场时间,统一yyyy-MM-dd HH:mm:ss格式
exit_timeTEXT出场时间,空表示还在场
feeREAL应收费用,出场时计算写入
statusINTEGER1 在场 / 0 已出场

为什么时间用 TEXT 而不是 SQLite 的 DATETIME 类型?因为 SQLite 是动态类型系统,用QDateTime::toString("yyyy-MM-dd HH:mm:ss")写入,查出来再用QDateTime::fromString解析,格式完全可控。这个格式还有一个好处:字符串排序等于时间排序,ORDER BY entry_time DESC不需要额外转换。

status字段很多人会忽略,直接靠exit_time IS NULL判断在场。效果等价,但状态索引比判空更快,而且status=0做月度统计时不需要再解析时间。索引加在车牌号和状态上就够了,记录量在几万条以内,再多加索引反而拖慢写入。

2.3 项目骨架:把计费规则独立出来

工程目录建议这样组织:

ParkingSystem/ ├── ParkingSystem.pro # qmake工程文件 ├── main.cpp # 程序入口 ├── mainwindow.h / .cpp # 主窗口与业务逻辑 ├── mainwindow.ui # 界面布局文件 ├── feeCalculator.h / .cpp # 计费规则独立模块 └── parking.db # SQLite数据库文件(运行时自动生成)

对应的 .pro 文件:

QT += core gui sql greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = ParkingSystem TEMPLATE = app CONFIG += c++11 SOURCES += \ main.cpp \ mainwindow.cpp \ feeCalculator.cpp HEADERS += \ mainwindow.h \ feeCalculator.h

这里的QT += sql是必须的,漏掉会导致QSqlDatabase头文件都找不到。TARGET 决定生成的可执行文件名,不用和文件夹同名,但建议保持简短,后面打包分发时省事。

把计费单独拆成 feeCalculator 模块,是为换费率规则时不动界面代码。这个拆分对新手来说显得多余,但当你接到"首小时免费改两小时""夜间封顶调价"这类需求时,就会感谢当初这个决定。main.cpp 保持干净,只做应用初始化和窗口显示,数据库初始化放进 MainWindow 的构造函数或独立方法里,方便以后改造。

3. 用信号槽串起进出场:主界面布局与核心业务代码

这个系统的交互路径就两条:进场和出场。界面布置不复杂,但控件职责要清楚,否则写代码时信号槽会接得混乱。这一章把布局和两条业务链路讲透。

3.1 主窗口控件分工:每个控件只干一件事

控件类型职责
plateEditQLineEdit车牌号输入框
spaceComboQComboBox车位选择,可手填也可下拉
entryButtonQPushButton触发进场登记
exitButtonQPushButton触发出场结算
recordTableQTableWidget显示全部停车记录
statusLabelQLabel实时显示在场车辆数

布局上把操作区放在窗口顶部,表格占中间剩余空间,底部放状态栏。小数据量场景直接选 QTableWidget 而不是 QTableView,不需要单独建 model 文件,几十行记录时性能完全够用,心智负担最小。

信号槽可以拖拽连接也可以手写。手写多几行代码,但搜索定位时直观:

// mainwindow.h 中声明槽函数 private slots: void onEntryButtonClicked(); void onExitButtonClicked(); void onPlateEditReturnPressed();
// mainwindow.cpp 构造函数里建立连接 connect(ui->entryButton, &QPushButton::clicked, this, &MainWindow::onEntryButtonClicked); connect(ui->exitButton, &QPushButton::clicked, this, &MainWindow::onExitButtonClicked); // 车牌输入框按回车等价于点击"进场登记" connect(ui->plateEdit, &QLineEdit::returnPressed, this, &MainWindow::onEntryButtonClicked);

参数说明:第三组 connect 把回车键映射到进场登记。中控台场景下值班人员可能手抄车牌或接扫码枪,输入完直接回车,省一次鼠标点击,这个小细节实际使用频率很高。

spaceCombo 里的车位列表,常见做法是启动时加载固定车位编号数组,比如 A07、B12。也可以在程序里做成可配置文本,每次启动从配置文件读取,这样车位调整不用重新编译。

3.2 进场登记:三条前置规则一条都不能少

进场登记的核心不是 INSERT 语句,而是三条前置校验:车牌格式对不对、车位是否被占用、这个车牌是不是已经在场。第三条最容易被忽略——同一个车牌重复进场,出场结算时会查到两条在场记录,收费就会乱。

void MainWindow::onEntryButtonClicked() { QString plate = ui->plateEdit->text().trimmed(); QString space = ui->spaceCombo->currentText(); // 车牌基础校验:国内蓝牌7位,新能源牌8位 if (plate.size() < 7 || plate.size() > 8) { QMessageBox::warning(this, "提示", "请输入正确的车牌号(7~8位)"); return; } // 规则一:检查同一车牌是否已进场 QSqlQuery checkPlate(m_db); checkPlate.prepare("SELECT COUNT(*) FROM parking_records " "WHERE plate_number = ? AND status = 1"); checkPlate.addBindValue(plate); if (!checkPlate.exec() || !checkPlate.next()) { QMessageBox::critical(this, "错误", "车牌状态查询失败"); return; } if (checkPlate.value(0).toInt() > 0) { QMessageBox::warning(this, "提示", "该车牌已在场内,请核对"); return; } // 规则二:检查车位是否被占用 QSqlQuery checkSpace(m_db); checkSpace.prepare("SELECT COUNT(*) FROM parking_records " "WHERE parking_space = ? AND status = 1"); checkSpace.addBindValue(space); if (!checkSpace.exec() || !checkSpace.next()) { QMessageBox::critical(this, "错误", "车位状态查询失败"); return; } if (checkSpace.value(0).toInt() > 0) { QMessageBox::warning(this, "提示", space + " 车位已被占用"); return; } // 写入进场记录 QSqlQuery insert(m_db); insert.prepare("INSERT INTO parking_records " "(plate_number, parking_space, entry_time, status) " "VALUES (?, ?, ?, 1)"); insert.addBindValue(plate); insert.addBindValue(space); insert.addBindValue(QDateTime::currentDateTime() .toString("yyyy-MM-dd HH:mm:ss")); if (!insert.exec()) { QMessageBox::critical(this, "错误", "登记失败:" + insert.lastError().text()); return; } refreshTable(); updateStatusLabel(); QMessageBox::information(this, "成功", "车辆进场登记完成"); }

逻辑说明:所有查询和写入都用QSqlQuery::prepare加addBindValue绑定参数,而不是字符串拼接 SQL。车牌号是外部输入,直接拼进 SQL 会被注入,本地单机程序虽然风险小,但养成绑定参数的习惯不会吃亏。

trimmed()去掉输入框首尾空格,真机上曾出现过车牌前后带空格导致查不到记录的问题。时间写入统一用QDateTime::currentDateTime().toString(...),保证字符串格式和表结构约定一致。

参数说明:车牌位数校验用的是硬编码 7~8 位,这只覆盖常见的蓝牌和新能源绿牌。如果系统要支持黄牌大车或警车,位数范围要放宽。我一般把校验长度做成可配置常量,而不是散落在代码里。

3.3 出场结算:按车牌查在场记录,算费后更新状态

出场结算比进场多两步:从在场记录里取进场时间,算出费用,然后更新同一条记录。核心是"查出最新一条在场记录"的 SQL 写法。

void MainWindow::onExitButtonClicked() { QString plate = ui->plateEdit->text().trimmed(); if (plate.isEmpty()) { QMessageBox::warning(this, "提示", "请输入车牌号"); return; } // 查出该车牌未出场的记录,按进场时间倒序取最新一条 QSqlQuery query(m_db); query.prepare("SELECT id, entry_time FROM parking_records " "WHERE plate_number = ? AND status = 1 " "ORDER BY entry_time DESC LIMIT 1"); query.addBindValue(plate); if (!query.exec() || !query.next()) { QMessageBox::information(this, "提示", "没有查到该车牌的在场记录"); return; } int recordId = query.value(0).toInt(); QDateTime entryTime = QDateTime::fromString( query.value(1).toString(), "yyyy-MM-dd HH:mm:ss"); QDateTime exitTime = QDateTime::currentDateTime(); // 调用独立计费模块 double fee = FeeCalculator::calcFee(entryTime, exitTime); // 更新出场信息 QSqlQuery update(m_db); update.prepare("UPDATE parking_records " "SET exit_time = ?, fee = ?, status = 0 " "WHERE id = ?"); update.addBindValue(exitTime.toString("yyyy-MM-dd HH:mm:ss")); update.addBindValue(fee); update.addBindValue(recordId); if (!update.exec()) { QMessageBox::critical(this, "错误", "出场结算失败:" + update.lastError().text()); return; } QMessageBox::information(this, "结算完成", QString("%1 停车时长 %2 分钟,应收 %3 元") .arg(plate) .arg(entryTime.secsTo(exitTime) / 60) .arg(fee, 0, 'f', 2)); refreshTable(); updateStatusLabel(); }

这里用LIMIT 1配合ORDER BY entry_time DESC,即使表里出现异常重复数据,也只取最新一条,避免程序崩掉。

计费规则单独放在 feeCalculator 模块里:

// feeCalculator.cpp double FeeCalculator::calcFee(const QDateTime &entry, const QDateTime &exit) { if (!entry.isValid() || !exit.isValid() || exit <= entry) { return 0.0; } // 总停车分钟数,秒数直接丢弃 qint64 totalMinutes = entry.secsTo(exit) / 60; // 超过24小时按整天算,每天封顶20元 qint64 days = totalMinutes / (24 * 60); qint64 remainMinutes = totalMinutes % (24 * 60); // 剩余时间不足15分钟按15分钟计,向上取整 qint64 quarterHours = (remainMinutes + 14) / 15; qint64 billableMinutes = quarterHours * 15; double fee = days * 20.0; // 30分钟内免费,30分钟~4小时收5元,超过4小时每小时加2元 if (billableMinutes <= 30) { fee += 0.0; } else if (billableMinutes <= 240) { fee += 5.0; } else { qint64 extraHours = (billableMinutes - 240 + 59) / 60; fee += 5.0 + extraHours * 2.0; } // 单日费用封顶20元 return qMin(fee, days * 20.0 + 20.0); }

逻辑说明:secsTo返回秒数,除以 60 是整除,余下的秒数会导致少算一分钟。计费规则里统一用"向上取整到15分钟"处理,让余秒的影响收敛到同一个边界。(remainMinutes + 14) / 15是整数向上取整的经典写法,避免浮点误差。

参数说明:30 分钟免费、4 小时基础时长、每小时 2 元、单日封顶 20 元,这些数字全是可以调整的费率参数。正式的费率不该写死在代码里,下一章会把它改成配置项。这个版本先跑通逻辑,做出来的系统能算对账就行。

4. 数据持久化与费率配置:从"能点"到"能用"

跑通进出场之后,系统的短板就暴露了:重启动态丢失、费率改一行代码就要重新编译、查询记录不方便。这一章把这三个短板补齐。

4.1 数据库连接管理:连接名、路径和建表一次性做对

数据库初始化最常见的错误是把连接名用默认的qt_sql_default_connection,又在多个地方重复调用addDatabase,结果 Qt 弹警告说连接重复定义。我习惯给连接起一个独立名字,所有查询都显式携带这个连接:

bool MainWindow::initDatabase(const QString &dbPath) { // 如果连接已存在,先移除,避免重复添加报警告 if (QSqlDatabase::contains("parking_conn")) { QSqlDatabase::removeDatabase("parking_conn"); } m_db = QSqlDatabase::addDatabase("QSQLITE", "parking_conn"); m_db.setDatabaseName(dbPath); if (!m_db.open()) { QMessageBox::critical(this, "数据库错误", m_db.lastError().text()); return false; } QSqlQuery query(m_db); bool ok = query.exec( "CREATE TABLE IF NOT EXISTS parking_records (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " plate_number TEXT NOT NULL," " parking_space TEXT," " entry_time TEXT NOT NULL," " exit_time TEXT," " fee REAL DEFAULT 0," " status INTEGER DEFAULT 1)"); if (!ok) { qCritical() << "建表失败:" << query.lastError().text(); return false; } // 车牌号和状态是查询高频字段,建索引加速 query.exec("CREATE INDEX IF NOT EXISTS idx_plate ON parking_records(plate_number)"); query.exec("CREATE INDEX IF NOT EXISTS idx_status ON parking_records(status)"); return true; }

逻辑说明:addDatabase("QSQLITE", "parking_conn")的第二个参数是自定义连接名,之后所有QSqlQuery query(m_db)都从 m_db 这个连接里取,代码里不会出现"查询跑到了另一个连接"的错乱。

建表语句外面包了一层IF NOT EXISTS,程序每次启动都执行,表不存在就创建,存在则跳过,天然具备"重启动态还原"能力。索引同样用IF NOT EXISTS,重复启动不会报错。

注意removeDatabase的使用顺序:必须先让所有使用该连接的 QSqlQuery 对象析构,再调用移除,否则 Qt 会报连接被占用。所以在 initDatabase 开头移除旧连接是最安全的位置,那时还没有衍生查询对象。

4.2 记录查询与表格刷新:用绑定参数而不是拼接SQL

停车记录查询要支持按车牌号模糊匹配,这是值班人员最常用的功能。查询和刷新写在一个方法里,界面操作结束后统一调用:

void MainWindow::refreshTable(const QString &plateFilter) { QString sql = "SELECT plate_number AS 车牌号," " parking_space AS 车位," " entry_time AS 进场时间," " CASE WHEN status = 1 THEN '在场' ELSE exit_time END AS 出场时间," " fee AS 费用" " FROM parking_records"; QSqlQuery query(m_db); if (plateFilter.isEmpty()) { query.prepare(sql + " ORDER BY entry_time DESC"); } else { sql += " WHERE plate_number LIKE ? ORDER BY entry_time DESC"; query.prepare(sql); query.addBindValue("%" + plateFilter + "%"); } if (!query.exec()) { qWarning() << "查询失败:" << query.lastError().text(); return; } ui->recordTable->setRowCount(0); int row = 0; while (query.next()) { ui->recordTable->insertRow(row); for (int col = 0; col < 6; ++col) { QTableWidgetItem *item = new QTableWidgetItem(query.value(col).toString()); if (col == 5 && !query.value(5).isNull()) { item->setText(QString::number(query.value(5).toDouble(), 'f', 2)); } ui->recordTable->setItem(row, col, item); } ++row; } }

逻辑说明:CASE WHEN status = 1 THEN '在场' ELSE exit_time END把"在场"状态直接体现在出场时间列,值班人员一眼看到哪条还没结算。费用列单独用QString::number(..., 'f', 2)保留两位小数,避免浮点数显示成一长串。

参数说明:LIKE ?配合addBindValue("%" + filter + "%")是模糊匹配的标准写法。如果用户输入了%或_,这两个字符会被当 SQL 通配符,处理方式是转义成\%和\_。本地小系统通常不处理也影响不大,但你要知道有这个边界。

刷新表格时先setRowCount(0)再逐行插入,这是最直观的写法。记录量到几千条后,这会开始闪烁,第 5 章会讲优化办法。

4.3 费率参数化:改价格不用重新编译

把费率写死在 calcFee 里,每一次调价都要改源码重新编译。正式交付的系统,费率应该是配置文件里的数字。QT 的 QSettings 是最省事的方案,跨平台自动适配存储位置:

struct FeeConfig { int freeMinutes = 30; // 免费停车时长 double baseFee = 5.0; // 基础费用 int baseMinutes = 240; // 基础时长 double hourlyFee = 2.0; // 超出后每小时加价 double dayCap = 20.0; // 单日封顶 };
void MainWindow::loadFeeConfig() { QSettings settings("ParkingSystem", "ParkingConfig"); m_feeConfig.freeMinutes = settings.value("fee/freeMinutes", 30).toInt(); m_feeConfig.baseFee = settings.value("fee/baseFee", 5.0).toDouble(); m_feeConfig.baseMinutes = settings.value("fee/baseMinutes", 240).toInt(); m_feeConfig.hourlyFee = settings.value("fee/hourlyFee", 2.0).toDouble(); m_feeConfig.dayCap = settings.value("fee/dayCap", 20.0).toDouble(); }

逻辑说明:QSettings 的第一个参数是程序名,第二个是配置分组。Windows 上写注册表,Linux 上写配置文件,QT 帮你抹平差异。value("fee/baseFee", 5.0)的第二个参数是默认值,首次运行时没有配置文件就回落到代码里的默认费率。

改造之后,FeeCalculator 的 calcFee 签名变成接收FeeConfig和两个时间,不再持有硬编码数字。这样值班人员或物业方可以自行调整费率文件,你作为开发者不需要为了两块钱的调价重新发布程序。

提示:上线前一定用历史收费记录反推验证费率参数。常见的坑是把 baseMinutes 的单位当成了"小时",代码里用 4 传进去,实际 4 分钟就把基础费扣了,这种错误靠眼睛看很难发现。

5. 避坑指南:QT停车场管理系统高频问题与排查步骤

这一章是实战里反复踩过的坑,每一条都是"现象→原因→解决"的结构。你在移植或照着写时,遇到类似的先来这里对号入座。

5.1 界面按钮和数据库记录显示乱码

现象:按钮文字显示成????或中文全变乱码,但 qDebug 打出来的中文字符串正常。

原因:源码文件编码和编译器读取编码不一致。MSVC 按 GBK 读取源文件,Qt 5 里const char*按 UTF-8 解析,两边错位。源码文件本身是 GBK 或带 BOM 的 UTF-8,也会触发同类问题。

解决:MSVC 环境在公共头文件顶部加#pragma execution_character_set("utf-8"),并把源码文件统一保存为 UTF-8 无 BOM。Qt Creator 右下角有编码切换按钮,文件编码不对时在这里改。MinGW 默认处理正确,不乱动文件编码即可。排查顺序:先确认文件编码,再确认有没有加宏,最后检查数据库字段本身存进去的值是否乱码——后者需要用 SQLite 工具单独打开数据库文件查看。

5.2 点进出场按钮后界面卡死

现象:点击进场登记,窗口失去响应几秒,数据量大时甚至长时间无响应。

原因:SQL 操作直接阻塞了 GUI 线程。本地 SQLite 在几百条记录下不会卡,但当表里积累到几万条记录,或查询语句里没有索引且用了LIKE '%xxx%'全表扫描时,主线程就会被拖住。

解决:先把索引建全,status和plate_number都要加。然后避免在循环里逐条 insert 或 update,批量操作用事务包住:

db.transaction(); for (int i = 0; i < rows.size(); ++i) { // 逐条执行INSERT或UPDATE } db.commit();

事务提交会把每次写盘的 fsync 合并成一次,写入速度能提升一个数量级。如果查询量真的到了几十万条,再考虑把数据库操作丢到工作线程,但停车管理这个体量通常不需要走到那一步。

5.3 数据库文件找不到:相对路径在不同工作目录下翻车

现象:在 Qt Creator 里按 F5 运行一切正常,双击 exe 运行就报unable to open database file,或者反过来。

原因:setDatabaseName("parking.db")用的是相对路径,工作目录不同则解析位置不同。IDE 运行的工作目录是工程的构建目录,双击运行的工作目录是 exe 所在目录,两边不是同一个地方。

解决:使用绝对路径,从程序目录或用户数据目录构造:

QString appDir = QCoreApplication::applicationDirPath(); QString dbPath = appDir + "/parking.db"; m_db.setDatabaseName(dbPath);

注意:如果程序装在 C 盘 Program Files 下,普通用户没有写权限,数据库文件根本创建不出来。更稳的做法是把数据库放到QStandardPaths::writableLocation(QStandardPaths::AppDataLocation),但那样文件位置不够直观,不方便备份。我的习惯是:程序目录可写就用程序目录,不可写就在程序启动时检测并提示用户选择数据目录。

5.4 停车时长算错、跨天费用异常

现象:停车 25 分钟被收了 5 元;停了两天费用比实际应缴多出一大截;出场时间比进场时间早,费用算成负数。

原因:分钟取整方式不一致,秒数丢弃规则不统一。secsTo返回的秒数除以 60 是整除,如果进场时间带秒而格式串里不含秒,解析时秒被舍掉,时长就会差几十秒。跨天场景下,简单按小时累加会导致每天封顶规则失效。

解决:时间格式统一为yyyy-MM-dd HH:mm:ss,进场和出场都用完整格式串。计费侧用向上取整到 15 分钟收敛余量,且 24 小时以上按天拆分计费,最后再用单日封顶做约束。出场时间早于进场时间的异常记录,calcFee 里直接返回 0,同时记录到日志里,方便追溯是手动改数据还是代码 bug。

5.5 表格刷新闪烁、记录顺序不稳定

现象:每次进出场操作后表格闪一下;刷新后记录的显示顺序和上次不一致。

原因:setRowCount(0)后逐行insertRow,每个单元格 new 一个 item,渲染次数多,窗口频繁重绘。顺序不稳定多半是查询语句没有 ORDER BY,SQLite 不保证无排序查询的返回顺序。

解决:刷新前后调用upsetUpdatesEnabled:

ui->recordTable->setUpdatesEnabled(false); // 执行刷新逻辑 ui->recordTable->setUpdatesEnabled(true);

这能让中间所有重绘画到离屏缓冲区,最后一次性显示,闪烁感明显减弱。排序则统一在 SQL 里加ORDER BY entry_time DESC,不要依赖数据库的默认顺序。

6. 进阶:加实时在场看板和今日营收,验证你的系统

系统跑通之后,离"能交给别人用"还差一点:值班人员不可能一直盯着表格数数。我的习惯是加一个状态栏,每 30 秒自动刷新一次,显示当前在场车辆数和今日营收,让值班人员一抬眼就掌握全貌。

实现不复杂,QTimer 加一个统计函数即可:

// mainwindow.h 中声明 private: QTimer m_refreshTimer; void updateStatusLabel();
// 构造函数里启动定时器 m_refreshTimer.setInterval(30 * 1000); connect(&m_refreshTimer, &QTimer::timeout, this, &MainWindow::updateStatusLabel); m_refreshTimer.start();
void MainWindow::updateStatusLabel() { QSqlQuery q(m_db); // 在场车辆数:status=1 的记录数 q.exec("SELECT COUNT(*) FROM parking_records WHERE status = 1"); q.next(); int inCount = q.value(0).toInt(); // 今日营收:当天出场记录的应收合计 q.exec("SELECT COALESCE(SUM(fee), 0) FROM parking_records " "WHERE status = 0 AND date(exit_time) = date('now', 'localtime')"); q.next(); double todayFee = q.value(0).toDouble(); ui->statusLabel->setText( QString("在场 %1 辆 | 今日营收 %2 元") .arg(inCount) .arg(todayFee, 0, 'f', 2)); }

这里有个 SQLite 的小知识点:date(exit_time)能从yyyy-MM-dd HH:mm:ss字符串里直接截出日期部分,date('now','localtime')取今天的本地日期,利用字符串排序特性做日期比对,比在 C++ 里拼时间范围再传参要省事得多。

验证这套逻辑的可靠做法不是盯着界面看,而是人为构造边界数据:把系统时间改到昨天,用两个假车牌进出场,再改回今天,看今日营收是否只统计当天记录。跨天记录造一条停 26 小时的,确认费用等于一天封顶 20 元加剩余两小时的费用。

我自己每次改完计费规则,一定会找两个边界 case 手算一遍:一个停 35 分钟,一个跨天停 26 小时。前者验证免费时段过后的加价逻辑,后者验证全天封顶逻辑,两个都对,才敢把程序交付出去。做到这一步,这个系统就不再是"能跑的 demo",而是一个随时能替换手动登记流程的小工具。希望这份拆解对你有帮助,也祝你把代码跑通后,踩坑时少绕远路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询