☰
Python工厂管理系统:Tkinter+SQLite驱动库存、订单与生产调度
2026/10/26 16:10:52 网站建设 项目流程

简介:这套基于Tkinter的Python工厂管理系统,是面向Python GUI开发学习与毕业设计场景的实战项目,旨在解决小型工厂生产流程的数字化管理问题,核心业务覆盖库存追踪、订单处理、生产调度、员工信息与权限控制等。压缩包共4456个文件,总体积约36.89MB,文件类型以py源码、pyc编译文件、dat数据文件、pyd扩展、txt文档为主,同时包含sql数据库脚本和docx说明文档,便于对照学习与二次开发。目前已有92人学习下载。资源内置完整可运行源码与配套测试、运维文档,展示了基于Tkinter的桌面界面搭建、SQL数据库持久化操作、异常捕获与日志记录等工程实现,并提供库存、订单、生产调度等模块的参考代码,适合需要完成毕设或课程设计的学生借鉴,也可作为入门Python桌面应用开发的完整案例。

1. python 工厂管理系统:从导入 Tkinter 到跑通一轮入库出库

如果你正在为毕业设计找一套基于 Tkinter 的 python 工厂管理系统源码,大概率已经在“界面能打开、按钮却点不动”和“一关窗口数据全丢”的 demo 之间反复横跳过。这套系统的价值不在技术多新,而在于它把库存、订单、生产、员工、权限这些模块在桌面端完整串了起来,数据用 SQLite 持久化,重启不丢,还带测试和运维文档,正好卡在“能演示、能讲原理、能应付答辩”这个位置。它解决的是小工厂生产流程怎么在桌面端跑通的真实问题,适合两类人:一是想学 Python GUI 开发的初学者,可以从事件绑定、多窗口切换、数据库读写这段完整链路里拿到能复现的代码;二是要交毕设的学生,文档和分层结构能直接支撑你回答“为什么这么设计”。

2. 系统底座怎么搭:Tkinter 界面框架与 SQLite 数据层设计

拿到源码先别急着跑,得先搞清楚这套东西的底座是怎么立起来的。整个系统是典型的三层结构:界面层用 Tkinter 组件,业务逻辑层写成独立的 Manager 类,数据访问层封装 SQLite 操作。这样分层的好处是,答辩时你可以直接说“界面不直接写 SQL,按钮回调只调用业务对象”,一句话就能压住最常见的技术提问。

2.1 为什么选 Tkinter + SQLite,而不是 PyQt 或 Web 方案

选型理由要能说出来。Tkinter 是 Python 标准库自带,Python 3.8 以后的安装包默认包含,不需要额外 pip install,这在毕设答辩现场很重要——评委机器上只要装了 Python 就能跑,不用折腾依赖。Tkinter 的事件驱动模型(mainloop + 回调函数)和工厂业务里的“按钮→操作→刷新界面”天然匹配,展示起来也直观。

PyQt 功能更强、界面更好看,但体积大、授权和打包都更麻烦;Web 方案虽然主流,但需要同时维护前端页面和后端接口,对于一个工厂管理系统毕设来说工作量翻倍。Tkinter 的边界也很明显:复杂图表和移动端适配做不了,所以这套系统的报表模块只做表格展示和 CSV 导出,把专业图表留给 Excel。SQLite 则是嵌入式数据库的合理选择,单文件、零配置、标准库 sqlite3 直接驱动,完全够承受演示阶段的并发量。

2.2 数据库初始化脚本:七张表怎么建

系统的数据层在 db 目录下,初始化脚本 init_db.py 里用 CREATE TABLE IF NOT EXISTS 建七张表。核心四张的字段设计如下:

PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'operator', created_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE IF NOT EXISTS materials ( material_id INTEGER PRIMARY KEY AUTOINCREMENT, material_name TEXT NOT NULL, spec TEXT, unit TEXT DEFAULT '件', safe_stock REAL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS orders ( order_id INTEGER PRIMARY KEY AUTOINCREMENT, order_no TEXT NOT NULL UNIQUE, product_id INTEGER NOT NULL, qty INTEGER NOT NULL CHECK(qty > 0), due_date TEXT, status TEXT NOT NULL DEFAULT '待生产', created_by INTEGER, FOREIGN KEY (product_id) REFERENCES materials(material_id), FOREIGN KEY (created_by) REFERENCES users(user_id) ); CREATE TABLE IF NOT EXISTS production_tasks ( task_id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, line_name TEXT DEFAULT '一号线', assign_qty INTEGER NOT NULL, finish_qty INTEGER DEFAULT 0, status TEXT DEFAULT '未开始', FOREIGN KEY (order_id) REFERENCES orders(order_id) );

执行方式是在项目根目录运行python db/init_db.py。注意这里我是建议在脚本里把PRAGMA foreign_keys = ON;放在第一行,因为 SQLite 默认不开启外键约束,如果漏了这行,orders 表里的 product_id 即使指向不存在的物料也能插入成功,数据层就失去了一重保护。库存表 stock 和操作日志表 operation_log 也用同样的模式创建:stock 保存当前库存数,operation_log 记录每次出入库流水,两张表通过 material_id 关联。

字段设计上有两个取舍值得答辩时讲:库存为什么用单独一张表而不是每次流水实时累加。因为实时累加意味着出库时必须全表扫描流水,数据量大了之后界面会卡;单独维护一个当前库存值,配合流水日志做审计和重算,是更常见的工程做法。另一个是材料表直接复用为产品表,材料和生产出来的产品都挂在 materials 下,用 type 字段区分,简化了订单对物料的引用,避免维护两套几乎一样的表结构。

2.3 主程序入口与启动流程

启动入口 main.py 的逻辑很直白,先初始化数据库,再弹登录窗口,登录成功后进入主控窗口。关键是要把“初始化数据库”放在创建 Tk 窗口之前,否则界面已经显示但表还没建好,用户第一次点击就会出现“no such table”的异常。

import tkinter as tk from db.init_db import init_db from ui.login import LoginWindow from ui.main_console import MainConsole def main(): db_path = init_db() print(f'数据库已初始化: {db_path}') root = tk.Tk() root.title('工厂管理系统') root.geometry('1024x640') LoginWindow(root, on_success=lambda user: MainConsole(root, user)) root.mainloop() if __name__ == '__main__': main()

这里有一个细节:LoginWindow接收一个on_success回调,登录成功后由回调去创建主控制台,而不是登录窗口直接去 import MainConsole。这样两个窗口类之间是解耦的,登录窗口不知道主控制台内部怎么实现,只管认证。1024x640是主窗口建议尺寸,比这个更小的屏幕会让右侧内容区的表格挤成一团,实际部署时可以在 MainConsole 里根据屏幕宽度动态设置。

2.4 界面骨架:导航栏与内容区切换

主控制台的界面骨架用了左导航、右内容的经典布局。左侧是一个 Listbox 或 Button 列表,列出库存、订单、生产调度、员工、报表五个入口,右侧是一个容器 Frame,点击导航时销毁旧的 Frame,创建新的模块页面。

def switch_page(self, page_key): for widget in self.content_frame.winfo_children(): widget.destroy() page_class = self.PAGES[page_key] page_class(self.content_frame, self.user)

winfo_children()拿到 content_frame 下的所有子控件,逐个 destroy 清空,再实例化新页面。这里容易踩的坑是只销毁了最外层 Frame,没销毁 Frame 里面嵌的 Treeview,导致旧数据残留在界面上。所以我习惯在页面类内部也做一次self.tree.delete(*self.tree.get_children()),双重保险。

数据展示全部用 ttk.Treeview,绑定列名后通过tree.insert('', 'end', values=row)逐行插入。为了减少重复代码,我在 common 工具里封装了一个refresh_tree函数,传入 tree 控件、SQL 查询语句和一个可选的参数元组,自动清空、查询、插入,统一处理列头显示和滚动条绑定。整个界面层不写任何原生 SQL,只调用业务类的方法,这是分层设计落到代码上的关键习惯。

3. 核心功能模块拆解:库存、订单与生产调度的代码落地

底座搭完了,才轮到真正打动人的业务模块。这套系统最值得逐行读的是三个 Manager 类:StockManager、OrderManager 和 ScheduleManager。它们分别对应库存、订单和生产调度,我按毕设里最常被问到的细节逐段拆。

3.1 库存管理:入库、出库与事务回滚

库存模块首先要处理的是数据一致性问题。入库和出库都要同时更新当前库存量并写一条流水日志,这两条 SQL 必须同一个事务里提交,否则就会出现库存变了但没有日志、或者日志记了但库存没变的脏状态。

def change_stock(self, material_id, delta, action, remark=''): if delta == 0: raise ValueError('变更数量不能为 0') conn = self.get_conn() try: conn.execute('BEGIN') conn.execute( 'UPDATE stock SET qty = qty + ? WHERE material_id = ?', (delta, material_id) ) conn.execute( 'INSERT INTO operation_log(material_id, action, change_qty, remark) VALUES (?,?,?,?)', (material_id, action, delta, remark) ) conn.commit() except Exception: conn.rollback() raise finally: conn.close()

这里的核心逻辑是事务包裹:先BEGIN再执行两条语句,最后统一commit,任何一步抛异常就rollback并重新抛出。参数delta入库传正数、出库传负数,这样一个方法同时处理两个场景,比分开写 add_stock 和 reduce_stock 更简洁,也避免了一个改一个漏改的尴尬。界面上,入库按钮的回调会把输入的文本框内容先校验,用int()或float()转换,失败就弹 warning,不进入业务层。出库时我额外要求在业务层检查扣减后库存是否低于安全库存,是的话在日志里写一条 WARNING,界面同步提示采购建议。这个细节在答辩演示时非常加分,评委能看到你对异常场景有考虑。

3.2 订单处理:从创建到完成的状态流转

订单模块的重心不在增删改查,而在状态机的设计。订单从创建到归档会经历“待生产→生产中→已完成”这样的合法路径,但系统必须拦截非法跳转,比如直接点“已完成”按钮绕过生产环节。

ALLOWED_TRANSITIONS = { '待生产': ('生产中', '已取消'), '生产中': ('已完成', '已取消'), '已完成': (), '已取消': ('待生产',), } def update_status(self, order_id, new_status): current = self.get_status(order_id) if new_status not in ALLOWED_TRANSITIONS.get(current, ()): raise ValueError(f'非法状态变更: {current} -> {new_status}') self.conn.execute( 'UPDATE orders SET status = ? WHERE order_id = ?', (new_status, order_id) ) self.conn.commit()

用映射表而不是一堆 if-else 的好处是:以后要增加“暂停”或“质检”状态,只需要在 ALLOWED_TRANSITIONS 里加一行,不用改动业务流程代码。表中的空元组表示终态,不允许再流转;被取消的订单可以恢复到“待生产”,应对“客户又回心转意”的业务场景。每次状态变更前校验合法性,校验失败抛 ValueError,上层界面捕获后弹提示并记录日志。这样设计还有一个附加价值——审计。日志里能看到每个订单在什么时间从什么状态变成什么状态,答辩被问到“订单异常怎么排查”时直接展示日志文件即可。

3.3 生产调度:按交期和产线负载排序

生产调度模块用的是最朴素的贪心策略:把所有待生产的订单按截止日期升序排列,然后逐个分配到当前负载最低的产线。不依赖约束求解器,代码可读性强,而且是能讲清楚原理的算法,对毕设来说远比塞一个调不明白的优化库要好。

def schedule(self, pending_orders, lines): plan = [] line_load = {line: 0 for line in lines} for order in sorted(pending_orders, key=lambda o: (o['due_date'], -o['qty'])): target_line = min(line_load, key=line_load.get) assign_qty = min(order['qty'], LINE_CAPACITY - line_load[target_line]) if assign_qty <= 0: continue plan.append({ 'order_id': order['order_id'], 'line': target_line, 'assign_qty': assign_qty, 'start_date': order['due_date'] - timedelta(days=LEAD_DAYS) }) line_load[target_line] += assign_qty return plan

排序参数key=lambda o: (o['due_date'], -o['qty'])的意思是:第一优先级是截止日期最早的先排,截止日期相同时数量大的先排,取负号实现倒序。分配时选择当前累计负载最小的产线,min(line_load, key=line_load.get)是 Python 里取字典最小值 key 的常用姿势。LINE_CAPACITY 和 LEAD_DAYS 是配置常量,分别表示单条产线日产能和提前期天数,放在调度模块顶部的常量区,方便你改参数。

这个实现不追求全局最优,但它是一个可以解释的启发式规则。演示时可以先塞几条截止日期不同的订单,点“自动排产”,在结果表格里让评委看到交期早的订单被优先分配,再手动改一条订单的截止日期重新排产,对比前后差异。这个对比过程讲清楚,生产调度这部分的分数基本就拿到了。

4. 从能用到好用:权限控制、日志与报表的实现细节

库存、订单、调度是系统的“功能骨架”,但要经得起答辩追问,还得看权限、日志、报表这几个补充模块。它们决定了一个系统是“能跑的程序”还是“设计过的软件”。

4.1 RBAC 简化:角色字段加装饰器控制

权限模块在真实系统里是整套 RBAC(基于角色的访问控制),在这套毕设里做了一次适度的简化:用户表里用 role 字段区分身份,admin 可以操作全部功能,manager 可以处理订单和生产调度,operator 只能做入库出库和数据查询。访问控制用装饰器实现,代码量小,而且挂在每个业务方法上,比在界面层到处 if 判断要整洁得多。

SESSION = {'user_id': None, 'role': None} def require_role(*allowed_roles): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): if SESSION['role'] not in allowed_roles and SESSION['role'] != 'admin': raise PermissionError('当前角色无权执行此操作') return func(*args, **kwargs) return wrapper return decorator

