☰
超市进销存系统开发全流程:用例图、数据库设计与Python实现
2026/9/26 15:12:24 网站建设 项目流程

简介:一套超市进销存管理系统的完整项目资料,面向超市运营人员、软件开发学习者及毕业设计学生,解决商品采购、销售、库存与订单管理中的数字化问题。系统涵盖供应商信息录入、采购订单自动生成、条形码扫码销售、销售发票生成、库存实时扣减、库存阈值预警、库存周转率分析与订单状态追踪,并附有用例图展示员工、顾客、供应商与各功能模块的交互关系。压缩包共103个文件,以18个aspx页面和24个cs源码文件为核心,配合css样式、gif/jpg/bmp/png图片、mdf/ldf数据库文件及mdl模型文件,完整覆盖界面、业务逻辑、数据库与设计文档,整体仅2.03MB,便于快速下载与部署。已有4378人学习,既能用于课程设计与毕业设计,也可作为二次开发或库存流程优化的基础实例。对于想掌握ASP.NET项目搭建、数据库设计及用例图绘制的读者,这是一份可运行的参考。

1. 超市进销存管理系统到底在解决什么:账对不上的根因往往在图里

一家三百平米的社区超市,货架上的商品和系统里的库存永远对不上,月底盘点时账面比实物多出几十件——这正是超市进销存管理系统要解决的核心问题。这类项目通常都会附用例图,但它不是用来装饰文档的:用例图先把角色和功能边界画清楚,后面的数据库建表、代码目录和验收清单,全都要从这张图反推出来。很多人一上来就写代码,结果交货时连“谁来用、能做什么”都说不清,返工成本远高于先画一张图。这篇笔记适合正在做课程设计、毕业设计,或者准备给门店上轻量系统的从业者,按“用例图 → 数据模型 → 核心流程 → 踩坑清单”的顺序走一遍,你能拿到一条可以照着复现的完整路径。

2. 从用例图搭建系统骨架:角色、用例与PlantUML落地

用例图是UML里最容易被低估的一张图。它不负责描述页面长什么样,也不管数据库字段,它只回答两个问题:谁在用这个系统,系统为这些人提供哪些完整的功能。系统分析与设计课程里,用例图是第一张可评审的交付物;在真实开发里,我一般也先画它,因为它能把“超范围开发”这个最常见的坑挡在编码之前。

2.1 用例图先框边界:识别角色与用例的正确顺序

画用例图的第一步是找角色,不是找功能。超市进销存里最常见的四个角色是收银员、采购员、店长和供应商,它们都在系统边界之外,是真正触发操作的人。很多人会把“数据库管理员”“系统维护员”也画进去,如果项目里没有独立的运维后台,这类角色就是虚构的,画上去只会让评审时被追问“这套权限在哪实现”。

角色定下来之后,再看每个角色要完成的完整业务动作。用例的命名习惯是动词短语,例如“采购入库”“销售出库”“库存预警”,而不是“库存管理”这种名词,后者太泛,没法映射到具体代码。比较稳妥的做法是:让每个用例都能回答“这个动作开始前是什么状态,结束后单据和库存发生了什么变化”。

新手最容易画错的是系统边界。边界框只圈系统内的用例,登录、采购入库、销售出库这些写在框里;供应商打电话催款、店长线下盘点这类动作不写进框里。还有一个常被忽略的细节是角色与用例之间的关联方向,actor指向usecase,箭头由系统外部指向内部,表示“谁发起这个功能”,反过来画在评审时一眼就会被看出问题。

2.2 用PlantUML把用例图落到文本:代码与关系说明

手工画图工具很多,但团队协作和版本管理都不方便。我习惯用PlantUML,因为它把图写成纯文本,可以进Git,改一个角色只需要改一行。下面是一个可以直接编译的超市进销存用例图脚本。

@startuml left to right direction skinparam actorStyle awesome actor "收银员" as cashier actor "采购员" as buyer actor "店长" as manager actor "供应商" as supplier rectangle "超市进销存" { usecase "登录" as UC01 usecase "采购入库" as UC02 usecase "销售出库" as UC03 usecase "库存查询与预警" as UC04 usecase "商品档案维护" as UC05 usecase "销售统计" as UC06 usecase "供应商管理" as UC07 usecase "查看订单状态" as UC08 } cashier --> UC01 cashier --> UC03 cashier --> UC04 buyer --> UC02 buyer --> UC07 manager --> UC04 manager --> UC06 manager --> UC05 supplier --> UC08 UC02 ..> UC04 : <<include>> UC03 ..> UC04 : <<include>> @enduml

