C++与Qt实战:智慧药店管理系统的架构设计与核心实现

📅 2026/7/14 3:12:23 👁️ 阅读次数 📝 编程学习
C++与Qt实战:智慧药店管理系统的架构设计与核心实现

1. 项目概述与核心价值

最近在整理过往的项目资料,翻到了一个几年前主导开发的“智慧药店购药系统”,用C++写的,感觉挺有代表性。当时市面上很多药店还在用Excel或者一些简单的进销存软件,管理混乱、效率低下是常态。我们团队接到的需求,就是打造一个从底层到界面都自主可控、性能高效、能处理复杂业务逻辑的桌面端管理系统。选择C++,一方面是因为它对性能的极致追求,能轻松应对高峰期大量并发的药品查询和销售记录写入;另一方面,它的面向对象特性让构建一个结构清晰、易于维护的中大型项目成为可能。这个系统最终实现了药品全生命周期管理、智能化的库存预警、会员体系以及多维度的销售分析报表。今天,我就把这个项目的设计思路、核心实现以及踩过的那些“坑”详细拆解一遍,希望能给正在学习C++项目实战,或者对商业系统开发感兴趣的朋友一些实实在在的参考。

2. 系统整体架构与设计思路拆解

2.1 为什么选择C++与Qt框架?

在项目启动的技术选型会上,关于语言和框架的讨论是最激烈的。有人提议用Java Swing或C# WinForms,开发速度快;也有人建议直接用Python+PyQt,原型迭代快。但我们最终拍板用C++搭配Qt,主要基于以下几点考量:

性能与资源控制:药店系统虽然看似业务不复杂,但一旦数据量上来(比如连锁药店,药品SKU上万,日交易记录数千条),对实时响应要求很高。C++的零成本抽象和直接内存管理能力,能确保在药品模糊查询、库存实时扣减、复杂报表生成等核心操作上保持毫秒级响应。这是托管语言或脚本语言在极端情况下难以保证的。

跨平台与原生体验:Qt框架提供了近乎完美的跨平台GUI支持,一套代码可以编译运行在Windows、Linux甚至macOS上。这对于未来可能向不同操作系统环境部署的药店来说,维护成本极低。同时,Qt的控件库丰富,能做出媲美原生应用的用户体验,这对于需要长时间操作的收银员界面至关重要。

面向对象与模块化设计:C++的类、继承、多态等特性,非常适合用来建模药店的业务实体。比如,我们可以设计一个Medicine基类,然后派生出PrescriptionDrug(处方药)和OTC(非处方药)子类,通过多态来处理不同的销售规则和库存管理逻辑。Qt的信号槽机制更是将界面与业务逻辑解耦的神器,让代码结构非常清晰。

技术栈的纯粹与深度:这个项目本身也是一个很好的C++进阶实践场。它会涉及到自定义数据结构(如用于快速检索的药品索引)、文件I/O、多线程(后台数据备份、网络通信)、数据库操作(SQLite/MySQL)以及设计模式(如工厂模式创建药品对象、观察者模式实现库存预警)的综合运用。

2.2 核心功能模块划分

基于对药店业务流程的深入调研,我们将系统划分为五个核心模块,各司其职又紧密联动:

  1. 药品信息管理模块:这是系统的基石。负责药品的增删改查,字段包括药品编码(唯一)、通用名、商品名、规格、剂型、生产厂家、批准文号、分类(处方/非处方/中药/器械等)、库存上下限、进货价、零售价、医保标识等。难点在于设计高效且灵活的检索方式,支持按编码、按名称(支持模糊)、按分类、按厂家等多条件组合查询。

  2. 库存管理模块:与药品模块强关联。除了基本的入库、出库、盘点功能外,核心在于实现智能预警。系统需要实时监控库存量,当库存低于设定的安全库存下限时,自动触发预警(界面高亮、生成采购建议单);当库存高于上限或药品近效期(需设置有效期字段)时,也要进行提示。这要求库存变动(销售、采购、退货)必须与药品信息实时、原子性地同步。

  3. 销售与收银模块:这是前台的核心交互界面。需要实现快速扫码/输入药品编码添加购物车,自动计算总金额(支持会员折扣、促销活动),处理支付(现金、银行卡、移动支付对接预留接口),打印小票,并最终完成销售单的创建。每一笔销售都必须自动、准确地扣减库存,并记录详细的销售明细。

  4. 客户与会员管理模块:用于建立客户档案,记录消费历史,实现会员积分、等级折扣等功能。这不仅是提升客户粘性的手段,也为后续的销售数据分析提供了数据基础。

  5. 报表与统计查询模块:这是管理决策的“驾驶舱”。需要能够按日、周、月、年等周期生成销售报表、利润报表、畅销品/滞销品分析、库存周转率报表等。数据透视和可视化(Qt Charts可以派上用场)在这里很重要。

