
简介本资源是一套面向计算机专业本科生的Python全栈药店药品管理系统毕业设计项目适用于课程设计、毕设选题与小型医药信息化实践场景。系统覆盖药品信息管理、库存预警、销售记录、用户权限控制等核心业务模块融合数据库操作SQLite/MySQL、Tkinter/PyQt GUI开发、前后端分离VueFlask/Django及数据导出CSV/JSON等关键技术点完整呈现从需求分析到部署落地的工程化流程。压缩包共392个文件含36个Python源码文件models/views/controllers等分层逻辑、16个Vue组件与35个TS类型定义支撑前端交互、174个JPEG/SVG/PNG素材界面图标与演示截图以及requirements.txt、README.md等工程配置文件整体20.63MB结构清晰便于模块化学习。目前已有120人下载学习提供可直接运行的完整代码、带注释的核心逻辑、典型报错解决方案及标准目录组织范式是掌握Python企业级应用开发的优质实战样本。1. 为什么一个“药店药品管理系统”毕业设计能让你在答辩现场被老师当场追问三次细节这不是一个套壳的 CRUD 演示项目——它直击高校计算机类专业课程设计最真实的痛点既要体现数据库设计能力、又要覆盖完整业务闭环、还得经得起现场调试和逻辑质询。我带过 7 届毕设指导每年都有学生用“超市商品管理”“图书馆借阅系统”交差结果答辩时被问一句“药品效期怎么预警处方药和非处方药权限怎么隔离库存低于阈值时是否触发采购建议”就卡壳。而这个基于 Python 的药店药品管理系统恰恰把「药品」这个特殊商品的业务约束批号、有效期、分类管理、销售登记、供应商追溯全链路落地了。它用 SQLite 做本地持久化用 tkinter 构建可交互界面核心逻辑全部手写不调用现成框架代码量控制在 800 行以内但每个模块都留有扩展接口比如把 SQLite 换成 MySQL 只需改 3 行。适合大三下学期动手能力中等偏上的同学2 周内能跑通基础功能再花 3 天补全打印报表、导出 Excel、权限分级等加分项。别被“毕业设计”四个字吓住——它本质是一个带真实业务规则的 Python 工程实践切片不是玩具。2. 从零搭起药品管理骨架数据库设计 主界面初始化2.1 药品表结构为什么必须包含batch_no和expire_date字段药品不是普通商品它的核心属性是可追溯性和时效性。很多同学直接照搬电商商品表id, name, price, stock漏掉两个致命字段batch_no生产批号和expire_date有效期至。没有批号就无法实现同种药品不同批次的独立库存管理没有有效期系统连“近效期药品预警”这种基础功能都做不了。我们用 SQLite 建表时必须强制这两个字段CREATE TABLE medicines ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, spec TEXT, -- 规格如0.5g×24片 unit TEXT, -- 单位如盒、瓶、支 category TEXT, -- 分类处方药/OTC/保健食品/医疗器械 manufacturer TEXT, batch_no TEXT UNIQUE NOT NULL, -- 批号必须唯一同一药品不同批次算不同记录 expire_date DATE NOT NULL, -- 日期类型便于后续计算剩余天数 purchase_price REAL, sale_price REAL, stock INTEGER DEFAULT 0, supplier_id INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );提示batch_no设为UNIQUE是关键。现实中一盒阿莫西林胶囊可能有多个生产批次每批入库时间、有效期都不同必须拆成多条记录。如果只按药品名查库存会把所有批次混在一起导致效期预警失效。2.2 用 tkinter 写主窗口为什么不用 PyQt 或 Web 框架毕业设计评审最看重可控性和可解释性。PyQt 学习成本高Web 框架Flask/Django引入 HTTP、路由、模板等额外概念容易让答辩陷入“你为什么选这个框架”的被动解释。而 tkinter 是 Python 自带 GUI 库无需 pip install所有控件行为按钮点击、列表双击、输入校验都能用几行 Python 直接描述老师一眼看懂逻辑流向。主窗口采用经典的三栏布局左侧药品分类树处方药 / OTC / 医疗器械中间药品列表带滚动条的 Treeview显示名称、规格、库存、有效期右侧操作面板添加/编辑/删除/入库/销售按钮初始化代码精简到 30 行以内重点是绑定事件import tkinter as tk from tkinter import ttk, messagebox class PharmacyApp: def __init__(self, root): self.root root self.root.title(药店药品管理系统) self.root.geometry(1200x700) # 创建三栏框架 self.left_frame ttk.Frame(root, width200) self.left_frame.pack(sideleft, filly) self.center_frame ttk.Frame(root) self.center_frame.pack(sideleft, fillboth, expandTrue) self.right_frame ttk.Frame(root, width300) self.right_frame.pack(sideright, filly) # 初始化药品树 self.category_tree ttk.Treeview(self.left_frame, showtree) self.category_tree.pack(filly, expandTrue) for cat in [处方药, OTC, 保健食品, 医疗器械]: self.category_tree.insert(, end, textcat, tags(cat,)) # 绑定双击事件点击分类加载对应药品 self.category_tree.bind(Double-1, self.on_category_select) def on_category_select(self, event): item self.category_tree.focus() if item: category self.category_tree.item(item, text) self.load_medicines_by_category(category) # 此方法后续实现这段代码的价值不在炫技而在可调试性所有 UI 元素变量名清晰category_tree,left_frame事件绑定明确Double-1是双击后续增删控件不会破坏结构。比用.place(x100,y50)绝对定位靠谱得多。3. 核心业务逻辑落地入库、销售、效期预警三步闭环3.1 入库操作为什么必须校验batch_no是否已存在药品入库不是简单加库存。真实场景中同一药品不同批次可能价格不同、有效期不同必须作为独立记录插入。如果用户误输已存在的批号系统应拦截并提示否则会导致数据错乱比如把新批次的 50 盒阿莫西林错误叠加到旧批次的库存上。校验逻辑写在入库提交函数里def add_medicine_to_db(self, name, spec, unit, category, manufacturer, batch_no, expire_date, purchase_price, sale_price, stock, supplier_id): conn sqlite3.connect(pharmacy.db) cursor conn.cursor() # 关键校验检查 batch_no 是否已存在 cursor.execute(SELECT id FROM medicines WHERE batch_no ?, (batch_no,)) if cursor.fetchone(): conn.close() raise ValueError(f批号 {batch_no} 已存在请检查是否重复录入) cursor.execute( INSERT INTO medicines (name, spec, unit, category, manufacturer, batch_no, expire_date, purchase_price, sale_price, stock, supplier_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) , (name, spec, unit, category, manufacturer, batch_no, expire_date, purchase_price, sale_price, stock, supplier_id)) conn.commit() conn.close()注意这里用raise ValueError而不是messagebox.showerror是为了把错误抛给上层 UI 函数统一处理。这样既保证逻辑清晰又方便后期改成日志记录或 API 返回。3.2 销售登记如何用事务保证“扣库存记流水”原子性销售动作必须同时完成两件事① 更新药品表的stock字段减去销售数量② 插入销售流水表记录时间、药品ID、数量、金额、操作员如果只执行①失败②成功会导致账实不符反之亦然。SQLite 支持BEGIN TRANSACTION必须包裹这两步操作def record_sale(self, medicine_id, quantity, operator): conn sqlite3.connect(pharmacy.db) cursor conn.cursor() try: conn.execute(BEGIN TRANSACTION) # 开启事务 # 步骤1检查库存是否足够 cursor.execute(SELECT stock FROM medicines WHERE id ?, (medicine_id,)) current_stock cursor.fetchone()[0] if current_stock quantity: raise ValueError(f库存不足当前库存 {current_stock}需 {quantity}) # 步骤2扣减库存 cursor.execute(UPDATE medicines SET stock stock - ? WHERE id ?, (quantity, medicine_id)) # 步骤3插入销售流水 cursor.execute( INSERT INTO sales (medicine_id, quantity, amount, operator, sale_time) SELECT ?, ?, sale_price * ?, ?, datetime(now) FROM medicines WHERE id ? , (medicine_id, quantity, quantity, operator, medicine_id)) conn.commit() # 提交事务 except Exception as e: conn.rollback() # 回滚事务 raise e finally: conn.close()这段代码的血泪经验是永远不要在事务里做耗时操作如网络请求、文件读写。上面的datetime(now)是 SQLite 内置函数毫秒级完成如果换成time.strftime()就可能因系统时间抖动引发不可预测问题。3.3 效期预警用 SQL 计算“距到期日剩余天数”并筛选预警不是简单查expire_date today而是要给出剩余天数方便药师判断优先级。SQLite 没有DATEDIFF函数但可以用julianday()计算儒略日差-- 查询所有距到期日 ≤ 30 天的药品并计算剩余天数 SELECT name, batch_no, expire_date, CAST(julianday(expire_date) - julianday(now) AS INTEGER) AS days_left, stock FROM medicines WHERE julianday(expire_date) - julianday(now) BETWEEN 0 AND 30 ORDER BY days_left;在 Python 中调用def get_near_expire_medicines(self, days_threshold30): conn sqlite3.connect(pharmacy.db) cursor conn.cursor() cursor.execute( SELECT name, batch_no, expire_date, CAST(julianday(expire_date) - julianday(now) AS INTEGER) AS days_left, stock FROM medicines WHERE julianday(expire_date) - julianday(now) BETWEEN 0 AND ? ORDER BY days_left , (days_threshold,)) results cursor.fetchall() conn.close() return results提示“剩余天数”必须是CAST(... AS INTEGER)否则返回浮点数如 29.999影响排序和展示。这是 SQLite 的坑不是 Python 的锅。4. 避坑指南答辩老师最爱揪的 4 个硬伤提前填平4.1 现象点击“添加药品”后界面卡死控制台无报错原因tkinter 是单线程 GUI 库所有耗时操作如数据库写入、网络请求必须放在主线程外否则阻塞 UI。很多同学把add_medicine_to_db()直接绑在按钮command上一旦数据库写入慢比如磁盘忙整个窗口冻结。解决用threading.Thread异步执行但注意 tkinter 控件不能跨线程访问。正确做法是① 启动线程执行数据库操作② 操作完成后用root.after(0, lambda: update_ui())把 UI 更新回调到主线程def on_add_click(self): # 获取输入框内容... def do_add(): try: self.add_medicine_to_db(...) # 耗时操作 # 成功后通知主线程更新UI self.root.after(0, lambda: messagebox.showinfo(成功, 药品添加成功)) except Exception as e: self.root.after(0, lambda: messagebox.showerror(错误, str(e))) threading.Thread(targetdo_add, daemonTrue).start()4.2 现象导出 Excel 时中文乱码显示为“涓枃”原因openpyxl默认用 UTF-8 编码但 Windows 系统默认编码是 GBK。当用pandas.DataFrame.to_excel()导出含中文列名的表格时若未指定引擎参数会调用xlsxwriter其对中文支持不稳定。解决强制使用openpyxl引擎并设置工作表字体from openpyxl import load_workbook from openpyxl.styles import Font def export_to_excel(self, data, filename): df pd.DataFrame(data) df.to_excel(filename, indexFalse, engineopenpyxl) # 修复中文字体 wb load_workbook(filename) ws wb.active font Font(name微软雅黑, size10) for row in ws.iter_rows(): for cell in row: cell.font font wb.save(filename)4.3 现象搜索药品时输入“阿莫”结果没匹配到“阿莫西林”原因用了WHERE name ?等值匹配而不是模糊查询。解决改用LIKE并拼接%通配符但注意 SQL 注入风险——必须用参数化查询不能字符串拼接# ✅ 正确参数化模糊查询 cursor.execute(SELECT * FROM medicines WHERE name LIKE ?, (f%{keyword}%,)) # ❌ 错误字符串拼接有注入风险 cursor.execute(fSELECT * FROM medicines WHERE name LIKE %{keyword}%)4.4 现象切换药品分类时Treeview 列表不刷新仍显示上一个分类的数据原因Treeview 的delete(*items)方法没清空所有子项或者忘记重置self.current_category状态变量。解决封装一个安全清空方法并确保每次加载前重置状态def clear_treeview(self): for item in self.tree.get_children(): self.tree.delete(item) def load_medicines_by_category(self, category): self.clear_treeview() # 必须先清空 self.current_category category # 更新状态 medicines self.get_medicines_from_db(category) for med in medicines: self.tree.insert(, end, valuesmed)5. 让答辩加分的三个进阶技巧打印小票、权限分级、Excel 模板导入5.1 打印销售小票用win32print调用本地打印机Windows 专属毕业设计常被问“系统能对接硬件吗” 打印小票是最轻量级的硬件交互验证。不用复杂驱动直接调用 Windows APIimport win32print import win32ui from PIL import Image, ImageDraw, ImageFont def print_receipt(self, sale_info): # 创建打印机DC printer_name win32print.GetDefaultPrinter() hprinter win32print.OpenPrinter(printer_name) try: # 生成小票图片简化版实际可加 logo img Image.new(RGB, (384, 600), white) # 384px 宽度适配热敏纸 draw ImageDraw.Draw(img) font ImageFont.truetype(simhei.ttf, 12) # 黑体支持中文 y 20 draw.text((10, y), f【XX药店】, fontfont, fillblack) y 25 draw.text((10, y), f时间{sale_info[time]}, fontfont, fillblack) y 20 draw.text((10, y), f药品{sale_info[name]}, fontfont, fillblack) y 20 draw.text((10, y), f数量{sale_info[quantity]} 金额¥{sale_info[amount]:.2f}, fontfont, fillblack) y 30 draw.line([(10, y), (374, y)], fillblack, width2) # 保存临时图片并发送到打印机 temp_path temp_receipt.bmp img.save(temp_path) # 发送到打印机 hdc win32ui.CreateDC() hdc.CreatePrinterDC(printer_name) hdc.StartDoc(Sales Receipt) hdc.StartPage() hdc.DrawBitmap(win32ui.CreateBitmapFromFile(temp_path), (0, 0, 384, 600)) hdc.EndPage() hdc.EndDoc() hdc.DeleteDC() finally: win32print.ClosePrinter(hprinter)注意此方案仅限 Windows且需安装pywin32pip install pywin32。Linux/macOS 用户可用cups库替代但毕业答辩场景 Windows 占比超 90%够用。5.2 权限分级用角色表 登录态控制按钮可见性很多同学只做“管理员”一种角色答辩时被问“药师和收银员权限怎么区分”就懵了。其实只需两张表CREATE TABLE users ( id INTEGER PRIMARY KEY, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT CHECK(role IN (admin, pharmacist, cashier)) DEFAULT cashier ); CREATE TABLE permissions ( role TEXT, action TEXT, -- add_medicine, record_sale, view_report PRIMARY KEY (role, action) );登录后根据role动态显示按钮def setup_ui_by_role(self, user_role): # 收银员只能销售不能入库 if user_role cashier: self.btn_add_medicine.pack_forget() self.btn_import_excel.pack_forget() self.btn_export_report.pack_forget() # 药师可入库、销售、查看近效期 elif user_role pharmacist: self.btn_add_medicine.pack(sidetop, pady5) self.btn_near_expire.pack(sidetop, pady5) # 管理员全功能 else: self.show_all_buttons()5.3 Excel 模板导入用 pandas 读取预设格式自动映射字段手动录入 100 种药品太耗时。提供 Excel 模板列名药品名称、规格、单位、分类、厂家、批号、有效期、进价、售价、库存导入时自动校验def import_from_excel(self, file_path): try: df pd.read_excel(file_path, dtype{batch_no: str, expire_date: str}) # 强制转换日期格式 df[expire_date] pd.to_datetime(df[expire_date]).dt.date for _, row in df.iterrows(): self.add_medicine_to_db( namerow[药品名称], specrow.get(规格, ), unitrow.get(单位, 盒), categoryrow[分类], manufacturerrow[厂家], batch_norow[批号], expire_datestr(row[expire_date]), purchase_pricefloat(row[进价]), sale_pricefloat(row[售价]), stockint(row[库存]), supplier_id1 # 默认供应商 ) messagebox.showinfo(成功, f成功导入 {len(df)} 条药品) except Exception as e: messagebox.showerror(错误, f导入失败{str(e)})表格字段映射是关键。row.get(规格, )防止 Excel 中该列为空时报错dtype{batch_no: str}避免 Excel 把批号“2023001”自动转成数字 2023001丢失前导零。6. 我坚持的三个习惯让代码从“能跑”变成“好维护”最后说点掏心窝的话。带过这么多届毕设我发现真正拉开差距的不是功能多炫而是代码能不能让别人 5 分钟看懂、10 分钟改出新需求。我自己写这类系统雷打不动坚持三件事第一所有 SQL 语句单独抽离成常量。不把INSERT INTO ...写在函数里而是SQL_INSERT_MEDICINE INSERT INTO medicines (name, spec, unit, category, manufacturer, batch_no, expire_date, purchase_price, sale_price, stock, supplier_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) # 使用时 cursor.execute(SQL_INSERT_MEDICINE, values)好处是全局搜索SQL_INSERT_MEDICINE就能找到所有插入点修改表结构时只改这一处还能加注释说明字段含义。第二数据库操作函数永远返回明确的数据结构。比如get_medicines_by_category()不返回 raw tuple而是def get_medicines_by_category(self, category): # ... 查询逻辑 return [ { id: row[0], name: row[1], spec: row[2], stock: row[9], days_left: row[10] # 效期剩余天数 } for row in cursor.fetchall() ]这样 UI 层直接for med in meds: print(med[name])不用记row[1]是名称、row[9]是库存。第三每个界面操作函数开头加一行日志。不是为了监控而是答辩时快速定位问题def on_sale_click(self): logging.info(f[SALE] 开始处理销售药品ID{self.selected_med_id}, 数量{self.quantity_var.get()}) # 后续逻辑...答辩老师问“销售功能在哪实现的”我直接打开日志文件CtrlF 搜[SALE]3 秒定位到函数。这比翻 800 行代码强十倍。这些习惯不增加功能但让整个项目从“交差作业”变成“可演化的工程切片”。希望帮到你。本文还有配套的精品资源点击获取