left to right direction让图横向展开,避免角色和用例堆成一条竖直长条;skinparam actorStyle awesome只是风格参数,去掉也不影响语义。图中两条虚线带<<include>>是重点:采购入库和销售出库都包含库存变化,这意味着在数据库层必须把“单据写入”和“库存变更”放进同一个事务,后面写代码时这就是硬性约束。

编译这张图的命令也很简单:

plantuml supermarket_usecase.puml -tpng -o output/

-tpng指定输出PNG格式,-o output/指定图片输出目录。如果本机没有PlantUML,用支持该语法的在线渲染器或IDE插件也可以,重点是puml文本本身必须能通过编译。

2.3 画用例图AI能做什么:生成初稿与三处人工校验点

这两年画用例图AI很流行,它确实能省掉从空白画布开始的痛苦。我的姿势是:先把角色和用例清单写成一段描述喂给AI,让它输出PlantUML或Mermaid文本,再人工补关系。AI生成的初稿常犯三类错误:一是把extend和include用反,二是漏掉系统边界框,三是角色和用例之间的关系画成双向。前两个直接改文案重生成就行,第三个必须人工确认。

拿上面的例子说,AI可能把UC02 ..> UC04 : <<include>>写成UC02 <|-- UC04,这是错误的泛化关系。校验时我只看三件事:角色是否重叠、用例是否都是动词短语、include方向是否从主用例指向被包含用例。这三项通过,图的质量基本就能拿去评审了。

提示:AI生成的图只能当草稿,Contain关系、Include关系这些语义需要人来定,机器替不了你把业务规则读明白。

3. 建表选型:商品、库存与流水怎么设计才不翻车

用例图定下来之后,数据库表结构就有了推导依据。“采购入库”对应入库单头和入库明细,“销售出库”对应销售单头和销售明细,“库存查询与预警”对应库存表,“商品档案维护”对应商品表。照这个顺序建表,不会出现“代码写完了发现表对不上图”的局面。

3.1 八张核心表:职责拆分与主外键关系

超市进销存里,我把表拆成八张,分别承担档案、单据和存量三类职责:

表名职责关键字段
product商品档案barcode、name、spec、unit、is_active
supplier供应商档案name、contact、phone
purchase_order采购入库单头order_no、supplier_id、created_at
purchase_item采购入库明细order_id、product_id、quantity、price
sale_order销售出库单头order_no、cashier_id、created_at
sale_item销售出库明细order_id、product_id、quantity、price
inventory实时库存product_id、stock、safety_stock、updated_at
sys_user系统用户username、password_hash、role

为什么进货单要拆成单头和明细两张表,而不是在一张表里塞多个商品?因为一张进货单里有若干个商品,每个商品的数量和价格都不一样,如果不拆,同一张单的字段会出现大量冗余。更关键的是“价格快照”:入库明细里的price记录的是这批货的实际进货价,不能去关联商品表里的现价,否则一个月后商品调价,历史单据的毛利就全算错了。销售明细同理,卖出去时的价格必须存下来,不能依赖商品表的当前售价。

商品表里的is_active是软删除标记,这个字段后面会专门讲到。sys_user表里不存明文密码,只存哈希值,哪怕这是课程设计也该养成这个习惯。

3.2 建表SQL:字段类型、默认值与约束的参数说明

下面是兼容SQLite和MySQL的建表脚本,字段参数按实际运行环境调整。价格字段我用整数“分”存储,避免浮点误差,这在大卖场对接收银机时尤其重要。