2.3 技术架构图与数据流设计

虽然不能画图,但可以用文字描述清楚层次关系。我们采用经典的三层架构:

  • 表示层(UI Layer):由Qt Widgets构建的用户界面,负责展示数据和接收用户输入。
  • 业务逻辑层(Business Logic Layer):一系列C++类,封装了所有药店业务规则,如“销售时检查库存”、“处方药销售需记录执业药师号”、“会员积分累计规则”等。这是系统的核心大脑。
  • 数据访问层(Data Access Layer, DAL):负责与数据库交互。我们封装了一个DatabaseManager单例类,提供统一的接口(如executeQuery,executeUpdate)来执行SQL,并将结果集转换为业务层需要的对象(如Medicine对象列表)。这样,即使未来更换数据库(从SQLite迁移到MySQL),也只需修改DAL,上层业务无感。

数据流以一次销售为例:UI触发销售信号 -> 业务逻辑层创建SalesOrder对象,并调用其addItem方法添加药品 -> 业务逻辑层验证库存、计算价格 -> 调用DAL在事务中执行:1. 插入销售主表记录,2. 插入销售明细表记录,3. 更新药品库存表 -> 事务提交,DAL返回成功信号 -> 业务逻辑层更新会员积分 -> UI显示成功,打印小票。

设计心得:在初期一定要花时间把数据库表结构设计好。我们当时就为“药品”和“库存”是分开两张表还是一张表争论了很久。最终决定分开:medicine_info表存静态信息(名称、规格等),inventory表存动态信息(当前库存量、批次、有效期)。这样更符合数据库范式,也方便做批次管理和效期追踪。

3. 核心模块的详细设计与C++实现

3.1 药品与库存管理的数据模型设计

这是整个系统的“心脏”,设计的好坏直接决定了系统的健壮性和性能。

数据库表结构核心字段示例:

-- 药品信息表 (medicine_info) CREATE TABLE medicine_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, -- 内部ID code VARCHAR(20) UNIQUE NOT NULL, -- 药品编码,唯一 generic_name VARCHAR(100) NOT NULL, -- 通用名 brand_name VARCHAR(100), -- 商品名 specification VARCHAR(50), -- 规格 manufacturer VARCHAR(100), -- 生产厂家 category INTEGER NOT NULL, -- 分类 (0:处方药,1:非处方药...) purchase_price DECIMAL(10,2), -- 进货价 retail_price DECIMAL(10,2) NOT NULL, -- 零售价 -- ... 其他字段 ); -- 库存表 (inventory) CREATE TABLE inventory ( id INTEGER PRIMARY KEY AUTOINCREMENT, medicine_id INTEGER NOT NULL, -- 关联medicine_info.id batch_number VARCHAR(50), -- 批号 expiry_date DATE, -- 有效期至 quantity INTEGER NOT NULL DEFAULT 0, -- 当前库存数量 alert_min INTEGER DEFAULT 10, -- 库存预警下限 -- ... 其他字段 FOREIGN KEY (medicine_id) REFERENCES medicine_info(id) ON DELETE CASCADE );

对应的C++类设计:我们使用简单的ORM思想,为每个主要实体创建对应的C++类。

