Python仓库管理系统开发实战:数据库设计与事务保障库存一致
简介一套面向计算机专业毕业设计及期末大作业场景的 Python 仓库管理系统源码项目主要解决库存管理、出入库记录与基础数据维护等典型业务问题适合正在做毕业设计的学生也适合具备一定 Python 基础、需要综合项目练手的中级学习者。压缩包共 148 个文件大小约 573KB主体为 57 个 Python 源码文件负责系统核心逻辑15 个 HTML 页面配合 CSS、JS 组成前端展示层sqlite3 数据库文件与 SQL 脚本用于数据存储与初始化另有少量 pyc 编译文件、配置文件及说明文档整体目录结构清晰便于按模块阅读和二次开发。已有 580 人浏览学习属于经过导师指导并获得 98 分评审的高分项目。源码经过本地编译调试确认可运行读者可直接导入相关环境后启动项目。借助前端页面模板、数据库脚本和配置文件可以理解系统分层结构并快速搭建可演示的仓储管理原型为毕业设计或期末大作业提供完整可行的代码基础。1. 仓库管理系统用Python做高分项目它到底在解决什么许多人的第一个课程设计就是被“仓库管理系统”这个题目敲开的。它看起来简单但老师要的从来不是几行增删改查而是“库存怎么保证不超卖”“出库单和库存能不能对得上”“盘点差异怎么处理”。Python写这个项目有一个天然优势不管选Tkinter做桌面端还是Flask做Web端都能用最短时间把逻辑跑通把精力留在业务完整性上。所以你能在免费python源码大全里看到大量仓库系统但真正值得照着做的反而是那些把表结构讲清楚的源码。这个题适合两类人正在做课程设计或毕设的学生以及刚学完python入门、想拿真实业务练手的开发者。它不大但是五脏俱全登录、权限、商品管理、入库出库、库存查询、盘点统计每一块都能单独讲出技术点。比图书管理多一层“数量约束”比电商系统少一半商品维度。本文就把这条完整落地路径拆开先建表再写业务最后处理你一定会遇到的那几个坑。2. 先设计数据表再写Python代码仓库系统的地基决定后面会不会返工2.1 核心表就这三张商品、入库单、出库单仓库系统的业务再复杂落到数据库里也无非是“有什么东西、进了多少、出了多少”。很多源码为了凑字段建了七八张表反而让查询和事务变得难维护。我一般会先建三张核心表其余报表都在这三张表上做聚合。表名用途关键字段products商品主数据id, name, sku, category, stock, min_stock, unitinbound_orders入库单id, product_id, quantity, operator, created_at, remarkoutbound_orders出库单id, product_id, quantity, operator, created_at, remark注意 products 表里有一个 stock 字段它是冗余数据因为理论上库存可以通过“累计入库减累计出库”算出来。但每次查询都实时聚合数据量大以后会慢而且写业务代码时很绕。保留库存数字用数据库事务保证它和流水一致是主流做法。另外加一个 min_stock 字段后面做低库存预警会非常省事。2.2 用SQLite还是MySQL单机演示选它答辩演示选它这是源码里最容易纠结的选型。我的建议很简单如果项目是Tkinter或PyQt做的桌面程序直接用SQLite零配置Python标准库就能驱动如果你打算做成Web系统或者老师有PostgreSQL/MySQL要求用MySQL。SQLite把数据库文件当成普通文件处理对学生来说有个好处打包、拷走、提交都方便答辩时U盘一插就能演示。import sqlite3 def get_conn(db_pathwarehouse.db): conn sqlite3.connect(db_path) conn.row_factory sqlite3.Row conn.execute(PRAGMA foreign_keys ON) return connrow_factory sqlite3.Row是为了让查询结果能用row[name]这种方式取值别小看这一行写界面代码时比元组下标舒服得多。PRAGMA foreign_keys ON是SQLite默认不检查外键必须每次连接都手动开一次否则你删除商品时会留下孤儿流水。如果改用MySQL连接核心参数是这三样host、user/password、database再加一个charset参数。常见坑是用默认latin1导致中文乱码后续有一章专门讲这里先记住连接串里写charsetutf8mb4。import pymysql conn pymysql.connect( host127.0.0.1, userroot, passwordyour_password, databasewarehouse, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor )参数说明charsetutf8mb4比utf8能多存emoji等四字节字符仓库备注字段很可能出现特殊符号。DictCursor让查询结果变成字典和SQLite的Row行为一致切换数据库时业务层代码不用改。SQLite在并发写入上有局限但课程设计单机演示完全够用不要为了“看起来高级”硬上MySQL而搞不定环境。2.3 初始化数据与管理员账号让源码下载下来第一眼能登录一个仓库系统源码最让人难受的是第一眼看不到效果。所以初始化脚本至少要完成两件事创建表、插入一个默认管理员账号。很多高分项目会顺带插入几款演示商品这个很聪明答辩时不用现造数据。import sqlite3 from werkzeug.security import generate_password_hash SCHEMA CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, sku TEXT UNIQUE NOT NULL, category TEXT, stock INTEGER NOT NULL DEFAULT 0, min_stock INTEGER NOT NULL DEFAULT 0, unit TEXT DEFAULT 件 ); -- inbound_orders 与 outbound_orders 结构对称只保留关键字段 CREATE TABLE IF NOT EXISTS inbound_orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL, quantity INTEGER NOT NULL, operator TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, remark TEXT ); CREATE TABLE IF NOT EXISTS outbound_orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL, quantity INTEGER NOT NULL, operator TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, remark TEXT ); CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT operator ); def init_db(): conn sqlite3.connect(warehouse.db) conn.executescript(SCHEMA) pw_hash generate_password_hash(admin123) conn.execute( INSERT OR IGNORE INTO users (username, password_hash, role) VALUES (?,?,?), (admin, pw_hash, admin) ) conn.commit() conn.close() if __name__ __main__: init_db()executescript可以一次执行多段SQL但注意它执行前会自动提交当前事务所以不要在写生产代码时拿它封装不可重复执行的脚本。INSERT OR IGNORE是为了让脚本可以重复跑不会因为第二次运行撞上username唯一约束而报错。密码用generate_password_hash而不是明文很多低分项目就挂在密码明文存储上老师一问“用户表泄露怎么办”就卡壳。3. 用Python把四条业务主线写出来登录、入库、出库、盘点到报表3.1 登录与权限管理员和操作员为什么不能共用一张表仓库系统里如果只有“能不能登录”一个状态那权限就是假的。实际业务中操作员只能做入库出库登记管理员才能看报表、改库存、删流水。网上有些源码在users表里加一个role字段这就是最小可用方案再往上就是单独建role表课程设计做到字段级别就够了。def login(username, password): conn get_conn() user conn.execute( SELECT * FROM users WHERE username ?, (username,) ).fetchone() conn.close() if not user: return None if not check_password_hash(user[password_hash], password): return None return {id: user[id], username: user[username], role: user[role]}这段代码里有一个容易踩的盲区先查询用户再校验密码而不是在SQL里直接WHERE username? AND password_hash?。这样能区分“用户不存在”和“密码错误”日志记录时能精确到是哪一类尝试失败。接口层可以通过返回空值统一处理为“用户名或密码错误”防止攻击者探测账号是否存在。生产级一点的做法是在users表加last_login_at和failed_attempts字段课程设计不强制但如果老师问“怎么防暴力破解”你回答“限制失败次数并加锁”就是加分项。这里的核心思想是前端按钮样式可以平凡但权限体系一定要有因为它能展示你理解业务边界。3.2 入库出库不翻车事务要么全成功要么全失败仓库系统最阴险的bug不是功能没写而是“入库成功了库存数字没加上”或者“两张单子同时出库库存变成负数”。原因都是没有用事务。入库业务本质是同一个事务里执行两步向流水表插一条记录更新产品表的库存数字。第一步成功、第二步失败你得到的就是脏数据。def create_inbound(product_id, quantity, operator, remark): conn get_conn() try: conn.execute(BEGIN) conn.execute( INSERT INTO inbound_orders (product_id, quantity, operator, remark) VALUES (?,?,?,?), (product_id, quantity, operator, remark) ) conn.execute( UPDATE products SET stock stock ? WHERE id ?, (quantity, product_id) ) conn.commit() except Exception as e: conn.rollback() raise RuntimeError(f入库失败已回滚{e}) from e finally: conn.close()参数说明sqlite3默认在execute(INSERT…)时隐式开启事务但为了可读性和跨数据库一致性我习惯显式写BEGIN。出库函数结构完全一样只是把SQL换成INSERT INTO outbound_orders和UPDATE products SET stock stock - ? WHERE id ? AND stock ?。在UPDATE里多一个AND stock 是防超卖的前置条件比先SELECT判断再UPDATE更安全因为SELECT到UPDATE之间库存可能被别的线程改了。这段代码是仓库系统的核心答辩时老师喜欢问“如果出库数量超过库存怎么办”。你回答“在SQL层加条件确认影响行数为0就抛异常回滚”比“前端弹窗提示”有说服力。事务的四个特性ACID不需要背全但“原子性”必须讲到这个例子。3.3 低库存预警一条SQL筛出需要补货的商品库存管理不能等货卖完才补所以产品表里那个min_stock字段开始发挥作用了。低库存预警本质就是一条查询当前库存小于最低库存的商品列表。def get_low_stock_products(threshold_fromNone): conn get_conn() sql SELECT id, name, sku, stock, min_stock, unit FROM products WHERE stock min_stock params () if threshold_from is not None: sql SELECT id, name, sku, stock, min_stock, unit FROM products WHERE stock ? AND min_stock ? params (min_stock, threshold_from) rows conn.execute(sql, params).fetchall() conn.close() return [dict(row) for row in rows]这个threshold_from参数是我个人习惯加的用来过滤“最低库存设置过低”的那些商品避免预警页面全是无意义数据。真实的仓库现场会按品类设置安全库存这里用数据库默认字段已经能把方案讲清楚。UI层拿到这个列表后把stock min_stock的行标黄就是最直观的预警界面。3.4 盘点差异账面数和实物数对不上时用盘点单修正盘点这个功能很多源码会忽略但它恰恰是仓库系统区别“玩具”和“项目”的分界线。盘点逻辑是输入某个商品实际数的盘点结果系统自动算出差异然后生成一条调整记录把库存改成实盘数。def stocktaking(product_id, actual_quantity, operator): conn get_conn() try: conn.execute(BEGIN) product conn.execute( SELECT stock FROM products WHERE id ? FOR UPDATE, (product_id,) ).fetchone() if not product: raise ValueError(商品不存在) diff actual_quantity - product[stock] if diff 0: return 0 # 统一记入 outbound_orders 表数量为负数代表盘盈 if diff 0: conn.execute( INSERT INTO outbound_orders (product_id, quantity, operator, remark) VALUES (?,?,?,?), (product_id, abs(diff), operator, 盘点盘亏) ) else: conn.execute( INSERT INTO inbound_orders (product_id, quantity, operator, remark) VALUES (?,?,?,?), (product_id, diff, operator, 盘点盘盈) ) conn.execute( UPDATE products SET stock ? WHERE id ?, (actual_quantity, product_id) ) conn.commit() return diff except Exception as e: conn.rollback() raise RuntimeError(f盘点失败{e}) from e finally: conn.close()注意这段代码里的FOR UPDATE是MySQL的写法SQLite不支持它。我用它一是为了演示行级锁思想二是在MySQL下能真正避免盘点过程中有人并发修改库存。SQLite场景可以去掉这行因为事务整体锁定数据库写入。这里体现的“账面数要能追到流水”是仓库系统最重要的可追溯性老师问你“怎么证明盘点结果合理”你可以说“在出入库流水里留了盘盈亏记录库存变更永远有据可查”。4. 把源码跑起来常见的5个坑现象、原因、解决4.1 中文乱码经典中的经典从建库到连接都要指定utf8现象表里明明显示中文Python读出来却是?或乱码写入又报Incorrect string value。原因绝大多数是MySQL创建数据库时默认字符集不是utf8或者连接字符串没指定charset。SQLite遇到这种情况较少但也会因为终端编码不对而显示乱码。解决MySQL建库时写死字符集CREATE DATABASE warehouse DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;连接时带上charsetutf8mb4。SQLite这边则在初始化脚本的connect之后执行conn.execute(PRAGMA encoding UTF-8)一劳永逸。乱码这种问题修复成本很低但一旦提交给老师检查印象分会掉一大截。4.2 并发出库导致库存负数事务加上WHERE条件才能兜底现象两个窗口同时出库同一商品都查到了库存里有10件各出8件最后库存变成-6。原因先SELECT再UPDATE这种“检查后更新”的路径有竞态窗口事务能保证原子性但保证不了业务约束。解决把约束写进UPDATEUPDATE products SET stock stock - ? WHERE id ? AND stock ?然后检查rowcount。如果为0抛异常并回滚让用户知道是库存不足而不是数据错误。这是我在第3章强调过的写法也是源码评审中最容易挑出的设计缺陷。4.3 时间显示差8小时数据库存的是UTC现象入库时间是下午14点页面上显示成凌晨6点。原因SQLite的CURRENT_TIMESTAMP返回UTC时间而中国时区是UTC8MySQL也默认使用服务器时区设置很多云数据库默认UTC。解决两种做法推荐第二种。一是在建表时用DEFAULT (datetime(now,localtime))让SQLite本地化存储二是在Python层统一用datetime.now()写入时间戳字段显示时自行格式化。不要既依赖数据库默认时间又依赖Python时间两套混用必然出现差8小时的玄学问题。from datetime import datetime created_at datetime.now().strftime(%Y-%m-%d %H:%M:%S) conn.execute( INSERT INTO inbound_orders (product_id, quantity, operator, remark, created_at) VALUES (?,?,?,?,?), (product_id, quantity, operator, remark, created_at) )时间字段的处理在仓库报表里很关键按月汇总盘点数据全靠它。如果入库单时间乱了后面的月度报表全部不可信。4.4 打包exe后提示找不到数据库文件现象源码里运行正常用PyInstaller打包成exe之后双击程序直接报错说找不到warehouse.db。原因代码里用了open(warehouse.db)这种相对路径而Windows下程序的当前工作目录并不是exe所在目录可能是C:\Windows\System32或快捷方式指定的目录。解决用文件绝对路径定位数据库。常规做法是取exe所在目录import os import sys def app_dir(): if getattr(sys, frozen, False): return os.path.dirname(sys.executable) return os.path.dirname(os.path.abspath(__file__)) DB_PATH os.path.join(app_dir(), warehouse.db)这样打包后数据库文件就是exe旁边那个文件。注意首次运行要自动初始化数据库否则新环境没有db文件程序又崩了。在启动代码里调用一次init_db()是最省事的方案。4.5 界面点击按钮卡死SQL写到了主线程里现象Tkinter界面点“生成报表”窗口直接白屏无响应过几十秒才弹出一堆数据。原因在GUI主线程执行了耗时SQL查询阻塞了消息循环系统认为程序未响应。课程设计的数据量可能不多但如果报表跨月扣JOIN流水照样卡。解决用threading.Thread把耗时查询丢到子线程结果通过队列传回主线程更新界面。这是Tkinter/PyQt程序最实用的一招也是那些免费python源码大全里大概率没写对的地方。import threading import queue def request_report(result_queue): def run(): rows get_monthly_report() result_queue.put(rows) threading.Thread(targetrun, daemonTrue).start()子线程里千万不要直接操作界面控件Tkinter不是线程安全的轻则闪退重则乱掉。排队回传是正规路子能讲清楚这段答辩时“界面卡死”这类问题就是送分题。5. 从“能跑”到“高分交付”用三招验证库存代码到底对不对5.1 手工造边界库存数据看系统崩不崩仓库系统的高分标准不是“能正常操作”而是“异常输入下不产生脏数据”。你至少要亲手测这几组数据入库数量为负数、出库数量大于库存、商品ID不存在、重复提交同一张出库单。常见做法是写一段临时脚本循环调用业务函数断言异常是否如预期抛出。import pytest from warehouse import create_inbound, create_outbound def test_outbound_over_stock(): with pytest.raises(RuntimeError): create_outbound(product_id1, quantity99999, operatortester)如果代码里少了AND stock ?这个条件这条测试立刻会失败。自动化测试的意义不仅是防回归更重要的是老师随手翻你项目目录看到一个test_文件印象分直接不一样。5.2 给核心业务加日志答辩时能讲出数据流日志不是print而要把每次入库出库的操作员、数量、结果、耗时记录到文件。用Python标准库logging即可不需要引入额外依赖。import logging logging.basicConfig( filenamewarehouse.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s ) def create_inbound(product_id, quantity, operator, remark): try: # 业务代码同上 logging.info(入库成功 product_id%s quantity%s operator%s, product_id, quantity, operator) except Exception as e: logging.warning(入库失败 product_id%s quantity%s operator%s error%s, product_id, quantity, operator, e) raise日志文件是答辩时最直观的“数据流证明”老师问“系统跑没跑过”你把日志调出来每一笔操作都有时间戳和操作人。我习惯在日志里加上每次事务的耗时方便后面发现性能瓶颈。5.3 回归测试加一个最简单的pytest用例写完仓库系统我会给自己立个规矩修改核心业务函数之前必须先把现有的pytest用例跑一遍。别嫌项目小仓库系统的报表、库存数字、流水记录三者之间的一致性改一行排序可能就毁掉。用pytest把这些约束固化下来比肉眼检查可靠十倍。def test_stock_matches_orders(): conn get_conn() products conn.execute(SELECT id, stock FROM products).fetchall() for p in products: inbound conn.execute( SELECT COALESCE(SUM(quantity),0) FROM inbound_orders WHERE product_id?, (p[id],) ).fetchone()[0] outbound conn.execute( SELECT COALESCE(SUM(quantity),0) FROM outbound_orders WHERE product_id?, (p[id],) ).fetchone()[0] assert p[stock] inbound - outbound, f商品 {p[id]} 库存与流水不一致这条测试把“库存必须等于累计入库减去累计出库”这个业务规则直接固化到了测试代码里。哪怕产品表里有过盘盈盘亏流水表也记录了修正所以依然成立。这部分是很多高分项目没有做到的而它恰恰是仓库系统最该验证的底层逻辑。我这些年写过不少仓库系统最大的教训就是不要先写界面更不要先抄源码调布局必须先让库存数字和流水对得上。界面丑可以改UI布局也可以换但库存账对不上的系统再好看也只是个空壳。从建表到事务从日志到回归测试把这条链路走稳仓库管理系统就真的是一个能讲清楚、查得出、敢演示的高分项目。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →