☰
含数据库文件的Python药物管理系统:运行、避坑与扩展全指南
2026/10/18 11:14:13 网站建设 项目流程

简介:基于python的药物管理系统完整项目包,内含数据库脚本,面向药店、医院药房以及需要积累Python工程项目经验的开发者。项目围绕药品库存管理场景,支持药品信息录入、查询、更新、删除等常用操作,属于典型的“Python + 数据库 + Web界面”实践项目。

资源为zip压缩包,共51个文件,约23.73MB,其中含5个Python源码模块、3个sql数据库脚本、24个html页面模板,另有xml配置、README文档、静态资源包及tests测试目录,结构清晰,便于对照学习与二次开发。已有111人学习下载。

代码组织较为规范,涉及Flask路由设计、数据库建模与增删改查、模板渲染、表单处理、依赖管理与单元测试等内容,各模块职责分明。读者可从中看到完整项目如何分层搭建,包括项目配置、静态资源、数据库文件和前端页面的组织方式,适合作为学习Web后端与数据库联动开发的参考样例,也可以直接改造成更完整的药房管理系统。

1. 拿到这个药物管理系统 zip:它到底能干什么、适不适合你

一个“基于 python 的药物管理系统(含数据库文件)”压缩包拿到手,最容易犯的错就是急着把代码跑起来。真正决定这个系统能不能用的,往往不是那几百行 Python,而是里面那个数据库文件——表结构、预置数据、账目流水全在它里面,代码只是操作它的外壳。这个系统的目标很清晰:管药品信息、盯库存数量、看有效期限,顺带记录每一笔出库入库;它适合课程设计或毕业设计的学生、想给门店搭内部小工具的从业者,以及刚接触“代码+数据库”配对的 Python 新手。

反直觉的地方在于:很多新手把代码改了一通,最后发现系统跑不起来的问题几乎都出在数据库文件的路径、Python 版本和依赖环境上,代码本身反而没动过。所以这篇笔记按“拆包看结构→把环境跑通→读懂核心功能→逐个避开数据库相关的坑→再做进阶扩展”的顺序来写,按这个顺序走,半天时间能让它在本地稳定运行。

2. 先拆 zip 看结构:技术选型与文件清单

2.1 解压后的第一件事:把文件按作用分类

拿到 zip 先解压,但不要双击任何.py文件。我一般会先把文件按“入口程序、数据库文件、依赖清单、说明文档”四类分清楚,常见的解压结果大致是下面这种结构:

类别常见文件名在系统里的作用
入口程序main.py / app.py / run.py启动系统,可能是命令行交互或 GUI 界面
数据库文件drug.db / drug_management.sqlite3存药品、用户、出入库流水等全部数据
依赖清单requirements.txt记录第三方库及版本,环境还原的依据
说明文档README.md / 使用说明.txt写清楚账号、初始化方式和数据库位置

分类时重点确认一件事:数据库文件路径是不是和代码里的默认路径一致。很多 zip 包在作者电脑上能跑,传到另一台机器上就报“no such table”或“database not found”,就是因为入口脚本用相对路径写死了数据库位置,而解压后的文件层级和预期不同。

如果解压后发现没有说明文档,那就直接看数据库文件。用任意 SQLite 工具打开看一眼表名,比猜代码高效得多。这一步花五分钟,能避免后面浪费两小时改错方向。

2.2 为什么这种层级的系统几乎都选 SQLite

大多数药物管理系统 zip 包里的“数据库文件”都是 SQLite 格式,这几乎是标准做法。原因很实际:Python 的sqlite3模块是内置的,不需要单独安装数据库服务;整个数据库就是一个普通文件,拷贝等于备份,交付等于发一个文件;对于药品数量在几万条以内、单机单人使用的场景,SQLite 的读写性能完全够用。

它不擅长的是高并发写入和多人同时操作同一个库文件,这在课程设计和门店内部工具场景里基本碰不到。所以收到这种包,优先按 SQLite 的思路去理解,而不是想着去装 MySQL 导数据。

