ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

超市进销存系统设计:用例图驱动与库存事务一致性实现

超市进销存系统设计:用例图驱动与库存事务一致性实现

简介:这份资源面向超市运营管理者、信息系统学习者及课程设计开发者,提供一套覆盖商品进货、销售管理、库存统计与订单管理全流程的进销存系统实现,并附系统用例图,帮助理解员工、顾客、供应商等参与者与各功能模块的交互关系,适合作为毕业设计、课程实践或小型超市信息化改造的参考。压缩包共103个文件,约2.03MB,以aspx页面、cs后台代码、jpg与gif界面截图为主,辅以css样式、bmp与png图片、mdl模型文件及mdf、ldf数据库文件,构成可运行的Web系统与配套素材。资源已有4380人学习下载,读者可从中获得进货录入与采购订单生成、销售出库与发票处理、库存阈值预警与周转分析、订单状态追踪等模块的完整实现思路,并借助用例图快速梳理系统功能边界与业务流程,为开发与优化提供直观依据。

1. 从一张用例图说起:超市进销存到底要解决什么问题

很多同学做超市进销存管理系统,第一步就扎进数据库建表,结果做到一半发现角色权限对不上、业务流程走不通,返工成本极高。我一般建议先把用例图画出来——这张图不是交差用的装饰,而是把「谁在什么场景下对哪些商品做了什么操作」这件事钉死的契约。超市进销存的核心矛盾其实很朴素:商品从进货入库、货架销售到库存预警,中间涉及采购员、收银员、库管员、店长四类角色,每类角色能碰的数据边界必须清晰。这套方案面向的正是要交毕设或课程设计的同学,以及需要给小型超市搭一套能跑起来的进销存系统的从业者,用例图先行、模块后置、验收兜底,三步走完就能交付。

2. 需求梳理与用例建模:四类角色、六条主用例怎么定

2.1 角色划分与权限边界

超市进销存最容易翻车的地方不是代码写不出来,而是角色权限设计得太粗。我见过不少方案把「管理员」一个角色包打天下,结果收银员能改进货价、库管员能删销售记录,答辩时导师一问就露馅。合理的做法是按最小职责原则拆成四类角色,每类角色只保留完成本职工作的用例。

角色核心职责可操作用例数据可见范围
采购员供应商管理、进货下单供应商维护、进货单录入、进货查询仅进货与供应商数据
库管员入库验收、库存盘点入库确认、库存调整、库存预警查看库存与商品基础信息
收银员前台销售、退货销售开单、退货处理、商品查询仅商品售价与库存数量
店长全局监控、报表销售统计、库存报表、角色管理全部数据只读加审批

这张表看着简单,但它直接决定了后面数据库表的外键约束和接口的鉴权逻辑。采购员看不到销售毛利、收银员改不了进货单价,这些边界在用例图里用「包含」和「扩展」关系标清楚,写代码时就不会出现越权接口。

2.2 六条主用例的流转关系

用例图的价值在于把用例之间的依赖画明白。超市进销存的主干用例通常有六条:供应商维护、进货单管理、入库确认、销售开单、退货处理、库存预警。它们不是并列的,而是有先后依赖。

进货单管理「包含」供应商选择,入库确认「扩展」进货单(只有审核通过的进货单才能入库),销售开单「依赖」库存扣减,退货处理「扩展」销售单。用 PlantUML 描述这层关系比手画更不容易漏:

@startuml left to right direction actor 采购员 as P actor 库管员 as K actor 收银员 as C actor 店长 as M rectangle 超市进销存系统 { usecase "供应商维护" as UC1 usecase "进货单管理" as UC2 usecase "入库确认" as UC3 usecase "销售开单" as UC4 usecase "退货处理" as UC5 usecase "库存预警" as UC6 usecase "销售统计报表" as UC7 } P --> UC1 P --> UC2 UC2 ..> UC1 : include K --> UC3 UC3 ..> UC2 : extend C --> UC4 C --> UC5 UC5 ..> UC4 : extend K --> UC6 M --> UC7 M --> UC6 @enduml

这段用例图描述里,include表示进货单必然用到供应商数据,extend表示入库和退货是在特定条件下才触发的扩展流程。画图时把箭头方向搞反是常见错误——扩展用例指向基础用例,不是反过来。用例图定稿后,每个用例对应一个后端接口组,前端按角色渲染菜单,权限校验在网关层统一拦截,这套映射关系就是后面模块设计的骨架。

