☰
药物管理系统实战:Flask + SQLite 构建可运行的 Python 全栈项目
2026/11/2 14:44:22 网站建设 项目流程

简介:一套基于Python开发的药物管理系统完整项目包,面向药店、医院或需进行药品库存管理的个人与开发者,解决药品录入、查询、更新、删除等日常管理问题。压缩包共51个文件,整体体积23.73MB,包含5个Python核心源码、3个SQL数据库文件、24个HTML前端页面,以及XML配置、项目依赖描述与README说明文档等,文件类型涵盖源码、数据、配置和文档,便于按模块对照学习整套业务流程。已有108人学习下载。项目涉及数据库设计、SQL语句编写、ORM映射、Web界面交互、数据验证、异常处理、模块化编程及自动化测试等关键知识点,同时附带可直接导入的数据库文件和完整工程配置,能帮助学习者理解从后端逻辑到前端展示的完整链路。适合Python中阶学习者作为综合实战项目,也可在此基础上扩展为更完善的医药进销存系统。

1. 药物管理系统:一个能跑的 Python 全栈小项目

药房月底盘点还靠手写台账、药品过期全靠人肉记忆,这种场景在中小药店和个人药箱管理里太常见了。这份基于 Python 开发的药物管理系统,正好补上这个缺口:它把药品录入、查询、更新、删除和库存管理串成完整链路,数据库文件已经备好,解压之后改几行配置就能跑。项目走的是 Flask + SQLite 的经典组合,入口在 app.py,业务路由在 route.py,数据模型在 models.py,前后端不分离,页面模板用 Jinja2 渲染。对于正在做 Python 课程设计的人,这是一份能直接复用的代码底子;对于刚学完 Python 语法想看看真实 Web 项目怎么组织的人,这份资源比零散的教程片段完整得多。

2. 拆解项目结构:Flask + SQLite + 模板渲染的经典组合

拿到压缩包先别急着运行,花十分钟把目录过一遍。这份资源解压之后的目录比一般网上下载的僵尸项目要规整得多:除了 .idea、.DS_Store、misc.xml 这类 IDE 和 macOS 系统残留文件,其余源码、配置文件、测试、依赖锁定文件都在。我先列一张关键文件清单,对照着看,整个项目骨架就清楚了。

文件/目录作用
app.py应用入口,创建 Flask 实例、加载配置、注册蓝图
route.py路由层,定义 URL 与处理函数映射
models.py数据模型层,药品表的字段定义与增删改查封装
config.py全局配置,数据库路径、密钥、调试开关
templates/Jinja2 模板,渲染药品列表和表单页面
static/ 与 static.zip前端 CSS/JS,压缩包需解压后使用
sql/数据库文件与建表脚本
tests/基于 pytest/unittest 的自动化测试
pyproject.toml项目依赖声明,含 Flask、SQLAlchemy 等
poetry.lock依赖版本锁定文件,保证复现一致
README.md作者写的项目说明,建议第一个打开

2.1 app.py、route.py、models.py 三个文件怎么分工

Flask 项目的经典组织方式,是把「入口、路由、模型」拆成独立模块。app.py 是进程起点,它创建 Flask 实例,读取 config.py 的配置,再把 route.py 里的蓝图注册进来。route.py 负责接收 HTTP 请求,调用 models.py 封装好的数据方法,然后把结果交给 templates 下的模板渲染。models.py 是数据的唯一出入口,所有 SQL 查询都收敛在这一层。

我一般会在 models.py 里把每一个表操作封装成函数,比如get_all_medicines()返回全部药品,get_medicine_by_id(med_id)查单条,add_medicine(data_dict)做插入,update_medicine(med_id, data_dict)做更新,delete_medicine(med_id)做删除。这样 route.py 里不需要出现一条 SQL,模板只需遍历 route 传过来的列表对象。分层的直接收益是:哪天想从 SQLite 换成 MySQL,改 models.py 一个文件就够了;哪天想给页面换一套 UI,只动 templates 和 static。