-- 商品档案 CREATE TABLE product ( id INTEGER PRIMARY KEY AUTOINCREMENT, barcode TEXT UNIQUE NOT NULL, -- 条码,启用后禁止修改 name TEXT NOT NULL, spec TEXT, -- 规格,如 500ml/瓶 unit TEXT NOT NULL DEFAULT '件', category TEXT, is_active INTEGER NOT NULL DEFAULT 1, -- 软删除:1有效,0停用 created_at TEXT NOT NULL ); -- 库存表:product_id 同时是主键和外键 CREATE TABLE inventory ( product_id INTEGER PRIMARY KEY, stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0), safety_stock INTEGER NOT NULL DEFAULT 10, updated_at TEXT NOT NULL, FOREIGN KEY (product_id) REFERENCES product(id) ); -- 采购入库单头 CREATE TABLE purchase_order ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_no TEXT UNIQUE NOT NULL, supplier_id INTEGER NOT NULL, status TEXT NOT NULL DEFAULT '已入库', created_at TEXT NOT NULL, FOREIGN KEY (supplier_id) REFERENCES supplier(id) ); -- 采购入库明细 CREATE TABLE purchase_item ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, product_id INTEGER NOT NULL, quantity INTEGER NOT NULL CHECK (quantity > 0), price INTEGER NOT NULL, -- 以“分”为单位,避免浮点误差 FOREIGN KEY (order_id) REFERENCES purchase_order(id), FOREIGN KEY (product_id) REFERENCES product(id) );

barcode字段加UNIQUE NOT NULL是硬约束,一个条码只能对应一个商品;CHECK (stock >= 0)是数据库层的最后一道防线,即便业务代码有漏洞,库存也不会变成负数。order_no用业务单号而不是自增id做主键的一部分,是因为单据号需要在报表、对账和客服查询时直接使用,自增id容易暴露业务量,也不方便跨系统流转。

created_at我统一存ISO格式文本,比如2025-08-01 09:30:00,排序和显示都直观。如果追求性能可以改成时间戳,但超市进销存的数据量完全没有必要。price存分之后,查报表时再除以100转成元,前端展示和Excel导出都不受影响。

注意:SQLite默认不启用外键约束,连接后要执行PRAGMA foreign_keys = ON;,否则上面FOREIGN KEY形同虚设。

3.3 用例图与数据模型的映射:一张表对齐设计和实现

表建完,把第2章的用例图拿过来做一次映射检查,这一步能验证设计没有漏项。我习惯用一张小表完成这个动作:

用例编号用例名称关联表核心事务
UC02采购入库purchase_order、purchase_item、inventory单头+明细+库存加增
UC03销售出库sale_order、sale_item、inventory单头+明细+库存扣减
UC04库存查询与预警inventory、product条件查询与阈值过滤
UC05商品档案维护product新增、编辑、软删除
UC07供应商管理supplier新增、编辑、停用

如果发现某个用例找不到对应表,说明数据库设计有遗漏;反过来,如果某张表没有任何用例引用,说明它可能是不必要的,或者用例图画漏了。这个检查动作花不了十分钟,但它能把“图是图、表是表”的脱节问题直接暴露出来。

4. 把进销存流程写成代码:入库、出库与库存预警的最小实现

设计落到代码这一步,我推荐用Python加SQLite起步。理由很简单:SQLite零安装、单文件,事务语义完整,适合课程设计和门店轻量系统;Python的sqlite3模块是标准库,不需要额外装依赖。如果你必须用Java加MySQL,下面的函数逻辑完全一致,只是把连接和预处理写法换掉。

4.1 最小工程结构:三个文件,不引入框架

常见的做法是保持三个文件,功能边界清晰:

文件职责
db.py创建连接、建表、开启外键约束
service.py入库、出库、库存预警等业务函数
main.py控制台入口,演示调用顺序

db.py里连接SQLite后第一件事就是执行PRAGMA foreign_keys = ON;,否则第3章的外键约束不会生效,删掉供应商后历史单据会变成悬空数据。这个细节很多人栽过,先写在前面。

4.2 采购入库:同一个事务里完成单头、明细与库存更新

采购入库的逻辑是三件事要么全成功,要么全失败:写入单头、写入明细、增加库存。缺少事务保护时,最常见的现象是单据保存了但库存没加,月底盘点怎么都对不上。下面是核心函数。

import sqlite3 from datetime import datetime def purchase_in(conn, supplier_id, items): cursor = conn.cursor() order_no = datetime.now().strftime("%Y%m%d%H%M%S") try: cursor.execute("BEGIN") cursor.execute( "INSERT INTO purchase_order(order_no, supplier_id, created_at) VALUES (?, ?, ?)", (order_no, supplier_id, datetime.now().isoformat(timespec="seconds")), ) order_id = cursor.lastrowid for product_id, quantity, price in items: cursor.execute( "INSERT INTO purchase_item(order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)", (order_id, product_id, quantity, price), ) cursor.execute( "UPDATE inventory SET stock = stock + ?, updated_at = ? WHERE product_id = ?", (quantity, datetime.now().isoformat(timespec="seconds"), product_id), ) conn.commit() except Exception as e: conn.rollback() raise e