2.3 从用例到功能清单的映射

用例图画完不能就扔了,要逐条翻译成功能清单和验收点。我的习惯是给每个用例补三列:触发条件、前置数据、成功判定。比如「入库确认」的触发条件是进货单状态为「已审核」,前置数据是进货明细和实际到货数量,成功判定是库存表对应商品数量增加且生成入库流水。这三列写清楚,开发时不会漏掉状态机,答辩时也能直接对着讲。

功能清单按模块归并后大致是:基础数据模块(商品、供应商、分类)、进货模块(进货单、审核、入库)、销售模块(开单、退货、小票)、库存模块(实时库存、盘点、预警)、报表模块(日销、库存周转)、系统模块(角色、用户、日志)。六个模块对应六组接口,每组接口的入参出参在用例的「前置数据」和「成功判定」里已经埋好了,后面写 Controller 基本是填空题。

3. 总体架构与数据库设计:三层结构撑起六个模块

3.1 技术选型与分层职责

这套系统不需要微服务,单体分层架构足够,硬上微服务反而是给自己挖坑。我一般选 Spring Boot 做后端、Vue 或原生 HTML 加 Thymeleaf 做前端、MySQL 存数据,三层结构划分清楚:Controller 层只做参数校验和权限拦截,Service 层承载业务状态流转,Mapper 层管数据读写。分层的关键不是目录好看,而是让「入库确认」这种跨表操作只出现在 Service 层,Controller 里绝不出现两条以上的数据库写操作。

选型理由说透一点:超市进销存的并发量不高,单体应用部署简单、调试直观,毕设场景下导师要看的是业务闭环而不是分布式组件堆砌。数据库选 MySQL 是因为事务支持成熟,进货入库和销售扣库存都依赖事务保证一致性,这一点比换任何花哨框架都重要。

3.2 核心数据表与字段设计

数据库设计直接决定后面代码好不好写。超市进销存的表不用多,但几张核心表的字段和约束必须一次到位。

-- 商品表:库存数量冗余在此,避免每次查库存都聚合流水 CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, barcode VARCHAR(32) UNIQUE NOT NULL COMMENT '条形码,收银扫码用', name VARCHAR(64) NOT NULL, category_id BIGINT NOT NULL, purchase_price DECIMAL(10,2) NOT NULL COMMENT '进货价,仅采购和店长可见', sale_price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0 COMMENT '当前库存,入库加、销售减', warn_threshold INT NOT NULL DEFAULT 10 COMMENT '预警阈值', status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架' ); -- 进货单主表:状态机驱动,草稿→已审核→已入库 CREATE TABLE purchase_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE NOT NULL, supplier_id BIGINT NOT NULL, operator_id BIGINT NOT NULL, total_amount DECIMAL(12,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1已审核 2已入库 3已作废', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- 库存流水表:所有库存变动留痕,盘点对账靠它 CREATE TABLE stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, change_type TINYINT NOT NULL COMMENT '1入库 2销售 3退货 4盘点调整', change_qty INT NOT NULL COMMENT '正数增加负数减少', ref_order_no VARCHAR(32) COMMENT '关联单号', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );

这三张表的设计要点在于:商品表冗余stock字段是为了收银时快速判断库存,但每次变动必须同步写stock_flow,否则对账时就是一笔糊涂账。进货单用status字段做状态机,0 到 1 到 2 的单向流转在 Service 层用条件更新控制,UPDATE ... WHERE status = 0这种写法能天然防并发重复审核。stock_flow的change_qty用正负号区分增减,比拆成两张表更省事,盘点时SUM(change_qty)和商品表stock对不上就说明有漏写流水的 bug。

3.3 库存扣减的事务边界

销售开单是最容易出问题的地方。收银员点「结算」的瞬间,要做三件事:写销售单、扣商品库存、写库存流水。这三步必须在一个事务里,任何一步失败全部回滚。我见过有方案先扣库存再写单,结果写单失败库存白扣了,只能手工补,这就是事务边界没划对。

