ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python库存管理系统课程设计:源码+SQL+报告全流程避坑指南

Python库存管理系统课程设计:源码+SQL+报告全流程避坑指南

简介:这是一套面向高校学生与Python初学者、用于课程设计或数据库大作业的库存管理系统完整资料,包含源码、SQL文件与设计报告三部分,可帮助读者理解从需求分析到功能落地的完整开发流程。压缩包共1473个文件,约28.52MB,以ts、html、js、scss、less等前端资源为主,辅以py、pyc等Python源码与编译文件,另有sql、sqlite3数据库文件及md、pdf、docx等说明文档,目录结构清晰,便于按模块查阅。系统覆盖商品信息管理、库存信息管理、入库出库操作、库存查询统计与用户权限管理等核心模块,设计报告则对数据库表结构、字段约束与表间关系作了详细阐述。目前已有44人学习下载,适合需要参考完整项目结构、数据库设计思路与Python业务实现细节的读者借鉴使用。

1. 从一份课程设计说起:Python 库存管理系统到底要交付什么

每年到了期末,总有一批人卡在“库存管理系统”这个选题上。标题里写着“源码+sql文件+设计报告”,看起来三样东西齐了就能交差,但真正动手才发现:源码跑不起来、sql 文件导入报错、设计报告和代码对不上号。我带过几届学生的课程设计,也帮朋友改过类似的单子,最常见的翻车现场不是代码写得多烂,而是环境没配对、数据库没连上、表结构跟代码里的字段名差一个字母。

这个方案适合谁?一是正在做课程设计、需要一套能跑通的 Python 库存管理系统的人;二是想拿它当练手项目、把增删改查和数据库操作串一遍的入门者。它解决的核心问题很具体:用 Python 把商品入库、出库、库存查询、低库存预警这几个动作跑通,数据落到 MySQL 里,最后能生成一份说得清设计思路的报告。下面我按实际落地的顺序,把环境、表结构、核心代码、避坑点拆开讲。

2. 环境与依赖:把 Python 和 MySQL 先接上

2.1 为什么选 PyMySQL 而不是 MySQLdb

Python 连 MySQL 的库有好几个,常见的是 MySQLdb、PyMySQL、mysql-connector-python。MySQLdb 在 Windows 上装起来经常卡在编译环节,mysql-connector 是官方出品但 API 风格跟课程设计里常见的写法不太一样。PyMySQL 是纯 Python 实现,pip 一条命令就能装,代码里import pymysql直接能用,对新手最友好。我一般会先确认 Python 版本,3.8 到 3.11 都行,再装依赖。

# 确认 Python 版本,建议 3.8 以上 python --version # 安装 PyMySQL,这是连 MySQL 的核心库 pip install pymysql # 如果要做界面,装 tkinter 相关(标准库自带,一般不用额外装) # 如果要用到日期处理,装 datetime(标准库自带)

装完之后别急着写代码,先确认 MySQL 服务是活的。Windows 下在服务里看 MySQL 是否运行,Linux 下用systemctl status mysql。这一步跳过的人,后面报Can't connect to MySQL server的时候会找半天。

2.2 建库建表的 sql 文件怎么导入

标题里的“sql 文件”通常包含建库、建表、插初始数据三部分。我习惯把库名定为inventory_db,字符集用utf8mb4,不然商品名里的中文会变问号。导入方式有两种:命令行和图形工具。命令行更稳,不容易因为工具版本差异出问题。

# 登录 MySQL,输入密码 mysql -u root -p # 在 MySQL 命令行里执行 sql 文件 source /path/to/inventory.sql; # 或者退出后用命令行直接导入 mysql -u root -p inventory_db < /path/to/inventory.sql

导入之后一定要验证:use inventory_db; show tables;看表在不在,select * from product limit 5;看数据有没有进去。我见过有人导入时没指定库,结果表建到了别的库里,代码连上去查不到表,报Table 'inventory_db.product' doesn't exist。

