简介:面向计算机专业课程设计与数据库入门实践的「小型自选商场商品管理系统」完整项目包,围绕商品进货、销售、库存、供应商与收银台等业务环节展开,适合需要完成数据库课程设计或桌面管理系统的学习者参考。压缩包共189个文件,约680KB,以60个SQL脚本、54个C#源码文件及24个resx资源文件为主体,另含项目解决方案、配置文件、数据库文件和可执行程序,覆盖建表、业务逻辑、界面布局与运行环境。已有1027人学习下载。读者可从中获取进货与售货记录、按月统计、日盘存与月盘存、动态库存刷新、供应商查询及收银清单打印等功能的实现思路,并通过库存、售货、进货、供应商四类表的设计理解关系型数据库建模方法,适合对照课程要求进行拆解与二次开发。
1. 收银台前的排队不致命,月底盘亏才要命
某社区自选商场有三个收银台、四千多个SKU,之前用表格记库存,每天打烊手动扣减,账实不符是常态——货架上还有货,系统显示零;系统显示有货,货架早空了。月底一盘亏两三万,老板说不清是丢货还是记错账。他要的不是大而全的ERP,而是一套能把商品档案、库存变动和销售流水串起来的数据库商品管理系统。这套方案解决的核心问题只有一个:让每一笔出入库都留痕、可追溯、能对账。适合拿它做数据库课设或毕设的学生,也适合给周边小店做轻量数字化的小团队。界面做得再花哨,数据库设计立不住,后面全是返工。这篇按需求、选型、表结构、模块实现、避坑、验收的顺序拆开讲,能照着搭一套跑得通的系统。
2. 需求拆解与技术选型:五个角色、三条数据流和一个单库架构
动手建表之前,先把“谁在用、数据怎么流、量有多大”这三件事定下来。小型自选商场的业务链条其实很短:采购把货进来,库管上架,收银员卖出去,店长看数和补货,老板月底对账。链条短不代表简单,角色交叉和数据重复录入最容易把系统做成四不像。
2.1 角色与场景:五种人的操作边界
自选商场的岗位分工不细,一个人可能同时管收货和上架,但系统里的权限必须按最小职责切开,否则收银员能改进货价,账就永远对不平。我一般会拆成五个角色:
| 角色 | 核心操作 | 数据权限 |
|---|---|---|
| 收银员 | 扫码结算、退货 | 只读商品价格,写销售单 |
| 库管 | 入库、出库、盘点 | 读写库存,看商品基础信息 |
| 采购 | 新建采购单、维护供应商 | 读写采购与供应商表 |
| 店长 | 商品维护、调价、看报表 | 全部业务表 |
| 老板 | 查报表、审单据 | 只读全部 |
权限落到数据库层就是账号与角色表,落到代码层就是接口拦截。学生做课设常犯的错是把权限写成前端按钮隐藏,后端接口裸奔,答辩时老师用一个改过的请求就能绕过,这点要提前防。
三条核心数据流要画清楚:销售流(收银→销售单→扣库存→记流水)、采购流(采购单→入库→加库存→记流水)、盘点流(实盘数→差异→调库存→记盘点记录)。所有库存变动最终都汇聚到一张库存流水表,这张表是整个系统的账本,后面第3章细讲。
2.2 技术选型:单库MySQL为什么比分布式更合适
四千个SKU、日均几百单、峰值并发不超过二十个收银终端的场景,上分布式数据库或分库分表纯属给自己找麻烦。单机MySQL 8.0配合InnoDB完全扛得住,事务、行锁、外键约束都是现成的。后端我习惯用Spring Boot加MyBatis,原因是课设和毕设环境下资料多、调试方便,遇到问题容易定位;如果团队更熟Python,Flask加SQLAlchemy同样可行,核心是SQL和事务逻辑,框架只是外壳。
前端用Vue或纯模板渲染都行,商品管理系统的前端复杂度不高,重点是表单校验和列表分页。缓存先不上,二十个并发用Redis是过度设计,反而多一个要维护的组件。真到了几百并发再考虑加缓存层,这是后话。
提示:选型的判断标准不是“先进”,而是“出了问题你能不能在两小时内定位”。毕设环境里,一个你完全没碰过的中间件往往是答辩翻车的起点。
2.3 总体架构:三层结构与多模块职责划分
整体按经典三层走,别嫌老套,小系统用分层能省掉大量扯皮:
- 表现层:收银台界面、后台管理页、报表页,负责输入校验和结果展示,不做业务判断。
- 业务层:商品服务、库存服务、销售服务、采购服务、报表服务。库存扣减、价格计算、权限校验全部在这一层完成。
- 数据层:MySQL加MyBatis映射,所有SQL集中管理,事务边界开在Service方法上。
模块之间的调用关系要单向:销售服务调库存服务扣减,库存服务调商品服务校验状态,反向依赖一律禁止。我见过有同学在商品服务里直接写销售逻辑,最后改一个字段要在三个文件里翻,血泪经验。模块划分清晰了,后面每个模块的代码才有可能被单独演示、单独测试,答辩时也能一块一块讲。
2.4 数据量估算与非功能指标
给数据库定几个硬指标,后面设计和验收都有依据:
- 商品表初始四千条,年增长不超过两千条;
- 库存流水表日均约一千条,一年三十六万条,三年百万级,单表完全可控;
- 销售单日均三百到五百单,明细按每单平均五条算,一年约九十万条明细;
- 商品列表查询响应控制在300毫秒内,收银结算单笔控制在500毫秒内;
- 数据保留三年,超期流水归档到历史表,主表只留近期数据保证查询速度。
这些数字不是拍脑袋,是从“三个收银台、每天营业十二小时”反推的。指标定下来,第3章建索引和第6章验收才有尺子可量。
3. 数据库设计:从商品主数据到库存流水的八张核心表
数据库设计是这个系统真正的骨架。界面可以改,框架可以换,表结构一旦上线再动就是伤筋动骨。这一章把实体关系、核心DDL、索引策略和库存事务四件事讲透,照着能直接建库。
3.1 实体关系梳理:八张表撑起整个业务
核心实体有八个:商品、分类、供应商、库存、库存流水、销售单、销售明细、采购单。关系上,分类对商品是一对多,供应商对商品是一对多(一个商品默认一个主供应商),商品对库存是一对一,商品对库存流水是一对多,销售单对销售明细是一对多,销售明细对商品是多对一。
有两处设计容易被做错。第一,库存数量不要直接挂在商品表上,单独建库存表,原因是商品基础信息和库存变动频率差好几个数量级,混在一起每次扣库存都要锁商品行,查询商品列表时又被库存字段拖慢。第二,销售明细里要冗余商品名称和成交单价,不能只存商品ID。商品改名或调价后,历史订单必须显示当时的名字和价格,这是对账的底线,会计上叫“历史快照”。这个冗余不是设计缺陷,是刻意为之。
3.2 核心表DDL与字段说明
先看商品表和库存表,这两张是主数据:
-- 商品表:存基础档案,不含库存数量 CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, barcode VARCHAR(32) NOT NULL COMMENT '条码,收银扫码用', name VARCHAR(100) NOT NULL COMMENT '商品名称', category_id BIGINT NOT NULL COMMENT '分类ID', supplier_id BIGINT DEFAULT NULL COMMENT '主供应商ID', purchase_price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '进货价', sale_price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '零售价', unit VARCHAR(16) NOT NULL DEFAULT '件' COMMENT '计量单位', status TINYINT NOT NULL DEFAULT 1 COMMENT '1在售 0停售', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_barcode (barcode), KEY idx_category (category_id), KEY idx_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品主表'; -- 库存表:一个商品一行,记录当前数量与预警线 CREATE TABLE stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL COMMENT '商品ID', quantity INT NOT NULL DEFAULT 0 COMMENT '当前库存', warn_threshold INT NOT NULL DEFAULT 10 COMMENT '库存预警线', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_product (product_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';几个字段值得单独说。barcode加唯一索引,收银扫码时按条码查商品走的是唯一索引,速度有保证,同时防止同一条码重复录入。purchase_price和sale_price都用DECIMAL(10,2)而不是FLOAT,金额计算用浮点数会出现0.1加0.2不等于0.3的玄学问题,对账时能查出几分钱的差异却找不到原因,这种坑没必要踩。status用0和1控制停售,商品下架不删记录,历史订单还要关联它。
库存流水的DDL是重点,它是整个系统的黑匣子:
-- 库存流水表:每次库存变动都必须写一条 CREATE TABLE stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL COMMENT '商品ID', change_type TINYINT NOT NULL COMMENT '1入库 2销售出库 3退货入库 4盘盈亏 5报损', change_qty INT NOT NULL COMMENT '变动数量,正数增加负数减少', before_qty INT NOT NULL COMMENT '变动前库存', after_qty INT NOT NULL COMMENT '变动后库存', ref_type VARCHAR(20) DEFAULT NULL COMMENT '关联单据类型', ref_id BIGINT DEFAULT NULL COMMENT '关联单据ID', operator VARCHAR(50) NOT NULL COMMENT '操作人', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_product_time (product_id, created_at), KEY idx_ref (ref_type, ref_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水表';before_qty和after_qty两个字段是我强烈建议加的。很多课设只记变动数量,出问题时没法还原当时的状态,加这两个字段后任何一笔变动都能倒推出整条链,相当于给库存上了后悔药。ref_type和ref_id指向来源单据,销售出库指向销售单,入库指向采购单,对账时按单据一查就知道这笔库存变动的来龙去脉。
3.3 索引策略与典型查询写法
索引不是越多越好,商品管理系统的查询集中在三个方向:按条码查单品、按名称或分类模糊查列表、按时间查流水。对应的索引在上面的DDL里已经埋好,这里说两个常被忽略的点。
商品名称的模糊查询,如果写成LIKE '%洗发水%',idx_name索引用不上,全表扫四千行其实也不慢,但数据量涨到几万行就会显形。我的做法是列表页默认按分类过滤加条码或名称前缀匹配,前缀匹配LIKE '洗发水%'能走索引;真要全文模糊搜索,再单独上全文索引或搜索引擎,小系统先用前缀匹配兜住,别一上来就上Elasticsearch。
库存预警查询是高频操作,写法上注意别用函数包字段:
-- 正确:能走索引,直接比较两列 SELECT p.id, p.name, s.quantity, s.warn_threshold FROM stock s JOIN product p ON p.id = s.product_id WHERE s.quantity <= s.warn_threshold AND p.status = 1; -- 错误示范:quantity - warn_threshold <= 0,列上做运算,索引失效预警查询在店长首页每次刷新都要跑,数据量不大但频率高,写法规范一点能省下不少CPU。
3.4 库存扣减的事务设计与并发控制
库存扣减是整个系统最容易出错的地方。收银员扫码结算,背后要完成三件事:写销售单、写销售明细、扣库存并记流水。这三步必须在一个事务里,任何一步失败全部回滚,否则会出现“卖了货没扣库存”或“扣了库存没单据”的账实不符。
并发场景下还要防超卖。两个收银台同时卖最后一件商品,如果先查库存再更新,两边都查到有货,都扣成负数。正确做法是用带条件的更新语句,把判断和扣减合成一步:
-- 扣减库存:WHERE 里带数量判断,影响行数为0说明库存不足 UPDATE stock SET quantity = quantity - #{qty} WHERE product_id = #{productId} AND quantity >= #{qty}; -- 紧接着写流水,before_qty 用查询结果,after_qty 由 before_qty 减 qty 得出 INSERT INTO stock_flow(product_id, change_type, change_qty, before_qty, after_qty, ref_type, ref_id, operator) VALUES(#{productId}, 2, -#{qty}, #{beforeQty}, #{beforeQty} - #{qty}, 'SALE', #{saleId}, #{operator});UPDATE的影响行数要判断,返回0就直接抛异常回滚事务,提示“库存不足”。这个写法依赖InnoDB的行锁,同一商品的并发扣减会排队执行,不会超卖。事务注解开在Service方法上,传播行为用默认的REQUIRED,隔离级别用READ_COMMITTED就够,别乱调成串行化,性能会掉得很难看。
4. 模块实现:商品录入、库存预警、收银结算与报表的代码落地
表结构定好,接下来把五个业务模块逐个落地。这一章每个模块给关键代码和设计说明,重点讲清楚“为什么这么写”,而不是把增删改查铺一遍。能照着跑通的最小实现,比十个花架子功能有用。
4.1 商品信息管理与分类维护
商品录入的难点不在表单,在重复条码和分类树。条码唯一性由数据库唯一索引兜底,插入时捕获DuplicateKeyException转成友好提示,比先查再插可靠,因为查和插之间有空窗期,并发下照样能插重。分类维护如果做两级(大类、小类),表里加个parent_id自关联即可,不必上无限级树,商场场景两级足够。
商品列表分页用LIMIT加COUNT,每页二十条。注意COUNT查询在数据量大时也慢,可以缓存总数或改成“下一页”游标式分页,小系统先用LIMIT,别过早优化。商品停售用状态位而不是物理删除,原因是历史订单和流水都要关联商品,删了外键就断,对账时查不出东西。
4.2 库存管理与预警机制
库存模块对外暴露三个核心方法:入库、出库、盘点调整。三个方法内部都走同一套“改库存加记流水”的模板,区别只在change_type和ref_type。抽一个公共方法出来,避免三处代码各写各的,后面改逻辑只改一处。
预警机制不用定时任务,直接在库存表上挂预警线,店长首页查询quantity <= warn_threshold的商品即可。如果非要自动推送,用Spring的@Scheduled每小时扫一次,把预警商品写进消息表,前端轮询展示,别上消息队列,这个体量用不上。
盘点是特殊场景,实盘数和账面数不一致时,差异要写进流水,change_type记为4,变动数量是实盘减账面。盘点单要保留原始实盘数,方便复核,很多同学直接把库存改成实盘数却不留记录,月底老板问“这批货怎么少的”就答不上来。
4.3 销售收银与采购入库流程
收银结算的代码结构要清晰:接收购物车明细,校验商品状态和库存,生成销售单号,在一个事务里写单、写明细、扣库存、记流水。销售单号用“日期加流水号”生成,比如S20240615001,方便按天对账。退货走反向流程,change_type记3,库存加回去,销售单标记退货状态而不是删除,账目必须留痕。
采购入库相对简单,采购单审核通过后调库存入库方法,change_type记1,ref_type记PURCHASE。要注意的是入库价格更新:如果这次进货价和商品表里的purchase_price不一致,是否更新商品主表?我的做法是更新,同时把旧价记进价格变更日志,方便追溯成本波动。这个日志表不是必须的,但对做毕设的同学是个加分项,答辩时能讲出成本核算的思路。
4.4 数据统计与报表导出
报表模块至少做三张:日销售汇总、商品销售排行、库存预警清单。日销售汇总按日期分组统计销售额和单数,SQL里用DATE(created_at)分组,注意这个写法用不上索引,数据量大时改成范围查询加应用层聚合。商品销售排行按销量降序取前二十,关联商品表带出名称。库存预警清单就是3.3节那条查询。
导出用POI或EasyExcel写Excel,别在数据库层拼字符串。报表数据量小,同步导出即可,不用上异步任务。店长月底对账要的就是这几张表,做扎实比做多更重要。
5. 常见问题与排查:库存对不上、查询变慢、事务失效的避坑手册
系统跑起来只是开始,真正折磨人的是运行中冒出来的怪问题。这一章列五个项目里最容易翻车的点,每条按“现象、原因、解决”写,都是能直接对照排查的。
5.1 库存流水和账面数量对不上
现象:库存表显示某商品还有五十件,把流水表按商品汇总一算只有四十八件,差两件。
原因:九成是某次库存变动只改了库存表没写流水,或者写了流水没改库存,两步没在同一个事务里。常见于盘点调整和退货入库这两个分支,开发时图省事直接UPDATE stock了事。
解决:把“改库存加记流水”封成一个不可拆分的服务方法,事务注解开在这个方法上,禁止任何地方直接操作库存表。排查时写一条对账SQL,按商品汇总流水变动和库存当前值比对,差额不为零的商品就是问题点,再按created_at倒序查该商品的流水,找断档的时间点。
-- 对账SQL:流水汇总与库存当前值比对 SELECT s.product_id, s.quantity AS stock_qty, COALESCE(SUM(f.change_qty), 0) AS flow_qty, s.quantity - COALESCE(SUM(f.change_qty), 0) AS diff FROM stock s LEFT JOIN stock_flow f ON f.product_id = s.product_id GROUP BY s.product_id, s.quantity HAVING diff <> 0;5.2 商品列表越用越慢
现象:刚上线时列表秒开,用了两个月,翻到后面几页要等两三秒。
原因:分页查询用了LIMIT 10000, 20这种大偏移量,MySQL要扫过前一万行再丢弃,越翻越慢;或者列表查询关联了流水表做统计,笛卡尔积把结果撑爆。
解决:列表页禁止关联流水表,需要统计数字就单独查或加冗余字段。大偏移量改成游标分页,前端记住上一页最后一条的ID,下一页用WHERE id > lastId LIMIT 20。另外检查EXPLAIN执行计划,确认走了索引,没走索引就补索引或改写SQL,别靠加机器硬扛。
5.3 事务回滚失效,卖了货库存没退
现象:测试时故意让销售明细插入失败,结果销售单回滚了,库存却扣掉了,数据彻底乱套。
原因:库存扣减方法被同类内部直接调用,Spring的AOP代理没生效,@Transactional形同虚设;或者异常被try-catch吞掉没重新抛出,事务管理器收不到回滚信号。
解决:事务方法必须通过代理调用,同类内部调用要么抽到另一个Service,要么用AopContext.currentProxy(),前者更干净。catch块里如果处理了异常,末尾一定要throw出去,或者手动调TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。这两个坑我都踩过,排查时看日志里事务有没有真正开启,比盯着代码猜快得多。
5.4 中文乱码和备份恢复翻车
现象:商品名里的生僻字变成问号,或者从备份恢复后部分数据乱码。
原因:建库时字符集用了utf8而不是utf8mb4,生僻字和表情符号存不下;连接串没指定字符集;备份用mysqldump没带--default-character-set=utf8mb4。
解决:建库建表统一utf8mb4加utf8mb4_general_ci排序规则,连接串加characterEncoding=utf8,备份脚本固定字符集参数。恢复前先在测试库跑一遍,确认数据完整再动生产库。备份这件事,没恢复验证过的备份等于没有备份。
6. 验收与答辩:用一组数据和一张核对表证明系统真跑通了
到了验收环节,最怕的是系统能点开但讲不清。答辩老师不会逐个按钮点,他们看三样东西:数据对不对、流程通不通、边界有没有考虑。提前准备一套完整的演示数据和一份核对清单,比现场手忙脚乱强太多。
6.1 验收清单:六项必须现场跑通
| 验收项 | 操作 | 通过标准 |
|---|---|---|
| 商品建档 | 新增一个带条码的商品 | 重复条码被拦截,提示明确 |
| 采购入库 | 采购单审核入库 | 库存增加,流水有一条入库记录 |
| 收银结算 | 卖两件商品 | 库存减二,流水有销售记录,金额正确 |
| 库存不足 | 卖超过库存的数量 | 事务回滚,提示库存不足,库存不变 |
| 退货 | 对已售商品退货 | 库存加回,销售单标记退货,流水有退货记录 |
| 对账 | 跑5.1节对账SQL | 所有商品差额为零 |
这六项跑完,系统的核心闭环就验证了。演示数据提前准备好:建十个商品、两个分类、一个供应商,造三笔采购和五笔销售,数据量小但覆盖所有变动类型,现场操作快,结果也好看。
6.2 答辩演示脚本与追问应对
演示按“建档、入库、销售、退货、对账”的顺序走,五分钟内讲完。每一步说完操作,立刻切到数据库客户端展示对应的表和流水记录,让老师看到界面背后的数据变化,这比口头讲架构有说服力。
老师最常追问三个点:库存并发怎么防超卖、事务边界划在哪、历史订单为什么能显示旧价格。前两个用3.4节的条件更新和事务注解回答,第三个用销售明细的字段冗余回答。如果被问到“数据量涨十倍怎么办”,老实说单表百万级加索引够用,分库分表是更大规模才考虑的事,别不懂装懂。
还有一个容易被忽略的细节:演示前把数据库备份一份,现场操作万一改错数据,五分钟内能恢复到演示初始状态。我一般会在答辩前把演示脚本在干净环境里完整走两遍,第一遍找流程漏洞,第二遍掐时间。从那以后我每次交付系统都强制自己按验收清单跑一遍,不管多赶,这一遍省不掉。希望这套结构和清单能帮到你,照着搭,少走我走过的弯路。
本文还有配套的精品资源,点击获取