@Service public class SaleService { @Transactional(rollbackFor = Exception.class) public void createSale(SaleDTO dto) { // 1. 乐观锁扣库存,stock 不足则影响行数为 0,直接抛异常 int affected = productMapper.deductStock(dto.getProductId(), dto.getQty()); if (affected == 0) { throw new BizException("库存不足,商品ID:" + dto.getProductId()); } // 2. 写销售单 saleOrderMapper.insert(buildOrder(dto)); // 3. 写库存流水,change_qty 为负 stockFlowMapper.insert(buildFlow(dto, -dto.getQty())); } }

deductStock对应的 SQL 是UPDATE product SET stock = stock - #{qty} WHERE id = #{id} AND stock >= #{qty},把库存判断和扣减合成一条原子语句,避免「先查后改」的竞态。@Transactional的rollbackFor显式指定 Exception,是因为默认只回滚运行时异常,业务异常如果继承自 Exception 而不加这个参数,事务不会回滚,这是血泪经验。参数上qty必须做正整数校验,负数传进来会变成加库存,收银端要拦一道。

4. 核心模块实现:进货、销售、库存三条主线怎么落地

4.1 进货入库的状态流转

进货模块的难点不在增删改查,而在状态流转的严谨性。一张进货单从草稿到入库,中间隔着审核和实际到货核对,任何一步跳过都会导致库存和账面对不上。我的做法是把状态机写死在 Service 层,每个流转方法只允许从特定前置状态进入。

public void auditPurchase(Long orderId, Long auditorId) { // 只有草稿状态可审核,条件更新防重复提交 int rows = purchaseOrderMapper.updateStatus(orderId, 0, 1, auditorId); if (rows == 0) { throw new BizException("单据状态已变更,请刷新后重试"); } } public void confirmInbound(Long orderId, List<InboundItem> items) { PurchaseOrder order = purchaseOrderMapper.selectById(orderId); if (order.getStatus() != 1) { throw new BizException("仅已审核单据可入库"); } for (InboundItem item : items) { // 实际到货数量可能少于订货数量,按实收入库 productMapper.addStock(item.getProductId(), item.getRealQty()); stockFlowMapper.insert(buildInboundFlow(order, item)); } purchaseOrderMapper.updateStatus(orderId, 1, 2, null); }

updateStatus的 SQL 带上前置状态做条件:UPDATE purchase_order SET status = #{to} WHERE id = #{id} AND status = #{from}。这样即使两个库管员同时点审核,也只有一个人成功,另一个人收到「状态已变更」的提示。入库时按realQty而不是订货数量加库存,是因为实际到货短少是常态,按订货量入库会让库存虚高。这套状态流转在用例图里对应「入库确认扩展进货单管理」那条关系,代码和设计图能对上,答辩时讲起来有据可依。

4.2 销售开单与退货的对称处理

销售和退货是一对镜像操作,设计时最好让它们共用一套库存变动逻辑,只是方向相反。销售时库存减、流水记负数;退货时库存加、流水记正数,并且要校验原销售单存在且未全额退过。

@Transactional(rollbackFor = Exception.class) public void returnGoods(String saleOrderNo, Long productId, int qty) { SaleOrder order = saleOrderMapper.selectByNo(saleOrderNo); if (order == null) throw new BizException("原销售单不存在"); // 已退数量不能超过原销售量 int returned = returnMapper.sumReturnedQty(saleOrderNo, productId); if (returned + qty > order.getQty()) { throw new BizException("退货数量超出原销售量"); } productMapper.addStock(productId, qty); stockFlowMapper.insert(buildReturnFlow(saleOrderNo, productId, qty)); returnMapper.insert(buildReturnRecord(saleOrderNo, productId, qty)); }

退货的坑在于「部分退货」和「重复退货」。sumReturnedQty先汇总该单该商品已退数量,加上本次不超过原销量才放行,这个校验必须在事务内做,否则并发退货会超退。库存加回用addStock而不是直接 set,保证和入库走同一条累加路径。退货流水关联原销售单号,报表里能追溯每一笔退货的来源,店长查账时不会抓瞎。

4.3 库存预警与盘点调整

库存预警不是定时任务扫全表那么简单,商品多了以后全表扫描很慢。我的做法是在库存变动时顺带判断:入库和销售扣减后,如果stock <= warn_threshold,就往预警表插一条记录,店长端只查预警表。这样预警是事件驱动而不是轮询,实时性更好。

盘点调整则是另一条路径:库管员实盘数量和系统数量不一致时,差额通过change_type = 4的流水修正,商品表stock直接改成实盘数。这里要注意盘点流水必须记录调整前后的值,否则事后无法解释库存为什么突变。预警阈值warn_threshold按商品设置而不是全局统一,矿泉水和大件家电的安全库存显然不是一个量级,这个字段放在商品表里比放配置表更合理。

5. 避坑与常见问题:五个最容易翻车的地方

5.1 库存数量和流水对不上

现象是商品表stock和stock_flow汇总值不一致,盘点时差额越积越大。原因通常是某条库存变动路径漏写了流水,比如直接UPDATE product SET stock = ?而没走统一的变动方法。解决办法是把所有库存变动收敛到一个StockService.changeStock(productId, qty, type, refNo)方法里,商品表和流水表在同一个事务内更新,任何模块都不允许绕过它直接改库存。

5.2 并发审核导致重复入库

现象是同一张进货单被入库两次,库存翻倍。原因是审核和入库接口没有做状态条件更新,两个请求同时读到「已审核」状态都放行了。解决办法是UPDATE ... WHERE status = #{from}条件更新加影响行数判断,影响行数为 0 就抛异常。这个模式在审核、入库、作废三个动作上都要用,不能只防一处。

5.3 收银端看到进货价

现象是收银员登录后商品列表里带出了进货价字段,属于越权泄露。原因是后端接口返回了整个商品实体,前端只是没显示。解决办法是接口按角色返回不同 DTO,收银端 DTO 里根本不含purchase_price字段,而不是靠前端隐藏。权限控制要在数据出口做,不能指望前端自觉。

5.4 退货后库存加错商品

现象是退 A 商品库存加到了 B 商品上。原因是退货时只传了销售单号和数量,商品 ID 从销售明细里取的时候取错了行。解决办法是退货接口强制要求传productId,并且校验该商品确实在原销售单明细里,双重确认后再加库存。销售单明细表的主键要能唯一定位到「某单某商品」,不能只靠行号。

5.5 报表统计把作废单算进去

现象是日销售报表金额比实际收款多。原因是统计 SQL 没过滤status,把作废和退货单也加进去了。解决办法是报表查询统一带状态条件,销售统计只算有效单,退货单独列一列冲减,不要混在一个SUM里。报表 SQL 最好单独维护,不要和业务查询复用同一个 Mapper 方法,避免改一处影响另一处。

6. 验收与答辩:怎么证明这套系统真的能跑

验收这件事,我一般分三层走:功能验收、数据验收、异常验收。功能验收对着用例图逐条点,六个主用例每个至少走一遍正常流程;数据验收在操作前后各查一次数据库,重点看stock和stock_flow汇总是否一致;异常验收专门制造边界情况,比如库存为 1 时卖 2 件、重复审核同一张单、退货数量超原销量,看系统是否按预期拦截。

答辩时导师最爱问的三个问题,提前准备好答案能省很多事。第一个是「库存并发怎么保证」,答条件更新加事务,现场打开deductStock的 SQL 给他看WHERE stock >= #{qty}。第二个是「角色权限怎么实现」,答网关拦截加 DTO 裁剪,演示收银员账号调商品接口返回的 JSON 里没有进货价。第三个是「用例图和代码怎么对应」,答每个用例对应一组接口,现场从用例图点到 Controller 方法,这条链路能走通,说明设计不是画着玩的。

验收清单可以做成一张表,答辩前自己先过一遍:

验收项操作步骤预期结果判定方式
进货入库闭环建单→审核→入库库存增加、流水生成查库对比
库存不足拦截库存 1 卖 2提示库存不足、不生成单看返回与库
重复审核防护两窗口同时审核仅一个成功看状态字段
退货超量拦截退 3 件原买 2 件提示超量看返回
权限数据裁剪收银员查商品无进货价字段看 JSON

这张表建议答辩前自己跑两遍,第二遍换个顺序跑,因为有些 bug 只在特定操作顺序下才暴露。我吃过一次亏:单独测每个功能都正常,连着操作「入库→销售→退货→再入库」时库存对不上,原因是退货流水关联单号写错了字段,单测根本发现不了。从那以后我每次交付前都强制走一遍「全链路串行操作加库存对账」,不管功能看起来多简单。希望这套结构和验收思路能帮到你,用例图先画、状态机写死、库存变动收敛到一个入口,这三条守住,超市进销存这类系统基本不会出大问题。

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

RELATED READING

延伸阅读

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