登录成功后,把用户信息写进全局 SESSION 字典,业务方法上用@require_role('manager')标注即可。这里有个在使用 Tkinter 时容易忽略的细节:装饰器不能直接挂在按钮的command上,因为command触发时不会捕获异常。我一般把调用包在一个 lambda 里并加 try-except:

btn_order = tk.Button( parent, text='创建订单', command=lambda: self.safe_call(OrderManager.create_order, form_data) )

safe_call捕获PermissionError和ValueError,弹出 messagebox 提示,其他异常写日志后显示“操作失败,详情见日志”。这样权限校验的异常不会让程序崩溃,演示时哪怕误点了按钮也只是弹个提示,不至于黑屏退出。

4.2 日志记录与全局异常兜底

日志模块在 logging 配置上用了两个 handler:一个 StreamHandler 输出到控制台,一个 FileHandler 写入 logs/run.log,格式统一为时间戳 - 级别 - 日志名 - 消息。业务层的每个 Manager 都通过logging.getLogger(__name__)获取自己的 logger,这样日志里能一眼看出来是库存模块写的还是订单模块写的。

比常规配置多一层的是全局异常钩子。Tkinter 的 mainloop 里发生未捕获异常默认会打印到 stderr,程序退出,对桌面应用来说不友好。我在程序初始化时覆盖了 sys.excepthook:

def global_excepthook(exc_type, exc_value, exc_tb): logging.critical('未捕获异常', exc_info=(exc_type, exc_value, exc_tb)) messagebox.showerror('系统错误', f'发生未预期错误,详细信息已写入 logs/run.log') sys.excepthook = global_excepthook

这段代码让任何没兜住的异常都会先落日志再弹窗,而不是直接退出。答辩演示时最怕的就是“点了某个按钮程序就没了”,有了这个钩子,至少能保证弹出一个信息框,你有机会圆场并解释问题所在。运维文档里的故障排查章节,本质就是对 run.log 中常见错误码的分析记录。

4.3 报表导出:Treeview 展示加 CSV 落盘

报表模块不追求在 Tkinter 里画复杂图表,而是做两件事:把统计结果展示在 Treeview 表格里,同时提供“导出 CSV”按钮,用 Excel 打开做进一步分析。查询逻辑按物料分组汇总期间内的出入库净量和订单完成率:

def stock_report(self, start_date, end_date): sql = ''' SELECT m.material_name, COALESCE(SUM(CASE WHEN o.action = '入库' THEN o.change_qty ELSE 0 END), 0) AS in_qty, COALESCE(SUM(CASE WHEN o.action = '出库' THEN o.change_qty ELSE 0 END), 0) AS out_qty, s.qty AS current_qty FROM materials m LEFT JOIN operation_log o ON m.material_id = o.material_id AND o.operated_at BETWEEN ? AND ? LEFT JOIN stock s ON m.material_id = s.material_id GROUP BY m.material_id ''' return [dict(zip(('材料', '入库量', '出库量', '当前库存'), row)) for row in self.query(sql, (start_date, end_date))]

这段 SQL 用了两个 LEFT JOIN 而不是 INNER JOIN,目的是让没有出入库记录的材料也出现在报表里,否则零流转的物料会被过滤掉,报表数据就不完整了。日期参数 BETWEEN 用的是字符串比较,要求传入的格式严格是YYYY-MM-DD,我在界面层用datetime.strptime做了输入校验,非法格式直接弹提示,不会让 SQL 拿到脏日期。

