
简介这是一套面向计算机相关专业学生与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 # 常见输出示例具体以实际文件为准 # Flask2.0.1 # Flask-SQLAlchemy2.5.1 # PyMySQL1.0.2 # WTForms2.3.3这里的关键是如果requirements.txt里出现了Flask-SQLAlchemy和PyMySQL说明这是一个基于 Flask 的 Web 应用数据库用的是 MySQL。如果出现的是tkinter相关字样那可能是桌面 GUI 版本。参数上Flask2.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] mysqlpymysql://root:passwordlocalhost/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 里打开项目文件夹后按CtrlShiftP输入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_DEBUG1开启调试模式代码修改后会自动重载。jinja设为true可以支持模板语法高亮。配置好后在终端里执行python src/app.py如果看到Running on http://127.0.0.1:5000说明环境通了。这时候别急着高兴数据库还没建页面大概率会报Table yaodian.drug doesnt exist。3. 数据库设计与药品信息管理模块从建表到增删改查3.1 数据表结构分析与 SQL 建表药店系统的核心表通常包括药品表、库存表、销售记录表、用户表。药品表里会有药品名称、规格、生产厂家、批准文号、单价、库存数量、预警阈值等字段。我一般会先根据models.py里的类定义反推 SQL 建表语句。# models.py 中常见的药品模型定义 class Drug(db.Model): __tablename__ drug id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(100), nullableFalse) spec db.Column(db.String(50)) manufacturer db.Column(db.String(100)) price db.Column(db.Numeric(10, 2)) stock db.Column(db.Integer, default0) warning_threshold db.Column(db.Integer, default10)对应的 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明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(namename, specspec, manufacturermanufacturer, priceprice, stockstock, warning_thresholdwarning_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_iddrug_id, quantityquantity, sale_timedatetime.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, mimetypetext/csv, headers{Content-Disposition: attachment;filenamepurchase_plan.csv})逻辑说明StringIO在内存里模拟文件对象csv.writer写入数据seek(0)回到开头Response返回 CSV 内容。参数上suggest的计算方式是「预警阈值 × 2 - 当前库存」这是一个经验公式保证采购后库存能回到安全线以上。如果药店有季节性波动这个公式可以改成基于历史销量的加权平均。4.3 销售统计与可视化数据准备销售统计一般按日、按月汇总。SQL 层面用GROUP BY和SUMSELECT 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 rootlocalhost)或(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_foldersrc/templates, static_foldersrc/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或者连接字符串没指定charsetutf8mb4。解决建库时指定DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci连接字符串写成mysqlpymysql://root:passwordlocalhost/yaodian?charsetutf8mb4。已经建好的表可以用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阿莫西林, stock10, warning_threshold5) 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布洛芬, stock2, warning_threshold5) db.session.add(drug) db.session.commit() response client.post(/sale/add, data{drug_id: drug.id, quantity: 5}, follow_redirectsTrue) assert 库存不足 in response.get_data(as_textTrue)逻辑说明fixture里用内存 SQLite 替代 MySQL每次测试都是干净的环境。test_sale_reduces_stock验证正常扣减test_sale_insufficient_stock验证库存不足时的提示。参数上follow_redirectsTrue让测试客户端跟随重定向才能拿到最终页面的内容。跑pytest -v就能看到每个用例的结果。6.3 用 Postman 或 curl 快速验证接口有时候不想开浏览器直接用 curl 测接口更快。# 测试添加药品接口 curl -X POST http://127.0.0.1:5000/drug/add \ -d name阿莫西林spec0.25gmanufacturer华北制药price12.5stock100warning_threshold20 # 测试销售接口 curl -X POST http://127.0.0.1:5000/sale/add \ -d drug_id1quantity5参数说明-X POST指定方法-d后面跟表单数据。如果接口返回 302说明重定向成功返回 400 或 500就要去看 Flask 控制台的报错堆栈。我一般会先用 curl 把接口跑通再去调前端页面这样能快速定位是后端逻辑问题还是前端渲染问题。从那以后我每次拿到一个毕业设计项目都强制走一遍「虚拟环境 → 数据库建表 → 接口 curl 验证 → 单元测试」的流程不再一上来就改代码。这套习惯帮我省下了大量排查环境问题的时间。希望这份拆包笔记能帮到你让你在答辩时不仅能演示功能还能讲清楚背后的设计取舍。本文还有配套的精品资源点击获取