简介:一套基于Python开发、附带数据库文件的药物管理系统完整工程,适用于药店或医疗机构药品库存管理场景,也是Python学习者、软件工程专业学生做课程设计或毕业设计的实用参考。系统围绕药品信息入库、检索、更新与删除展开,代码按照业务逻辑分层,并预先提供SQL数据源文件,可降低环境配置门槛。压缩包共51个文件,约23.73MB,主要包含Python源码、数据库SQL脚本、HTML模板、XML配置文件以及Markdown说明等,覆盖数据库管理、GUI/Web界面、异常处理、文件读写和单元测试等常见知识点。目前已有111人学习下载。借助该项目,可系统掌握Python连接MySQL/SQLite、Tkinter等界面开发、数据表设计以及打包发布流程,同时具备清晰的目录结构和测试代码,方便二次改造,是提升综合编程能力的完整参考。
1. 基于python的药物管理系统:不是桌面软件,是个开箱即用的Web系统
第一次解压「基于python的药物管理系统(含数据库文件).zip」的人,十有八九会去找一个exe双击,然后发现根本没有这个文件。这个项目的真实形态是一个B/S架构的Web应用:后端入口是app.py,数据库文件和建表SQL放在sql目录里,页面模板在templates下,浏览器就是操作界面。对药店、诊所、医院库房这类需要多人同时操作的场景,这种形态反而比单机版实用——不用每台机器都装客户端,开个浏览器就能录入、查询、更新、删除药品信息。
压缩包里已经把数据库文件、路由、模型、配置、模板和测试都备齐了,理论上解压、装依赖、启动三步就能跑起来。适合两类人:一是想快速接手一个完整Python Web项目的开发者,看看Flask应用、SQLAlchemy模型和前端模板是怎么组织在一起的;二是药品管理业务的刚需用户,需要一个能直接在局域网里用的库存系统。这篇文章会把项目结构、数据库初始化、启动参数和五个高频坑一次讲透,照着做,一小时内能跑起来。
2. 先拆代码结构再动手:路由、模型、配置各自管什么
拿到压缩包别急着装依赖,先把目录结构过一遍。这个项目的组织方式很规矩,一个文件管一件事,搞清楚每个文件的职责,后面排查问题时能少走一半弯路。
2.1 三级目录与文件职责对照
解压后第一层是Medicine-System-main,里面除去IDE配置和版本控制文件,真正影响运行的是下面这些:
Medicine-System-main ├── app.py # Web应用入口,启动时跑这个文件 ├── config.py # 全局配置:数据库路径、密钥、调试开关 ├── models.py # SQLAlchemy数据模型,定义表结构 ├── route.py # 路由与视图函数,URL和页面逻辑的映射 ├── requirements(项目用pyproject.toml声明) ├── sql # 数据库文件与建表脚本 ├── templates # Jinja2 HTML模板 ├── static # CSS、JS、图片等静态资源 └── tests # 单元测试与集成测试app.py的作用是创建应用实例、注册蓝图、把config里的配置加载进来。route.py是所有业务逻辑的集中地:药品的增删改查、库存预警、分类统计都在这一个文件里完成。models.py里定义的是数据库表对应的Python类,比如药品表Medicine,类里的每个属性对应表里的一个字段。templates目录下是HTML页面,用Jinja2模板语法接收route.py传过来的数据并渲染成用户看到的表格和表单。config.py是全局调参的地方,数据库连哪个文件、密钥是什么、开不开调试模式,都在这改。
值得注意的一个细节是,项目用pyproject.toml而不是requirements.txt来管理依赖,同时保留了poetry.lock锁定文件。这意味着作者是用Poetry做依赖管理的。如果你本机没装Poetry,后面我会给出手动安装的替代方案。
2.2 依赖安装:Poetry与pip两种路线
先看pyproject.toml里声明了哪些核心依赖,这是整个系统运行的基础,我一般用下面的命令快速过一遍:
grep -E "^(name|version|flask|sqlalchemy|python)" pyproject.toml这条命令会输出项目名、版本号、Python解释器版本约束,以及Flask和SQLAlchemy这些直接依赖的版本区间。输出结果一般长这样:
name = "medicine-system" version = "1.0.0" requires-python = ">=3.8,<4.0" flask = "^2.2" sqlalchemy = "^2.0"说明这个项目要求Python 3.8以上,Flask用2.x系列,SQLAlchemy是2.x系列。参数含义是:requires-python限定了解释器版本,poetry的^符号表示主版本不变、次版本可向上兼容,比如^2.2允许装2.3、2.9,但不会自动升到3.0,避免接口不兼容把代码搞崩。
依赖装好之后,用python -c验证一下导入是否正常:
python -c "import flask; import sqlalchemy; print(flask.__version__, sqlalchemy.__version__)"能打印出版本号说明核心依赖没问题。装依赖有两条路线,你机器上有哪个用哪个:一条是用Poetry执行poetry install,它会严格按照poetry.lock锁定的版本安装,最稳妥,项目作者本意就是让你这么干的;另一条是本机没Poetry时,手动创建虚拟环境再装包。这里有个小坑,项目里没有requirements.txt,所以手动路线需要从pyproject.toml里把flask和sqlalchemy的版本约束摘出来,手工执行pip install,或者自己生成一份requirements.txt。
我一般推荐手动路线的同学照下面这样做,能少踩一个混装依赖的坑:
python -m venv venv source venv/bin/activate pip install flask==2.2.* sqlalchemy==2.0.* pip install pytest第一行创建虚拟环境venv,第二行激活它(Windows下换成venv\Scripts\activate),第三行按pyproject的版本约束安装两个核心包,第四行装测试框架,因为tests目录里的用例要用pytest跑。这里特意指定flask和sqlalchemy的大版本,是因为SQLAlchemy 1.x和2.x的查询写法差异很大,如果装成1.x,models.py里的db.session.execute写法可能直接报错。虚拟环境存在的意义是隔离系统Python的全局包,避免你机器上其他项目的依赖版本污染这个系统。
依赖安装是后续一切操作的前提,也是后面第五章节里最容易出问题的地方,先装对了再往下走。
3. 数据库与配置:sql目录里放的是什么,连接参数怎么改
这个项目定位是“含数据库文件”,所以数据库这步是核心。搞清楚sql目录里文件的格式、models.py定义的表结构、config.py里怎么指定数据库路径,系统才能正常读写数据。
3.1 sql目录与models.py表结构对照
解压后sql目录里通常会有两种东西:一个已经创建好的SQLite数据库文件(比如medicine.db),或者一份建表脚本medicine.sql。我把SQLite比作一个轻量版的“文件型数据库”,它不需要像MySQL那样先启动一个服务,就是一个.db文件,Flask应用通过SQLAlchemy直接读写它,非常适合这种小规模的进销存系统。
models.py里的表结构才是数据库的“图纸”。我拆过不少类似项目,药品表字段一般长这样:
from datetime import datetime from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() class Medicine(db.Model): __tablename__ = "medicine" id = db.Column(db.Integer, primary_key=True, autoincrement=True) name = db.Column(db.String(100), nullable=False, index=True) category = db.Column(db.String(50), nullable=False) manufacturer = db.Column(db.String(100), nullable=True) batch_no = db.Column(db.String(50), nullable=False, unique=True) quantity = db.Column(db.Integer, nullable=False, default=0) price = db.Column(db.Float, default=0.0) expiry_date = db.Column(db.Date, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow)这个Medicine类映射的就是实际数据库里的medicine表。id是主键,自增;name是药品名称,加了index=True表示对这个字段建索引,搜索药品时能快一点;batch_no是批号,设置unique=True会让每个批号只能出现一次,防止同一个批号重复录入。quantity是当前库存数量,price是单价,expiry_date是有效期——库存预警逻辑就是拿当前日期和这个字段做比较,效期不足三个月或已经过期的药品,在页面列表里会单独标红。
SQLAlchemy这层ORM的作用,是让开发者不用手写原生SQL,直接操作Python对象就能完成数据库的增删改查。比如新增一条药品记录,常见做法是:
new_medicine = Medicine( name="阿莫西林胶囊", category="抗生素", manufacturer="某制药厂", batch_no="20250301A", quantity=100, price=12.50, expiry_date=date(2026, 2, 28) ) db.session.add(new_medicine) db.session.commit()这里new_medicine是一个Python对象,add将它放入数据库会话,commit才真正写入数据库文件。罗列的参数里,name、category、batch_no、expiry_date是必填项,quantity和price不填时按表定义里的default默认值处理(库存0、价格0.0)。值得注意的一个坑是:SQLAlchemy的db.session.commit()不能漏,只add不commit,重启应用后数据就丢了。
3.2 config.py参数与数据库初始化步骤
config.py是整个系统的总闸,数据库连到哪个文件、应用以什么模式运行、密钥是什么,全在这里配置。一个典型的配置长这样:
import os BASE_DIR = os.path.abspath(os.path.dirname(__file__)) class Config: SECRET_KEY = "change-this-in-production" SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(BASE_DIR, "sql", "medicine.db") SQLALCHEMY_TRACK_MODIFICATIONS = False DEBUG = True逐行解释一下:BASE_DIR动态获取项目根目录的绝对路径,好处是代码拷贝到任何一台机器,数据库路径都不会硬编码死掉。SQLALCHEMY_DATABASE_URI决定了应用连哪个数据库,这里用的是SQLite,路径拼到sql目录下的medicine.db。需要提醒的是,SQLAlchemy 2.x里sqlite的URI写法是四个斜杠加上绝对路径,少一个斜杠就会拼出错误的文件路径,系统会报找不到数据库的错误。DEBUG = True开启调试模式,代码改动后服务自动重启,开发阶段用起来很方便,但上线部署时必须改成False,否则异常堆栈和源码路径会直接暴露给浏览器访问者。
数据库连接参数确认无误后,启动应用时会发生一次隐式建表。如果sql目录里本身已经有medicine.db文件,这一步会把缺失的表自动补建,但不会改已有表的结构。所以你要在系统上录入真实药品数据之前,最好先确认两个事:第一,sql目录里有没有.db文件;第二,这个.db文件里的表和models.py里的Medicine类对不对得上。
如果sql目录里只有medicine.sql而没有.db文件,那就得手动执行建表脚本初始化。SQLite的初始化命令很简单:
sqlite3 sql/medicine.db < sql/medicine.sql这行命令把medicine.sql里的建表语句直接喂给sqlite3工具,生成一个空白的medicine.db文件。执行这个命令前要确认当前目录在项目根目录,并且sqlite3命令可用(Windows用户如果不带sqlite3,可以先在Python里跑一句import sqlite3确认解释器内置了这个库,再用脚本方式执行)。建表完成后,可以用下面这条命令快速验证表是否建出来:
sqlite3 sql/medicine.db ".tables"输出结果里能看到medicine这张表,说明数据库初始化成功,可以进入下一步启动应用了。
4. 启动与业务操作:从app.py到浏览器完成一次药品入库
数据库准备好了,接下来就是把整个系统跑起来,完成第一次真实业务操作。这一章按“启动参数 → 功能模块走一遍 → 查询与统计”的顺序来,每一步都能落地。
4.1 首次启动与三个关键启动参数
先看app.py里的启动写法,这是整个系统的总入口:
from flask import Flask from config import Config from models import db from route import bp app = Flask(__name__) app.config.from_object(Config) db.init_app(app) with app.app_context(): db.create_all() app.register_blueprint(bp) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)app.run这行的参数直接决定了系统怎么被访问。host="0.0.0.0"表示监听本机所有网卡的地址,这样同一局域网里的其他电脑可以用http://192.168.x.x:5000访问,不只是本机能打开。port=5000是Flask默认端口,如果被其他程序占用了,改成5001等不冲突的端口即可。debug=True对应config里的DEBUG开关,开发时开着方便,热重载功能也会在代码修改后自动重启服务。
启动的方式是在项目根目录执行:
python app.py看到Running on http://0.0.0.0:5000这行输出,说明服务已经起来了。这时在浏览器地址栏输入http://127.0.0.1:5000,就能看到系统的登录页面或药品列表页面,具体取决于项目里有没有做用户登录模块——我拆的这个版本里,进入系统默认是药品管理首页。测试时建议用127.0.0.1而不是localhost,某些Windows机器的hosts文件解析localhost时会指向IPv6地址,导致连接被拒。
4.2 药品入库与库存预警的业务闭环
进入系统后,最核心的操作是药品录入。点击页面上的“新增药品”按钮,会进入一个表单页,里面需要填写名称、分类、厂家、批号、数量、价格、有效期。逐项填完并提交,数据就会写入SQLite数据库。这一套业务流程对应的后端代码在route.py里,核心逻辑类似这样:
@bp.route("/medicine/add", methods=["POST"]) def add_medicine(): name = request.form.get("name") batch_no = request.form.get("batch_no") expiry_date = request.form.get("expiry_date") exists = Medicine.query.filter_by(batch_no=batch_no).first() if exists: return "该批号已存在,请勿重复录入", 400 new_medicine = Medicine( name=name, category=request.form.get("category"), manufacturer=request.form.get("manufacturer"), batch_no=batch_no, quantity=int(request.form.get("quantity")), price=float(request.form.get("price")), expiry_date=datetime.strptime(expiry_date, "%Y-%m-%d").date(), ) db.session.add(new_medicine) db.session.commit() return redirect("/medicine/list")这段代码有三个值得说的点:第一,批号唯一性校验是写在应用层的,先filter_by查询一次,查到了就直接返回400并拒绝插入,这是为了避免数据库抛unique约束异常导致页面报500;第二,前端表单过来的数据全是字符串,所以要做int和float转换,以及用datetime.strptime把"2025-06-30"这种字符串解析成Date对象,转换失败时表单会抛ValueError,说明前端传的日期格式不对;第三,入库成功后的redirect跳转到列表页,是POST/Redirect/GET模式的标准做法,防止用户刷新页面时表单一式两份地重复提交。
录入完成后,列表页会显示所有药品和库存数量。常见的做法是写一个库存状态判断,在quantity低于某个阈值或expiry_date临近时,页面用不同颜色高亮提醒。这类预警逻辑在项目里一般是模型层的属性方法:
@property def is_low_stock(self): return self.quantity <= 20 @property def is_almost_expired(self): return (self.expiry_date - date.today()).days <= 90is_low_stock表示库存量小于等于20时视为低库存,is_almost_expired表示剩余有效期小于90天算临期。这两个阈值是写死的,实际使用时要根据业务调整——门诊药房和药库的库存预警线完全不是一个量级,有的药几个星期才消耗一盒,20这个下限形同虚设。跑通流程后,第一件事就是把这两个阈值改成适合自己的值。
4.3 查询、更新、删除与分类统计的常见做法
药品入库只是起点,日常高频操作是查库、改库存、删错录数据。列表页通常自带一个模糊搜索框,搜索逻辑对应后端的查询:
@bp.route("/medicine/search") def search_medicine(): keyword = request.args.get("keyword", "") results = Medicine.query.filter( Medicine.name.like(f"%{keyword}%") | Medicine.batch_no.like(f"%{keyword}%") ).all() return render_template("medicine_list.html", medicines=results)这条查询用like做模糊匹配,名字或批号包含关键词就能命中。参数keyword从URL的查询串里取,没传时默认空字符串,空字符串like后面对应的f"%{}%"等价于全表查询,所以搜索框清空后能看到全部药品。更新库存的操作更简单,找到对应记录后改quantity字段,再commit一次。删除操作则要注意,正式系统一般做软删除(加一个is_deleted字段标记),而不是直接把记录从数据库里抹掉,万一删错还能恢复,属于给自己留一手后悔药。原版项目里如果有真删除,也需要先确认这条药品没有被订单表外键引用,否则会触发外键约束异常。
分类统计一般挂在首页仪表盘上,统计逻辑是分组聚合:
from sqlalchemy import func stats = db.session.query(Medicine.category, func.sum(Medicine.quantity)).group_by(Medicine.category).all()这行SQL等价于“按分类分组,统计每个分类下所有药品的总库存量”,func.sum是聚合函数,结果集里每一行是(分类名, 总库存)。这种聚合查询在前台通常渲染成柱状图——但要注意,很多版本的项目前端并没有集成图表库,页面里最朴素的是一个HTML表格,不要指望下载下来就有大屏可视化,那是加分项不是标配。
5. 避坑与排查:启动失败、乱码、依赖冲突的五个真实案例
到这一步,理论上的整个链路已经走通了,但实际运行中我至少见过下面五个高频问题。每一条都按“现象 → 原因 → 解决”的顺序写,你照着对照处理就行。
5.1 启动报Address already in use,页面打不开
现象:执行python app.py后控制台抛OSError: [Errno 98] Address already in use,或者启动时Flask显示的端口是5001而不是预期里的5000。原因:5000端口被本机其他进程占了,常见的是之前调试时没关干净残留的Python进程,或者另一个服务占用了5000。解决:先找到占用进程再决定杀不杀,命令是lsof -i:5000(Windows是netstat -ano | findstr 5000),确认是残留进程后可用kill命令结束它;如果这个端口确实属于其他正在运行的服务,改端口更稳妥,直接把app.py里app.run的port参数改成5002,然后刷新页面访问新端口。
5.2 数据库报no such table或OperationalError,表没建出来
现象:启动时没报错,一访问药品列表页面就抛sqlite3.OperationalError: no such table: medicine。原因:这个报错几乎都是数据库文件里缺表。有三种可能:sql目录里没有.db文件,应用启动时没有触发建表;或者sql目录里虽然有.db文件,但它是从网上下的半成品,表结构不完整;还有一种可能是SQLALCHEMY_DATABASE_URI拼错路径,应用连上了一个自己不存在的空库。解决:先确认sql/medicine.db文件在不在,不在就按3.2节手动建表;在的话用.tables命令检查表是否存在;表名不一致,就检查models.py里__tablename__写的是medicine,跟建表脚本里的表名是否一致。注意SQLite是文件型数据库,它的表结构对不上时不会报警,除非你直接查这个表。
5.3 控制台或页面中文乱码,药品名变成乱字符
现象:页面表格里中文正常显示,但控制台print输出的中文是乱码,或者MySQL环境下整个页面都是问号。原因:Windows控制台默认编码是GBK,而Python 3的字符串是Unicode,直接print中文时控制台用GBK解码就把UTF-8字节流搞乱了;页面乱码则是模板没有声明UTF-8编码。解决:控制台乱码在代码里加两行,或者执行命令时设置环境变量;页面乱码检查base模板的head里有没有<meta charset="utf-8">,没有就补上。如果是连接MySQL,还要在数据库URI里追加?charset=utf8mb4参数,否则写入数据库的中文也会是乱码,这是Python连MySQL最经典的编码坑之一。
5.4 同时用Poetry和pip装依赖,SQLAlchemy版本冲突
现象:系统跑得好好的,某天为了装另一个项目执行了一次pip install sqlalchemy,再启动这个药物管理系统就报AttributeError: 'Engine' object has no attribute 'execute',或者各种莫名的TypeError。原因:poetry.lock锁定的SQLAlchemy版本和pip后来装的版本不一致,poetry install时SQLAlchemy版本是2.0.x,而pip把全局环境升到了1.4.x,1.x和2.x的API差异非常大,models.py里的查询写法直接崩。解决:严格执行“一个项目一个虚拟环境”的纪律,项目用自己的venv,别用全局pip装依赖;如果已经装了,把虚拟环境删掉重建,再执行poetry install。以后凡是这个项目要装新依赖,都在项目根目录下先执行poetry add 包名,让它统一维护pyproject.toml和poetry.lock,不要手动pip装。
5.5 改了models.py里表的字段,启动后数据库表结构没变化
现象:给Medicine类加了一个字段status,重启应用后发现数据库里根本没有这列,插入数据时报no such column: medicine.status。原因:SQLAlchemy的create_all只负责建新表,不管已有表的字段变更。它不会自动迁移,这是设计使然,否则生产环境的数据表结构可能会被无意中破坏。解决:这个系统规模小,手动改数据库最直接——把sql/medicine.db备份一份,然后执行ALTER TABLE medicine ADD COLUMN status VARCHAR(20),或者干脆删掉.db文件让应用按新models.py重建表。如果是空库,我一般直接删db重新生成;如果有真实数据,一定要先备份、再改表,别数据没了才想起来后悔。
以上五条覆盖了我实际接手同类项目时九成以上会碰到的问题。前三条侧重环境,后两条侧重依赖与表结构,属于跑通之后改动项目时大概率遇到的雷区。
6. 进阶:批量导入药品数据与启动后的自检流程
系统跑通之后,最烦的一件事是往系统里一条条录药品。几十盒药还能忍,几百条的时候录到半夜眼睛花。这个项目没有内置批量导入,但作为使用者,你可以自己补一个。一个常见的做法是写一个独立的Python脚本,从CSV文件里批量往SQLite写数据。
CSV文件格式第一行是表头,后面每行对应一条药品记录:
name,category,manufacturer,batch_no,quantity,price,expiry_date 阿莫西林胶囊,抗生素,某制药厂,20250301A,100,12.50,2026-02-28 布洛芬缓释片,解热镇痛,某药业,20250402B,200,15.00,2027-01-15 维生素C片,维生素,某健康,20250315C,50,3.80,2025-12-30对应的批量导入脚本:
import csv import sqlite3 from datetime import datetime conn = sqlite3.connect("sql/medicine.db") cursor = conn.cursor() with open("medicines.csv", "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: expiry = datetime.strptime(row["expiry_date"], "%Y-%m-%d").date() cursor.execute( """ INSERT OR IGNORE INTO medicine (name, category, manufacturer, batch_no, quantity, price, expiry_date) VALUES (?, ?, ?, ?, ?, ?, ?) """, ( row["name"], row["category"], row["manufacturer"], row["batch_no"], int(row["quantity"]), float(row["price"]), expiry ) ) conn.commit() conn.close() print("导入完成")这个脚本里最关键的是INSERT OR IGNORE。因为models.py里batch_no有unique约束,如果csv文件里混了重复批号,直接插入会让整个事务报错、前面的数据全部回滚。IGNORE让重复的批号被静默跳过,不影响其他条目的导入。int和float转换放在执行前,转换失败时控制台会抛出ValueError,并且打印出错时的药品名,排查起来一目了然。导入完成后,做一个数据验证:用SELECT COUNT(*) FROM medicine对比CSV行数和数据库行数,数量对不上就说明有重复批号被跳过了。
导入完数据,我建议无论如何都强制走一遍自检流程再交付给业务方使用:
python app.py # 1. 服务能正常启动 curl http://127.0.0.1:5000/medicine/list # 2. 列表页返回200,不是500 sqlite3 sql/medicine.db "SELECT COUNT(*) FROM medicine" # 3. 数量与导入记录一致第一项验证服务本身是否健康,第二项验证路由和数据库整体链路没有断,第三项专门确认数据落库数量。三步都过了,这套系统才算真正可以扔给业务方用。
这个项目是个很标准的Flask+SQLAlchemy骨架,花半小时把它跑通、把表结构摸熟,以后再接手其他基于Flask的进销存项目会省力很多。从那以后我每次拿到这类源码包,都会先检查虚拟环境隔离、数据库路径和SQLAlchemy版本这三个点,强制走一遍上面的自检流程再开始改代码,希望能帮到你。
本文还有配套的精品资源,点击获取