简介:基于Python的药物管理系统是一套可用于药店、医院药房库存管理的完整Web项目实践。系统围绕药品信息录入、查询、更新与删除等核心业务展开,后端基于Flask编写,前端配套HTML模板与静态资源,并通过SQL数据库完成数据持久化,整体交互清晰,适合具备基础Python知识的学习者作为课程设计或毕业设计参考。资源包共51个文件,包含5个Python源文件、3个SQL脚本、24个HTML页面,另有若干XML配置、Markdown文档、依赖清单与静态资源打包文件,全部内容约23.73MB,目录按应用、模板、数据库等模块划分,便于对照研读。目前已有111人浏览学习,项目覆盖Flask路由设计、SQLAlchemy对象关系映射、前端页面渲染、数据校验与异常处理等技术点。通过阅读代码和数据库脚本,可直观理解一个Web管理系统的分层结构、数据库表设计以及常见业务逻辑的封装方式,是一份能直接运行学习的实战型素材。
1. 基于Python的药物管理系统:这套带数据库文件的源码到底能干什么
如果你拿到的压缩包叫「基于python的药物管理系统(含数据库文件).zip」,里面除了若干个.py文件还有一个.db文件,那它大概率是「Python + SQLite + GUI」这种组合的小型进销存系统。这类项目的典型能力是:药品基础信息维护、入库/出库登记、库存自动变更、按药品名或批号检索、销售记录统计,部分版本还带登录用户和过期提醒。它的边界也很清晰——扛不住大型药房的高并发,但非常适合教学、毕业设计、小药店前台,或者作为你理解「数据库增删改查」整套链路的第一块跳板。这套东西的价值不在界面多好看,而在数据库文件里那几张表怎么建、哪些字段不能为空、哪条 SQL 会把库存扣成负数。把骨架读透,后面换 MySQL、改成前后端分离,都是时间问题。
2. 拆解系统架构:药品品种、批次库存和销售记录怎么连成闭环
很多初学者拿到这个项目先打开主窗口代码,看到一堆tkinter控件就头晕。我会反过来,先打开.db文件看表结构。因为整个系统的业务规则全部体现在表关系和约束上:什么药品、哪个批次、剩多少、什么时候过期、谁卖的,最后都会落到数据库里。窗口只是外壳,表才是本体。
2.1 为什么用一个单文件数据库:SQLite 在药物管理里的取舍
药物管理系统常见的数据存储方案有三种:纯内存对象、SQLite 单文件库、MySQL 服务端。纯内存对象适合 demo,程序一关数据全没,没法做多窗口共享;MySQL 需要单独装服务、配账号密码,对一台收银电脑或课程设计来说明显过重。所以绝大多数这类源码直接用 Python 标准库里的sqlite3,把数据库做成一个.db单文件,随项目一起打包进压缩包。
SQLite 的好处是零配置、跨平台、单文件备份。把整个drugstore.db复制走,就等于把全部药品数据背走了。它的限制也明确:同一时刻只有一个写连接能提交事务,写入并发不高;但药店的录入和收银节奏完全能接受。真正要小心的不是性能,而是连接路径写死、事务不回滚、日期格式乱掉这些细节,第 5 章会专门讲。
拿到.db文件后,先用工具确认里面到底有哪些表,而不是直接改代码。常见做法是在项目根目录下执行sqlite3 drugstore.db ".tables"。Windows 没装命令行工具就用 Python 一行指令查看,后面 2.3 会给你命令。
2.2 三张核心表的设计:外键、唯一约束和过期时间的存储格式
这类系统不管界面怎么变,核心业务表就三张:药品品种表、批次库存表、销售记录表。注意我把「批次」单独拆出来了,而不是把有效期直接挂在药品名称上。原因是同一盒药会因为采购批次不同而有不同有效期,你把expire_date放在品种表,一次入库新批次就会把老批次的有效期覆盖掉,这是一个非常大的设计错误。
药品品种表drug_info存不随批次变化的公共信息:药品编码、通用名、规格、单位、生产厂家。药品编码必须唯一,这是整个系统的锚点。批次库存表drug_batch存每次采购进来的批次号、有效期、当前剩余数量、最低库存预警线、货位号。批次号和药品 ID 联合唯一,能防止同一批次被重复插入。销售记录表sale_record存每次售出的批次、数量、单价、总额、操作员、销售时间。销售明细通过batch_id指向批次,这样每笔卖出都能追溯到具体某个批号和对应的有效期。
日期字段统一用TEXT存YYYY-MM-DD,时间字段统一用YYYY-MM-DD HH:MM:SS。SQLite 里date()函数可以直接处理这种格式,排序也符合字典序。别用2024/05/01、01-05-2024这类格式,否则筛选过期药品时你会被字符串比较坑得很惨。
2.3 最小表结构 SQL:拿到 .db 文件先确认这几张表
先给你一份可以直接跑在 SQLite 上的最小建表脚本。你拿到压缩包后,把里面已有的.db文件.schema导出来对照,看字段名和约束是不是和这份对齐。如果差异很大,说明源码版本和你手上的数据库版本不匹配,建议直接重跑初始化脚本。
-- drugstore_schema.sql 最小药物管理系统表结构 PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS drug_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, drug_code TEXT NOT NULL UNIQUE, -- 药品编码,系统内唯一 name TEXT NOT NULL, -- 药品通用名 spec TEXT, -- 规格,如 0.25g*24片 unit TEXT DEFAULT '盒', -- 单位 manufacturer TEXT, -- 生产厂家 created_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE IF NOT EXISTS drug_batch ( id INTEGER PRIMARY KEY AUTOINCREMENT, drug_id INTEGER NOT NULL REFERENCES drug_info(id), batch_no TEXT NOT NULL, -- 采购批号 expire_date TEXT NOT NULL, -- 有效期,格式 YYYY-MM-DD quantity INTEGER NOT NULL DEFAULT 0 CHECK(quantity >= 0), -- 当前库存,禁止负数 min_stock INTEGER NOT NULL DEFAULT 5, -- 最低库存预警线 location TEXT, -- 货位号,如 A-03 UNIQUE(drug_id, batch_no) -- 同一药品同一批号只能有一条 ); CREATE TABLE IF NOT EXISTS sale_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, batch_id INTEGER NOT NULL REFERENCES drug_batch(id), drug_id INTEGER NOT NULL REFERENCES drug_info(id), quantity INTEGER NOT NULL CHECK(quantity > 0), unit_price REAL NOT NULL, -- 销售单价 total_amount REAL NOT NULL, -- 数量 * 单价 sale_time TEXT DEFAULT (datetime('now', 'localtime')), operator TEXT -- 操作员工号或姓名 ); CREATE INDEX IF NOT EXISTS idx_batch_drug ON drug_batch(drug_id); CREATE INDEX IF NOT EXISTS idx_batch_expire ON drug_batch(expire_date); CREATE INDEX IF NOT EXISTS idx_sale_time ON sale_record(sale_time);这段 SQL 里三个约束值得重点看:UNIQUE(drug_id, batch_no)防止重复批次被当新库存叠加;CHECK(quantity >= 0)是最后一道负库存防线;外键REFERENCES配合PRAGMA foreign_keys = ON才能阻止你删除还有库存的药品。索引不是可有可无,expire_date和sale_time建立索引后,过期提醒和销售日汇总的速度在几千条记录下会明显快。
你拿到.db文件后,用下面命令快速验证表结构:
# 方法一:有 sqlite3 命令行工具时 sqlite3 drugstore.db ".tables" sqlite3 drugstore.db ".schema drug_batch" # 方法二:只装 Python 时 python -c "import sqlite3;c=sqlite3.connect('drugstore.db');print(c.execute(\"select name from sqlite_master where type='table'\").fetchall())"如果第二个方法打印出来的是一个表名列表,里面有drug_info、drug_batch、sale_record,那项目结构就比较正统。如果还多出users、login_log之类,说明源码带了登录功能,你在调试时要留意初始化脚本里是否创建了默认账号。看到像stock_record这种表也不用紧张,很多项目用一张流水表同时记录入库和出库,本质上就是把我的drug_batch变动过程抽单独一张表,逻辑等价,只是查询方式不同。
3. 数据库增删改查的完整写法:连接参数、事务和三类核心 SQL
表结构看明白之后,重头戏就是增删改查。很多源码把数据库操作散落在每个窗口按钮的回调里,同一个connect()写七八遍,改了路径就要全局搜索。我一般会抽出一个db.py统一管连接,再按业务封装函数。下面这些代码不是某个框架的专属写法,就是纯标准库sqlite3,你拿回去可以直接替换原项目里的散装连接。
3.1 连接与事务边界:row_factory、外键开关和 busy_timeout 怎么设
先说连接参数。sqlite3.connect()第一个参数是数据库路径,此外有四个参数很常用:timeout是等待锁的秒数;isolation_level控制事务行为,默认值是空字符串,表示自动开启事务;row_factory不是连接参数而是游标属性,设成sqlite3.Row后,取出来的行可以像字典一样用列名访问,可读性好很多。
# db.py —— 统一数据库连接管理 import os import sqlite3 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.path.join(BASE_DIR, "drugstore.db") def get_conn(): conn = sqlite3.connect(DB_PATH, timeout=5) conn.row_factory = sqlite3.Row # 每次连接都显式开启外键约束 conn.execute("PRAGMA foreign_keys = ON") # 忙时最多等 3 秒,避免立刻抛 database is locked conn.execute("PRAGMA busy_timeout = 3000") return conn这里用os.path.dirname(os.path.abspath(__file__))把路径固定在.py文件所在目录,而不是进程当前目录。很多项目直接在代码里写sqlite3.connect("drugstore.db"),看起来没问题,一旦你用 PyCharm 运行、从命令行别处运行、或者双击启动,工作目录一变就提示unable to open database file。timeout=5和busy_timeout=3000是配合用的,前者是连接层面的等待秒数,后者是 SQLite 引擎层面的锁等待毫秒数,两者都设上,偶发并发写才不容易崩。
事务边界要明确:conn.commit()代表一批写入生效,conn.rollback()代表把本次连接里未提交的写入全部撤销。一个写操作从开始到提交的时间越短越好,不要让连接在 GUI 线程里一直开着跨用户操作,否则锁的持有时间会很长。我实践下来最舒服的模式是「函数内部获取连接、函数内提交、函数内关闭」,线程之间天然隔离。
3.2 入库与销售扣减:同一事务里锁库存,避免超卖
入库和销售是库存系统的两个旋转门。入库的核心逻辑是:药品品种不存在就先建档,然后对批次库存做「有则追加、无则插入」的合并操作。SQLite 3.24 以上支持UPSERT语法,Python 3.8 自带的 SQLite 版本足够,所以可以直接用ON CONFLICT DO UPDATE。
# 入库:新增批次或累加已有批次数量 def add_batch(drug_info_vals, batch_vals): conn = get_conn() try: cur = conn.execute( """INSERT INTO drug_info(drug_code, name, spec, unit, manufacturer) VALUES(?, ?, ?, ?, ?) ON CONFLICT(drug_code) DO NOTHING""", drug_info_vals ) # 拿到品种ID:查一次比 lastrowid 更稳,因为可能冲突未插入 drug_id = conn.execute( "SELECT id FROM drug_info WHERE drug_code = ?", (drug_info_vals[0],) ).fetchone()["id"] conn.execute( """INSERT INTO drug_batch(drug_id, batch_no, expire_date, quantity, min_stock, location) VALUES(?, ?, ?, ?, ?, ?) ON CONFLICT(drug_id, batch_no) DO UPDATE SET quantity = drug_batch.quantity + excluded.quantity, expire_date = excluded.expire_date, min_stock = excluded.min_stock, location = excluded.location""", (drug_id, *batch_vals) ) conn.commit() except Exception: conn.rollback() raise finally: conn.close()这段代码的关键在ON CONFLICT(drug_id, batch_no)那一行。它依赖表中UNIQUE(drug_id, batch_no)约束,冲突时执行quantity = drug_batch.quantity + excluded.quantity,excluded代表你本次想插入的新值。这样同一批号再来一箱,就会自动把数量叠加上去,而不会出现两行同批号记录。品种表中ON CONFLICT(drug_code) DO NOTHING配合后面的SELECT id,是为了避免「先插入再查询」时因冲突而拿不到正确 ID。
销售扣减正好相反,要同时满足「批次存在」和「库存足够」两个条件。最稳的写法是把「扣库存」和「写销售单」放在同一个事务里,并且用一条带quantity >= ?条件的UPDATE来实现原子判断,而不是先SELECT再UPDATE——后者在并发下会踩空。
# 销售:原子扣减库存并写入销售流水 def sell(batch_id, drug_id, quantity, unit_price, operator): conn = get_conn() try: cur = conn.execute( """UPDATE drug_batch SET quantity = quantity - ? WHERE id = ? AND quantity >= ?""", (quantity, batch_id, quantity) ) if cur.rowcount == 0: raise ValueError("库存不足或批次不存在") conn.execute( """INSERT INTO sale_record (batch_id, drug_id, quantity, unit_price, total_amount, operator) VALUES(?, ?, ?, ?, ?, ?)""", (batch_id, drug_id, quantity, unit_price, round(unit_price * quantity, 2), operator) ) conn.commit() except Exception: conn.rollback() raise finally: conn.close()UPDATE ... WHERE id = ? AND quantity >= ?是防超卖的利器。如果当前数量小于要卖的数量,SQLite 一条记录都不会更新,rowcount返回 0,这时直接抛异常回滚,连销售流水也不会写进去。如果先SELECT quantity再看够不够,两个操作之间另一个连接可能已经扣掉了库存,等第 7 章讲并发时你会更明白。注意total_amount在 Python 里先算好再写库,比 SQL 里用unit_price * quantity少一次类型转换,也方便后面做折扣。
3.3 查询与统计:模糊搜索、临期筛选和销售日汇总
查询是这类系统使用频率最高的部分。最容易翻车的写法是字符串拼接 SQL 参数,比如"WHERE name LIKE '%" + keyword + "%'"。一旦药品名里出现单引号,SQL 就断了;更危险的是注入风险。统一用占位符?传参。
# 模糊搜索:名称或编码匹配 def search_drugs(keyword): conn = get_conn() pattern = f"%{keyword}%" rows = conn.execute( """SELECT i.id, i.drug_code, i.name, i.spec, i.unit, b.batch_no, b.expire_date, b.quantity FROM drug_batch b JOIN drug_info i ON i.id = b.drug_id WHERE i.name LIKE ? OR i.drug_code LIKE ? ORDER BY i.name, b.expire_date""", (pattern, pattern) ).fetchall() conn.close() return [dict(row) for row in rows]LIKE ?里的通配符放在参数里,而不是拼进 SQL 字符串,这是安全底线。返回前用dict(row)把sqlite3.Row转成字典,前端拿row["quantity"]也好、直接塞表格控件也好,都更顺手。
临期筛选的逻辑放在查询层,不用等 GUI 触发。下面的 SQL 可以直接套到第 6 章的自动提醒脚本里:
# 未来 30 天过期但仍有库存的药品 conn = get_conn() rows = conn.execute( """SELECT i.name, b.batch_no, b.expire_date, b.quantity FROM drug_batch b JOIN drug_info i ON i.id = b.drug_id WHERE b.expire_date >= date('now') AND b.expire_date <= date('now', '+30 days') AND b.quantity > 0 ORDER BY b.expire_date""" ).fetchall()销售日汇总注意date(sale_time)的用法。sale_time存的是YYYY-MM-DD HH:MM:SS,直接WHERE sale_time = ?只能匹配整点整秒,必须用date()函数把时间部分截掉再比较。
# 某天销售汇总 def daily_summary(day_str): conn = get_conn() row = conn.execute( """SELECT COUNT(*) AS order_count, COALESCE(SUM(total_amount), 0) AS total FROM sale_record WHERE date(sale_time) = ?""", (day_str,) ).fetchone() conn.close() return {"order_count": row["order_count"], "total": row["total"]}COALESCE(SUM(...), 0)处理没有销售记录的情况,否则SUM返回NULL,前端一显示就变成空。day_str统一传YYYY-MM-DD,不要传YYYY/MM/DD,否则date()函数解析不出来,查询结果会少一半数据。
4. 把系统跑起来的完整步骤:解压、初始化数据库、启动 GUI
数据库代码看懂了,接下来要让它真实跑起来。很多人在这一步卡住,不是代码问题,而是环境问题。这里按我习惯的顺序给你一套排查链路:先确认 Python 环境,再重建数据库,最后用命令行启动主窗口。
4.1 环境准备:Python 版本、tkinter 和第三方依赖检查
先确定你装的是 Python 3.8 及以上版本。sqlite3是标准库,不需要额外安装;GUI 部分如果源码用tkinter,那在 Windows 官方安装包里是默认带上的。打开命令行执行:
python --version python -m tkinter第一条打印版本号,第二条会弹出一个测试小窗口。如果python -m tkinter报ModuleNotFoundError: No module named 'tkinter',说明你当初装的不是 python.org 官方包,而是精简版或嵌入式版,建议去 python 官网下载重新安装,安装向导里勾选「tcl/tk and IDLE」那一项。这一步是很多「双击没反应」的元凶。
再看压缩包里有没有requirements.txt。这类标准库项目大多数不需要第三方依赖,如果有这份文件,说明作者用了pandas、openpyxl(导出 Excel)或pyinstaller(打包 exe)之类的库。常规操作是:
pip install -r requirements.txt装的时候注意看下载进度里有没有红字报错。pandas这种包在 Python 3.12 上可能会有版本兼容问题,真遇到就单独装低版本,比如pip install pandas==2.2.2。对纯tkinter + sqlite3的项目,这一步可以完全跳过。
4.2 初始化数据库文件:重建 drugstore.db 并写入演示数据
压缩包自带.db文件不代表你就能直接连上。源码改过表结构、drugstore.db是旧版本、或者.db文件的路径和代码对不上,都会导致运行时报no such table。最省事的做法是先把数据库重建一次,让表和源码完全同步。
先看项目里有没有init_db.py。这类项目一般都会有初始化脚本,没有的话,你把第 2.3 节那份建表 SQL 保存成schema.sql,再用下面这段脚本执行:
# Windows 下先切换工作目录,再执行 cd /d D:\code\drugstore python init_db.py如果init_db.py不存在,用 Python 一行执行建表文件:
python -c "import sqlite3; conn=sqlite3.connect('drugstore.db'); conn.executescript(open('schema.sql', encoding='utf-8').read()); conn.commit(); conn.close(); print('db ready')"关键点是先确认当前目录在项目根目录,因为sqlite3.connect('drugstore.db')里的相对路径就是按当前工作目录找的。建议初始化完马上验证一次表和数据:
python -c "import sqlite3; c=sqlite3.connect('drugstore.db'); print(c.execute('select count(*) from drug_info').fetchone()[0])"能打印出数字,说明数据库文件能正常打开,表也建好了。如果这里报no such table: drug_info,别急着改代码,先dir看当前目录下有没有drugstore.db,没有就是路径没对。
4.3 启动主程序:命令行进入项目目录再运行,别双击
初始化完数据库,下一步是启动 GUI。项目入口文件名常见的有main.py、login.py、start.py,具体看压缩包里哪个文件含mainloop()。用命令行启动比双击安全得多,因为你能直接看到异常堆栈:
cd /d D:\code\drugstore python main.py如果项目带登录界面,入口一般是login.py,或main.py里先跳登录窗。启动后黑窗口一闪而过又没反应,一般有三种情况:tkinter缺失、数据库路径没对齐、代码里 import 了不存在的模块。这时不要慌,回到命令行运行,看最后一行报错信息。比如ModuleNotFoundError: No module named 'tkcalendar',说明源码依赖一个日期选择控件,你去pip install tkcalendar再启动即可。
还有一种常见问题是main.py里用__file__定位资源,却在 Jupyter 或交互式环境里运行,导致__file__不存在。记住:这类 GUI 脚本永远从命令行脚本方式跑,不要用交互式环境当入口。
5. 药物管理系统避坑指南:路径、锁库、负库存和日期格式
这个项目最大的坑不在逻辑多难,而在细节非常碎。我按踩过的优先级给你列五条,每一条都是真实会发生在运行和验收阶段的问题。
5.1 双击启动报 no such table:工作目录不是项目根目录
现象:双击main.py,控制台闪过后弹出异常,或者用 PyCharm 运行时报sqlite3.OperationalError: no such table: drug_info。原因:代码里写的是相对路径sqlite3.connect("drugstore.db"),但双击 / IDE 运行时的工作目录不一定在.db文件所在目录。解决:所有数据库连接统一用os.path.join(os.path.dirname(os.path.abspath(__file__)), "drugstore.db"),不要裸写文件名。如果项目已经到处裸写,在db.py顶部重定向当前目录:
import os, sys os.chdir(os.path.dirname(os.path.abspath(__file__)))这一行把进程工作目录强制切到脚本目录,能救活大部分路径坑,但属于治标不治本,逐项替换成绝对路径才是正解。
5.2 界面能开但查不到数据:数据库路径写死成绝对路径
现象:换一台电脑,登录界面正常,列表却全空,或者操作时提示unable to open database file。原因:源码作者把自己电脑上的路径写死了,比如C:\Users\zhang\Desktop\drugstore\drugstore.db,你的项目不在这个路径。解决:全局搜索.db和connect(,把所有硬编码路径改成相对__file__的路径。
还有一个连带问题:C:\Users\中文用户名\...这种中文路径在部分 Windows 配置下会让 SQLite 打开失败。如果项目目录带中文且无法改路径,把数据库文件名改成拼音或英文,比如drugstore.db保持英文通常就能绕过。
5.3 并发写入报 database is locked:WAL 模式和 busy_timeout 设置
现象:两个窗口同时操作时,某个按钮回调报sqlite3.OperationalError: database is locked。原因:SQLite 默认回滚日志模式下,读和写互相阻塞;写事务没及时提交或连接没关闭,锁就释放不掉。解决:连接建立时加上busy_timeout,并把数据库切到 WAL 模式。WAL 模式允许读和写并存,写入冲突概率会低很多。在初始化脚本里执行:
conn.execute("PRAGMA journal_mode = WAL") conn.execute("PRAGMA busy_timeout = 3000")注意开启 WAL 后,.db文件同目录会出现drugstore-wal和drugstore-shm两个附属文件,这是正常现象,不要当成垃圾删掉。备份数据库时要让连接都关闭,或者直接复制三个文件一起拷,否则会丢未合并的日志。
5.4 库存扣成负数:先查后改和并发不匹配
现象:卖药后表格数量显示成-2,或者明明看着库存还有 10,一售 11 也成功了。原因:代码用「先SELECT quantity,再UPDATE SET quantity = new_value」的模式,中间没有任何条件判断;两个连接同时卖同一批次时,后提交的覆盖了先提交的扣减。解决:统一用带条件的UPDATE ... WHERE quantity >= ?,再检查rowcount。我给第 3.2 节的sell()就是这个方案。同时保留表里的CHECK(quantity >= 0)作为最终防线,万一业务层漏判,数据库也会直接拒绝并抛CHECK constraint failed。
5.5 日期筛选总是漏数据:TEXT 日期格式不统一
现象:查询「下月过期药品」时,某几条明明在范围内却查不出来,或者排序乱。原因:有的地方存2024-05-01 10:30:00,有的地方存2024/05/01,还有的存24-05-01,字符比较一旦混用就全乱套。解决:入库前强制用datetime.strptime校验格式,统一转成YYYY-MM-DD;查询日期范围一律用date(字段) BETWEEN date(?) AND date(?),参数也传YYYY-MM-DD。我遇到过一个项目,销售时间在插入用了datetime('now'),而查询用了strftime('%Y-%m-%d', sale_time),两者格式不一致导致date(sale_time)解析为空,这种一定要在初始化 SQL 里把默认值都定义好。
6. 进阶:把过期提醒和库存预警做成任务计划自动跑
到这里的系统已经能正常卖药了,但药房真正需要的往往不是「能录入」,而是「到期有人提醒」。把过期判断从 GUI 里抽出来,做成一个独立的命令行脚本,然后用 Windows 任务计划每天自动执行,这是成本最低的进阶方案。
6.1 一个不依赖 GUI 的检查脚本
核心就是第 3.3 节那段临期查询,加上输出逻辑。你把它保存成expiry_check.py,放在项目根目录:
# expiry_check.py 独立运行:扫描未来30天过期药品 import sqlite3 from datetime import date, timedelta conn = sqlite3.connect("drugstore.db") conn.row_factory = sqlite3.Row today = date.today().isoformat() deadline = (date.today() + timedelta(days=30)).isoformat() rows = conn.execute( """SELECT i.name, b.batch_no, b.expire_date, b.quantity FROM drug_batch b JOIN drug_info i ON i.id = b.drug_id WHERE b.expire_date BETWEEN ? AND ? AND b.quantity > 0 ORDER BY b.expire_date""", (today, deadline) ).fetchall() conn.close() if rows: print("以下药品30天内到期:") for r in rows: print(f"{r['name']} 批号{r['batch_no']} 有效期{r['expire_date']} 余量{r['quantity']}") else: print("暂无近效期药品")脚本里BETWEEN ? AND ?的两个参数都是YYYY-MM-DD字符串,跟第 3.3 节里date('now', '+30 days')等价。独立成脚本的好处是你可以先在命令行跑一遍,输出正确后再挂计划任务。
6.2 把检查脚本挂到系统任务计划
Windows 下用schtasks命令最直接,Linux 下放crontab即可。这里给 Windows 的写法:
schtasks /Create /TN "DrugExpiryAlert" /TR "C:\Python311\python.exe D:\code\drugstore\expiry_check.py" /SC DAILY /ST 09:00 /F/TR里的 python 路径必须写成绝对路径,不能只写python,否则任务计划里的PATH环境跟你手动命令行不一样,可能找不到解释器。都写绝对路径后,每天 9 点就会自动执行一次,输出可以通过任务计划的重定向到文本文件做日志。更进一步的库存预警同理:把quantity <= min_stock加进脚本条件,输出的就是缺货清单。
我早期做这类系统时,把过期提醒写在主窗口的刷新事件里,窗口不开就没有提醒,后来才改成任务计划调后台脚本。这个习惯一直留到现在:凡是「每天固定要做的检查」,都从界面里摘出来,让数据库和脚本自己转,界面只是给人看结果。这套基于 Python 和 SQLite 的药物管理系统做到这一步,已经不只是能交差的课设,而是一个真正能长期跑在药店后厨的实用工具。希望帮到你。
本文还有配套的精品资源,点击获取