导出 CSV 时有一个坑必须注意:直接open(file, 'w')写出的中文用 Excel 打开全是乱码,必须指定encoding='utf-8-sig'。这是血泪经验,UTF-8 编码在写普通文件时没问题,但 Excel 默认按本地代码页解析 CSV,不写 BOM 就不认中文。代码实现就是一个 writer 加一个循环:

with open('report.csv', 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) writer.writerow(['材料', '入库量', '出库量', '当前库存']) writer.writerows(report_rows)

界面上的 Treeview 只展示最近 30 天的数据,报表页顶部加了一个日期区间选择,默认值用(今天-30天, 今天),避免一次查询全量数据导致界面卡顿。对于数据量不大的演示环境,这个方案完全够用,而且能把“查询条件→SQL 执行→结果展示”整条链路讲得清清楚楚。

5. 避坑清单:六个我在调试 Tkinter 工厂系统时踩过的真实坑

这一章全部是实操时出的问题,每条都是现象、原因、解决三段式。你照着源码跑的时候,遇到类似症状直接来这里对照,能少走不少弯路。

坑一:点按钮后界面卡死,鼠标转圈

现象:点击“开始生产”按钮后,整个窗口无响应,标题栏显示“未响应”,过几秒后才恢复,有时直接闪退。

原因:按钮回调里直接做了耗时操作。比如生产调度模块里写了个time.sleep(3)模拟排产计算,Tkinter 是单线程模型,mainloop 阻塞期间无法处理重绘事件,表现就是卡死。如果回调里再嵌套了数据库查询,卡顿更明显。

解决:耗时操作放子线程,界面更新用root.after回到主线程。我封装了一个简易的异步执行器,把耗时的调度函数丢给threading.Thread跑,结束后通过root.after(0, callback, result)把结果投递回 UI。注意不要在子线程里直接操作 Tkinter 控件,Tkinter 不是线程安全的,直接改 Treeview 偶尔会崩溃,这个玄学问题排查起来极耗时间。

坑二:提示 no such table 但数据库文件确实存在

现象:程序一启动就报sqlite3.OperationalError: no such table: stock,但用 SQLite 工具打开 db 目录下的文件,里面明明有表。

原因:数据库文件的绝对路径和程序运行时的当前工作目录不一致。比如在项目根目录用 IDE 运行没问题,但用了os.getcwd()去拼数据库路径,启动时工作目录变了,系统直接在错误的位置新建了一个空库文件,表还没建好,程序就开始查询了。

解决:数据库路径必须绑定到项目根目录,用os.path.dirname(os.path.dirname(os.path.abspath(__file__)))逐级向上取,不能依赖os.getcwd()。init_db.py 里我把数据库文件的绝对路径打印出来,第一次运行先确认这个路径和 SQLite 工具打开的是同一个文件,避免“你以为写到了 A 库,实际读的是 B 库”的隐蔽问题。

坑三:写入数据后重启程序,数据不见了

现象:界面里录入了几十条物料,正常退出后再启动,列表是空的,但 db 文件的大小没有变。

原因:用conn.execute('UPDATE ...')执行了写操作,但忘了commit()。SQLite 在 Python 的标准用法里,执行写语句后必须显式提交,否则连接关闭时事务回滚,数据只存在了内存里。还有另一种情况:代码里开了多个连接,写入用一个连接,查询用另一个连接,写入的连接没提交就关了。

解决:业务层每个写操作都按BEGIN → 执行 → COMMIT的固定结构组织,异常时 rollback。我在 StockManager 里用with self.conn:的上下文管理方式,__enter__时开启事务,正常退出时自动 commit,异常时 rollback。把所有数据访问收口到一个 Manager 类里,而不是在界面上到处开连接,这样事务边界才看得清。

坑四:并发写入报 database is locked

现象:两个窗口同时操作,比如一个在录入出库单,另一个在跑报表刷新,报表窗口突然弹sqlite3.OperationalError: database is locked。

原因:SQLite 同一时刻只允许一个写事务,多个连接同时写会互相锁库。默认超时时间很短,等不到锁释放就直接报错。这个坑在毕设演示中特别容易触发,因为录演示数据时往往开两个窗口。

解决:共享同一个数据库连接,或者给写操作加锁。我采用的是“全局单连接”方案,程序启动时初始化一个连接,业务层消息通过这个连接收发,再用threading.Lock包住写事务。如果一定要多连接,就设置sqlite3.connect(db_path, timeout=10),给锁等待留足时间,同时把PRAGMA busy_timeout调大。注意单连接方案在跨线程使用时依然要加锁,否则 SQLite 连接对象本身也可能出现线程竞争。

