基于Python+PyQt5+SQLite的药房管理系统实现:事务一致性与界面解耦全流程解析
一、系统整体架构与技术选型
1.1 分层架构设计
系统采用经典的三层架构模式,实现关注点分离,提升代码可维护性:
-
表示层:基于PyQt5控件构建,负责用户交互与数据展示。主窗口采用标签页式布局,整合各业务模块,通过信号/槽机制与业务层通信,界面样式通过QSS统一管理,实现表现与逻辑分离。
-
业务逻辑层:封装业务规则与流程控制,负责参数校验、业务编排、异常处理,协调数据层完成业务操作,是系统的核心调度层。
-
数据访问层:通过DatabaseManager类封装所有数据库操作,对外提供领域化接口,内部屏蔽SQL细节与连接管理,采用参数化查询保障数据安全,通过事务保障写操作的原子性。
横向支撑模块包括工具函数库(日期处理、格式校验、数值计算)与样式管理模块,为各层提供通用能力。
1.2 技术选型与理由
| 技术选型 | 选型理由 |
|---|---|
| Python 3.8 | 语法简洁,开发效率高,标准库功能完善,内置sqlite3模块无需额外依赖 |
| PyQt5 | 控件库丰富,文档完善,跨平台兼容性好,信号/槽机制适合模块化桌面开发 |
| SQLite | 嵌入式零配置数据库,单文件存储便于备份迁移,支持事务与标准SQL,适配单机应用场景 |
| PyQtChart | Qt官方图表组件,原生集成度高,支持多种图表类型与动画效果,满足数据可视化需求 |
| QSS样式表 | 语法类似CSS,可集中管理界面样式,支持差异化控件定制,便于主题调整 |
1.3 核心业务流程
系统核心业务围绕药品全生命周期管理展开:药品信息录入 → 库存维护 → 销售登记(事务性扣减库存+记录流水) → 效期监控预警 → 销售数据统计分析。所有写操作均经过业务校验与事务封装,确保数据完整性。
二、核心模块深度实现
2.1 事务性销售数据处理
销售操作是系统的核心写场景,涉及药品表库存更新与销售历史表记录写入两个关联操作,必须保证原子性,避免出现数据不一致。
设计思路
利用SQLite的事务支持,将两个更新操作放在同一事务上下文中执行。通过上下文管理器封装数据库连接,正常退出时自动提交事务,发生异常时自动回滚,从机制上保证数据一致性。同时在销售历史表中冗余存储药品快照信息,即使后续药品记录被删除,历史销售数据仍可完整追溯。
核心实现
def record_sale(self, sale_data): |
""" |
事务性提交销售订单 |
:param sale_data: 包含drug_id, sell_quantity, sell_price, sell_amount, sell_date的字典 |
:return: 执行成功返回True |
""" |
with self.get_connection() as conn: |
conn.row_factory = sqlite3.Row |
cursor = conn.cursor() |
# 查询药品当前状态 |
cursor.execute("SELECT id, stock, drug_name, spec, batch_no FROM drugs WHERE id = ?", |
(sale_data['drug_id'],)) |
drug_info = cursor.fetchone() |
if not drug_info: |
raise ValueError("目标药品不存在") |
if drug_info['stock'] < sale_data['sell_quantity']: |
raise ValueError("库存不足,无法完成销售") |
# 计算库存变化 |
remain_stock = drug_info['stock'] - sale_data['sell_quantity'] |
# 更新药品主表 |
cursor.execute(""" |
UPDATE drugs |
SET stock = ?, |
sell_quantity = sell_quantity + ?, |
sell_amount = sell_amount + ?, |
sell_date = ? |
WHERE id = ? |
""", (remain_stock, sale_data['sell_quantity'], |
sale_data['sell_amount'], sale_data['sell_date'], |
sale_data['drug_id'])) |
# 写入销售流水,冗余存储药品快照 |
cursor.execute(""" |
INSERT INTO sales_history ( |
drug_id, drug_name, spec, batch_no, |
sell_quantity, sell_price, sell_amount, |
sell_date, original_stock, remaining_stock |
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
""", ( |
drug_info['id'], drug_info['drug_name'], drug_info['spec'], drug_info['batch_no'], |
sale_data['sell_quantity'], sale_data['sell_price'], sale_data['sell_amount'], |
sale_data['sell_date'], drug_info['stock'], remain_stock |
)) |
return True |
调优说明
-
采用参数化查询,避免SQL注入风险
-
单次事务内完成两次写操作,减少连接开销
-
流水表保留库存前后快照,满足审计与问题排查需求
2.2 基于信号/槽的模块化通信
桌面应用中多模块数据同步是常见难点,直接跨模块调用会导致代码耦合严重,难以维护。系统利用PyQt5的信号/槽机制,实现模块间的低耦合通信。
设计思路
为每个功能模块定义独立的更新信号,当某一模块执行了影响全局数据的操作(如完成销售)时,仅需发射对应信号,其他关联模块监听信号并执行自身的数据刷新,无需知道信号发射方的内部实现。
实现方案
class MainWindow(QMainWindow): |
# 定义全局数据更新信号 |
drug_data_changed = pyqtSignal() |
sales_data_changed = pyqtSignal() |
def __init__(self, db_manager, username): |
super().__init__() |
self.db_manager = db_manager |
self.username = username |
# 信号绑定:销售数据变更后刷新药品表与效期表 |
self.sales_data_changed.connect(self.refresh_drug_table) |
self.sales_data_changed.connect(self.refresh_expiry_table) |
def submit_sales(self): |
# 销售提交逻辑 |
if self.db_manager.record_sale(sale_data): |
# 发射数据变更信号,通知关联模块刷新 |
self.sales_data_changed.emit() |
QMessageBox.information(self, "成功", "销售登记完成") |
该方案实现了模块间的解耦,新增功能模块时只需绑定对应信号,无需修改原有业务代码,扩展性良好。
2.3 效期预警的视觉化渲染
效期管理是药房系统的高频使用功能,纯数字展示效率低下,通过视觉化高亮可以显著提升信息获取效率。
设计思路
将效期预警逻辑与表格渲染分离,工具函数负责计算剩余天数与对应预警等级,渲染逻辑根据等级批量设置单元格样式。支持多维度筛选,可快速定位不同临期等级的药品。
关键实现
def highlight_expiry_rows(table_widget, row_index, remaining_days): |
"""根据剩余天数为指定表格行设置预警背景色""" |
color_map = { |
"expired": QColor("#f8d7da"), |
"urgent": QColor("#fff3cd"), |
"warning": QColor("#ffe5d0"), |
"normal": None |
} |
if remaining_days < 0: |
level = "expired" |
elif remaining_days <= 7: |
level = "urgent" |
elif remaining_days <= 30: |
level = "warning" |
else: |
level = "normal" |
bg_color = color_map[level] |
if bg_color: |
for col in range(table_widget.columnCount()): |
item = table_widget.item(row_index, col) |
if item: |
item.setBackground(bg_color) |
三、关键技术难点与解决方案
3.1 销售操作的数据一致性问题
问题表现:销售操作同时涉及库存更新与流水写入,若中途程序异常退出,可能出现只扣库存不记流水,或只记流水不扣库存的情况,导致账目不符。
产生原因:两次数据库操作独立执行,缺少原子性保障。
解决方案:采用数据库事务封装两次写操作,利用SQLite事务的ACID特性,保证操作要么全部成功,要么全部回滚。通过上下文管理器自动管理事务提交与回滚,避免手动处理异常遗漏。
3.2 表格动态按钮的变量捕获问题
问题表现:在表格循环生成行内操作按钮时,直接绑定槽函数会出现所有按钮都指向最后一行数据的问题。
产生原因:Python闭包的延迟绑定特性,循环变量在槽函数触发时才会取值,此时变量已变为最后一次循环的值。
解决方案:使用lambda表达式的默认参数进行值捕获,在创建按钮时就将当前行数据绑定到函数参数中,确保每个按钮对应正确的行数据。
# 正确写法:通过默认参数d=drug捕获当前循环值 |
edit_btn.clicked.connect(lambda checked, d=drug: self.edit_drug(d)) |
3.3 多页面数据同步刷新问题
问题表现:在销售页面完成操作后,药品管理、效期管理等页面的数据不会自动更新,用户需手动刷新,体验不佳。
产生原因:各标签页独立封装,缺少数据变更的通知机制。
解决方案:在主窗口定义全局数据变更信号,各功能模块初始化时绑定对应信号。当数据发生变更时发射信号,所有关联模块自动执行刷新逻辑,实现一次操作、全局同步。
四、系统效果与性能分析
功能覆盖
系统覆盖用户管理、药品进销存、销售追溯、效期预警、数据统计五大核心模块,完整满足中小型药房日常运营的基础管理需求,操作流程贴合实际业务场景。
性能表现
基于测试环境(Intel i5-8250U,8GB内存)实测:
-
100条药品数据加载耗时约0.15秒
-
1000条销售记录的条件查询耗时约0.08秒
-
30天维度的销售统计图表渲染耗时约0.2秒
各项操作响应均在1秒以内,满足日常使用的性能要求。
适用场景与局限
本方案适用于单用户使用的中小型药房、社区诊所、卫生室等场景,部署简单,维护成本低。当前版本为单机桌面应用,暂不支持多用户并发访问与远程联网使用。
五、优化方向与扩展思路
-
安全增强:用户密码采用哈希加盐存储,替代明文存储方案;增加操作日志记录,满足审计需求。
-
功能扩展:增加药品入库管理模块,支持批次进价管理与利润核算;增加报表打印功能,支持标准化单据输出。
-
架构升级:可升级为C/S架构,替换为MySQL等网络数据库,支持多终端并发访问;也可迁移为Web版本,适配更多使用场景。
-
智能预警:增加库存下限预警、滞销药品提醒等功能,辅助库存优化决策。
更多推荐
所有评论(0)