2.3 数据库连接配置的写法与参数说明

连接配置我一般单独放一个db_config.py,不硬编码在主程序里。这样换环境只改一个文件。关键参数有五个:host、port、user、password、database,再加一个 charset。

# db_config.py import pymysql def get_connection(): return pymysql.connect( host='localhost', # 数据库地址,本机就是 localhost port=3306, # MySQL 默认端口,改过就填改后的 user='root', # 用户名 password='your_pass', # 换成你自己的密码 database='inventory_db', # 库名,要和 sql 文件里一致 charset='utf8mb4', # 字符集,防止中文乱码 cursorclass=pymysql.cursors.DictCursor # 让查询结果以字典返回 )

cursorclass设成DictCursor之后,fetchone()返回的是{'product_name': '螺丝刀', 'stock': 20}这种形式,取值用row['product_name'],比用下标row[1]可读性好得多。密码不要写死成123456然后提交到公开仓库,这是血泪经验,改起来一分钟,泄露了很麻烦。

3. 核心功能落地:入库、出库、查询、预警怎么写

3.1 商品表的字段设计与 sql 建表语句

表结构设计不好,后面代码怎么写都别扭。商品表我一般叫product,字段包括:自增主键id、商品编号product_code(唯一)、商品名product_name、规格spec、库存数量stock、库存下限min_stock、创建时间create_time。库存下限是用来做预警的,低于这个值就提示补货。