# models.py 里常见的一段封装(示意) import sqlite3 from flask import current_app def get_db(): # 从 Flask 的配置里拿数据库文件路径 db_path = current_app.config['DATABASE'] conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row return conn def add_medicine(data): conn = get_db() cur = conn.execute( "INSERT INTO medicines (name, batch_no, dosage, stock, expire_date) " "VALUES (?, ?, ?, ?, ?)", (data['name'], data['batch_no'], data['dosage'], data['stock'], data['expire_date']) ) conn.commit() conn.close() return cur.lastrowid

这是一段示意代码,作用是展示 models 层怎么把 SQL 和调用方隔离开。?是 SQLite 的占位符,用来代替字符串拼接,能挡住最常见的 SQL 注入。row_factory = sqlite3.Row让查询结果可以按列名取值,比如row['name'],页面模板里写着顺手。conn.commit()之后别忘了close(),连接数一多,不关连接会在日志里刷一堆SQLite objects created in a thread警告。

2.2 数据库文件:sql 目录下到底放了什么

sql 目录是这份资源最值钱的部分。里面通常放两类东西:一类是建表语句 .sql 文件,另一类是实际的 SQLite 数据库文件,后缀可能是 .db、.sqlite3 或者干脆叫 medicine.db。SQLite 的好处是零配置,数据库就是单个文件,Python 内置的 sqlite3 模块直接读写,不需要像 MySQL 那样先装服务、再建用户赋权限。对课程设计来说,答辩时把数据库文件拷到另一台机器上,项目立刻能跑,这是加分项。

连接方式有两种常见路线。第一种是直接sqlite3.connect()拼接路径;第二种是用 SQLAlchemy 这类 ORM,把表定义成 Python 类,增删改查靠对象方法完成。项目里同时出现 models.py 和 pyproject.toml,依赖里大概率声明了 Flask-SQLAlchemy,走的是 ORM 路线。不管是哪种,都要去 config.py 里确认数据库路径是否指向 sql 目录下真实存在的文件,否则出现的问题就属于这篇文章后面要写的坑位。

# config.py 中的典型数据库配置(按下述格式核对你的项目) import os BASE_DIR = os.path.abspath(os.path.dirname(__file__)) class Config: SECRET_KEY = 'change-me-in-production' # 路径直接指向 sql 目录下的数据库文件 DATABASE = os.path.join(BASE_DIR, 'sql', 'medicine.db') DEBUG = True

BASE_DIR用os.path.abspath算出来,是为了防止从别的目录启动项目时相对路径对不上。这是 Python 项目最常见的路径翻车点:用相对路径sql/medicine.db,在项目根目录启动没问题,一旦用 IDE 的调试模式改从 tests 目录跑,路径就断了。SECRET_KEY是 Flask 用来签名 session 的密钥,默认值要换掉,不然部署出去谁都能伪造会话。

2.3 静态资源与模板渲染:前端部分怎么衔接

templates 目录下放的是 Jinja2 模板,常见文件有 base.html、index.html、add_medicine.html、edit_medicine.html。base.html 定义页面骨架,其他模板通过{% extends "base.html" %}继承。模板里写{% for med in medicines %}遍历 route 传过来的药品列表,用{{ med.name }}输出字段。这是 Flask 默认的前后端协作模式,数据在 Python 侧准备好,模板只负责显示。

static.zip 单独打包,通常是作者为了减小压缩包体积。解压之后应该得到一个 static 目录,内容是 css 和 js 文件夹。Flask 会自动把/staticURL 映射到这个目录,模板里用url_for('static', filename='css/app.css')生成引用路径。解压时最容易出的问题,是把 zip 直接解到了项目根目录,导致路径变成 static/static/css,页面加载后样式全丢。正确做法是解压后检查:如果发现多了嵌套,把内层 static 的内容挪上来,让项目根目录下只保留一个 static。

提示:跑之前先读 README.md。很多项目把启动步骤、Python 版本要求、数据库初始化命令都写在里面,比任何人网上搜到的二手教程都可靠。

