基于Python与SQLite的药物管理系统:从数据库设计到库存防超卖实战 简介基于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;csqlite3.connect(drugstore.db);print(c.execute(\select name from sqlite_master where typetable\).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, timeout5) 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。timeout5和busy_timeout3000是配合用的前者是连接层面的等待秒数后者是 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.quantityexcluded代表你本次想插入的新值。这样同一批号再来一箱就会自动把数量叠加上去而不会出现两行同批号记录。品种表中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 pandas2.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; connsqlite3.connect(drugstore.db); conn.executescript(open(schema.sql, encodingutf-8).read()); conn.commit(); conn.close(); print(db ready)关键点是先确认当前目录在项目根目录因为sqlite3.connect(drugstore.db)里的相对路径就是按当前工作目录找的。建议初始化完马上验证一次表和数据python -c import sqlite3; csqlite3.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 lockedWAL 模式和 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(days30)).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 的药物管理系统做到这一步已经不只是能交差的课设而是一个真正能长期跑在药店后厨的实用工具。希望帮到你。本文还有配套的精品资源点击获取