2026/9/14 19:06:12

C++与Qt图书管理系统实战:从数据库设计到事务处理与报表可视化

C++与Qt图书管理系统实战:从数据库设计到事务处理与报表可视化 简介基于C与QT实现的图书管理系统课程设计完整源码面向计算机专业正在完成期末大作业、课程设计或需要项目实战练习的高校学生也可作为毕业设计选题参考。项目涵盖图书入库、借还管理、读者信息维护等核心模块代码结构清晰附带完整实验报告可直接运行适合理解Qt界面开发、信号槽机制与数据库联动的基本流程。压缩包共232个文件包含cpp、h源码文件o编译中间文件sql数据库脚本ui界面布局定义以及dll、exe运行依赖与可执行程序同时提供docx实验报告、xls数据文件和mp4演示录屏整包约37.16MB。目前已有113人学习下载作为大三高分项目经导师指导并认可评审分99分完整性能得到保障。解压后按pro工程文件组织目录可用Qt Creator一键加载结合实验报告与录屏回放能快速掌握从数据库设计到界面交互的完整实现思路节省排错与重构时间适合零基础学生独立完成课设要求。1. 图书管理系统同样是增删改查为什么有的作业只能拿 80 分越是经典的课设题目越容易写出千篇一律的代码。图书管理系统就是一个典型网上随便一搜就是几百份源码但绝大多数都停留在「能跑就行」的阶段——控制台黑窗口、数据写死在数组里、没有事务概念、界面和逻辑全部耦合在一起。而一份能稳定拿到 95 分以上的作业真正拉开差距的往往不是功能多华丽而是架构清晰、边界处理严谨、代码可维护、报告能讲清楚设计决策。这篇文章就围绕 C 与 Qt 实现图书管理系统这条主线从头到尾捋一遍完整落地路径从数据表设计、Qt 项目骨架搭建到增删改查的标准化写法、报表与图表呈现再到报告里必须写清楚的设计说明和踩坑记录。照这个思路走做完的不只是一个系统更是一份可以作为面试谈资的完整项目经验。2. 系统设计与数据层把表结构想清楚代码就成功了一半2.1 图书管理系统的最小实体模型三张表还是五张表图书管理系统的核心业务是围绕「书」和「读者」展开的。很多初学者上来就建一张大宽表把书名、作者、出版社、价格、库存、借阅人、借阅日期全塞进去短时间跑通没问题但一旦涉及「一本书被借走后再归还」「一个读者借多本书」的场景就会暴露更新异常和数据冗余。常规做法是拆成五张核心表books图书基本信息book_id 为主键书名、作者、ISBN、出版社、价格、库存总量readers读者信息reader_id 为主键姓名、学号/工号、联系电话、可借额度borrow_records借阅流水record_id 为主键book_id 和 reader_id 作为外键借出时间、应还时间、实际归还时间categories图书分类多对一关联 books 表users系统登录账号区分管理员和普通操作员其中 borrow_records 表是整个系统的关键——它决定了你能否回答「谁借了什么书」「哪本书被借走了」「有多少书逾期未还」这三个核心问题。如果只做图书的增删改查而不做借还流水那这个系统本质上只是个电子台账谈不上「管理」。2.2 SQLite 还是 MySQL课设场景下的合理选择对于 Qt 课设来说数据层选择 SQLite 通常是更务实的方案。理由有三一是 SQLite 是嵌入式数据库不需要单独安装服务端Qt 的 QSqlDatabase 默认就带 SQLite 驱动省去部署麻烦二是整个数据库就是一个 .db 文件交作业时连同代码一起打包即可三是 SQLite 支持标准 SQL 的大部分语法事务、外键约束、索引都能用完全够课设规模的数据量。如果你所在学校的实验环境要求必须用 MySQL 或 SQL ServerQt 侧只需改一行数据库类型字符串和连接参数SQL 语句本身几乎不需要变动。下面是在 Qt 中初始化 SQLite 的最小代码#include QSqlDatabase #include QSqlQuery #include QSqlError #include QDebug bool initDatabase() { // 添加 SQLite 数据库连接连接名可以自定义 QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE); // 数据库文件路径相对路径和绝对路径都可以 db.setDatabaseName(library.db); if (!db.open()) { qDebug() 数据库打开失败: db.lastError().text(); return false; } QSqlQuery query; // 开启外键约束SQLite 默认不启用 query.exec(PRAGMA foreign_keys ON;); // 建表语句使用 IF NOT EXISTS保证重复运行不会报错 bool ok query.exec( CREATE TABLE IF NOT EXISTS books ( book_id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR(200) NOT NULL, author VARCHAR(100) NOT NULL, isbn VARCHAR(20) UNIQUE, publisher VARCHAR(100), price REAL, total_count INTEGER DEFAULT 1, available_count INTEGER DEFAULT 1, category_id INTEGER ); ); if (!ok) { qDebug() 建表失败: query.lastError().text(); } return ok; }这里的逻辑要点有三个。第一addDatabase(QSQLITE)的字符串参数不可写错大小写敏感写错会报 “driver not loaded”。第二AUTOINCREMENT在 SQLite 中要求主键类型必须是INTEGER PRIMARY KEY否则不生效。第三PRAGMA foreign_keys ON;必须每次连接都执行一次因为 SQLite 默认关闭外键检查——这一点最容易被忽略会导致外键约束形同虚设。2.3 数据层封装为什么不要在主窗口里直接写 SQL课设代码最常见的架构问题是所有 SQL 语句直接散落在按钮的槽函数里。功能少的时候还能看一旦加上借阅超期判断、图书检索、统计报表主窗口会膨胀到两三千行改一个字段名要全局搜索替换。合理的做法是做一层独立的数据访问封装。常见做法是建一个DatabaseManager类用单例模式持有全局唯一的数据库连接提供查询和更新的统一接口class DatabaseManager { public: static DatabaseManager instance() { static DatabaseManager inst; return inst; } bool init() { db QSqlDatabase::contains(qt_sql_default_connection) ? QSqlDatabase::database(qt_sql_default_connection) : QSqlDatabase::addDatabase(QSQLITE); db.setDatabaseName(library.db); if (!db.open()) return false; QSqlQuery q; q.exec(PRAGMA foreign_keys ON;); return true; } QSqlQuery execute(const QString sql, const QVariantList args {}) { QSqlQuery q(db); q.prepare(sql); for (int i 0; i args.size(); i) { q.addBindValue(args[i]); } q.exec(); return q; } private: QSqlDatabase db; };这段代码解决的是连接管理和 SQL 注入两个问题。QVariantList作为参数列表传入所有值都走bindValue绑定而不是拼进 SQL 字符串能防注入。返回值直接是QSqlQuery对象上层可以取结果集也可以取numRowsAffected()。课设评分时老师可能会问你「为什么用预处理语句」这个封装就是回答的底气。3. Qt 界面骨架搭建从 QMainWindow 到页面切换的整洁实现3.1 界面框架选型QMainWindow 左侧导航是课设标准解图书管理系统的界面布局最常见的方案是左侧功能导航栏 右侧堆叠页面。左侧用 QListWidget 列出「图书管理」「读者管理」「借阅管理」「统计报表」等模块右侧用 QStackedWidget 切换不同页面。这个布局的好处是结构清晰、代码直观、后续添加新模块的成本极低。主窗口骨架搭建#include QMainWindow #include QListWidget #include QStackedWidget #include QSplitter class MainWindow : public QMainWindow { public: MainWindow(QWidget* parent nullptr) : QMainWindow(parent) { setWindowTitle(图书管理系统); resize(1200, 800); // 左侧导航 auto* navList new QListWidget(this); navList-addItem(图书管理); navList-addItem(读者管理); navList-addItem(借阅管理); navList-addItem(统计报表); navList-setFixedWidth(160); // 右侧页面容器 auto* stackedPages new QStackedWidget(this); stackedPages-addWidget(createBookManagePage()); stackedPages-addWidget(createReaderManagePage()); stackedPages-addWidget(createBorrowManagePage()); stackedPages-addWidget(createStatsPage()); // 用 QSplitter 让两边可拖动调节 auto* splitter new QSplitter(this); splitter-addWidget(navList); splitter-addWidget(stackedPages); splitter-setStretchFactor(1, 1); // 右侧可拉伸 setCentralWidget(splitter); // 导航切换联动 connect(navList, QListWidget::currentRowChanged, stackedPages, QStackedWidget::setCurrentIndex); navList-setCurrentRow(0); } };QSplitter在这里是提升体验的细节——用户拖动分割条可以调节导航宽度这个交互虽然简单但出现在课设里会让操作感明显比固定布局更好。槽函数连接用QListWidget::currentRowChanged信号直接映射到QStackedWidget::setCurrentIndex不需要写 lambda 中转。3.2 图书管理页面的核心布局TableView 搜索栏 按钮区图书管理页面是系统最常用的界面布局上需要注意表单区、工具栏区、表格区三个区域的职责分离。搜索栏和操作按钮放在上方中间是图书表格底部是状态栏或分页信息。用 QSqlQueryModel 配合 QTableView 的实现路径#include QSqlQueryModel #include QTableView class BookManagePage : public QWidget { public: explicit BookManagePage(QWidget* parent nullptr) : QWidget(parent) { auto* layout new QVBoxLayout(this); // 顶部搜索区 auto* searchBar new QHBoxLayout(); auto* keywordEdit new QLineEdit(this); keywordEdit-setPlaceholderText(输入书名或作者关键字); auto* searchBtn new QPushButton(搜索, this); auto* refreshBtn new QPushButton(刷新, this); searchBar-addWidget(keywordEdit); searchBar-addWidget(searchBtn); searchBar-addWidget(refreshBtn); searchBar-addStretch(); layout-addLayout(searchBar); // 表格区 model new QSqlQueryModel(this); tableView new QTableView(this); tableView-setModel(model); tableView-setSelectionBehavior(QAbstractItemView::SelectRows); tableView-setEditTriggers(QAbstractItemView::NoEditTriggers); layout-addWidget(tableView); // 刷新表格数据 auto refresh [this]() { model-setQuery(SELECT book_id, title, author, isbn, publisher, price, total_count, available_count FROM books); }; refresh(); // 点击搜索时按关键字模糊查询 connect(searchBtn, QPushButton::clicked, this, [this, keywordEdit, refresh]() { QString kw keywordEdit-text().trimmed(); if (kw.isEmpty()) { refresh(); return; } QSqlQuery query; query.prepare(SELECT book_id, title, author, isbn, publisher, price, total_count, available_count FROM books WHERE title LIKE ? OR author LIKE ?); query.addBindValue(% kw %); query.addBindValue(% kw %); query.exec(); model-setQuery(std::move(query)); }); connect(refreshBtn, QPushButton::clicked, this, refresh); } private: QSqlQueryModel* model; QTableView* tableView; };这段代码里有一个容易踩的坑直接调用model-setQuery(query)时如果 query 已经用prepare()绑定了参数查询在 exec 时已经执行完毕直接传给模型是没问题的。但如果你写的是model-setQuery(SELECT ... WHERE title LIKE % kw %)这种字符串拼接方式SQL 注入的风险先不说单是关键字里带单引号就会直接报语法错误。统一走上面的绑定写法一劳永逸。QTableView的setEditTriggers(QAbstractItemView::NoEditTriggers)是必须设置的——默认情况下表格是可以直接双击编辑的这会绕过所有业务校验直接改数据库课设演示时手滑点一下就会出丑。3.3 弹窗表单新增和编辑共用一个 QDialog 的写法图书的新增和编辑功能共用同一个表单弹窗是提效的关键。弹窗内字段相同只是初始数据不同用一个标志位区分当前是新增还是编辑模式#include QDialog #include QFormLayout #include QLineEdit #include QSpinBox #include QDoubleSpinBox #include QDialogButtonBox class BookEditDialog : public QDialog { public: // editId 传 -1 表示新增传有效 ID 表示编辑 BookEditDialog(int editId, QWidget* parent nullptr) : QDialog(parent), m_editId(editId) { setWindowTitle(editId -1 ? 新增图书 : 编辑图书); auto* form new QFormLayout(this); titleEdit new QLineEdit(this); authorEdit new QLineEdit(this); isbnEdit new QLineEdit(this); publisherEdit new QLineEdit(this); priceSpin new QDoubleSpinBox(this); priceSpin-setRange(0, 9999); priceSpin-setDecimals(2); totalSpin new QSpinBox(this); totalSpin-setRange(0, 999); form-addRow(书名:, titleEdit); form-addRow(作者:, authorEdit); form-addRow(ISBN:, isbnEdit); form-addRow(出版社:, publisherEdit); form-addRow(价格:, priceSpin); form-addRow(库存总量:, totalSpin); auto* buttons new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); form-addRow(buttons); connect(buttons, QDialogButtonBox::accepted, this, BookEditDialog::onAccept); connect(buttons, QDialogButtonBox::rejected, this, QDialog::reject); // 编辑模式下预填数据 if (m_editId ! -1) { QSqlQuery query; query.prepare(SELECT title, author, isbn, publisher, price, total_count, available_count FROM books WHERE book_id ?); query.addBindValue(m_editId); if (query.exec() query.next()) { titleEdit-setText(query.value(0).toString()); authorEdit-setText(query.value(1).toString()); isbnEdit-setText(query.value(2).toString()); publisherEdit-setText(query.value(3).toString()); priceSpin-setValue(query.value(4).toDouble()); totalSpin-setValue(query.value(5).toInt()); m_availableCount query.value(6).toInt(); } } } private slots: void onAccept() { if (titleEdit-text().trimmed().isEmpty()) { QMessageBox::warning(this, 提示, 书名不能为空); return; } if (m_editId -1) { QSqlQuery query; query.prepare(INSERT INTO books (title, author, isbn, publisher, price, total_count, available_count) VALUES (?, ?, ?, ?, ?, ?, ?)); query.addBindValue(titleEdit-text().trimmed()); query.addBindValue(authorEdit-text().trimmed()); query.addBindValue(isbnEdit-text().trimmed()); query.addBindValue(publisherEdit-text().trimmed()); query.addBindValue(priceSpin-value()); query.addBindValue(totalSpin-value()); query.addBindValue(totalSpin-value()); // 新增时可借数等于总量 if (!query.exec()) { QMessageBox::critical(this, 错误, query.lastError().text()); return; } } else { QSqlQuery query; query.prepare(UPDATE books SET title ?, author ?, isbn ?, publisher ?, price ?, total_count ? WHERE book_id ?); query.addBindValue(titleEdit-text().trimmed()); query.addBindValue(authorEdit-text().trimmed()); query.addBindValue(isbnEdit-text().trimmed()); query.addBindValue(publisherEdit-text().trimmed()); query.addBindValue(priceSpin-value()); query.addBindValue(totalSpin-value()); query.addBindValue(m_editId); query.exec(); } accept(); } private: int m_editId; int m_availableCount 0; QLineEdit* titleEdit; QLineEdit* authorEdit; QLineEdit* isbnEdit; QLineEdit* publisherEdit; QDoubleSpinBox* priceSpin; QSpinBox* totalSpin; };这里必须解释一个业务细节编辑图书时available_count可借数量不能直接跟着total_count改。比如一本书总量 10 本、已借出 3 本、可借 7 本管理员把总量改成 15可借数量应该变成 12 而不是 15。所以编辑时available_count的更新逻辑是新的可借数 旧的可用数 (新总量 - 旧总量)。上面的代码里把m_availableCount在预填数据时保存下来就有这个用途UPDATE 时需要单独处理这一列。我故意没有把这一步写进 UPDATE 语句里留给你自己实现——这个坑报告里写清楚是评分的一个加分细节。4. 借阅与归还事务、状态判断与边界条件处理4.1 借阅流程的完整状态机从可借到借出再到逾期借阅操作不是简单的 INSERT 一条记录而是涉及两个数据表的联动修改。一次完整的借书操作在业务逻辑上分五步检查读者是否存在且未注销检查该读者当前未还数量是否已达上限检查图书是否还有可借库存插入一条 borrow_records 记录状态为「借出」将 books 表的 available_count 减 1这五步中任何一步失败前面的操作都不能生效。如果不做事务假设插入记录成功但库存更新失败数据库的一致性就破坏了。下面是完整的事务示例bool borrowBook(int bookId, int readerId) { QSqlDatabase db QSqlDatabase::database(); bool ok db.transaction(); // 开启事务 if (!ok) return false; QSqlQuery query(db); // 1. 检查图书可借数量 query.prepare(SELECT available_count FROM books WHERE book_id ?); query.addBindValue(bookId); if (!query.exec() || !query.next()) { db.rollback(); return false; } int available query.value(0).toInt(); if (available 0) { db.rollback(); QMessageBox::warning(nullptr, 提示, 该书无可借库存); return false; } // 2. 检查读者未还数量 query.prepare(SELECT COUNT(*) FROM borrow_records WHERE reader_id ? AND return_date IS NULL); query.addBindValue(readerId); if (!query.exec() || !query.next()) { db.rollback(); return false; } int unreturned query.value(0).toInt(); if (unreturned 5) { // 假设每人最多借 5 本 db.rollback(); QMessageBox::warning(nullptr, 提示, 该读者借阅已达上限); return false; } // 3. 插入借阅记录应还日期为当前日期 30 天 QDateTime now QDateTime::currentDateTime(); QDateTime dueDate now.addDays(30); query.prepare(INSERT INTO borrow_records (book_id, reader_id, borrow_date, due_date) VALUES (?, ?, ?, ?)); query.addBindValue(bookId); query.addBindValue(readerId); query.addBindValue(now.toString(yyyy-MM-dd HH:mm:ss)); query.addBindValue(dueDate.toString(yyyy-MM-dd HH:mm:ss)); if (!query.exec()) { db.rollback(); return false; } // 4. 库存减一 query.prepare(UPDATE books SET available_count available_count - 1 WHERE book_id ?); query.addBindValue(bookId); if (!query.exec() || query.numRowsAffected() ! 1) { db.rollback(); return false; } return db.commit(); }注意几个细节。第一reader_id对应的读者是否存在在界面上已经通过下拉选单选定了所以查询时不必再查 readers 表但如果你用的输入框手填 ID就必须补这个校验。第二插入记录时用的是QDateTime::currentDateTime()再格式化成字符串而不是在 SQL 里写datetime(now)这样做的目的是让 C 侧能拿到准确的本地时间不受 SQLite 时区设置的影响。第三UPDATE后要检查numRowsAffected()如果返回 0 说明 book_id 不存在需要回滚。4.2 归还流程逾期计算与联动更新归还操作的业务逻辑相对简单核心是更新 borrow_records 表状态和恢复库存bool returnBook(int recordId) { QSqlDatabase db QSqlDatabase::database(); db.transaction(); QSqlQuery query(db); // 1. 查出借阅记录中的 book_id query.prepare(SELECT book_id, borrow_date, due_date FROM borrow_records WHERE record_id ?); query.addBindValue(recordId); if (!query.exec() || !query.next()) { db.rollback(); return false; } int bookId query.value(0).toInt(); QDateTime dueDate QDateTime::fromString( query.value(2).toString(), yyyy-MM-dd HH:mm:ss); // 2. 计算是否逾期并写入归还信息 QDateTime now QDateTime::currentDateTime(); bool overdue now dueDate; query.prepare(UPDATE borrow_records SET actual_return_date ?, status ? WHERE record_id ?); query.addBindValue(now.toString(yyyy-MM-dd HH:mm:ss)); query.addBindValue(overdue ? overdue : returned); query.addBindValue(recordId); if (!query.exec()) { db.rollback(); return false; } // 3. 恢复库存 query.prepare(UPDATE books SET available_count available_count 1 WHERE book_id ?); query.addBindValue(bookId); query.exec(); return db.commit(); }归还时的逾期判断如果直接用时间戳对比会忽略自然日语义——比如应还日期是今天 23:59用户在 23:30 归还时间戳上也差 29 分钟不算逾期。正确的做法是比较日期部分才有意义。如果你需要在报表里统计「逾期天数」可以将actual_return_date转换为日期后与due_date的日期部分做差。这里还要考虑一个业务边界是否允许「无证归还」即管理员只拿到了书但不知道是哪个读者借的。现实中当然应该通过图书 ID 查出当前借阅记录再归还。实现方式是界面上选择某一本书程序自动在借阅记录表中查找该书未归还的记录按借出时间最早的那条处理。这个逻辑放在 UI 层比放在数据库层更灵活。4.3 借阅列表显示用 JOIN 查询替代多次查询展示借阅记录的 TableView 不能直接 SELECT borrow_records 表因为读者名和书名都在其他表里。正确的做法是 JOIN 查询SELECT br.record_id, r.reader_name, b.title, br.borrow_date, br.due_date, br.actual_return_date, CASE WHEN br.actual_return_date IS NULL AND br.due_date date(now) THEN 已逾期 WHEN br.actual_return_date IS NULL THEN 借出中 ELSE 已归还 END AS status_text FROM borrow_records br JOIN readers r ON br.reader_id r.reader_id JOIN books b ON br.book_id b.book_id ORDER BY br.borrow_date DESC;这段 SQL 用了CASE WHEN语法把状态映射成中文文本直接作为 TableView 的显示列不需要在 C 侧再做字符串映射。JOIN的字段br.reader_id和r.reader_id是关联条件br.book_id和b.book_id同理。需要强调一点ON子句的驱动表是brborrow_records 的别名因为一个借阅记录必然对应一个已存在的读者和图书INNER JOIN 不会丢失任何记录。5. 数据可视化与报表用 Qt Charts 做出让老师眼前一亮的统计页5.1 Qt Charts 模块的引入与基本配置Qt 5 中 Qt Charts 作为独立模块存在需要在工程文件中声明QT charts并在代码中包含QtCharts头文件。如果使用 Qt 6模块更名为QtChartsqmake 写法不变但 CMake 中需要find_package(Qt6 REQUIRED COMPONENTS Charts)。一个统计报表页面通常包含三种图形图书分类占比饼图、月度借阅量柱状图、图书借阅排行折线图。下面是分类占比饼图的实现#include QtCharts/QChart #include QtCharts/QChartView #include QtCharts/QPieSeries #include QtCharts/QPieSlice QChart* createCategoryPieChart() { QSqlQuery query; query.exec(SELECT c.category_name, COUNT(b.book_id) AS cnt FROM categories c LEFT JOIN books b ON c.category_id b.category_id GROUP BY c.category_id); auto* series new QPieSeries(); while (query.next()) { QString name query.value(0).toString(); int count query.value(1).toInt(); if (count 0) { series-append(name, count); } } auto* chart new QChart(); chart-addSeries(series); chart-setTitle(图书分类占比); chart-setAnimationOptions(QChart::SeriesAnimations); // 显示百分比标签 for (auto* slice : series-slices()) { slice-setLabel(QString(%1 %2%) .arg(slice-label()) .arg(slice-percentage() * 100, 0, f, 1)); } return chart; }LEFT JOIN在这里比INNER JOIN合理如果一个分类下没有任何图书INNER JOIN会丢掉这个分类饼图上完全看不到LEFT JOIN能保留分类行但 COUNT 为 0配合if (count 0)的过滤条件既显示所有非空分类又不让 0 值生成空扇区。5.2 自定义颜色与交互让图表不只是默认的 Chrome 蓝色Qt Charts 默认主题是蓝色系看起来很「默认」。提升观感的最快方式是逐个扇区手动着色QColor colors[] { QColor(#3498db), QColor(#e74c3c), QColor(#2ecc71), QColor(#f39c12), QColor(#9b59b6), QColor(#1abc9c) }; int colorIdx 0; for (auto* slice : series-slices()) { slice-setBrush(QBrush(colors[colorIdx % 6])); // 分离最大的扇区形成强调效果 if (slice-percentage() 0.3) { slice-setExploded(true); slice-setLabelVisible(true); } colorIdx; }这里setBrush传入的QColor是值拷贝而非引用所以每次循环重新赋值即可。setExploded(true)会将扇区向外偏移视觉上突出占比最大的分类——老师打开系统的第一眼看到这个交互细节印象分就上去了。5.3 柱状图月度借阅趋势的统计 SQL 设计月度借阅统计的难点在于SQLite 没有专门的月份字段需要用strftime函数从日期字符串中提取年月QChart* createMonthlyBarChart() { QSqlQuery query; query.exec(SELECT strftime(%Y-%m, borrow_date) AS month, COUNT(*) AS cnt FROM borrow_records WHERE borrow_date date(now, -6 months) GROUP BY month ORDER BY month ASC); auto* series new QBarSeries(); QStringList categories; QBarSet* barSet new QBarSet(借阅量); while (query.next()) { categories query.value(0).toString(); *barSet query.value(1).toInt(); } series-append(barSet); auto* chart new QChart(); chart-addSeries(series); chart-setTitle(近六个月借阅趋势); auto* axisX new QBarCategoryAxis(); axisX-append(categories); chart-addAxis(axisX, Qt::AlignBottom); series-attachAxis(axisX); auto* axisY new QValueAxis(); axisY-setRange(0, 20); axisY-setLabelFormat(%d); chart-addAxis(axisY, Qt::AlignLeft); series-attachAxis(axisY); return chart; }这里容易出问题的点是axisY-setRange(0, 20)——如果某月借阅量超过 20柱状图会顶到上边界导致显示不全。更稳妥的写法是先查询MAX(cnt)再动态设置 range或者直接不设 range 让 Qt Charts 自动缩放。课设里数据量通常不大固定一个较大值也能接受但如果你想让代码更专业就加上QSqlQuery maxQuery; maxQuery.exec(SELECT COUNT(*) FROM borrow_records WHERE borrow_date date(now, -6 months) GROUP BY strftime(%Y-%m, borrow_date) ORDER BY COUNT(*) DESC LIMIT 1); int maxVal 10; if (maxQuery.next()) maxVal maxQuery.value(0).toInt() 5; axisY-setRange(0, maxVal);Y 轴上限加 5 是为了给柱状图顶部留出视觉缓冲不然最高的柱子紧贴图表上边缘观感会压抑。6. 实验报告的加分写法从代码注释到测试用例的完整呈现6.1 报告结构章节划分与各部分的写作重量一份 95 分以上的实验报告结构上通常这样分配需求分析15%不是复述题目要求而是画出用例图、列出功能需求和非功能需求系统设计25%包含数据库 E-R 图、系统架构图、模块划分说明关键功能实现30%核心代码片段 讲解选择增删改查、事务处理、报表生成系统测试15%测试用例表 结果截图 边界条件说明总结与展望10%遇到的困难、踩过的坑、后续改进方向权重集中在系统设计和关键功能实现上。老师评审时最看重的不是「每个按钮都能点」而是「设计决策是否合理、代码是否体现了面向对象思想、异常情况是否考虑到位」。6.2 报告中必须秀出来的三个设计亮点第一个是异常处理闭环。不要只在代码里捕获错误但没有任何反馈——报告里应贴出QMessageBox::critical弹出错误提示的截图并写出以下说明系统所有数据库操作都检查了exec()的返回值出错时用户能立即看到明确提示而非程序被杀。第二个是事务一致性。把借阅操作的完整流程图放进报告标注第 4.1 节中的那五步每一步的失败回滚路径都用不同颜色标出来。文字说明重点强调没有事务时「INSERT 成功但 UPDATE 失败」的中间态会导致库存数据错误有了事务保证「要么全部成功要么全部回滚」。第三个是参数化查询防注入。报告截取一小段代码对比不用参数拼接的写法SELECT * FROM books WHERE title kw 和prepare bindValue的写法。说清楚后者如何规避单引号闭合问题——然后补一句「虽然课设系统没有公网暴露但这一习惯能直接迁移到企业级项目」。6.3 测试用例表与边界条件验证测试章节是最能拉分的部分大多数课设只写「功能正常」90 分以上的报告会列一张完整的边界测试表测试场景操作步骤预期结果实际结果借阅超出库存对 available_count0 的图书发起借阅弹窗提示「无可借库存」符合预期读者超量借阅使读者未还数量达上限后再借弹窗提示「借阅已达上限」符合预期归还已归还图书对已归还记录重复点击回迁不应再增加库存符合预期搜索关键字含单引号输入OBrian不崩溃且能正常查询符合预期ISBN 重复录入两条相同 ISBN第二条被拒绝并提示唯一约束冲突符合预期测试用例写的标准是「可复现」——任何人照着操作步骤都能得到同样的结果而不是笼统的「测试通过」。每一条用例后附截图尽量展示弹窗提示和数据库状态的对应关系。6.4 报告排版中的图表规范正文中的 E-R 图、用例图、时序图、界面截图图片下方一律加图注格式如「图 3-1 借阅事务流程图」。图和表按章编号第 3 章的第一张图是「图 3-1」第一张表是「表 3-1」。代码片段用等宽字体排版并标注语言类型。这些细节看起来琐碎但在评分标准明确的前提下排版整洁本身就是独立的加分项。最后提示一句报告的致谢和参考文献不是必填项但如果用了 Qt 官方文档、SQLite 官方资料建议列出。参考格式用 GB/T 7714这条写在报告末尾不需要单独的章节标题归属于参考文献即可。本文还有配套的精品资源点击获取