CREATE TABLE `product` ( `id` INT NOT NULL AUTO_INCREMENT, `product_code` VARCHAR(50) NOT NULL UNIQUE COMMENT '商品编号', `product_name` VARCHAR(100) NOT NULL COMMENT '商品名称', `spec` VARCHAR(100) DEFAULT NULL COMMENT '规格', `stock` INT NOT NULL DEFAULT 0 COMMENT '当前库存', `min_stock` INT NOT NULL DEFAULT 10 COMMENT '库存下限', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

product_code加 UNIQUE 约束,防止重复录入同一个商品。stock和min_stock用 INT,别用 VARCHAR,不然排序和比较会出玄学问题。create_time默认当前时间,插入时不用手动传。

3.2 入库操作的完整函数与事务处理

入库不是简单地把 stock 加一下。正确做法是:先查商品是否存在,存在就更新库存,不存在就插入新商品,整个过程放在一个事务里。这样中途出错可以回滚,不会出现“库存加了但日志没记”的情况。

import pymysql from db_config import get_connection def stock_in(product_code, product_name, spec, quantity, min_stock=10): """ 入库操作:商品存在则增加库存,不存在则新建 quantity: 入库数量,必须为正整数 """ if quantity <= 0: return False, "入库数量必须大于 0" conn = get_connection() try: with conn.cursor() as cursor: # 先查商品是否存在 sql_check = "SELECT id, stock FROM product WHERE product_code = %s" cursor.execute(sql_check, (product_code,)) result = cursor.fetchone() if result: # 存在,更新库存 new_stock = result['stock'] + quantity sql_update = "UPDATE product SET stock = %s WHERE product_code = %s" cursor.execute(sql_update, (new_stock, product_code)) else: # 不存在,插入新记录 sql_insert = """INSERT INTO product (product_code, product_name, spec, stock, min_stock) VALUES (%s, %s, %s, %s, %s)""" cursor.execute(sql_insert, (product_code, product_name, spec, quantity, min_stock)) # 记录入库日志(假设有 log 表) sql_log = """INSERT INTO stock_log (product_code, change_type, change_qty) VALUES (%s, 'IN', %s)""" cursor.execute(sql_log, (product_code, quantity)) conn.commit() return True, "入库成功" except Exception as e: conn.rollback() return False, f"入库失败:{str(e)}" finally: conn.close()

with conn.cursor()这种写法会自动关闭游标,不用手动cursor.close()。conn.commit()放在with外面,确保所有操作都成功才提交。except里先rollback()再返回错误信息,这样数据不会脏。参数用%s占位,不要用字符串拼接,否则商品名里带单引号就会 SQL 报错,这也是常见的注入隐患。

3.3 出库与库存不足的判断逻辑

出库比入库多一层判断:库存够不够。不够就直接拒绝,不要先减再补。判断和更新要在同一个事务里,避免并发时超卖。

def stock_out(product_code, quantity): """ 出库操作:先判断库存是否充足,充足则扣减 """ if quantity <= 0: return False, "出库数量必须大于 0" conn = get_connection() try: with conn.cursor() as cursor: # 加 FOR UPDATE 锁住这一行,防止并发扣减 sql_check = "SELECT stock FROM product WHERE product_code = %s FOR UPDATE" cursor.execute(sql_check, (product_code,)) result = cursor.fetchone() if not result: return False, "商品不存在" if result['stock'] < quantity: return False, f"库存不足,当前库存 {result['stock']}" sql_update = "UPDATE product SET stock = stock - %s WHERE product_code = %s" cursor.execute(sql_update, (quantity, product_code)) sql_log = """INSERT INTO stock_log (product_code, change_type, change_qty) VALUES (%s, 'OUT', %s)""" cursor.execute(sql_log, (product_code, quantity)) conn.commit() return True, "出库成功" except Exception as e: conn.rollback() return False, f"出库失败:{str(e)}" finally: conn.close()

FOR UPDATE是行级锁,在事务里锁住查询到的行,别的事务要改这一行就得等。课程设计里并发场景不多,但写上这个能体现你考虑过数据一致性。注意FOR UPDATE必须在事务里用,autocommit 模式下不生效。

3.4 库存查询与低库存预警的 sql 写法

查询分两种:按商品编号精确查,和查全部。预警就是查stock < min_stock的记录。sql 本身不复杂,关键是返回结果的处理。

def query_stock(product_code=None): """查询库存,传编号查单个,不传查全部""" conn = get_connection() try: with conn.cursor() as cursor: if product_code: sql = "SELECT * FROM product WHERE product_code = %s" cursor.execute(sql, (product_code,)) return cursor.fetchone() else: sql = "SELECT * FROM product ORDER BY stock ASC" cursor.execute(sql) return cursor.fetchall() finally: conn.close() def low_stock_alert(): """返回所有低于库存下限的商品""" conn = get_connection() try: with conn.cursor() as cursor: sql = "SELECT * FROM product WHERE stock < min_stock" cursor.execute(sql) return cursor.fetchall() finally: conn.close()

ORDER BY stock ASC让库存少的排前面,一眼能看到哪些快没了。预警查询用<而不是<=,因为等于下限时通常还算安全,低于才需要补。这个细节设计报告里可以写一句,显得有思考。

4. 避坑与排查:那些让系统跑不起来的常见问题

4.1 中文乱码:从建库到连接的三处字符集

现象:商品名存进去变成???或者乱码。原因通常有三处:建库时没指定字符集、建表时没指定、连接时没指定。解决方法是三处都统一成utf8mb4。建库语句写成CREATE DATABASE inventory_db DEFAULT CHARSET utf8mb4;,建表末尾加DEFAULT CHARSET=utf8mb4,连接参数里加charset='utf8mb4'。三处缺一处都可能出问题,我一般建完库先show variables like 'character%';确认一下。

4.2 导入 sql 文件报 1064 或 1046

现象:source导入时提示语法错误或者“No database selected”。原因一是 sql 文件里没有USE inventory_db;,二是文件编码不是 UTF-8。解决方法是打开 sql 文件,在开头手动加一行USE inventory_db;,并确认文件保存为 UTF-8 无 BOM 格式。Windows 记事本默认可能带 BOM,用 VS Code 或 Notepad++ 转一下。

4.3 连接报 2003:MySQL 服务没起或端口不对

现象:pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on 'localhost'")。原因一是 MySQL 服务没启动,二是端口不是 3306,三是防火墙拦了。解决方法是先net start mysql(Windows)或systemctl start mysql(Linux),再确认端口,show variables like 'port';能看到实际端口。如果改过端口,连接配置里也要改。

4.4 出库后库存变负数

现象:明明判断了库存不足,还是出现了负数。原因通常是判断和更新之间没有锁,两个请求同时读到相同库存,都判断通过,然后都扣减。解决方法是像 3.3 那样用FOR UPDATE锁行,或者把判断和更新合并成一条 sql:UPDATE product SET stock = stock - %s WHERE product_code = %s AND stock >= %s,看影响行数是否为 1。后者更简洁,推荐。

4.5 设计报告和代码对不上

现象:报告里写的表字段和代码里用的不一致,或者报告里的功能代码里没实现。原因是先写报告后写代码,或者改代码没同步改报告。解决方法是先定表结构,代码按表结构写,报告里的 ER 图和字段说明直接从DESC product;的输出复制。改代码时顺手在报告里标一下,别等最后对不上再返工。

5. 进阶技巧:把系统从“能跑”推到“好用”

5.1 用上下文管理器简化数据库操作

每次写try...finally关连接太啰嗦。可以写一个上下文管理器,把连接和关闭包起来。

from contextlib import contextmanager @contextmanager def db_cursor(): conn = get_connection() try: with conn.cursor() as cursor: yield cursor conn.commit() except Exception: conn.rollback() raise finally: conn.close() # 用的时候 with db_cursor() as cursor: cursor.execute("SELECT * FROM product") rows = cursor.fetchall()

这样业务代码里不用重复写 commit 和 close,出错自动回滚。yield之前是进入时的操作,之后是退出时的操作。注意conn.commit()放在with conn.cursor()外面,确保游标关闭后再提交。

5.2 给查询加分页,避免数据多了卡死

商品上千条之后,fetchall()一次全取出来会占内存,界面也会卡。加 LIMIT 和 OFFSET 分页。

def query_stock_page(page=1, page_size=20): offset = (page - 1) * page_size conn = get_connection() try: with conn.cursor() as cursor: sql = "SELECT * FROM product ORDER BY id LIMIT %s OFFSET %s" cursor.execute(sql, (page_size, offset)) return cursor.fetchall() finally: conn.close()

page从 1 开始,offset是跳过的行数。page_size一般 20 到 50,看界面能显示多少行。别忘了再写一个count查询算总页数,不然不知道有多少页。

5.3 用日志表追溯每一次库存变动

只改product表的 stock,出了问题查不到是谁改的、什么时候改的。加一张stock_log表,每次入库出库都插一条。

CREATE TABLE `stock_log` ( `id` INT NOT NULL AUTO_INCREMENT, `product_code` VARCHAR(50) NOT NULL, `change_type` ENUM('IN','OUT') NOT NULL, `change_qty` INT NOT NULL, `change_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

change_type用 ENUM 限制只能填 IN 或 OUT,防止写错。查某个商品的所有变动:SELECT * FROM stock_log WHERE product_code = 'P001' ORDER BY change_time DESC;。这个表在写设计报告时也是加分项,说明你考虑了审计和追溯。

5.4 验证系统是否真的跑通:三个必做测试

写完代码别急着交,跑三个测试。第一,入库一个新商品,查库存是否为入库数量。第二,出库数量大于库存,看是否返回“库存不足”且库存没变。第三,把某个商品库存改到低于下限,调预警函数看是否返回该商品。三个都过了,基本功能就没问题。我习惯把测试结果截图放进设计报告,比空口说“已实现”有说服力。

最后说个习惯:每次改完代码,先把db_config.py里的密码换成自己的再跑,别用别人的配置。数据库连接失败时,先看服务起没起,再看端口和密码,最后看库名和表名。这个顺序能省很多时间。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进