
简介这套基于Java SpringBoot与MyBatis的超市仓库管理系统源码面向课程设计、毕业设计以及希望上手企业级后端项目的初学者。项目采用SpringBoot整合MybatisPlus并引入Shiro安全框架实现用户认证与权限控制业务模块涵盖商品分类、库存管理、供应商管理、订单处理等核心场景同时附带完整的MySQL数据库初始化脚本和Maven构建配置导入IDE后即可运行调试。资源压缩包共392个文件大小约10.3MB其中106个Java源码文件构成业务逻辑主体75个GIF动图用于界面效果演示49个HTML页面与42个JS、CSS文件支撑前端交互14个XML与JSON文件包含配置文件及数据交换结构另有SQL脚本、项目文档、Git版本管理信息等整体目录层次分明方便按模块查看代码。目前已有425人学习通过阅读源码和运行项目能够深入理解SpringBoot整合MyBatis与Shiro的完整流程掌握从数据库设计到前后端联调的实战技能适合作为课设拓展或毕业设计的参考基础。1. 超市仓库管理系统用 SpringBoot MyBatis SQL Server先想清楚这一件事我第一次做这种系统时先把“商品的增删改查”这个思维丢掉。真正让收银和仓管头疼的不是商品档案能不能存而是库存台账和出入库流水之间的关系。货架上的库存数字是结果入库单、出库单、盘点单才是原因。这套系统里SpringBoot 负责把收银、采购、仓库的请求变成稳定的服务接口MyBatis 负责把业务规则写成一段段可控 SQLSQL Server 数据库则保证库存数字在并发扣减时不被写坏。它适合跑结业设计、外包交付也适合门店量不大的中小型自研项目不引入消息队列也不需要微服务那套基础设施一台装好 SQL Server 2019 的 Windows 机器就能完整跑起来。2. SQL Server 库表建模把商品、库存、单据流水一次定清楚2.1 台账与流水分离是这套系统最容易抄错的地方很多初版实现包括一些赶工的外包代码习惯在商品表里直接放一个 quantity 字段卖一瓶就减一。这个设计在数据量小、流程简单的时候看不出问题一旦出现退货、破损报损、盘点差异就没有任何追溯依据因为库存数字被直接覆盖了。正确做法是台账与流水分离product 表只管商品信息stock 表只记录当前可用库存所有数量变动都必须写进 stock_flow 流水表。库存台账可以被流水重建盘点差异也能逐单对账这是我在设计库存类系统时不会让步的约定。表名职责关键字段与库存的关系product商品主数据barcode, product_name, spec, unit静态信息不存数量stock库存台账warehouse_id, product_id, quantity当前库存只做加减stock_flow出入库流水biz_no, flow_type, quantity, before_qty, after_qty驱动台账变化的依据stocktake盘点单stocktake_no, book_qty, real_qty, diff_qty盘点后校正台账stock_flow 表里同时记录 before_qty 和 after_qty这一点很多人会省。省掉的代价是日后想排查“为什么这次入库后数量不对”时只能靠猜。两个字段多占一点存储但换来的是一对一的对账能力在做超市管理系统时这条成本值得付。2.2 建库建表 SQLSQL Server 语法下的最小可跑版本下面这段 T-SQL 能直接建出四张核心表。字段类型我按 SQL Server 的习惯写金额用 decimal库存用 int时间用 datetime2字符串用 nvarchar。这样做可以避免字符集和精度上的隐藏问题。CREATE DATABASE supermarket; GO USE supermarket; GO CREATE TABLE product ( product_id INT IDENTITY(1,1) PRIMARY KEY, barcode NVARCHAR(32) NOT NULL UNIQUE, product_name NVARCHAR(64) NOT NULL, spec NVARCHAR(64) NULL, unit NVARCHAR(16) NOT NULL DEFAULT N瓶, category_id INT NULL, create_time DATETIME2 DEFAULT SYSDATETIME() ); CREATE TABLE stock ( warehouse_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 0, PRIMARY KEY (warehouse_id, product_id) ); CREATE TABLE stock_flow ( flow_id BIGINT IDENTITY(1,1) PRIMARY KEY, biz_no NVARCHAR(32) NOT NULL, warehouse_id INT NOT NULL, product_id INT NOT NULL, flow_type TINYINT NOT NULL, -- 1 入库 2 出库 3 盘点调整 quantity INT NOT NULL, before_qty INT NOT NULL, after_qty INT NOT NULL, remark NVARCHAR(200) NULL, create_time DATETIME2 DEFAULT SYSDATETIME() ); CREATE TABLE stocktake ( stocktake_no NVARCHAR(32) PRIMARY KEY, product_id INT NOT NULL, book_qty INT NOT NULL, real_qty INT NOT NULL, diff_qty AS real_qty - book_qty, take_time DATETIME2 DEFAULT SYSDATETIME() );这段 SQL 在 SQL Server 2019 和 2022 上可以直接跑。IDENTITY(1,1) 是自增主键stocks 的主键用联合主键确保同一个仓库、同一个商品只会有一行库存记录。diff_qty 是计算列由 SQL Server 计算不需要在 SpringBoot 或者 MyBatis 里再算一遍。建表脚本建议手工维护不要依靠 SpringBoot 自动建表。提示不要在 application.yml 里配置 ddl-auto 相关自动建表选项。生产环境里一旦自动执行容易把人工加的索引和约束覆盖掉。schema 变更应该走 SQL 脚本和源码一起提交。2.3 索引与约束MyBatis 查询慢先回来看这里库存类系统最常见的性能问题是条码查询。barcode 已经建了唯一索引但 MyBatis XML 里如果写成LIKE % #{keyword} %这个索引就会失效。前端扫码通常是完整条码最实用的查询是条码前缀匹配barcode LIKE CONCAT(#{keyword}, %)可以走索引商品名称的人名模糊搜索量小走全表扫也问题不大。stock_flow 会随时间增长得很快它上面应该有联合索引。常见做法是在 warehouse_id、product_id、create_time 三列上建联合索引这样按仓库、按商品、按时间窗口查流水时执行计划不会每次都回表扫全量数据。排序规则统一使用 Chinese_PRC_CI_AS注意 SQL Server 里的排序规则同源问题否则在不同环境下字符串比较可能区分大小写或在中文排序上出现诡异行为。2.4 初始化数据不要用 MyBatis 循环 insert源码包里常见的 init.sql 一般分三部分建表、插入分类、插入商品。手动初始化几千条商品数据时不应该在 Java 里循环调用单条 insert既慢又会产生大量事务日志。SQL Server 里更推荐用 INSERT ... SELECT 加 UNION ALL 批量插入比如把五条商品数据一次性写入INSERT INTO product (barcode, product_name, spec, unit, category_id) SELECT N690123456001, N纯牛奶, N250ml, N盒, 1 UNION ALL SELECT N690123456002, N面包, N原味, N袋, 2 UNION ALL SELECT N690123456003, N矿泉水, N550ml, N瓶, 3;批量插入的 SQL 仍然放在 MyBatis XML 里管理保持项目里只有一种 SQL 书写入口。初始化数据表时还需要顺手插入每个商品的库存初始行否则后端联查库存时会得到 null而不只是 0。3. SpringBoot 集成 MyBatis 连 SQL Server依赖、yml、XML 一次跑通3.1 最小依赖三个 starter别引错 JDBC 驱动dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.microsoft.sqlserver/groupId artifactIdmssql-jdbc/artifactId version12.4.2.jre11/version /dependency这里最容易踩坑的是版本对应关系。SpringBoot 2.x 配 mybatis-spring-boot-starter 2.3.xSpringBoot 3.x 必须换成 mybatis-spring-boot-starter 3.x否则自动装配直接失效。mssql-jdbc 的 jre8 和 jre11 版本按 JDK 选JDK 17 上用 jre8 驱动会报 UnsupportedClassVersionError。如果项目用了 SpringBoot 较高版本优先看官方对应的 MyBatis starter而不是盲目升最新版。3.2 application.yml 配置 SQL Server 连接参数spring: datasource: url: jdbc:sqlserver://localhost:1433;DatabaseNamesupermarket;encryptfalse;trustServerCertificatetrue username: sa password: 你的密码 driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.supermarket.entity configuration: map-underscore-to-camel-case: trueJDBC URL 里的参数是有含义的。encryptfalse 和 trustServerCertificatetrue 是为了关掉 SQL Server JDBC 驱动默认的 SSL 主机名校验本地开发必须加生产环境按公司安全策略决定是否保留。mybatis 的 mapper-locations 告诉 SpringBoot 去哪里加载 XML 映射文件type-aliases-package 让 XML 里写 resultType 时不用写全限定类名。map-underscore-to-camel-case 打开后SQL 查询出来的 product_id 会自动映射到实体类字段 productId不用手写 resultMap。实体类里比较重要的是 stock 查询结果不要用基本类型 int而是用 Integer。库存数量在数据库里是 NULL 时int 接收会报空指针Integer 会得到 null这样才能在业务层区分“没有库存记录”和“库存为 0”。我在设计库存更新逻辑时通常先判断 stock 行是否存在不存在就插入一行初始数量。3.3 Mapper 接口与 XML商品分页查询的写法商品查询是超市管理系统的高频路径。Mapper 接口定义如下package com.supermarket.mapper; import org.apache.ibatis.annotations.Param; import com.supermarket.entity.Product; import java.util.List; public interface ProductMapper { ListProduct selectPage(Param(keyword) String keyword, Param(offset) int offset, Param(rows) int rows); }对应的 XML 映射文件select idselectPage resultTypeProduct SELECT product_id, barcode, product_name, spec, unit FROM product where if testkeyword ! null and keyword ! barcode LIKE CONCAT(#{keyword}, %) OR product_name LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY product_id OFFSET #{offset} ROWS FETCH NEXT #{rows} ROWS ONLY /select这段配置里有两个关键点。一是 SQL Server 不能用 MySQL 的 LIMIT 分页必须用 OFFSET ... FETCH NEXT。二是 MyBatis 的where标签会自动处理条件为空时多余的 AND 和 WHERE这比在 Java 里拼接 SQL 字符串安全得多。条码匹配用前缀商品名匹配用双百分号模糊查两种命中的索引行为完全不同。3.4 减库存的 update 必须防超卖update iddecreaseStock UPDATE stock SET quantity quantity - #{qty} WHERE warehouse_id #{warehouseId} AND product_id #{productId} AND quantity gt; #{qty} /update这是库存并发问题上的第一道防线。SQL Server 执行 UPDATE 时本身会加行级排他锁两个请求同时扣最后一个库存时只有一个请求的 WHERE 条件能匹配到 quantity qty另一个 update 返回影响行数为 0。Service 层收到 0 时抛业务异常“库存不足”不进入后面的支付或出库流程。页面显示有货但扣减失败绝大多数情况都是这里没写好而不是前端的问题。4. 盘点调整与事务边界行锁、数据库事务、MyBatis 缓存4.1 Transactional 放在哪一层才算对Controller 里直接调用 Mapper 是不行的事务会被拆散。我一般在 Service 层写 StockService把入库、出库、盘点调整都收敛成独立方法。以出库为例完整操作包括查库存、写流水、更新库存。这三个动作必须在一个事务里否则流水写成功、库存没减账就平不上。Service public class StockService { private final StockMapper stockMapper; private final StockFlowMapper stockFlowMapper; public StockService(StockMapper stockMapper, StockFlowMapper stockFlowMapper) { this.stockMapper stockMapper; this.stockFlowMapper stockFlowMapper; } Transactional(rollbackFor Exception.class) public void outbound(StockFlow flow) { int beforeQty stockMapper.selectQuantity(flow.getWarehouseId(), flow.getProductId()); int rows stockMapper.decreaseStock(flow.getWarehouseId(), flow.getProductId(), flow.getQuantity()); if (rows 0) { throw new BusinessException(库存不足); } flow.setBeforeQty(beforeQty); flow.setAfterQty(beforeQty - flow.getQuantity()); stockFlowMapper.insert(flow); } }rollbackFor Exception.class 这个参数必须写。Spring 默认只会对 RuntimeException 回滚而很多项目里的 BusinessException 是受检异常不显式配置的话库存已经扣了异常向上抛时事务还是提交了。另一个坑是同类内部调用this.outbound() 调用不会经过 Spring 代理Transactional 会失效。要保证事务生效必须通过注入的 StockService 对象跨类调用。4.2 SQL Server 的锁写法与 MySQL 的差异MySQL 里常用 SELECT ... FOR UPDATE 锁行SQL Server 的语法完全不同。如果业务上需要先查询库存再根据查到的值决定下一步SQL Server 里最好的做法是不要用普通 SELECT而是用锁提示SELECT quantity FROM stock WITH (UPDLOCK, ROWLOCK) WHERE warehouse_id #{warehouseId} AND product_id #{productId};UPDLOCK 指定在行上加更新锁直到事务结束才释放ROWLOCK 告诉优化器使用行级锁而不是页锁。这样两个事务并发读同一行库存时后一个会被阻塞在当前事务提交之后读到的都是上一个事务提交后的结果。日常出库场景推荐直接用条件 update因为条件 update 本身锁的行范围更小也不用先查后改但盘点前锁定库存做数量确认时上面的 SELECT WITH 写法更直观。4.3 盘点差异不直接改库存盘点流程不能直接 UPDATE stock 把账面数改成实盘数那样会失去差异记录。正确步骤是先记录盘点单算出差异值然后照常走一次盘点调整流水flow_type 为 3quantity 为正数表示盘盈负数表示盘亏。库存台账的更新仍然走 increaseStock 或 decreaseStock 方法只是业务单号填盘点单号。这样盘点动作可以复现、可以审计也可以在下个盘点周期做趋势对比。场景推荐 SQL 写法锁行为日常出库扣减UPDATE stock SET quantity quantity - ? WHERE quantity ?更新行时排他锁盘点前读取并锁定SELECT ... WITH (UPDLOCK, ROWLOCK)更新锁保持到事务结束库存展示查询普通 SELECT快照读不加锁4.4 MyBatis 缓存在库存查询上的教训MyBatis 一级缓存默认开启作用域是同一个 SqlSession。在库存频繁变动的场景里同一个事务里第一次查库存得到 100接着另一个请求把库存改成 90同一个 SqlSession 第二次查询可能直接命中一级缓存返回旧的 100。这个问题不容易复现但会导致页面显示和数据库不一致。我的建议是库存表和流水表彻底避开缓存查询方法上显式设置 flushCache 或者直接用普通查询商品主数据相对静态可以把数据交给 Spring 的本地缓存或 Redis 管理而不是依赖 MyBatis 的二级缓存。凡是需要实时读数的表都不要放在 MyBatis 二级缓存里。5. SQL Server 2019 部署与首启动验证建库账号、排序规则、回滚测试5.1 用 sqlcmd 建库建账号绕开 SSMS 交互SQL Server 2019 安装时选混合认证模式并设置 sa 密码。安装完成后打开命令行工具执行sqlcmd -S localhost -U sa -P MyPssw0rd -Q CREATE DATABASE supermarket不要用 sa 跑业务再创建一个最小权限的登录名sqlcmd -S localhost -U sa -P MyPssw0rd -d master -Q CREATE LOGIN shop_user WITH PASSWORDShop123; sqlcmd -S localhost -U shop_user -P Shop123 -d supermarket -Q CREATE USER shop_user FOR LOGIN shop_user; sqlcmd -S localhost -U shop_user -P Shop123 -d supermarket -Q EXEC sp_addrolemember db_datareader, shop_user; EXEC sp_addrolemember db_datawriter, shop_user;然后修改 application.yml 里的 username 和 password使用 shop_user 连接。首启动前执行 init.sql 建表并插入基础数据再启动 SpringBoot 项目。SpringBoot 能够正常启动只是第一步真正要验证的是读写链路。5.2 最容易翻车的三项配置问题现象原因处理方式Caused by: SSL connection errorJDBC 驱动默认校验主机名证书URL 加 encryptfalse;trustServerCertificatetrue无法登录 sa 用户安装时没启用混合认证模式用 SQL Server 配置管理器打开混合认证并重启服务中文乱码或比较异常实例排序规则不是 Chinese_PRC_CI_AS建库时指定 COLLATE Chinese_PRC_CI_AS如果是 1433 端口连不上先检查 Windows 防火墙是否放行 TCP 1433。SQL Server 的默认实例没有动态端口问题命名实例才需要启用 SQL Browser 服务这类系统通常不需要命名实例。5.3 用一条必失败的出库验证事务和流水项目跑通后先做一个回滚测试。临时写一个部署阶段专用的 Service 方法插入流水后立刻抛异常观察数据库里是否残留数据Transactional public void testRollbackOnPurpose() { StockFlow flow new StockFlow(); flow.setBizNo(TEST-ROLLBACK-001); flow.setFlowType((byte) 1); stockFlowMapper.insert(flow); throw new BusinessException(故意失败验证回滚); }调用这个方法后查 stock_flow 表如果 TEST-ROLLBACK-001 这条流水不存在说明事务回滚机制正常。如果这条流水还在就去查两个地方方法上是否写了 rollbackFor Exception.class以及调用方有没有在同一个类里直接调用这个方法绕过了 Spring 代理。这个验证通过后再讨论性能优化和锁等待才有意义。本文还有配套的精品资源点击获取