3. 把项目跑起来:环境准备、依赖安装与启动命令

这一章的目标是让系统在你自己的机器上跑出第一个页面。按顺序做,不要跳步。Python 版本差异、依赖安装源、数据库路径,这三个环节是翻车重灾区。

3.1 确认 Python 版本与依赖声明

先看 pyproject.toml 里的requires-python字段,确认项目要求的 Python 版本。这份项目目录里有 poetry.lock,说明作者使用 Poetry 做依赖管理,但你不一定需要装 Poetry,用 pip 一样能安装依赖。关键是看清依赖清单里有哪些包:Flask、Flask-SQLAlchemy 大概率在列,可能还有 pytest。

# 在项目根目录执行,先确认 Python 版本 python --version # 推荐创建虚拟环境,避免污染全局环境 python -m venv venv # Windows 激活虚拟环境 venv\Scripts\activate # macOS / Linux 激活虚拟环境 source venv/bin/activate

虚拟环境是必做的一步,不是可选项。我之前见过有人图省事直接 pip install 到全局,结果机器上另一个项目的 Flask 版本被升级,那个项目启动直接崩了。虚拟环境把依赖隔离在项目自己的目录里,删除项目时把 venv 删掉就干净了,不会给系统留下后患。

# 方式一:用 pip 直接从 pyproject.toml 安装 pip install -e . # 或按依赖名逐个安装 pip install flask flask-sqlalchemy # 方式二:如果装了 Poetry poetry install

pip install -e .会解析 pyproject.toml 中的依赖并安装到当前虚拟环境,-e表示可编辑模式,源码改动即时生效,Python Web 项目一般推荐这种方式。如果你的 pyproject.toml 没有配置好构建信息,-e安装可能报错,这时退回pip install flask flask-sqlalchemy逐个装,效果一样。如果你的 Python 版本偏旧,某些最新版依赖可能装不上,这时候可以指定版本,比如pip install flask==3.0.0,不影响这个系统正常的药品管理功能。

3.2 数据库准备:建表脚本与初始数据

sql 目录里的 .sql 文件需要执行一次,保证 medicines 表和初始测试数据存在。如果你拿到的压缩包里已经包含了 .db 文件,这一步通常可以跳过;但为了养成好习惯,我建议还是手动跑一遍建表,至少知道表结构长什么样。

# 查看 sql 目录下的文件 ls -la sql/ # 如果只有建表脚本,用 sqlite3 命令行初始化 sqlite3 sql/medicine.db < sql/schema.sql # 初始化后查一下表结构 sqlite3 sql/medicine.db ".schema medicines"

上面命令里的schema.sql是示意名,把它替换成你从 sql 目录里实际看到的脚本文件名。.schema输出能看到 medicines 表的完整字段定义。以药品场景为例,核心字段应该是 id 主键、name 药品名称、batch_no 批号、stock 库存数量、expire_date 有效期、dosage 规格/用法等。如果表里没有 expire_date,后面要做效期提醒就得自己加字段,加字段的 SQL 写法是ALTER TABLE medicines ADD COLUMN expire_date TEXT;。有数据库文件时,用 Navicat 这类图形工具打开看也可以,但命令行最省事。

3.3 修改 config.py 并启动应用

启动前最后检查一遍 config.py。调试模式DEBUG = True在开发时开着,能看到详细报错和自动重载;部署到真实环境必须关掉,并替换 SECRET_KEY。数据库路径建议改成绝对路径或基于 BASE_DIR 拼接,避免从不同目录启动时找不到数据库。

# 在项目根目录启动 python app.py

正常启动会在终端输出Running on http://127.0.0.1:5000,看到这行说明 Flask 已经起来了。保持这个终端不要关,最小化它,然后打开浏览器访问 http://127.0.0.1:5000。如果页面显示药品列表,右上角有「新增药品」按钮,说明项目已经跑通,可以进入下面的增删改查验证。

