
简介包含源码、原型与数据库的水果库存管理系统资源包面向需要完成课程设计、毕业设计、系统开发练习或学习库存管理流程的读者可作为从零搭建同类小型系统的起步素材。资源共4个文件压缩包仅1.08兆字节内含数据库脚本、数据库备份文件、源码压缩包及说明文档脚本与备份文件可直接恢复数据表结构源码压缩包内为系统实现代码说明文档用于了解原型界面与使用逻辑整体轻量且层次清晰。内容围绕水果库存的入库、出库、查询与盘点等常见业务读者可对照源码理解模块划分借助说明文档把握界面交互通过数据库脚本快速还原初始数据缩短二次开发与调试时间。目前已有1518人学习下载适合作为课程设计、期末项目或答辩展示的参考蓝本尤其对初学者厘清数据库、后端逻辑与前端原型之间的协作关系很有帮助。整套资料按源码、原型、数据库分层组织便于按需查阅和二次扩展。1. 水果库存管理系统为什么要“源码原型数据库”三件套做课程设计或给生鲜门店做内部管理工具时“源码原型数据库 水果库存管理系统”的交付清单很常见。可代码能跑、答辩时依然被要求重做的情况太多了原型里没标库存规则数据库没有批次字段一并发就负库存。这套组合的价值是在编码前把业务漏洞暴露出来而不是上线后靠手工改数据救火。水果库存比日用品库存麻烦在两点损耗和批次。进货按箱、出货按斤要换算同一款苹果不同日期进的货保质期和成本都不一样。一个能交付的水果库存管理系统必须把这两点体现在原型、数据库字段和源码逻辑里。适合在准备课程设计、毕业设计的学生以及要做生鲜轻量库存工具的开发者。顺序是关键。先画原型、再建表、最后写代码跳过原型直接建表写代码时大概率发现缺字段回头改表。2. 从水果损耗倒推数据库设计这张库存表该怎么建2.1 业务规则才是表结构的来源水果库存系统里最容易被写错的是“库存数量”这个字段。老师在评审时经常会追问账面库存、可用库存、实际库存分别是什么如果系统只给一个字段等于把这三个概念混在一起。账面库存是累计进货减去累计出货可用库存还要扣除锁定和过期批次的数量实际库存靠盘点校准——因为搬运损耗、腐烂损耗都会让账面和实物对不上。我习惯从这样的业务规则反推表最少要7张用户表、水果分类表、水果信息表、入库单表、入库明细表、出库单表、盘点记录表。再附带一张库存快照表用于承载并发扣减。这个规模对于课程设计和门店小系统来说不多不少每张表都有明确归属文档也好写。这几张表的关系很直观分类表一对多水果信息水果信息一对多入库明细和出库明细盘点表按水果加批次记录。2.2 MySQL建表的具体写法与参数说明MySQL 5.7InnoDB就够用。核心表结构要避免两个典型错误数量用浮点数、批号不单独建字段。我给出直接可用的建表SQL字段注释已经写进去方便写文档时原样引用。CREATE TABLE fruit_category ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 分类名, 如热带水果, sort_order INT NOT NULL DEFAULT 0 COMMENT 展示排序, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT水果分类表;CREATE TABLE fruit_info ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, category_id INT UNSIGNED NOT NULL COMMENT 所属分类ID, name VARCHAR(64) NOT NULL COMMENT 水果名称, shelf_life_days INT NOT NULL DEFAULT 7 COMMENT 保质期天数, stock_unit VARCHAR(16) NOT NULL DEFAULT 斤 COMMENT 零售单位, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT水果信息表;这里把shelf_life_days放在fruit_info而不是批次表是默认同一水果保质期一致如果同一水果不同产地保质期不同就把它挪到批次表。对课程设计来说放fruit_info更简单。CREATE TABLE in_stock_detail ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, in_stock_id INT UNSIGNED NOT NULL COMMENT 入库单ID, fruit_id INT UNSIGNED NOT NULL, batch_no VARCHAR(32) NOT NULL COMMENT 批次号, 如B20250101A, quantity DECIMAL(10,2) NOT NULL COMMENT 入库数量(斤), purchase_price DECIMAL(10,2) NOT NULL COMMENT 进货单价, produced_at DATE DEFAULT NULL COMMENT 采摘日期, PRIMARY KEY (id), KEY idx_batch (batch_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT入库明细表;quantity用DECIMAL(10,2)而不是FLOAT是为了避免浮点数累加导致的账面偏差。batch_no单独成一个字段这是后来做保质期预警和先进先出的基础不能省。CREATE TABLE stock_snapshot ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, fruit_id INT UNSIGNED NOT NULL, batch_no VARCHAR(32) NOT NULL, available_qty DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 可用库存, locked_qty DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 锁定库存, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_fruit_batch (fruit_id, batch_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存快照表;快照表是并发扣减的关键。UNIQUE KEY(fruit_id, batch_no)让数据天然按水果批次唯一后续使用INSERT...ON DUPLICATE KEY UPDATE或者UPDATE ... WHERE都方便锁定目标行。locked_qty字段虽然课程设计里用得少但保留它会让表结构更接近真实进销存。2.3 盘点表不能省把盘盈盘亏变成可审计的调整单很多课程设计实现会在界面上放一个“手动修改库存”的按钮直接在快照表改数量。这种做法在答辩时很难解释清楚你改的依据是什么差异怎么留下记录正确做法是建盘点表让每一次盘盈盘亏都成为一张单据。CREATE TABLE stocktaking_record ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, fruit_id INT UNSIGNED NOT NULL, batch_no VARCHAR(32) NOT NULL, book_qty DECIMAL(10,2) NOT NULL COMMENT 盘点前账面数, actual_qty DECIMAL(10,2) NOT NULL COMMENT 实际盘点数, diff_qty DECIMAL(10,2) NOT NULL COMMENT 差异实际-账面, reason VARCHAR(255) NOT NULL DEFAULT 正常损耗 COMMENT 损耗原因, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_fruit_batch (fruit_id, batch_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT盘点记录表;盘点流程是先锁定一行快照读账面数操作员录入实际数系统计算diff_qty并写一条盘点记录再在同一事务中把快照的available_qty加上diff_qty。2.4 统一字符集与字段类型的三个约定第一所有表统一utf8mb4。第二金额与数量统一DECIMAL(10,2)入库明细和快照表不能混用FLOAT。第三时间字段用DATETIME。这三个约定在写代码前就锁死能规避后面一大类乱码和金额偏差问题。索引方面入库明细和出库明细都要保留idx_in_stock_id、idx_fruit_id如果查询经常带时间范围再加复合索引(fruit_id, created_at)。3. 用Axure把页面和交互规则画清楚原型做到哪一步算合格3.1 水果库存原型最少要有3类页面Axure原型的核心不是画得漂亮而是让评审的人看到系统怎么操作。我建议页面按三类划分列表页水果列表、入库单列表、出库单列表、表单页新建入库、新建出库、盘点录入、状态反馈页库存预警、操作成功/失败提示。这三类基本覆盖库存管理系统的主要交互流程。水果列表页的表格列建议直接对齐数据库字段名称、分类、批次号、可用库存、锁定库存、剩余保质期天数。不要在原型里只放一个“库存数”字段——答辩时对方一定会问同一款水果不同批次怎么区分。用Axure中继器Repeater模拟这个表格并为每个批次行加上“查看流水”的按钮列表页的可信度就能上来。表单页里新建出库单是重中之重。页面要包含选择水果下拉联动、选择批次联动显示剩余量与单价、输入出库数量、提交。这里需要把联动关系用交互事件做出来中继器模拟真实数据后再决定提交按钮是否可用。3.2 把“可用库存”和“锁定库存”写进交互规则原型里最容易偷懒的做法是在输入框边上加一行注释“此处要校验库存”。交互规则应该在原型里直接体现而不是留给开发去猜。具体做法在Axure中给“出库数量”输入框设置文本改变交互。从当前行中继器中读取available_qty和locked_qty计算可用数量为两者差值。当输入数量大于可用数量时弹出一个红色提示框并把“提交”按钮置灰。这个逻辑在Axure里用条件判断和局部变量就能实现不要求写JavaScript但必须让评审人员直观看到校验存在。给每个批次行做一个“锁定确认”按钮也值得推荐。在某些场景下用户先锁定一批货防止被别人买走这个操作会让locked_qty增加。原型中把这个按钮放出来会让后续写源码时的状态字段说明更有说服力。这里要顺带说清原型和demo的区别。demo是给外行看效果的半成品原型是给开发看规则的设计交付物。哪怕用AI辅助生成Axure原型最终也要在这一步把校验规则补齐否则AI生成得再漂亮开发也拿不到约束条件。3.3 角色权限在原型里怎么做店长能看到进货成本、毛利报表收银员只能看到销售操作和库存数。这个差异在原型里可以用两个母版Master来实现。Axure Master的好处是切换角色时只需要替换导航栏和表格中可见列不用复制整套页面。实现方式是在每个页面引入一个“当前用户类型”变量在列表页表格列的中继器过滤条件中写上如果用户类型是收银员就隐藏“进货单价”列。这套操作虽然会花一点时间但拿着它去评审时能直接说明你对权限模型有思考。记得把权限模型写进数据库设计也就是用户表加一个role字段。3.4 原型评审时容易被人盯住的5个交互细节以下5个细节在演示时被点名的概率最高建议在原型评审前逐项与页面核对首先是单位换算。入库按箱、出库按斤页面必须提供“箱到斤”的换算控件在表单页明确标注换算率否则系统上线后一线操作员会困惑。第二是空态页面。比如没有批次数据时列表页不要显示空表格要显示“请先入库”的引导按钮。第三是负数限制。出库数量输入框要限制只能输入数字并且当可用数量不足时明确提示剩余量。第四是回执反馈。提交出库单成功后页面要显示“扣减成功当前剩余XX斤”而不是笼统的“操作成功”。第五是未完成录入的保护。用户在新建入库单时如果点击离开页面要弹出“当前单据未保存”的提示。把这5个点画进原型后评审通过率会高很多后续编码阶段也不需要反复回来改交互。4. 用PHPMySQL写最小可跑源码三个关键接口的落地过程4.1 项目结构、MySQL的数据库连接池与PDO参数项目不大我建议直接用PHP 7.4加原生PDO不引入框架。目录结构保持清晰api/放接口文件includes/放数据库连接和公共函数pages/放简单的后台页面sql/放建表脚本。这样课程设计文档里画模块图、写功能说明都很方便。数据库连接时要注意连接池和连接复用的区别。真实场景里会用专门的连接池组件管理连接但在这个量级下打开PDO的持久连接就能满足需求效果接近轻量连接池。?php // includes/db.php $host 127.0.0.1; $port 3306; $dbname fruit_inventory; $user fruit_app; $pass Fruit_2025; $dsn mysql:host$host;port$port;dbname$dbname;charsetutf8mb4; $options [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, PDO::ATTR_PERSISTENT true, ]; try { $pdo new PDO($dsn, $user, $pass, $options); } catch (PDOException $e) { http_response_code(500); exit(json_encode([code 500, msg 数据库连接失败])); }参数说明charsetutf8mb4写进DSN是解决中文乱码的第一道防线。ATTR_EMULATE_PREPARES设为false让PDO使用MySQL原生预处理既能防SQL注入也能让整数和字符串参数被MySQL正确区分。ATTR_PERSISTENT为true让PHP-FPM进程复用PDO连接避免每次请求重复握手建立新连接。这里要留意持久连接与事务的配合使用持久连接时事务结束务必提交或回滚否则连接归还池子时会残留未关闭的事务状态。4.2 进货接口入单写明细、快照累加进货接口接收一张入库单头表和若干明细行items数组在同一个事务里完成所有写入。核心是这个逻辑如果快照表已有同水果同批次的行就累加数量如果没有就新插入一行。public function doInStock(array $order, array $items): array { $pdo-beginTransaction(); try { // 1. 插入入库单头表 $sql INSERT INTO in_stock_order (order_no, supplier, total_amount, operator_id) VALUES (:order_no, :supplier, :total_amount, :operator_id); $stmt $pdo-prepare($sql); $stmt-execute($order); $inStockId $pdo-lastInsertId(); $totalAmount 0; // 2. 逐条处理明细 foreach ($items as $item) { // 防止写入不存在的水果ID $fruit $pdo-prepare(SELECT id FROM fruit_info WHERE id ?); $fruit-execute([$item[fruit_id]]); if (!$fruit-fetch()) { throw new RuntimeException(水果ID {$item[fruit_id]} 不存在); } $batchNo $this-generateBatchNo($item[fruit_id], $item[produced_at]); $lineAmount $item[quantity] * $item[purchase_price]; $totalAmount $lineAmount; // 3. 写入库明细 $sqlDetail INSERT INTO in_stock_detail (in_stock_id, fruit_id, batch_no, quantity, purchase_price, produced_at) VALUES (?, ?, ?, ?, ?, ?); $pdo-prepare($sqlDetail)-execute([ $inStockId, $item[fruit_id], $batchNo, $item[quantity], $item[purchase_price], $item[produced_at] ]); // 4. 累加快照库存 $sqlUpsert INSERT INTO stock_snapshot (fruit_id, batch_no, available_qty, locked_qty) VALUES (?, ?, ?, 0) ON DUPLICATE KEY UPDATE available_qty available_qty VALUES(available_qty); $pdo-prepare($sqlUpsert)-execute([$item[fruit_id], $batchNo, $item[quantity]]); } // 5. 回填头表总金额 $pdo-prepare(UPDATE in_stock_order SET total_amount ? WHERE id ?) -execute([$totalAmount, $inStockId]); $pdo-commit(); return [code 0, order_id $inStockId]; } catch (Throwable $e) { $pdo-rollBack(); return [code 1, msg $e-getMessage()]; } }逻辑说明这个接口用beginTransaction包住全部写操作任何一步抛出异常都会回滚保证不会出现“头表写了但明细没写”的脏数据。第4步的ON DUPLICATE KEY UPDATE是快照更新的核心它靠stock_snapshot唯一索引(fruit_id, batch_no)判断是插入还是累加。总金额不在插入时写死而是在明细全部计算完后回填避免前端传一个错误的总金额污染数据。参数说明execute的数组参数必须与SQL中的占位符数量一一对应。batch_no由服务端生成格式建议B日期序号比如B20250101A001这种格式在排障时一眼就能看出是哪天的货。4.3 出库接口条件更新防超卖出库比入库多一个需要处理的点并发时不能把库存卖成负数。如果先SELECT再UPDATE两个请求都可能看到剩余5斤然后同时扣减最终变成负数。解决方案是把“库存充足”条件放进UPDATE语句本身。UPDATE stock_snapshot SET available_qty available_qty - :delta WHERE fruit_id :fruit_id AND batch_no :batch_no AND available_qty - locked_qty :delta执行这条UPDATE后如果受影响行数为0说明该批次可用库存不足以扣减回滚整个出库事务并返回“库存不足”。这是数据库层面的原子操作不需要额外加锁也避免了死锁问题。出库接口的完整事务里除了更新快照还要写out_stock_order和out_stock_detail两张表。建议把出库单号也按日期生成方便和入库单对账。加上前面列表查询这三个写接口加一个读接口就把库存系统的数据库增删改查闭环覆盖了。4.4 盘点接口差异更新而不是覆盖更新盘点接口的逻辑是三段式锁行读账面、计算差异、写记录并更新快照。这里有一个面试和答辩都爱问的点为什么不是直接把actual_qty覆盖到快照表因为如果直接覆盖就无法区分这次差异到底是销售造成的还是损耗造成的审计信息就丢了。// 盘点更新的核心片段 foreach ($items as $item) { $stmt $pdo-prepare(SELECT available_qty FROM stock_snapshot WHERE fruit_id ? AND batch_no ? FOR UPDATE); $stmt-execute([$item[fruit_id], $item[batch_no]]); $bookQty $stmt-fetchColumn(); $diff $item[actual_qty] - $bookQty; $stmt $pdo-prepare(INSERT INTO stocktaking_record (fruit_id, batch_no, book_qty, actual_qty, diff_qty, reason) VALUES (?, ?, ?, ?, ?, ?)); $stmt-execute([ $item[fruit_id], $item[batch_no], $bookQty, $item[actual_qty], $diff, $item[reason] ]); $stmt $pdo-prepare(UPDATE stock_snapshot SET available_qty available_qty ? WHERE fruit_id ? AND batch_no ?); $stmt-execute([$diff, $item[fruit_id], $item[batch_no]]); }逻辑说明FOR UPDATE会在事务期间锁住这一行防止并发盘点或出库同时修改。在事务里写盘点记录后再更新快照这两步是一体的如果中途抛异常整个事务回滚快照不会变成不一致状态。注意这个代码在盘点批次很多时锁行时间会长适合小规模门店。如果需要大批量盘点后续可以改成每批独立事务。4.5 一个简单的库存列表查询SQL最后给出一个高频查询——带批次的水果库存列表。这里要区分“表里的快照”和“页面要显示的剩余保质期”后者通常实时计算。SELECT f.name AS fruit_name, c.name AS category_name, s.batch_no, s.available_qty, s.locked_qty, DATEDIFF(DATE_ADD(fi.produced_at, INTERVAL f.shelf_life_days DAY), CURDATE()) AS left_days FROM stock_snapshot s LEFT JOIN fruit_info f ON f.id s.fruit_id LEFT JOIN fruit_category c ON c.id f.category_id LEFT JOIN in_stock_detail fi ON fi.batch_no s.batch_no WHERE s.available_qty s.locked_qty 0 ORDER BY left_days ASC;为什么这里要LEFT JOIN in_stock_detail取produced_at而不是在快照表里直接存采摘日期因为快照表只负责“当前数量”生产日期属于业务属性两者分离后如果同一批次多次入库日期维护不会冲突。5. 避坑与排查连接池、并发扣减、中文字段这些坑逐个踩5.1 页面中文全部变成问号先查连接串再查表字符集现象在MySQL命令行下看数据完全正常但打开PHP页面后中文全部变成“?”。原因大多是PHP连接MySQL时字符集不一致。MySQL终端默认连接字符集可能和代码里不一致PDO没有把charsetutf8mb4写进DSN时连接使用默认字符集导致中文在传输过程中被破坏。解决在DSN里加charsetutf8mb4并在创建PDO后执行一次SET NAMES utf8mb4。同时确认表和字段的字符集是utf8mb4。排查顺序是先用SQL检查表字符集再检查DSN最后检查HTML的meta charset。如果改完连接串还有问题确认一下PHP文件本身保存为UTF-8无BOM格式。5.2 库存被扣成负数先查事务隔离级别和条件更新现象收银高峰期同一个批次的水果卖出的数量超过了入库数量库存快照出现负数。原因在于代码写成“先查后改”先SELECT可用库存判断大于0再UPDATE。两个请求同时读到5斤分别扣了2斤和4斤第二次更新前没有重新校验最后变成-1斤。解决把条件写进UPDATE语句使用UPDATE ... SET available_qty available_qty - ? WHERE available_qty - locked_qty ?来判断。如果影响行数为0就回滚。另外确认表引擎是InnoDBMyISAM不支持行锁条件更新也保护不了并发。排查时可以用SHOW ENGINE INNODB STATUS查看最近的事务等待记录确认没有死锁。5.3 金额对账差出一分钱FLOAT类型背锅现象系统跑了一段时间后汇总的进货金额和明细表对不上总是差几分钱。原因quantity和purchase_price用了FLOAT或DOUBLE浮点数在二进制中无法精确表示0.1累加多了偏差就暴露出来。解决把表和接口里的所有金额、数量字段改为DECIMAL(10,2)。历史数据通过ALTER TABLE转换前先备份原表并检查有没有视图依赖。金额和数量相关计算禁止使用FLOAT这是一条可以写进团队规范的红线。5.4 拿原型验收不了系统开发和原型对不上现象原型评审时甲方确认了交互结果开发出来的系统页面上找不到对应交互或者逻辑与原型的文字描述不符。原因原型里用注释说明的规则太多真正的交互没做出来。比如“出库数量不能大于可用库存”只写了一句提示没有做置灰和提示弹窗开发就按普通输入框实现了。解决评审前用本文前面列出的5个交互细节逐项检查原型把规则全部做成可操作的交互不要留注释。代码完成后把验收清单表格打印出来逐条对照弹窗文案、按钮置灰条件、空态引导、回执内容四项都一致才算通过。5.5 电脑重启后数据丢失没有备份和binlog现象磁盘故障或误删库后发现最近的入库出库记录全部丢失只剩手动导出的那次备份。原因数据库没有开启binlog也没有定时备份恢复只能靠之前的零星SQL导出。解决在MySQL配置中开启binlog设置保留时间。同时写一个cron每天凌晨导出全量备份。命令大致是mysqldump加--single-transaction和--routines把导出的SQL放到带日期的目录里。恢复时用mysql backup.sql回放如果binlog也存在就可以恢复到出问题前几分钟的状态。6. 从能跑到能用验收清单、压测方法和下一版怎么改在收尾之前先用一张验收清单把所有关键功能过一遍这张表会直接变成你的测试脚本编号验收项操作方式期望结果1入库单提交新建苹果入库10箱每箱10斤快照增加100斤批次号可查2出库扣减同一批次出货30斤剩余70斤出库单可追溯3超卖拦截并发出货超过剩余量提示库存不足库存不为负4盘点调整账面100斤实盘95斤diff-5快照变为95斤5中文与特殊字符输入“火龙果”“枣”页面显示正常6并发压测20个并发出货请求库存总数一致无超卖第6项压测是最有价值的。先用SQL把某个批次剩余数重置为100斤然后用ab工具并发发20个出货3斤的请求。预期结果是剩余40斤如果出现负数或剩余不等于40说明条件更新没生效或事务没有回滚需要回到第4章的出库接口排查。# 准备按照表单格式的请求体文件用ab模拟并发出货 ab -n 20 -c 10 -T application/x-www-form-urlencoded \ -p outbound.txt http://127.0.0.1:8080/api/out_stock.php参数说明-n 20是总请求数-c 10是并发数-T指定Content-Type为表单格式-p指定请求体文件。这里的outbound.txt内容是一段fruit_id1batch_noB20250101quantity2这样的表单数据。跑完再查stock_snapshot表核对剩余数量。压测过后可以顺手做的事是检查MySQL慢查询日志。把这20个请求过程中耗时的SQL捞出来看是不是缺失索引导致逐行扫描。下一版扩展我会优先加保质期预警。实现不难在fruit_info和in_stock_detail之间做一次日期计算把剩余保质期小于3天的批次在列表页标成红色并生成一张预警表。我的习惯是每完成一个功能就回到Axure原型里走一遍对应的页面确认原型描述的交互和代码实现一致。这个习惯救过我很多次希望帮到你。本文还有配套的精品资源点击获取