// Medicine.h class Medicine { public: Medicine(); int id() const; void setId(int id); QString code() const; void setCode(const QString &code); // ... 其他属性的getter/setter // 关键业务方法 static QList<Medicine> search(const QString &keyword, int category = -1); bool save(); // 保存到数据库 bool remove(); // 从数据库删除 private: int m_id; QString m_code; QString m_genericName; QString m_brandName; // ... 其他字段 double m_retailPrice; }; // Inventory.h class Inventory { public: Inventory(); int id() const; int medicineId() const; void setMedicineId(int id); int quantity() const; bool setQuantity(int newQuantity); // 设置库存,内部可包含校验 QDate expiryDate() const; bool isExpired() const; // 判断是否过期 bool isBelowAlert() const; // 判断是否低于预警线 // 库存操作 static bool stockIn(int medicineId, const QString &batch, const QDate &expiry, int qty); static bool stockOut(int medicineId, int qty); // 销售出库 static QList<Inventory> getAlertList(); // 获取所有需要预警的库存记录 };

实现细节与难点:

  1. 药品检索优化Medicine::search方法是高频操作。直接对数据库进行LIKE ‘%keyword%’查询在数据量大时极慢。我们的优化策略是:

    • 建立内存索引:在程序启动时,将药品的关键信息(id, code, generic_name, brand_name)加载到一个QMap<QString, int>QHash中,键是拼接的搜索字符串(如“阿莫西林-胶囊-XX制药”),值是药品ID。这样,模糊匹配先在内存中进行,快速筛选出候选ID,再用WHERE id IN (...)去数据库精确查询,性能提升巨大。
    • 异步查询:对于非常庞大的数据集,在用户输入时使用QTimer做延迟,避免每输入一个字就查询一次,最后使用单独的线程执行数据库查询,防止界面卡顿。
  2. 库存操作的原子性与事务:销售时扣减库存是最关键的环节,必须保证数据一致性。我们使用SQLite的事务行级锁(实际上SQLite是库级锁,但通过事务能保证一致性)。

    bool Inventory::stockOut(int medicineId, int qty) { QSqlDatabase db = DatabaseManager::instance().database(); db.transaction(); // 开始事务 QSqlQuery query(db); // 1. 查询当前库存(加锁) query.prepare(“SELECT quantity FROM inventory WHERE medicine_id = :mid FOR UPDATE”); query.bindValue(“:mid”, medicineId); if (!query.exec() || !query.next()) { db.rollback(); return false; } int currentQty = query.value(0).toInt(); if (currentQty < qty) { db.rollback(); // 库存不足,回滚 return false; } // 2. 更新库存 query.prepare(“UPDATE inventory SET quantity = quantity - :qty WHERE medicine_id = :mid”); query.bindValue(“:qty”, qty); query.bindValue(“:mid”, medicineId); if (!query.exec()) { db.rollback(); return false; } // 3. 插入出库记录(日志) query.prepare(“INSERT INTO stock_log (...) VALUES (...)”); // ... 绑定参数 if (!query.exec()) { db.rollback(); return false; } return db.commit(); // 全部成功,提交事务 }

    踩坑记录:早期版本没有用事务,先扣库存再写销售单,中间如果程序崩溃,会导致库存少了但销售记录没生成,对不上账。务必记住:涉及多个表联动的核心业务操作,一定要放在数据库事务中!

3.2 销售收银模块的实现

收银界面是典型的“主-从”表结构。主表是订单头(订单号、时间、总金额、会员ID等),从表是订单明细(药品ID、数量、单价、小计等)。

购物车设计:我们使用QList<CartItem>作为购物车的数据结构。CartItem是一个结构体,包含药品指针、数量、是否处方药标志等。在界面上用一个QTableWidget来展示购物车。

核心流程代码片段:

// 在收银界面类中 void CheckoutWidget::onAddToCartClicked() { QString code = ui->lineEdit_code->text().trimmed(); Medicine med = Medicine::findByCode(code); // 根据编码查找药品 if (med.isValid()) { // 检查是否是处方药,是否需要登记药师信息(此处省略校验逻辑) // 检查库存 Inventory inv = Inventory::getByMedicineId(med.id()); if (inv.quantity() <= 0) { QMessageBox::warning(this, “库存不足”, “该药品库存为零或不存在!”); return; } CartItem item; item.medicine = med; item.quantity = 1; // 默认数量1,可调整 item.price = med.retailPrice(); // 获取零售价 m_cart.append(item); // 加入购物车列表 updateCartTableView(); // 更新UI显示 calculateTotal(); // 重新计算总价 } } void CheckoutWidget::onCheckoutClicked() { if (m_cart.isEmpty()) return; // 1. 创建销售订单对象 SalesOrder order; order.setOrderTime(QDateTime::currentDateTime()); order.setMemberId(currentMemberId); // ... 设置其他属性 // 2. 添加明细项 for (const CartItem &item : m_cart) { order.addDetail(item.medicine.id(), item.quantity, item.price); } // 3. 提交订单(内部会处理库存扣减、订单保存、会员积分更新等所有事务) if (order.submit()) { QMessageBox::information(this, “成功”, “销售完成!”); printReceipt(order); // 打印小票 clearCart(); // 清空购物车 } else { QMessageBox::critical(this, “错误”, “销售失败,请检查库存或网络连接!”); } }

小票打印:我们使用Qt的QPrinterQPainter来生成小票的PDF或直接驱动热敏打印机。需要精确计算每行文本的位置,绘制表格线,并包含店铺名称、订单号、时间、药品清单、合计、收款、找零等信息。

3.3 报表统计模块的设计

报表模块的关键在于高效的数据库查询灵活的数据展示。我们避免在业务逻辑中进行大量的数据聚合计算,而是尽量利用SQL的聚合函数(SUM,COUNT,GROUP BY)在数据库端完成。

示例:生成日销售报表

-- 获取指定日期的销售汇总 SELECT DATE(s.order_time) as sale_date, COUNT(DISTINCT s.id) as order_count, SUM(s.total_amount) as total_amount, SUM(sd.quantity) as total_quantity FROM sales_order s JOIN sales_detail sd ON s.id = sd.order_id WHERE DATE(s.order_time) = ‘2023-10-27’ GROUP BY DATE(s.order_time);

在C++中,我们封装一个ReportGenerator类,它接受查询参数(如时间范围、药品分类),动态构建SQL,执行查询,并将结果填充到QStandardItemModel中,最后绑定到QTableView或传递给QtCharts生成柱状图、折线图。

性能考虑:对于需要统计大量历史数据的报表(如年度销售趋势),直接查询操作表可能会很慢。我们采用了定时任务生成汇总数据的策略。每天凌晨,由一个后台线程运行,将前一天的详细销售数据聚合后,写入一张daily_summary表。当需要月报、年报时,直接对这张小得多的汇总表进行查询,速度极快。这是一种典型的“空间换时间”的做法。

4. 开发中的难点与解决方案实录

4.1 数据库并发访问与连接管理

在多人同时操作(如多个收银台)的场景下,数据库并发访问是必须解决的问题。SQLite在默认情况下对于写操作是串行的,虽然可以通过WAL模式提升读并发,但在高并发写的桌面应用场景仍需谨慎。

我们的解决方案:

  1. 单例数据库连接管理器:确保整个应用只有一个QSqlDatabase连接(对于SQLite)。这个管理器负责连接的打开、关闭和错误处理。
    class DatabaseManager { public: static DatabaseManager& instance() { static DatabaseManager dm; return dm; } QSqlDatabase database() { return m_db; } private: DatabaseManager() { m_db = QSqlDatabase::addDatabase(“QSQLITE”); m_db.setDatabaseName(“pharmacy.db”); if (!m_db.open()) { qCritical() << “Cannot open database:” << m_db.lastError(); } // 启用外键和WAL模式(如果支持) QSqlQuery(“PRAGMA foreign_keys = ON”); QSqlQuery(“PRAGMA journal_mode = WAL”); } QSqlDatabase m_db; };
  2. 短事务原则:尽量缩短事务持有时间,操作完成立即提交或回滚。避免在事务中进行耗时的人机交互。
  3. 重试机制:对于因并发导致的SQLITE_BUSY错误,实现简单的重试逻辑。
    bool executeWithRetry(const QString &sql, int maxRetries = 3) { for (int i = 0; i < maxRetries; ++i) { QSqlQuery query; if (query.exec(sql)) return true; if (query.lastError().nativeErrorCode() == “5”) { // SQLITE_BUSY QThread::msleep(50 * (i + 1)); // 指数退避 continue; } break; } return false; }

4.2 处方药销售的特殊逻辑与审计追踪

处方药销售是监管重点,系统必须实现严格的管控和审计。

  • 逻辑控制:在Medicine类中增加isPrescription()属性。在销售时,如果购物车中存在处方药,则必须弹出一个强制窗口,要求录入执业药师工号处方单号(可扫描或手动输入),并验证药师工号的有效性。这些信息必须随销售订单一起保存到sales_order表的特定字段中。
  • 审计日志:所有对处方药的库存操作(入库、销售、退货、报损)都必须记录到专门的audit_log表,包含操作人、时间、药品、数量、前后库存变化、关联单号等,确保每一步都可追溯。

4.3 数据备份与恢复策略

药店数据至关重要,必须有一套可靠的备份机制。

  1. 自动定时备份:使用QTimer每天在业务空闲时间(如凌晨2点)触发备份。备份不仅仅是复制数据库文件,而是执行VACUUM INTO命令(SQLite)生成一个压缩的、整理过的备份文件,并加上时间戳命名。
    QString backupPath = QString(“backup/pharmacy_backup_%1.db”) .arg(QDateTime::currentDateTime().toString(“yyyyMMdd_hhmmss”)); QString sql = QString(“VACUUM INTO ‘%1’”).arg(backupPath); executeWithRetry(sql);
  2. 保留策略:本地保留最近7天的备份,更早的备份自动上传到指定的网络存储或云盘(需集成相关SDK)。
  3. 一键恢复功能:在系统设置中提供恢复界面,选择备份文件后,关闭主数据库连接,用备份文件替换当前数据库文件,然后重新初始化连接。此操作前必须有强烈警告,且最好在技术人员指导下进行。

5. 项目部署、优化与扩展思考

5.1 编译部署与依赖管理

项目使用Qt Creator作为IDE,采用qmake进行构建。为了简化部署,我们使用静态编译Qt库,最终生成一个独立的可执行文件,避免了目标机器上安装Qt运行库的麻烦。虽然这会使最终文件变大(约20-30MB),但对于商业软件部署来说,稳定性优先。

部署包包含:

  • 主程序EXE文件。
  • 数据库文件(初始为空或带有基础数据)。
  • 配置文件config.ini(用于配置数据库路径、店铺信息、小票打印机型号等)。
  • README.txt说明文档。

5.2 性能优化点回顾

  1. 数据库索引:在medicine_info(code),sales_order(order_time),inventory(medicine_id)等经常用于查询和连接的字段上建立索引,这是提升数据库性能性价比最高的手段。
  2. 界面懒加载:对于报表等数据量可能很大的界面,采用分页加载或虚拟滚动(Qt的QTableView配合自定义模型可以实现),而不是一次性加载所有数据。
  3. 图片等资源处理:药品图片如果直接以BLOB形式存数据库会影响性能。我们改为存储图片路径,将图片文件放在专门的images目录下,按药品ID命名。
  4. 日志输出优化:在Release版本中,关闭所有Debug级别的日志输出,减少I/O开销。

5.3 未来可能的扩展方向

  1. 网络化与云端同步:当前是单机版。未来可改造为C/S架构,使用Qt Network模块实现客户端与中心服务器的通信,数据集中存储在服务器端的MySQL/PostgreSQL,实现多门店数据同步和统一管理。
  2. 集成硬件:通过串口或网络API集成扫码枪、钱箱、顾客显示屏、电子秤等外设,让收银流程更流畅。
  3. 数据分析与智能推荐:积累足够销售数据后,可以引入简单的机器学习库(如使用C++的libsvm或dlib),实现关联商品推荐(“买了感冒药,推荐维生素C”),或者更精准的库存预测。
  4. 移动端补充:开发一个简单的Qt Quick或独立的Android/iOS App,用于库存盘点、经理移动审批等场景。

回过头看,这个基于C++和Qt的智慧药店项目,不仅仅是一个毕业设计或课程作业级别的Demo,而是一个真正考虑了业务完整性、数据安全性和性能的商用级软件雏形。它几乎涵盖了桌面应用开发的所有核心知识点:从面向对象设计、数据库操作、多线程、UI/UX,到具体的业务逻辑和异常处理。最大的体会是,清晰的架构设计比过早的代码优化更重要,而对业务逻辑的深刻理解是写出好代码的前提。在开发过程中,一定要多和未来的使用者(药师、收银员)沟通,他们的一个痛点往往就是你系统的一个关键特性。最后,扎实的C++功底和Qt框架的熟练运用,能让你在开发这类高性能、高定制化的桌面应用时游刃有余。