3.4 把一条药品记录完整走一遍

系统跑通之后,别急着关,实际走一遍核心操作。点「新增药品」,填一个测试数据,比如阿莫西林、批号 20240601、库存 100、有效期 2026-06-01,提交。回到列表页应该能看到这条记录。点编辑,把库存改成 80,保存。再点删除,确认弹窗,这条记录消失。走完这三步,说明页面、路由、模型、数据库四层全部连通。任何一个环节出问题,都能在下面对应的坑里找到排查思路。

4. 药品增删改查的完整链路:从页面表单到数据库落库

这章把「增删改查」四个字拆开,追踪一条药品数据从浏览器表单到数据库文件落库,再回到页面显示的完整路径。理解了这条链路,以后写任何 Flask + 数据库的项目都能举一反三。

4.1 药品录入:表单提交与数据清洗

新增药品页面通常是一个 HTML 表单,字段和 medicines 表的列一一对应。用户在浏览器填完点提交,浏览器把表单数据打包成 POST 请求,发到 route.py 里注册的/add地址。route 的处理函数拿到request.form里的字段值后,先做一层简单的数据校验,再调用 models 层写进数据库。

# route.py 中新增药品的路由处理(项目常见写法) from flask import Blueprint, request, redirect, url_for, flash from models import add_medicine bp = Blueprint('main', __name__) @bp.route('/add', methods=['POST']) def add(): # request.form 拿到前端表单提交的字段 name = request.form.get('name', '').strip() batch_no = request.form.get('batch_no', '').strip() stock = request.form.get('stock', 0) expire_date = request.form.get('expire_date', '') # 必填项检查,空值直接打回,不进数据库 if not name or not batch_no: flash('药品名称和批号不能为空', 'error') return redirect(url_for('main.index')) # 库存转 int,转换失败就按 0 处理 try: stock = int(stock) except ValueError: stock = 0 add_medicine({ 'name': name, 'batch_no': batch_no, 'dosage': request.form.get('dosage', ''), 'stock': stock, 'expire_date': expire_date, }) flash('药品添加成功', 'success') return redirect(url_for('main.index'))

这段代码里有三个关键点。第一,request.form.get('name', '')永远带上默认值,前端没传该字段时不会抛 KeyError,而是返回空字符串。第二,必填项校验放在入库之前,不满足条件就redirect回列表页,这是防止脏数据进数据库的第一道门。第三,stock转整型时用 try-except 兜底,用户填了「abc」这种非法值,程序不会崩溃,而是按 0 处理。实际项目里还可以加正则表达式校验有效期格式,比如用re.match(r'^\d{4}-\d{2}-\d{2}$', expire_date)判断日期格式,防止数据库里存进乱七八糟的字符串。

4.2 药品查询:模糊搜索与列表渲染

列表页是所有 Web 系统的门脸。查询功能有两种常见复杂度:最简单的是一次性把全表查出来,SELECT * FROM medicines然后前端表格渲染;进一点的是支持按名称模糊过滤,配合搜索框使用。模糊搜索对应的 SQL 是WHERE name LIKE '%关键词%',在 models 层包装成函数,route 层接收request.args.get('q')作为查询参数。

# models.py 中带搜索条件的查询封装 def search_medicines(keyword): conn = get_db() if keyword: cur = conn.execute( "SELECT * FROM medicines WHERE name LIKE ? OR batch_no LIKE ? ORDER BY id DESC", (f'%{keyword}%', f'%{keyword}%') ) else: cur = conn.execute("SELECT * FROM medicines ORDER BY id DESC") rows = cur.fetchall() conn.close() return rows

LIKE配合%是 SQLite 常用的模糊匹配写法,f'%{keyword}%'会让匹配定位在字符串任意位置。用参数占位符而不是f"SELECT ... WHERE name LIKE '%{keyword}%'",是因为直接把用户输入拼进 SQL 等于把 SQL 注入漏洞送上家门,这是底线问题。ORDER BY id DESC让新录入的药品排在最前,列表页体验上更符合直觉。查询结果拿到后,route 把它塞进模板渲染:

# route.py 中的列表路由 @bp.route('/') def index(): keyword = request.args.get('q', '') medicines = search_medicines(keyword) return render_template('index.html', medicines=medicines, keyword=keyword)

render_template的第三个参数keyword=keyword用于搜索框回显:用户搜完「阿莫西林」,页面上的搜索框里还留着刚才的词,不然每次搜索都要重新打字。这一点经常被新手忽略,看起来是小细节,实际体验差别很大。

4.3 更新与删除:POST 重定向与数据一致性

更新药品的完整操作分两步:先进入编辑页,把这条记录的现有值填充到表单里,让用户改;再提交 POST 请求,把新值更新到数据库。编辑页要能从 URL 或表单里拿到药品 id,route 根据 id 查出原记录,模板渲染时把value="{{ med.name }}"填进输入框。更新操作的 SQL 用参数占位:

# models.py 中的更新与删除 def update_medicine(med_id, data): conn = get_db() conn.execute( "UPDATE medicines SET name=?, batch_no=?, dosage=?, stock=?, expire_date=? WHERE id=?", (data['name'], data['batch_no'], data['dosage'], data['stock'], data['expire_date'], med_id) ) conn.commit() conn.close() def delete_medicine(med_id): conn = get_db() conn.execute("DELETE FROM medicines WHERE id=?", (med_id,)) conn.commit() conn.close()

删除是高危操作,实际项目里至少要做两件事:一是页面端用 confirm 弹窗让用户二次确认,二是 route 层判断 id 是否存在,删一个不存在的 id 虽然 SQLite 不会报错,但日志里会留下一条rowcount = 0的可疑记录。代码里删完后紧跟着conn.commit(),别把 commit 漏掉,SQLite 默认在一个事务里,不 commit 的话,数据其实没有真正落到磁盘,重启应用后记录原封不动,这个坑我踩过不止一次。

4.4 完整的请求-响应链条

把上面几个小节串起来看,一次药品编辑操作是这样的:浏览器发 POST /edit/3 → route.py 收到请求 → 调用 models.update_medicine(3, data) → 执行 UPDATE SQL → commit 落盘 → route 用 redirect 回到列表页 → 浏览器发起 GET / → route 调 search_medicines 查全表 → 模板渲染出新列表 → 浏览器展示。这条路每一步都是单向的,职责边界非常清晰,这也是为什么这类项目适合作为课程设计模板:要讲架构,一张时序图画完;要改数据库,只动 models;要改界面,只动 templates。

5. 避坑指南:跑这个系统最常见的 5 个问题

这一章写的全部是真实踩过的坑。每条按现象、原因、解决的顺序来写,建议把这一章截图存下来,跑项目时对照着查。

5.1 现象:页面提示 no such table: medicines

刚启动项目,打开列表页直接报sqlite3.OperationalError: no such table: medicines。原因几乎都是同一个:数据库文件存在,但表还没建;或者你的 config.py 里 DATABASE 指向了一个新建的空 db 文件,而不是 sql 目录里那个已经初始化好的文件。解决方法是:第一,确认 config.py 的 DATABASE 路径指向真正有表结构的文件;第二,如果没有现成 db 文件,手动执行 sql 目录下的建表脚本;第三,执行完查一遍PRAGMA table_info(medicines),能看到字段列表才算真建好。

5.2 现象:ImportError: cannot import name 'xxx' from 'models'

项目启动或访问某个路由时报 ImportError。原因一般是路径问题:你没有在项目根目录启动,导致 Python 的模块搜索路径里找不到 app.py、models.py 这些兄弟模块;或者改动了目录结构,比如把 route.py 挪进了子目录。解决方法是:回到项目根目录启动,不要在 IDE 的临时目录或 tests 目录下运行;如果你必须从别的位置启动,用sys.path.insert(0, '/你的项目绝对路径')把根目录加进搜索路径。还有一个隐蔽原因是从 static.zip 解压时把源码目录也解乱了,这种建议重新解压原压缩包。