调用时items是(product_id, quantity, price)元组列表,price单位是分。order_no用时间戳生成,精度到秒,同一秒并发两张单的概率在进销存场景里极低,如果追求严谨可以加随机后缀。UPDATE inventory SET stock = stock + ?是原地加增,不先SELECT再更新,这个写法天然避免并发覆盖,后面避坑章节会细讲。

事务的原子性靠BEGIN和rollback()保证:第4行写入单头成功,第8行明细写入失败,整个事务回滚,不会出现“半张单”的脏数据。

4.3 销售出库与库存预警:扣减前校验与阈值参数

销售出库比入库多一个约束:库存不能为负。判断和扣减如果分成两条语句,中间就会被其他操作插队,所以必须合并成一条带条件的UPDATE。

def sell_out(conn, cashier_id, items): cursor = conn.cursor() order_no = datetime.now().strftime("%Y%m%d%H%M%S") try: cursor.execute("BEGIN") cursor.execute( "INSERT INTO sale_order(order_no, cashier_id, created_at) VALUES (?, ?, ?)", (order_no, cashier_id, datetime.now().isoformat(timespec="seconds")), ) order_id = cursor.lastrowid for product_id, quantity, price in items: cursor.execute( "UPDATE inventory SET stock = stock - ?, updated_at = ? " "WHERE product_id = ? AND stock >= ?", (quantity, datetime.now().isoformat(timespec="seconds"), product_id, quantity), ) if cursor.rowcount == 0: raise ValueError(f"商品 {product_id} 库存不足或商品不存在") cursor.execute( "INSERT INTO sale_item(order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)", (order_id, product_id, quantity, price), ) conn.commit() except Exception: conn.rollback() raise

WHERE product_id = ? AND stock >= ?这行是关键。UPDATE执行时数据库会先按条件定位到那条库存记录,再扣减数量;如果条件不满足,影响行数为0,cursor.rowcount == 0就抛异常,整个事务回滚。这样判断和扣减在数据库层是原子操作,两个收银台同时扫同一个商品也不会扣成负数。

库存预警的查询很简单,但阈值不能全店一刀切,调味品一周卖几瓶的阈值,和生鲜一天卖几十份的阈值显然不一样,safety_stock字段就是为这个设计的。

def low_stock(conn, threshold=10): cursor = conn.cursor() cursor.execute( "SELECT p.id, p.name, i.stock, i.safety_stock " "FROM product p JOIN inventory i ON p.id = i.product_id " "WHERE i.stock < i.safety_stock AND p.is_active = 1 " "ORDER BY (i.safety_stock - i.stock) DESC", ) return cursor.fetchall()

这里把阈值判断写成stock < safety_stock,而不是在外面用Python比较,是为了让数据库直接返回结果,减少无用数据的网络传输。如果门店刚开业没有历史数据,可以在建表时按品类给默认值:日用百货5件,生鲜20件,促销品单独调高。库存预警的“预警线”不是拍脑袋定的,等系统跑一个月后,按日均销量乘以补货周期来反推,比任何初始阈值都靠谱。

5. 进销存开发避坑:库存负数、并发扣减与日期范围查询的五种翻车现场

代码写起来都顺,真上线第一天就会遇到下面几个问题。这些坑我基本都踩过,每条按“现象 → 原因 → 解决”写,可以直接对号入座。

5.1 库存扣成负数:查询再更新是最常见的翻车现场

现象:门店卖出一件可乐后,库存显示-1,盘点时账实完全对不上。 原因:代码先SELECT stock判断库存是否充足,再执行UPDATE inventory SET stock = stock - 1。两个操作之间,另一个线程可能已经扣过库存,第一次判断的结果已经过期。这在单机SQLite里也会发生,因为SELECT和UPDATE之间有时间差。 解决:把判断和扣减合并成一条UPDATE,使用WHERE stock >= quantity条件,再判断rowcount是否等于0,就是第4章sell_out里的写法。数据库的CHECK (stock >= 0)约束也保留,作为最后一道防线。

5.2 并发扣减导致超卖:两个收银台同时扫同一个商品

