简介超市进销存管理系统配套资源面向超市运营人员、管理信息系统学习者及.NET开发工程师覆盖商品进货、销售管理、库存统计、订单管理等核心业务环节。压缩包共103个文件约2.03MB包含24个C#源码文件业务逻辑、18个aspx页面界面层、CSS样式与图片素材界面设计以及mdf/ldf数据库文件、mdl模型和用例图可直观了解系统功能与用户交互。已有4379人学习浏览资源通过用例图清晰展示了员工、顾客、供应商与系统的协作关系并提供了库存预警、销售趋势分析等模块的实现思路。适合作为课程设计参考或中小型超市信息化改造的基础模板便于在此基础上扩展会员管理、报表统计等功能。1. 先从不靠谱的库存账本说起为什么动手前必须画用例图带过太多人写超市进销存管理系统见过最典型的翻车现场是系统上线演示时库存表里剩 5 件商品实际货架上有 12 件老板一盘点就露馅又或者是收银台卖出去的货没有扣减库存后台库存越跑越多最后成了黑匣子谁都不知道哪笔数据是对的。这类问题的根源不是 SQL 写得差、不是数据库选错了而是项目从一开始就没有把「谁、在什么场景下、对商品做什么操作」这件事梳理清楚。超市进销存管理系统听起来就是进货、卖货、管库存三件事但加上退货、报损、盘点、调拨、供应商结算、会员价之后功能边界立刻糊成一团。用例图的价值恰恰在这里它用一张图把参与者、功能模块和使用场景之间的关系钉死让开发照着图写代码让测试照着图验证让老板照着图确认需求。这篇文章从用例图出发一路拆到数据库表结构、后端事务代码和库存一致性校验最后落在一套能直接抄作业的实现路径上。2. 用例拆分与用例描述把业务写成能让开发直接开工的需求规格2.1 三类参与者与十三个核心用例画超市进销存管理系统的用例图第一步不是画椭圆而是先定参与者。常见的做法是让系统范围涵盖日常运营的三类人物角色负责进货和供应商往来的采购员、在前台处理销售和退货的收银员、以及拥有最高数据权限的店长。有人会把「供应商」也画成参与者这在用例图里是允许的——供应商发起送货、接收退货单确实是一个外部交互场景——但对大部分课设和内部管理系统来说把供应商相关操作收敛到采购员用例里系统的边界会清爽很多。我一般维护三个参与者把供应商当作档案数据而不是外部角色来管理。参与者核心职责关联用例采购员补货、退货给供应商、查询库存进货入库、采购退货、库存查询、供应商档案维护收银员前台卖货、处理顾客退货销售开单、销售退货、小票打印、会员价核算店长全局掌控、纠错、调价盘点录入、报损报溢、调拨管理、统计报表、库存预警、基础档案维护用例图里每个椭圆就是一个用例但椭圆与椭圆之间不要乱连线。很多初学者会把「进货入库」和「库存查询」之间画一条 include 关系理由是入库后库存变了、查询结果也会变——这是逻辑上的先后关联不是用例图上的关系。用例图的关系只有 include一个用例必然包含另一个用例、extend一个用例在特定条件下扩展另一个用例、以及参与者和用例之间的关联线。举个例子「销售开单」在扫码枪识别不出条码时扩展出「手动输入条码」这就是一条典型的 extend「进货入库」必然需要「登录系统」就可以用 include 表达但登录这种公共用例我一般干脆画在系统边界之外不污染业务图。2.2 用例描述的字段长什么样用例图本身是有极限的椭圆只能告诉你有这件事不能告诉系统该返回什么异常。真正让开发照着写代码的是用例描述use case specification通常一张用例图配一个描述表格。关键的字段是前置条件、主流程编号步骤、扩展流程、后置条件。这在课设文档里是必写的在真实项目里更是评审核心。下面把「进货入库」这个最核心的用例写成可执行的描述。字段内容用例名称进货入库参与者采购员前置条件采购员已登录商品档案已维护商品存在该订单对应的采购单状态为「待入库」主流程1. 采购员录入或扫描商品编码2. 系统校验商品存在且允许采购3. 采购员输入进货数量、进价、生产日期与保质期4. 系统校验数量为正数、进价不低于商品档案中的最低进价5. 系统写入进货明细、更新库存表、生成入出库流水6. 如该商品有保质期信息同步生成批次库存记录7. 系统更新采购单状态为「已完成」扩展流程3a. 商品已过保质期或临期系统给出警告并要求二次确认4a. 进价低于最低进价时系统校验当前用户是否有店长授权5a. 库存表并发更新失败回滚整个事务并提示重试后置条件库存表数量增加流水表新增一条入库记录采购单状态置为已完成写完这张表后端开发的工作量基本能估出来一个入库接口、一个库存更新事务、一个流水的插入逻辑、一个状态流转判断。用例图让你看清边界用例描述让边界变成实现细节。这一步省下来后面写事务代码基本靠猜——猜出来的代码上线就跑不了账。2.3 公共用例与系统边界的取舍还有一类让人反复纠结的用例登录、修改密码、操作日志、数据备份。它们不属于超市业务但每个角色都要用。我习惯把它们收进一个「系统管理」分组在用例图里单独用包package框起来。这么做的好处是核心用例图不会因为塞入登录这种公共用例而变得拥挤文档评审时也能一眼看出系统的业务主链。另有一个容易踩的坑把「库存查询」当作和「进货入库」平级的用例画在采购员下面但收银员也要查库存。正确的画法是把「库存查询」独立画在系统内部分别拉两条关联线连到采购员和收银员而不是复制两个同名椭圆。一次性把用例名称和维护矩阵定准数据库表设计时自然就有参照。用例分组包含用例适用参与者采购管理进货入库、采购退货、供应商档案维护采购员销售管理销售开单、销售退货、小票打印收银员库存管理库存查询、盘点录入、报损报溢、调拨管理店长、采购员系统管理登录、用户权限维护、操作日志查询店长3. 数据库建模库存表与流水表的主次关系怎么定3.1 表结构设计的关键决策数据库设计是进销存的重灾区。新手看进销存第一反应是建一张库存表字段是商品名、数量、进价、供应商然后所有逻辑都在这张表上硬算。这套设计只能撑住单机演示只要出现一次退货、一次盘点盈亏、一次报损数据就乱得没法对账。做进销存的通用方案是把账和物分开库存表当前快照负责快速读入出库流水表流水明细负责追根溯源。流水表才是老大库存表是它的增量投影。库存数量要能从流水表重新算出来这件事是进销存系统的账实相符的地基。拿商品档案、库存快照、入出库流水、进货单、销售单、盘点单这六类主体来说核心表就可以按以下结构设计。字段选用是多年下来最省心的一套不刻意追求范式最整洁但每张表都能交代清楚「这笔数据从哪来」。3.2 核心建表 SQL 与初始化语句-- 商品档案表 CREATE TABLE t_product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_code VARCHAR(32) NOT NULL COMMENT 商品编码扫码枪读取的条码, product_name VARCHAR(64) NOT NULL COMMENT 商品名称, category VARCHAR(16) COMMENT 分类如饮料/日化/生鲜, sale_price DECIMAL(10,2) NOT NULL COMMENT 标准售价, low_stock INT NOT NULL DEFAULT 0 COMMENT 库存预警下限, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, UNIQUE KEY uk_code (product_code) ) COMMENT 商品档案; -- 库存快照表 CREATE TABLE t_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL COMMENT 商品档案id, quantity INT NOT NULL DEFAULT 0 COMMENT 实时库存数量, lock_version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_product (product_id) ) COMMENT 库存快照;注意上面这段 SQL 里的 lock_version是专门留给并发控制的乐观锁字段。对库存这种高频写字段直接用 update t_stock set quantity quantity 1 在低并发下没毛病但秒杀、促销场景下会出现超卖乐观锁可以让冲突的写请求直接失败并让用户重试而不是把库存扣成负数。-- 入出库流水表账实相符的审计基础 CREATE TABLE t_stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, change_type TINYINT NOT NULL COMMENT 1入库 2销售出库 3采购退货 4盘点盘盈 5盘点盘亏 6报损, change_qty INT NOT NULL COMMENT 正数增加负数减少, before_qty INT NOT NULL COMMENT 操作前快照库存, after_qty INT NOT NULL COMMENT 操作后快照库存, ref_bill_no VARCHAR(32) NOT NULL COMMENT 关联单号如PO20250101001, operator_id BIGINT NOT NULL COMMENT 操作人, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_product_time (product_id, create_time) ) COMMENT 库存流水表;流水表里 before_qty 和 after_qty 两个字段很关键。当天对账、排障时直接看某件商品的操作前后值就能定位是哪一笔操作把数量改错了没有这两个字段出了问题只能靠猜。ref_bill_no 把流水和业务单据挂钩这是审计和核对单据一致性的唯一线索。各表之间还有引用完整性商品档案被库存快照引用、流水表引用商品 id这些外键在演示项目里可以不用建物理外键用逻辑外键冗余 product_id就行。物理外键在分批导入和删单时会带来连锁阻塞一个最典型的问题是删商品档案时如果被流水表引用外键约束会让删除失败且报错不够直观实际踩一次就知道多难受。3.3 库存初始化和幂等约束库存初始化最常见的方法是盘点当前仓库实际商品数量为每个商品 INSERT 一条 t_stock 记录同时在 t_stock_flow 里写一条 type4盘盈的初始记录。这比单独把初始库存置为 0 更合理因为从第一天起库存表就能和流水表相互审计。-- 初始化库存并写入流水注意必须在一个事务里 START TRANSACTION; INSERT INTO t_stock (product_id, quantity, lock_version) VALUES (1, 50, 0); INSERT INTO t_stock_flow (product_id, change_type, change_qty, before_qty, after_qty, ref_bill_no, operator_id) VALUES (1, 4, 50, 0, 50, INIT20250101, 1); COMMIT;这个事务的时序是有讲究的先写流水、后写快照或者两者同时提交。顺序无所谓但必须保证要么都成功、要么都失败。另一个要提防的是重复入库同一张采购单被提交两次库存加了两遍。最稳妥的方案是在采购单表加一个 status 字段入库时用「UPDATE t_purchase_order SET status 2 WHERE id ? AND status 1」这种条件更新来夺锁。受影响行数为 0说明这张单已经被处理过直接拒绝第二次入库请求。这就是幂等控制。4. 后端落地从用例图到 Service 方法的实现路径4.1 分层结构与用例的映射关系用例图到代码的映射不需要任何高深设计模式常规做法是分层Controller 接收参数Service 落业务逻辑Mapper 操作数据库。复杂项目还会加 Manager 层处理跨模块事务但超市进销存管理系统的边界足够清晰三层就够用。Controller、Service、Mapper 的文件名尽量从用例名翻译过来这样以后回看项目对照用例图找人问代码效率会高不少。用例名称ControllerService 核心方法进货入库PurchaseInControllerdoPurchaseIn(PurchaseInCommand)销售开单SaleOrderControllercreateSaleOrder(SaleOrderCommand)盘点录入StockTakeControllersubmitStockTake(StockTakeCommand)库存查询StockQueryControllerqueryStock(productCode, page, size)4.2 进货入库的 Service 实现与事务边界进货入库的完整逻辑是校验采购单状态、写进货明细、更新库存、写流水、更新采购单状态。这五步必须在同一个事务里任何一个环节失败都要全部回滚。下面给出核心 Service 方法的骨架代码可以说明事务和并发锁的实际落点。Service public class PurchaseInService { Transactional(rollbackFor Exception.class) public void doPurchaseIn(PurchaseInCommand cmd) { // 1. 幂等校验只有状态为“待入库”的采购单才能入库 PurchaseOrder order purchaseOrderMapper .selectByIdForUpdate(cmd.getOrderId()); if (order null || !order.isPending()) { throw new BizException(采购单不存在或已入库禁止重复提交); } // 2. 对库存行加悲观锁锁住商品对应的库存记录 Stock stock stockMapper.selectByProductIdForUpdate(cmd.getProductId()); if (stock null) { throw new BizException(该商品未初始化库存); } int beforeQty stock.getQuantity(); int afterQty beforeQty cmd.getQuantity(); stock.setQuantity(afterQty); stockMapper.updateById(stock); // 3. 写入库存流水保留操作前后快照 stockFlowMapper.insert(StockFlow.builder() .productId(cmd.getProductId()) .changeType(StockFlowType.PURCHASE_IN.getCode()) .changeQty(cmd.getQuantity()) .beforeQty(beforeQty) .afterQty(afterQty) .refBillNo(cmd.getOrderNo()) .operatorId(cmd.getOperatorId()) .build()); // 4. 更新采购单状态完成入库流程 purchaseOrderMapper.updateStatus(cmd.getOrderId(), PurchaseOrderStatus.COMPLETED.getCode()); } }这段代码里的 selectByIdForUpdate 是悲观锁的核心。它把采购单行锁住防止两个入员同时对同一张单操作selectByProductIdForUpdate 锁住库存行保证 beforeQty 读到的是最新值。悲观锁适合进销存后台这种低并发、强一致的场景。缺点是锁的粒度粗、并发能力弱但对超市内部系统来说完全够用且逻辑简单。4.3 并发控制乐观锁与悲观锁怎么选销售场景与进货场景的锁选择不同。收银台是高频操作如果每个销售单都用 select for update数据库的锁竞争相当大而且销售出库的核心矛盾是库存不能为负可以采用乐观锁改造。下面是一条典型的变更 SQL它在扣减时校验版本号冲突就让业务层抛异常重试。-- 乐观锁扣减库存返回0表示版本冲突本次扣减失败 UPDATE t_stock SET quantity quantity - #{qty}, lock_version lock_version 1, update_time NOW() WHERE product_id #{productId} AND quantity #{qty} AND lock_version #{expectedVersion}这条 SQL 的两个条件quantity qty 是业务约束保证不能扣成负数lock_version expectedVersion 是并发控制保证没有人改过库存快照。进销存项目里我偏好把数量约束放在 SQL 里而不是只放在 Java 代码里原因是 Java 的 if 判断有并发间隙而 SQL 的条件判断和更新是一个原子操作。方案适用场景优点缺点悲观锁for update采购入库、盘点审核逻辑简单读到的值一定最新并发低锁等待可能拖慢请求乐观锁version销售出库、促销扣减吞吐高不会阻塞读冲突多时用户体验差需要重试条件更新quantity qty所有库存扣减天然防负库存无法单独处理并发须配合锁或版本号5. 进销存避坑盘点轧差、负数库存、删单断链四类经典故障5.1 盘点单轧差后账实不符现象盘点单录入实际数量后系统的库存数没有变成盘点数反而是盘点单保存了差异、库存表纹丝不动。第二天盘点差异报表里差异数越滚越大。原因盘点单审核时只更新了盘点单自身的状态没有把盈亏数同步到库存表。解决盘点审核必须是一个事务先根据盘点数减账面数算出盈亏再调用库存更新逻辑同时写入盘盈盘亏流水。血泪经验是盘点流程一定要有一张盘盈盘亏单做中间载体直接改库存和在盘点单上记录差异是两种完全不同的说法前者没有审计痕迹。5.2 收银并发导致库存变成负数现象促销期间同一个 SKU 被两个收银台同时下单库存剩余 1 件两单都成功了库存变成 -1。原因Service 层先查库存再判断再更新查和更新之间隔了一条网络 I/O另一个请求趁虚而入。解决把库存判断下推到 SQL 的 where 条件里配合受影响行数判断是否超卖。如果项目已经用了乐观锁让失败请求重新读取库存并提示用户减少购买数量。5.3 一键删单导致流水断链现象发现某张销售单开错了开发图省事直接 delete 掉销售单和对应流水。一个月后对账时流水金额与进货记录对不上又找不到是谁删的。原因单据删除属于物理删除破坏了一单一流水的审计链。解决零容忍物理删单。所有单据只做状态流转比如销售单增加「作废」状态作废时库存回补并写一条 type作废回补 的流水。平时只允许对未审核单据做反审核已审核的单据必须走红冲流程。5.4 收银员直接改数据库现象某收银员权限配置失误前端能访问库存调整接口把 0 库存改成 1000。原因用例图里画的权限边界没有落到接口鉴权上。解决权限校验放在网关或拦截器层每个接口都绑定角色码。库存调整和盘点审核这类敏感操作在日志里记录 operator_id、操作时间和变更前后的数值。数据权限和功能权限要同时配置某员工既能看销售单又能改库存这在权限模型里是不允许出现的组合。5.5 库存表与流水表对不齐时怎么定位现象后台界面显示库存 35把流水表 SUM 一遍却是 42。原因大概率是某条业务逻辑只更新了库存表或只写了流水典型场景是定时任务回补库存时漏写了流水。解决的顺序是先订正、后复盘。订正 SQL 一般这样写先把库存表数量归零再用流水表分组求和回填。但必须强调这只是对账缝合手段不会解决业务漏洞根因在 Service 层没有把库存更新和流水插入放进同一事务。-- 对账缝合以流水表为准重建库存快照仅在排查期使用 UPDATE t_stock s LEFT JOIN ( SELECT product_id, SUM(change_qty) AS net_qty FROM t_stock_flow GROUP BY product_id ) f ON s.product_id f.product_id SET s.quantity COALESCE(f.net_qty, 0) WHERE s.id IS NOT NULL;这条 SQL 跑完必须立刻做一次全量盘点复核因为如果流水表本身少了一笔缝合后库存会保持错误的正确。定期对账的方式是每天凌晨跑库存快照校验把异常项推进告警队列。6. 验证方法用用例图反推测试用例给系统上一道保险6.1 用例描述转测试用例每个用例至少两条路径用例描述表里的主流程和扩展流程就是测试用例的来源。拿「进货入库」来演主流程测正向入库验证库存增加、流水生成、采购单状态变化扩展流程测超量入库数量超采购单剩余量、过期商品、进价越权、重复提交。每一条扩展流程都对应一个异常分支这些分支覆盖得越全协作方交付的时候就越硬气。6.2 库存一致性自动化校验脚本如果系统已经有测试环境可以写一段 Python 脚本定期对账逻辑是连接数据库分别查询库存快照和流水结果对比每个商品的两个数字。这个脚本跑在凌晨任务里任何一次差异都会把商品编码和差异数打进日志第二天上班第一件事就是排查。import pymysql conn pymysql.connect(host10.0.0.5, userstore_reader, password****, databasestore_db) cur conn.cursor() # 两侧来源数据查询只读操作不会污染业务库 cur.execute(SELECT product_id, quantity FROM t_stock) stock_map {pid: qty for pid, qty in cur.fetchall()} cur.execute(SELECT product_id, SUM(change_qty) FROM t_stock_flow GROUP BY product_id) flow_map {pid: qty for pid, qty in cur.fetchall()} # 对比两个数据源找出不一致的商品 diff [] for pid in set(stock_map) | set(flow_map): a, b stock_map.get(pid, 0), flow_map.get(pid, 0) if a ! b: diff.append((pid, a, b)) for pid, a, b in diff: print(f商品id{pid}, 库存快照{a}, 流水净变化{b})脚本里的两个字典分别快照了两个数据源diff 列表就是差异清单。这个方案跑在只读账号上不会对业务数据产生副作用。对账如果天天有差异优先查 Service 层有没有漏掉流水的分支而不是怀疑脚本。6.3 进阶方向缓存与消息队列的边界在哪里系统跑顺畅之后有人会问要不要上 Redis 缓存库存、用 MQ 异步扣减。我的看法是超市进销存管理系统的量级完全不需要。Redis 缓存库存适合高并发秒杀但会把库存一致性变成一个分布式问题处理补偿的复杂度远超收益。正确进阶路线是先把数据库层的索引、锁、事务边界打磨干净再考虑把「库存查询」类只读接口做短 TTL 缓存而把「扣减库存」永远留在数据库事务里。我在真实项目里吃过一次亏某个订单系统图省事把扣减逻辑放到了异步队列结果服务重启丢了消息库存直接腐化。从此养成一个习惯凡是涉及进销存数字变动的操作一律同步事务处理异步只做通知和报表。用例图上的每一个用例落到代码里都要能回答「谁在什么条件下调用了哪个方法」这个项目才能睡得着觉。对新手最有用的一句建议是先花半天时间把用例图改到逻辑闭环再花两天把数据库表和流水字段设计清楚写代码反而是其中最机械的一步。希望帮到你。本文还有配套的精品资源点击获取