5.3 现象:页面是纯文本,样式全部丢失

列表页内容能显示,但 CSS 没生效,整个页面像是 1998 年的网页。原因就是 static 路径不对,最常见的是把 static.zip 解压后多了一层嵌套,导致项目里存在 static/static 结构。解决方法是:删除解压出的目录,重新解压,然后检查项目根目录下应该有且只有一个 static 文件夹,里面的目录结构是 css/xxx.css、js/xxx.js,而不是 static/css 外面再套一层 static。如果你改了模板里的url_for('static', ...)引用,也要顺带核对文件名大小写。

5.4 现象:把数据库从 SQLite 改成 MySQL 后,启动就报 ModuleNotFoundError

想把它换到 MySQL,改了 config.py 里连接串,结果启动阶段就报ModuleNotFoundError: No module named 'pymysql'。原因是用 SQLAlchemy 连接 MySQL 必须显式安装对应的 DBAPI 驱动,SQLite 是内置的,MySQL 不是;而且连接串需要写成mysql+pymysql://用户名:密码@127.0.0.1:3306/medicine?charset=utf8mb4这种完整格式,少写任何一段都会在导入时挂掉。解决方法是先pip install pymysql,再把 config.py 里的 URL 改成上面这种格式,然后同步处理建表脚本的语法差异,比如AUTOINCREMENT换成AUTO_INCREMENT、TEXT类型换成 MySQL 的VARCHAR,跑初始化脚本之前先把原 SQLite 数据导出成 SQL 语句,避免字段类型兼容问题。

5.5 现象:端口 5000 被占用,启动直接报错

终端显示OSError: [Errno 98] Address already in use或 Windows 下的WinError 10048。原因是有另一个进程占用了 5000 端口,常见的是你之前启动的 Flask 实例没杀干净,或者机器上别的服务用了这个端口。解决方法是:在项目根目录执行lsof -i:5000(Windows 用netstat -ano | findstr 5000)找到占用进程,杀掉它;或者改启动端口:python app.py里如果用了app.run(port=5000),临时改命令行启动参数app.run(port=5001)也行。我自己的习惯是写死一个不太常用的端口比如 5800,避免每次和系统服务打架。

6. 进阶玩法:从课设到可用系统,还能加什么功能

如果把核心增删改查跑通了,这份资源在你手里已经有了一个可运行的骨架。接下来不是继续加花哨按钮,而是把系统的实际可用性往上抬。第一个建议是加有效期预警:在 models 层写一个方法get_expiring_medicines(days=30),用 SQL 的date('now', '+30 days')和expire_date比较,把未来 30 天内过期的药品捞出来,在列表页顶部醒目位置展示。这个功能对药房场景是刚需,课程设计答辩时也是最容易讲清楚价值的点。

第二个建议是补一组单元测试。项目里已经有 tests 目录,但很多同学拿到后从没跑过。你可以用 pytest 给 models 层写最简单的一组测试:创建临时数据库,插入一条药品,断言查询返回的结果里有它,更新后再断言库存变成新值。测试不要求覆盖全部代码,先覆盖增删改查四个核心动作,就能让代码质量在后面改功能时有点保障。具体写法是 pytest 的 tmp_path fixture 建临时文件,把 config 里的 DATABASE 指过去,测完自动清理,不会污染正式数据库。

我自己的习惯是拿到任何 Python Web 项目,第一件事不是点运行,而是先打开 models.py 看一行conn.close()有没有写、commit 有没有漏,再看 config.py 里的路径是相对还是绝对。这两个地方能看到作者对工程细节的态度,也能预判这个系统后面会不会给我埋坑。这份药物管理系统的好处是结构足够典型、数据库文件现成、坑也不难排,你按这篇笔记从头走一遍,等于把 Flask + SQLite 的完整开发链路亲手过了一次。希望帮到你。

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

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

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

立即咨询