坑五:金额算错,0.1 加 0.2 不等于 0.3

现象:录入单价 18.9 元的物料,数量 3 件,总价显示 56.699999999999996,导出到 Excel 后和手工算的差一分钱。

原因:浮点数二进制存储的精度问题,SQLite 里字段类型如果是 REAL,从库里读出来继续参与运算,误差会不断累积。明细数据一多,报表对不上账,评委一眼就能看出问题。

解决:金额、数量这类精度敏感字段统一用Decimal处理,数据库里建议存整数分而不是浮点元。界面上输入元,业务层转成Decimal(str(value))做运算,转成整数分写库。读取时再转回Decimal格式化。这一步改造不复杂,但能让报表数据完全对得上,属于性价比极高的改动。

坑六:打包成 exe 后双击没反应

现象:在 IDE 里跑一切正常,用 PyInstaller 打包后双击 exe,程序在任务管理器闪现一下就退出,或者卡在启动界面上不动。

原因:PyInstaller 打包时默认不收集 Tkinter 的 Tcl/Tk 数据文件,也没有把项目里的数据库文件和配置文件打进去。exe 运行时找不到tcl/tk目录,初始化 Tk 直接失败退出。

解决:打包命令里明确指定 Tcl/Tk 数据路径,并把数据文件加到 bundle 里。我的 spec 文件写法是在datas中加入( 'db', 'db' ),把整个 db 目录打进去,同时确保 init_db.py 在数据库文件已存在时不重头建表。打包后先在一台没有安装 Python 的干净虚拟机上测一遍启动,确认不依赖 Python 解释器。另外注意打包命令要用--windowed,否则控制台窗口会和 GUI 一起弹出来,很影响观感。

6. 让系统从“能跑”到“能答辩”:模拟数据、测试与打包

源码拿到手只是第一步,真正让这套系统在你的答辩现场站得住脚,需要额外做三件不起眼但决定性的事:灌一批可信的模拟数据、跑一遍自动化测试、把灵敏度调好的版本打包出来。这三件事分别对应演示观感、代码质量和交付形式。

先说模拟数据。刚开始演示时挨个手工录入太慢,而且数据量小看不出调度效果,所以我写了个独立脚本 seed_data.py,放在 tools 目录下。它做的事情是:清空现有业务表,生成 20 个物料、300 条跨三个月的订单、500 条出入库流水,订单状态随机落在“生产中”和“已完成”,截止日期覆盖过去和未来各 30 天。生成订单号时按NO + 年月日 + 三位序号拼接,保证唯一且看起来像真数据。有了这批数据,生产调度页面一打开就有几十条待排产订单,演示排产效果时不用现编数据。

然后是自动化测试。毕设源码如果只在界面点来点去,评委问“你怎么验证功能正确”容易答空。我在 tests 目录下用 pytest 写了库存和订单的冒烟用例,不依赖 GUI,直接测业务层:

def test_outbound_negative_stock_raises(): stock_mgr = StockManager() with pytest.raises(ValueError): stock_mgr.change_stock(material_id=1, delta=-10000, action='出库')

这条用例验证的是扣减数量超过现有库存时必须抛异常,防止库存变负。跑pytest -v能一次执行全部用例,答辩时当场演示测试通过,比口头解释“我测过了”有力得多。测试数据用内存库或独立的 test.db,不能污染演示用的真实数据。

最后是打包。我用 PyInstaller 的--onefile --windowed --name 工厂管理系统出单文件版,加--hidden-import带上tkinter相关的隐含模块。打包完成后先手动走一遍登录、入库、出库、排产、导出 CSV 的完整流程,确认所有功能在无 Python 环境的机器上正常。数据库文件放在程序同目录,启动时自动建库,用户不需要懂任何运维知识。

那次答辩前夜,我打包的 exe 首次双击直接报缺 tcl/tk,当场冷汗就下来了。从那以后,我每次演示前都强制走一遍“清库 → 初始化 → 灌模拟数据 → 跑 pytest → 重新打包 → 干净虚拟机验证”,全流程半小时,换来的是演示现场零意外。这套系统的代码和文档已经把这些步骤都写进了运维说明里,你按顺序执行一遍,大概率不会重蹈我的覆辙。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询