连接数据库的最小代码长这样,几乎每个文件都会用到:

import sqlite3 DB_PATH = "drug_management.db" def get_conn(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row # 查询结果支持用字段名取值 return conn

sqlite3.connect的参数就是数据库文件的路径;文件不存在时 SQLite 会自动创建,这就是为什么很多程序第一次运行不会报“缺数据库”而会报“缺表”。“row_factory = sqlite3.Row”这行建议保留,没有它,查询出来的行只能按下标取,代码里写row["name"]就会报错。

配套的建表初始化逻辑通常长这样:

def init_db(): conn = get_conn() cur = conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS medicines ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, spec TEXT, batch_no TEXT, stock_qty INTEGER DEFAULT 0, unit_price REAL, expire_date TEXT, supplier TEXT ) """) conn.commit() conn.close()

id用AUTOINCREMENT是为了保证删除记录后新插入的行号不重用,流水对账时不容易错。“expire_date存 TEXT”是刻意为之,药品有效期按 ISO 格式存成2026-05-14,字符串比较和日期计算都能直接用,比存成时间戳直观得多。stock_qty和unit_price一个用整数一个用浮点数,单价涉及钱,建议只在录入时做两位小数校验,不要用浮点数做累计运算。

2.3 登录入口:默认账号藏在初始化数据里

系统通常还有一张用户表,登录校验就是对着这张表做查询。默认账号往往写在初始化脚本里,常见的是admin / admin123,也有的包要求第一次运行后自动创建。校验登录的写法,通行做法是参数化查询:

def check_login(username, password): conn = get_conn() row = conn.execute( "SELECT * FROM users WHERE username = ? AND password = ?", (username, password) ).fetchone() conn.close() return row is not None

用?占位符而不是拼字符串,是为了避免用户输入内容破坏 SQL 结构;fetchone只取一行,存在即代表登录成功。需要留意的是,很多课程设计级别的系统里密码是明文存的,如果要把这个系统往真实场景推,至少得改成哈希存储,后面进阶章会提到。查登录之前先确认users表里真有预置账号,否则来回报“登录失败”查半天,实际是初始化没跑。

3. 本地跑通它的最小路径:环境和数据库要一起到位

3.1 先建虚拟环境:依赖装错地方是很多麻烦的起点

拿到代码第一步不应该是直接python main.py,而是先建虚拟环境。常见做法是:

python -m venv venv # Windows 下激活 venv\Scripts\activate # macOS / Linux 下激活 source venv/bin/activate pip install -r requirements.txt

python -m venv venv的意思是“用当前 Python 解释器创建一个名为 venv 的虚拟环境”,激活后终端命令行的pip install只装进这个环境,不污染系统全局。如果解压目录里没有requirements.txt,就先跑一次主程序看报错缺什么库,挨个补装;药物管理系统最常依赖的是tkinter(界面)、pandas/openpyxl(表格导入导出)、Pillow(图片或验证码)。

这里有一个高频翻车点:Windows 的 cmd 里运行python,而 macOS 或 Linux 上可能要用python3,两个指的可能不是同一个解释器版本。先跑python --version确认版本,再决定依赖怎么装。

3.2 让代码和数据库文件对上:路径是第一道坑

数据库打不开的报错,一半以上是路径问题。双击启动脚本时,程序的工作目录是脚本所在目录;在命令行里用绝对路径启动脚本时,工作目录又变成了命令行当前位置。如果代码里写的是相对路径"drug_management.db",同一个脚本在不同启动方式下可能连到两个完全不同的文件。

更稳的写法是用脚本文件自身的位置来推导数据库路径:

import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.path.join(BASE_DIR, "drug_management.db")

__file__是当前脚本的完整路径,os.path.abspath把它转成绝对路径,dirname取到脚本所在目录,再用os.path.join拼出数据库路径。这样无论从哪个目录启动,连的始终是同一个数据库文件。

如果改完路径还是报错,把数据库文件也放到项目根目录,保持main.py和.db文件同级。不要自作聪明把数据库单独挪到别的文件夹,然后改代码里去适配——除非你有没有看清全部代码的把握。新手阶段,目录结构保持和压缩包一致是最省事的。

3.3 冒烟测试:三种验证动作确认系统真的通

环境装好、路径改对之后,不要直接进业务功能,先做一个最小冒烟测试。所谓冒烟测试,就是用一段独立脚本验证“数据库能连上、表存在、能写入和读回”。我一般会临时写一个smoke_test.py:

import sqlite3 conn = sqlite3.connect("drug_management.db") cur = conn.cursor() cur.execute("INSERT INTO medicines(name, spec, stock_qty, expire_date) VALUES(?, ?, ?, ?)", ("感冒灵颗粒", "10g*9袋", 100, "2026-05-01")) conn.commit() rows = conn.execute("SELECT name, stock_qty, expire_date FROM medicines").fetchall() print(rows) conn.close()

这段脚本做了三件事:写入一条测试药品、提交事务、查询并打印。如果这三步都正常,说明数据库文件可用、表结构正确、读写权限正常,主程序跑不起来大概率是界面或登录环节的问题,和数据库无关了。跑完记得删除这条测试数据,或者重新解压一遍数据库文件,避免影响后面看到的业务数据。

4. 核心功能代码:药品增删改查、库存扣减与有效期预警

4.1 入库:事务、参数顺序和回滚是三个关键词

药品管理系统的地基操作是入库。一段典型的入库方法长这样:

def add_medicine(name, spec, batch_no, qty, price, expire_date, supplier=None): conn = get_conn() try: conn.execute( "INSERT INTO medicines(name, spec, batch_no, stock_qty, unit_price, expire_date, supplier) " "VALUES(?, ?, ?, ?, ?, ?, ?)", (name, spec, batch_no, qty, price, expire_date, supplier) ) conn.commit() # SQLite 默认不自动提交,必须显式调 commit except Exception: conn.rollback() # 写一半失败时回滚,保证数据完整 raise finally: conn.close()

参数顺序必须和 SQL 里列名的顺序一一对应,这也是后面排查“数据对不上”时先看的地方。commit是显式提交,rollback是异常回滚,这两行保证了一个药品要么完整写进去,要么完全不写,不会出现“名字写进去了批次号丢了”的半截数据。

这里提一个常见误用:有人图省事,用conn.execute("INSERT ... %s" % name)直接拼 SQL,一旦药品名里出现单引号或特殊字符,轻则报语法错误,重则产生 SQL 注入——含数据库文件的系统,用户表就是靠这种漏洞被改密码的。用?占位符成本极低,养成习惯就好。

4.2 出库扣库存:条件更新比“先查再减”更稳

出库可能会做加购和扣库存两个动作,中间不能断。扣库存最稳妥的写法是直接在UPDATE语句里加数量条件:

def reduce_stock(medicine_id, qty): conn = get_conn() cur = conn.execute( "UPDATE medicines SET stock_qty = stock_qty - ? " "WHERE id = ? AND stock_qty >= ?", (qty, medicine_id, qty) ) conn.commit() if cur.rowcount == 0: raise ValueError("库存不足或药品不存在") conn.close()

关键在WHERE id = ? AND stock_qty >= ?:如果当前库存小于要扣的数量,这个UPDATE不会更新任何行,rowcount为 0,代码直接抛异常,防止库存被扣成负数。这比我以前惯用的“先SELECT查库存,再判断够不够,再UPDATE”的方式更安全,因为查和改之间如果有别的请求混进来,容易把库存扣超。这种场景在单机版系统里不容易触发,但换个对接多窗口的项目,条件更新是唯一靠谱的写法。

4.3 有效期预警:日期格式不统一等于白做

药品管理系统最常被忽视但最实用的功能是有效期管理。预置数据里的expire_date只要都按YYYY-MM-DD格式存,预警 SQL 就可以直接用字符串比较:

SELECT name, batch_no, expire_date, stock_qty FROM medicines WHERE expire_date <= date('now', '+30 days') AND expire_date >= date('now', 'now');

date('now', '+30 days')是 SQLite 自带的日期计算,含义是“当前日期往后推 30 天”。这条 SQL 查出来的是没过期但未来 30 天内到期的药;把第二个条件去掉,查的就是已经过期的药。要注意,如果数据库里混入了2026/05/14这种带斜杠的日期,字符串比较就会错乱,所以“写入时就规范格式”比“查询时到处兼容”重要得多。

如果想在 Python 里做分类,不用改 SQL,直接执行SELECT name, batch_no, expire_date FROM medicines,然后逐行用datetime.date.fromisoformat解析再比较。但数据量大时还是应当把过滤交给数据库,Python 只处理业务逻辑。

4.4 查询与流水:让数据有迹可循

药品列表最常见的需求是模糊搜索和按有效期排序:

SELECT name, spec, batch_no, stock_qty, unit_price, expire_date, supplier FROM medicines WHERE name LIKE ? ORDER BY expire_date ASC;

调用时传入"%关键字%"即可。ORDER BY expire_date ASC把最早过期的排在最前,采购时直接看前几条就知道要补哪些货。

除了药品表,好的系统还应该有一张出入库流水表,记录每次变化的药品、数量、操作时间和操作人。一条典型的流水插入语句是:

INSERT INTO stock_logs(medicine_id, change_qty, trade_type, created_at) VALUES(?, ?, ?, datetime('now', 'localtime'));

change_qty用正数表示入库、负数表示出库,最后对账时直接SUM(change_qty)就能和药品表的stock_qty对平。如果发现两边对不上,流水表就是排查账目和库存打架的唯一依据。这种表结构不需要提前设计得多复杂,但一开始就建,后面少很多事。

5. 避坑与排查:数据库文件打不开、乱码、版本不兼容的真实翻车记录

这一章记的是我帮 A 同学调这类系统时反复踩过的坑,也是数据库文件相关的真实翻车记录。

5.1 数据库被锁定:database is locked

现象:程序运行中,尤其在写入库存时,抛出sqlite3.OperationalError: database is locked。

原因:SQLite 同一时刻只允许一个连接写库。最常见的是某个查询或者窗口打开数据库后没有关闭连接,再有新的写入进来就撞锁;其次是杀毒软件或同步盘频繁扫描数据库文件。

解决:确保程序的每个操作都走try ... finally并在其后close(),或者用上下文管理器with sqlite3.connect(...) as conn。同时可以在连接时加超时时间:sqlite3.connect(DB_PATH, timeout=10),让写入排队而不是立即报错。杀毒软件的实时扫描目录要把项目文件夹排除掉。

5.2 中文乱码:界面显示全是问号

现象:药品名、供应商名在命令行或界面上显示成???或乱码。

原因:Python 3 源码默认 UTF-8,但 Windows 命令行默认编码可能是 GBK;GUI 里如果用了print输出中文也会撞上控制台编码。另一类原因是某些代码里对字符串手动调了encode("gbk")或decode("utf-8"),多转了一次编码反而乱掉。

解决:在所有字符串读写的地方保持一致——代码文件存成 UTF-8、数据库连接不指定编码、输出到控制台前先确认终端编码;Windows 命令行里执行chcp 65001切到 UTF-8 再运行脚本。别在代码里做多余的手工编码转换,Python 3 的str类型足够处理好中文字符。

5.3 Python 版本不兼容:依赖装得上但启动报语法错误

现象:pip install -r requirements.txt顺利装完,运行python main.py却出现SyntaxError,报错指向f"..."之类的地方。

原因:系统默认的 Python 版本过旧,或者环境里同时有多个 Python,激活虚拟环境后仍调到了旧解释器。比如某些代码用了f-string,需要 Python 3.6+;用了dataclass则需要 3.7+。

解决:先执行python --version确认解释器版本,如果系统默认版本旧,就显式用py -3.10(Windows)或python3.10(Linux/macOS)创建虚拟环境。安装依赖前先看requirements.txt是否注明了python_requires,没有就自己在虚拟环境里锁一个版本,别赌系统自带的 Python。

5.4 路径或文件名带中文导致数据库找不到

现象:双击启动时报sqlite3.OperationalError: unable to open database file,但数据库文件明明就在项目目录里。

原因:一部分老版本 Windows 下,Python 的sqlite3模块对非 ASCII 路径处理不友好;另一部分原因是代码用了中文目录名,而数据库文件路径里含有中文导致连不上。这种报错看起来像玄学,本质上还是路径解析问题。

解决:把整个项目放到纯英文路径下,例如D:\drug_system\,目录名不要带中文和空格。同时按第 3.2 节的方式用os.path.dirname(os.path.abspath(__file__))动态拼接数据库路径,而不是写死一个带中文的绝对路径。

5.5 数据改了但界面列表不刷新

现象:在数据库里手动插入或更新了记录,界面上点刷新还是旧数据。

原因:界面里的表格控件加载后,数据被缓存或没有重新触发查询;也可能是查询按钮绑定的方法里fetchall()之后没重新set表格数据。

解决:在刷新按钮的回调函数里重新执行查询并重建表格行,不要复用旧的结果集。如果界面用的ttk.Treeview,标准动作是tree.delete(*tree.get_children())清空所有行,再insert新查询结果。另外确认查询连接用的是同一个DB_PATH,别出现“界面连 A 库、手动改的是 B 库”这类低级但真实的错误。

6. 进阶玩法:批量导入、自动备份与换数据库的三个实用扩展

跑通只是第一步,这种系统真正用起来还差三样东西:批量录入、自动备份、换数据库的能力。

批量导入是最常用的。药店第一次启用药房系统时,积压的药品录入靠手工敲会累死人。用csv和sqlite3就能搞定,根本不需要额外装重量级库:

import csv, sqlite3 conn = sqlite3.connect("drug_management.db") with open("drugs_import.csv", encoding="utf-8-sig") as f: reader = csv.DictReader(f) for row in reader: conn.execute("INSERT INTO medicines(name, spec, stock_qty, expire_date) VALUES(?, ?, ?, ?)", (row["名称"], row["规格"], row["库存"], row["有效期"])) conn.commit() conn.close()

注意encoding="utf-8-sig",这是为了兼容 Excel 导出 CSV 时写入的 BOM 头,不指定这个,第一行中文列名经常读不出来。模板列名建议做成固定模板,每次导入前先复制一版模板填写,能省掉大量字段错位的麻烦。

备份是后悔药级的操作。我现在每次改数据库结构或批量导入数据前,都会先备份一次:

import shutil, time bak_path = f"backup/drug_{time.strftime('%Y%m%d_%H%M%S')}.db" shutil.copy2("drug_management.db", bak_path)

copy2会保留文件的修改时间,备份文件名带时间戳后可以按日期排序查找历史版本。备份前确认数据库连接都关闭了,不然拷到的可能是个写到一半的文件。

如果将来要换成服务器级数据库,替换思路很简单:把medicines表里的AUTOINCREMENT换成 MySQL 的AUTO_INCREMENT,把date('now', '+30 days')换成DATE_ADD(CURDATE(), INTERVAL 30 DAY),其余 SQL 基本不变。真正需要改的是并发控制逻辑,因为 SQLite 时期的“单连接单写入”习惯在 MySQL 下会成为死锁隐患。

我的习惯是:拿到任何带数据库文件的 Python 项目,先复制一份数据库到 backup 目录再动代码——这个习惯是从一次误删数据翻车之后养成的。数据库文件是你的后悔药,别等丢了才拍大腿。希望帮到你。

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

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

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

立即咨询