现象:早高峰两台收银机同时卖出同一款洗衣液,库存从5件直接变成0,但系统生成了3笔订单。 原因:SQLite默认的写事务是DEFERRED模式,事务在第一次写操作时才获取锁,两个事务先读后写就会互相覆盖。 解决:不需要引入分布式锁,把事务改成BEGIN IMMEDIATE,让事务一开始就获取写锁,后到的事务会等待。SQLite支持这个语法,Java的JDBC里对应的是setAutoCommit(false)配合Connection级别的锁;第二道防线依然是第3章建表时加的CHECK (stock >= 0),它保证数据库永远不会出现负库存。

5.3 日期范围查询把当天漏掉:闭区间边界

现象:查8月整月的销售明细,8月31日的数据总是少了几条,而且漏掉的多是晚上八九点的单子。 原因:created_at存的是2025-08-31 23:40:00这种带时间的文本,查询条件写成created_at BETWEEN '2025-08-01' AND '2025-08-31',BETWEEN是闭区间,但只匹配到8月31日零点,23:40的单子自然被排除。 解决:日期范围查询一律用半开区间created_at >= '2025-08-01' AND created_at < '2025-09-01'。如果字段是时间戳,同样用>= start AND < end + 1 day。这是从报表需求里总结出来的血泪经验,几乎所有日期统计翻车都在边界时刻。

5.4 用例图与代码脱节:画过的用例找不到实现

现象:答辩验收时老师照着用例图问“供应商查看订单状态这个功能在系统哪里”,你翻遍代码找不到对应模块。 原因:用例图画完就丢,编码时全凭记忆,图上的“查看订单状态”在实现阶段被忽略了,或者做成了别的入口。 解决:建表映射之后,再做一次“用例编号进代码”的动作。每个业务函数命名里带上用例编号,比如UCO3_do_sale、UC02_purchase_in,或者在函数的docstring第一行写UC03-销售出库。验收时照着用例图编号逐一找函数,任何断点都能被及时发现。这是治本的做法,不靠记性。

5.5 删除供应商把历史单据搞没:软删除的代价

现象:删掉一家不再合作的供应商后,报表里历史采购单的供应商名称全部变成空白,外键关联失败。 原因:直接把供应商从supplier表物理删除,purchase_order里的supplier_id成了悬空引用,关联查询查不到数据。 解决:所有档案表都加is_active字段,删除时执行UPDATE supplier SET is_active = 0 WHERE id = ?,查询时默认过滤is_active = 1。历史单据里的supplier_id依然有效,关联查询能查到旧名称,只是该供应商不再出现在下拉选择里。商品、用户表同理,这招对进销存类系统几乎是必备。

6. 把用例图变成可运行的验收路径:从图到交付的最后一公里

设计评审完成不等于可以交货,还有一个动作最容易被忽略:把用例图里的每个用例转成一条条可执行的验收步骤。我见过太多图是图、代码是代码的交付,最后翻车的都是当初懒得补这一步。

6.1 给每个用例编一个验收编号

回到第2章的用例图,先给每个用例建编号,例如UC02采购入库、UC03销售出库、UC04库存预警。然后为每个用例拆出两条路径:正常路径和边界路径。正常路径验证功能能用,边界路径专门验证系统不会翻车。销售出库的正常路径是库存充足时扣减成功;边界路径是库存刚好为1时卖出2件,系统必须报错且不生成订单。

6.2 从用例到验收步骤:一条可执行的检查路径

验收步骤不建议写模糊描述,每一条都要有具体操作和预期结果。下面是一段可以直接用的验收表:

验收编号对应用例操作步骤预期结果
UC03-01销售出库添加2件库存充足的商品,点击结算库存减少2,生成销售单号
UC03-02销售出库库存1件商品,数量填2,点击结算提示库存不足,不生成订单
UC04-01库存预警把某商品库存改成低于预警值,重开预警页商品出现在预警列表,按差额排序
UC02-01采购入库新建入库单,添加5件商品,提交库存加5,单据状态为已入库

跑验收时注意,每一条都要在干净的数据状态下执行,避免上一条测试的脏数据影响下一条的结果。我用得最顺手的方式是准备一份独立的测试数据库,每次跑完直接删掉重建,几秒钟的事。这套验收路径走完,系统能不能交给门店用、哪些用例还没实现、哪些边界没兜住,全部一目了然。希望这种“先图后码、图码对应”的做法能帮你在交付时少一点临时补课的慌张。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询