简介:这是一套面向计算机相关专业学生与Python初学者的药店药品管理系统完整项目源码,可作为毕业设计、课程设计或自学练手参考。系统围绕药品库存、销售记录、采购计划与库存预警等日常业务展开,帮助理解数据库设计、前后端交互与用户界面搭建的整合思路。压缩包共392个文件,约20.63MB,以jpeg、svg、jpg、png等图片资源为主,配合36个py源码文件、35个ts与16个vue前端组件,另有js、json、less及md说明文档,整体呈现前后端分离的工程结构。项目涵盖数据模型、视图与控制器分层,涉及SQLite或MySQL数据库操作、Tkinter或Web框架界面实现、CSV与JSON数据导入导出及异常处理等知识点。目前已有120人学习下载,适合需要完整项目案例、模块划分参考与排错思路的读者对照研读。
1. 药店药品管理系统拆包:一份能跑起来的 Python 毕业设计长什么样
很多同学做毕业设计时,最头疼的不是写代码,而是找不到一个结构完整、能跑通、还能讲清楚原理的参考项目。这份基于 Python 的药店药品管理系统,恰好就是冲着这个痛点来的。它把药品信息管理、库存预警、销售记录、采购计划这些药店日常运营的核心环节,用一套代码串了起来,适合计算机相关专业做毕业设计或课程设计的同学直接上手拆解。拿到压缩包后,你会看到python_yaodian-master这个目录,里面包含了源码、静态资源、模板文件以及依赖清单。它不是那种只有几个函数的玩具 demo,而是一个带数据库、带界面、带业务逻辑的完整应用。接下来我会按实际拆包顺序,把环境配置、数据库设计、核心模块实现和常见翻车点逐一讲清楚,让你不仅能跑起来,还能在答辩时说出个所以然。
2. 环境配置与项目结构:从 python 安装到 vscode 调试
2.1 依赖清单与 Python 版本选择
拿到项目后,第一件事不是急着pip install,而是先确认 Python 版本。这类毕业设计项目通常是在 Python 3.8 到 3.10 之间开发的,太新的版本反而容易因为第三方库兼容性问题翻车。我一般会先看requirements.txt,里面列出的库基本决定了你该用哪个版本的解释器。
# 先查看项目根目录下的依赖文件 cat requirements.txt # 常见输出示例(具体以实际文件为准) # Flask==2.0.1 # Flask-SQLAlchemy==2.5.1 # PyMySQL==1.0.2 # WTForms==2.3.3这里的关键是:如果requirements.txt里出现了Flask-SQLAlchemy和PyMySQL,说明这是一个基于 Flask 的 Web 应用,数据库用的是 MySQL。如果出现的是tkinter相关字样,那可能是桌面 GUI 版本。参数上,Flask==2.0.1这种锁定版本号的写法,意味着你不能随意升级到 Flask 3.x,否则SQLAlchemy的初始化方式会报错。我一般会建一个虚拟环境,把版本严格对齐。
# 创建虚拟环境,推荐用 venv python -m venv venv # 激活虚拟环境(Windows) venv\Scripts\activate # 激活虚拟环境(macOS/Linux) source venv/bin/activate # 安装依赖,建议加国内镜像源加速 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple逻辑说明:虚拟环境的作用是把项目依赖和系统全局环境隔离,避免不同项目之间的库版本冲突。-i参数指定镜像源,能显著提升下载速度,尤其是PyMySQL和Pillow这类包。安装完成后,用pip list核对一下版本,确保没有出现ERROR或版本降级警告。
2.2 目录结构与入口文件定位
解压后进入python_yaodian-master,你会看到类似src/、static/、templates/、data/这样的目录。src/里通常放着app.py或run.py,这是整个项目的入口。static/下是 CSS、JavaScript 和图片,templates/里是 HTML 模板。data/可能包含初始的 SQL 脚本或 CSV 数据。
# 查看项目目录树(Linux/macOS) tree -L 2 # 或者用 find 命令 find . -maxdepth 2 -type d我一般会先打开app.py,看它怎么初始化 Flask 应用、怎么注册蓝图、怎么连接数据库。重点看这几行:
# app.py 中常见的初始化代码 app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:password@localhost/yaodian' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app)参数说明:SQLALCHEMY_DATABASE_URI里的root:password要换成你本地 MySQL 的用户名和密码,localhost是数据库地址,yaodian是数据库名。SQLALCHEMY_TRACK_MODIFICATIONS设为False是为了关闭不必要的信号追踪,减少内存开销。如果这里不改,启动时会直接报连接错误。
2.3 vscode python 环境配置与调试启动
在 vscode 里打开项目文件夹后,按Ctrl+Shift+P输入Python: Select Interpreter,选中刚才创建的虚拟环境。然后配置.vscode/launch.json,方便直接按 F5 调试。
{ "version": "0.2.0", "configurations": [ { "name": "Python: Flask", "type": "python", "request": "launch", "program": "${workspaceFolder}/src/app.py", "env": { "FLASK_ENV": "development", "FLASK_DEBUG": "1" }, "args": [], "jinja": true } ] }逻辑说明:program指向入口文件,FLASK_DEBUG=1开启调试模式,代码修改后会自动重载。jinja设为true可以支持模板语法高亮。配置好后,在终端里执行python src/app.py,如果看到Running on http://127.0.0.1:5000,说明环境通了。这时候别急着高兴,数据库还没建,页面大概率会报Table 'yaodian.drug' doesn't exist。
3. 数据库设计与药品信息管理模块:从建表到增删改查
3.1 数据表结构分析与 SQL 建表
药店系统的核心表通常包括药品表、库存表、销售记录表、用户表。药品表里会有药品名称、规格、生产厂家、批准文号、单价、库存数量、预警阈值等字段。我一般会先根据models.py里的类定义反推 SQL 建表语句。
# models.py 中常见的药品模型定义 class Drug(db.Model): __tablename__ = 'drug' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) spec = db.Column(db.String(50)) manufacturer = db.Column(db.String(100)) price = db.Column(db.Numeric(10, 2)) stock = db.Column(db.Integer, default=0) warning_threshold = db.Column(db.Integer, default=10)对应的 SQL 建表语句:
CREATE TABLE drug ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, spec VARCHAR(50), manufacturer VARCHAR(100), price DECIMAL(10, 2), stock INT DEFAULT 0, warning_threshold INT DEFAULT 10 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;参数说明:DECIMAL(10, 2)表示总长度 10 位,小数 2 位,适合存金额。utf8mb4字符集能支持中文和特殊符号。warning_threshold是库存预警阈值,低于这个数就触发提醒。建表时如果用了utf8而不是utf8mb4,插入某些生僻字会报错,这是血泪经验。
3.2 药品信息增删改查的接口实现
在 Flask 里,增删改查通常通过路由函数实现。下面是一个添加药品的典型写法:
@app.route('/drug/add', methods=['GET', 'POST']) def add_drug(): if request.method == 'POST': name = request.form['name'] spec = request.form['spec'] manufacturer = request.form['manufacturer'] price = request.form['price'] stock = request.form['stock'] warning_threshold = request.form['warning_threshold'] new_drug = Drug(name=name, spec=spec, manufacturer=manufacturer, price=price, stock=stock, warning_threshold=warning_threshold) db.session.add(new_drug) db.session.commit() return redirect(url_for('drug_list')) return render_template('drug_add.html')逻辑说明:request.form获取表单提交的数据,db.session.add()把对象加入会话,db.session.commit()提交事务。如果中间任何一步出错,比如price传了空字符串,DECIMAL字段会报错。我一般会在前端加required属性,后端再加一层try...except捕获异常。
try: db.session.commit() except Exception as e: db.session.rollback() flash('添加失败:' + str(e))参数说明:rollback()是后悔药,事务失败时回滚,避免脏数据留在数据库里。flash()用于给用户提示,需要在模板里配合get_flashed_messages()使用。
3.3 库存预警逻辑与查询优化
库存预警的核心逻辑是:查询所有stock < warning_threshold的药品,在首页或单独页面展示。常见做法是用 SQLAlchemy 的过滤查询:
warning_drugs = Drug.query.filter(Drug.stock < Drug.warning_threshold).all()如果药品数量多,这个查询会变慢。我一般会加索引:
CREATE INDEX idx_stock_warning ON drug(stock, warning_threshold);逻辑说明:联合索引能加速stock < warning_threshold这种范围查询。但要注意,如果warning_threshold经常变,索引维护成本也会上升。对于毕业设计的数据量,不加索引也能跑,但答辩时能说出索引优化思路,是个加分项。
4. 销售记录与采购计划模块:业务逻辑怎么串起来
4.1 销售出库与库存扣减的事务处理
销售模块不是简单插一条记录就完事,它涉及库存扣减。如果扣减失败,销售记录也不应该写入。这就需要用事务包起来。
@app.route('/sale/add', methods=['POST']) def add_sale(): drug_id = request.form['drug_id'] quantity = int(request.form['quantity']) drug = Drug.query.get(drug_id) if drug.stock < quantity: flash('库存不足') return redirect(url_for('sale_add')) try: drug.stock -= quantity sale = Sale(drug_id=drug_id, quantity=quantity, sale_time=datetime.now()) db.session.add(sale) db.session.commit() except Exception as e: db.session.rollback() flash('销售失败:' + str(e)) return redirect(url_for('sale_list'))逻辑说明:先查库存,再扣减,再写销售记录,三步在一个事务里。如果扣减后写记录失败,rollback()会把库存加回去。参数上,quantity要做整数校验,防止传入负数或非数字导致库存异常增加。
4.2 采购计划生成与数据导出
采购计划通常是基于库存预警结果,生成建议采购清单。常见做法是查询预警药品,计算建议采购量,然后导出 CSV。
import csv from io import StringIO @app.route('/purchase/plan') def purchase_plan(): warning_drugs = Drug.query.filter(Drug.stock < Drug.warning_threshold).all() output = StringIO() writer = csv.writer(output) writer.writerow(['药品名称', '规格', '当前库存', '预警阈值', '建议采购量']) for drug in warning_drugs: suggest = drug.warning_threshold * 2 - drug.stock writer.writerow([drug.name, drug.spec, drug.stock, drug.warning_threshold, suggest]) output.seek(0) return Response(output, mimetype='text/csv', headers={'Content-Disposition': 'attachment;filename=purchase_plan.csv'})逻辑说明:StringIO在内存里模拟文件对象,csv.writer写入数据,seek(0)回到开头,Response返回 CSV 内容。参数上,suggest的计算方式是「预警阈值 × 2 - 当前库存」,这是一个经验公式,保证采购后库存能回到安全线以上。如果药店有季节性波动,这个公式可以改成基于历史销量的加权平均。
4.3 销售统计与可视化数据准备
销售统计一般按日、按月汇总。SQL 层面用GROUP BY和SUM:
SELECT DATE(sale_time) AS sale_date, SUM(quantity) AS total_quantity FROM sale GROUP BY DATE(sale_time) ORDER BY sale_date DESC;在 Flask 里,把结果转成 JSON 传给前端图表库:
from sqlalchemy import func daily_sales = db.session.query( func.date(Sale.sale_time).label('date'), func.sum(Sale.quantity).label('total') ).group_by(func.date(Sale.sale_time)).all() data = [{'date': str(item.date), 'total': int(item.total)} for item in daily_sales] return jsonify(data)参数说明:func.date()提取日期部分,func.sum()求和。jsonify返回 JSON 格式,前端用 ECharts 或 Chart.js 渲染。注意item.total可能是Decimal类型,要转成int或float才能被 JSON 序列化。
5. 避坑与常见问题排查:那些让我熬夜的报错
5.1 数据库连接报错:Access denied 与 Unknown database
现象:启动时抛出pymysql.err.OperationalError: (1045, "Access denied for user 'root'@'localhost'")或(1049, "Unknown database 'yaodian'")。
原因:SQLALCHEMY_DATABASE_URI里的用户名、密码或数据库名和本地 MySQL 不一致。很多人改了代码里的密码,但忘了在 MySQL 里创建对应的数据库。
解决:先用mysql -u root -p登录,执行CREATE DATABASE yaodian DEFAULT CHARSET utf8mb4;,然后核对app.config里的连接字符串。如果密码里有特殊字符如@或#,需要 URL 编码。
5.2 模板渲染失败:TemplateNotFound 与静态文件 404
现象:页面报jinja2.exceptions.TemplateNotFound: drug_list.html,或者 CSS/JS 加载不出来。
原因:Flask 默认在templates/和static/目录下找文件。如果项目结构是src/templates/,但app.py在src/下,路径是对的;如果app.py在根目录,就会找不到。
解决:确认app = Flask(__name__, template_folder='src/templates', static_folder='src/static')是否正确。或者把app.py放到和templates/同级的目录。静态文件 404 通常是url_for('static', filename='...')里的路径写错了。
5.3 表单提交后数据没进库:CSRF 与字段类型不匹配
现象:点击保存后页面刷新,但数据库里没有新记录,也没有报错。
原因:可能是 Flask-WTF 的 CSRF 保护开启了,但表单里没加{{ form.csrf_token }},请求被静默拒绝。或者price字段传了空字符串,DECIMAL类型转换失败但被try...except吞掉了。
解决:检查表单模板里是否有 CSRF token,或者在开发阶段临时设置WTF_CSRF_ENABLED = False。对于字段类型,在db.session.commit()之前打印一下new_drug.price的值,确认不是None或空字符串。
5.4 库存扣减出现负数:并发与校验缺失
现象:两个销售请求同时提交,库存从 1 扣成了 -1。
原因:先查后改的逻辑在并发下不安全,两个请求都查到了库存 1,都执行了扣减。
解决:用数据库层面的原子操作,比如UPDATE drug SET stock = stock - 1 WHERE id = ? AND stock >= 1,然后检查rowcount是否为 1。或者用 SQLAlchemy 的with_for_update()加行锁。毕业设计里并发场景少,但答辩时能说出这个坑,说明你考虑过生产环境。
5.5 中文乱码:数据库字符集与连接编码不一致
现象:药品名称显示成???或乱码。
原因:MySQL 数据库、表、连接三处的字符集不统一。常见的是数据库用了latin1,或者连接字符串没指定charset=utf8mb4。
解决:建库时指定DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci,连接字符串写成mysql+pymysql://root:password@localhost/yaodian?charset=utf8mb4。已经建好的表可以用ALTER TABLE drug CONVERT TO CHARACTER SET utf8mb4;修复。
6. 进阶技巧:用 Flask-Migrate 管理数据库版本与自动化测试
6.1 数据库迁移:告别手动删表重建
刚开始做毕业设计时,我改一次模型就手动DROP TABLE再重建,数据全丢。后来用了 Flask-Migrate,才体会到什么叫后悔药。
# 安装 Flask-Migrate pip install Flask-Migrate # 初始化迁移仓库 flask db init # 生成迁移脚本 flask db migrate -m "add warning_threshold column" # 应用迁移 flask db upgrade逻辑说明:flask db init只执行一次,生成migrations/目录。migrate对比模型和数据库的差异,生成 Python 脚本。upgrade执行脚本,更新表结构。参数上,-m后面的描述要写清楚改了什么,方便回滚时定位。如果迁移报错,用flask db downgrade回退到上一个版本。
6.2 单元测试:用 pytest 验证核心业务逻辑
答辩时老师常问「你怎么保证库存扣减是对的」。这时候如果有单元测试,直接跑一遍给他看。
# test_sale.py import pytest from app import app, db, Drug, Sale @pytest.fixture def client(): app.config['TESTING'] = True app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' with app.test_client() as client: with app.app_context(): db.create_all() yield client db.drop_all() def test_sale_reduces_stock(client): drug = Drug(name='阿莫西林', stock=10, warning_threshold=5) db.session.add(drug) db.session.commit() response = client.post('/sale/add', data={'drug_id': drug.id, 'quantity': 3}) assert response.status_code == 302 updated_drug = Drug.query.get(drug.id) assert updated_drug.stock == 7 def test_sale_insufficient_stock(client): drug = Drug(name='布洛芬', stock=2, warning_threshold=5) db.session.add(drug) db.session.commit() response = client.post('/sale/add', data={'drug_id': drug.id, 'quantity': 5}, follow_redirects=True) assert '库存不足' in response.get_data(as_text=True)逻辑说明:fixture里用内存 SQLite 替代 MySQL,每次测试都是干净的环境。test_sale_reduces_stock验证正常扣减,test_sale_insufficient_stock验证库存不足时的提示。参数上,follow_redirects=True让测试客户端跟随重定向,才能拿到最终页面的内容。跑pytest -v就能看到每个用例的结果。
6.3 用 Postman 或 curl 快速验证接口
有时候不想开浏览器,直接用 curl 测接口更快。
# 测试添加药品接口 curl -X POST http://127.0.0.1:5000/drug/add \ -d "name=阿莫西林&spec=0.25g&manufacturer=华北制药&price=12.5&stock=100&warning_threshold=20" # 测试销售接口 curl -X POST http://127.0.0.1:5000/sale/add \ -d "drug_id=1&quantity=5"参数说明:-X POST指定方法,-d后面跟表单数据。如果接口返回 302,说明重定向成功;返回 400 或 500,就要去看 Flask 控制台的报错堆栈。我一般会先用 curl 把接口跑通,再去调前端页面,这样能快速定位是后端逻辑问题还是前端渲染问题。
从那以后我每次拿到一个毕业设计项目,都强制走一遍「虚拟环境 → 数据库建表 → 接口 curl 验证 → 单元测试」的流程,不再一上来就改代码。这套习惯帮我省下了大量排查环境问题的时间。希望这份拆包笔记能帮到你,让你在答辩时不仅能演示功能,还能讲清楚背后的设计取舍。
本文还有配套的精品资源,点击获取