SpringBoot+MyBatis仓库管理系统源码:库存事务与Shiro权限实战解析
2026/9/16 23:23:37 网站建设 项目流程

简介:这是以Java语言为基础,采用SpringBoot集成MyBatis框架开发的超市仓库管理系统,项目中整合了Apache Shiro安全组件与MyBatisPlus增强工具,适用于高校课程设计、毕业设计以及想深入学习主流后端框架的开发者,满足学习与工程实践双重需求。压缩包为zip格式,共包含392个文件,文件类型非常丰富,其中Java源码有106个,HTML页面49个,JavaScript脚本42个,CSS样式9个,图片类资源较多,还有SQL脚本、XML映射文件、JSON配置以及Maven构建相关的配置说明,整体包体约10.3MB,结构有层次,便于查阅和学习,附带完整数据库初始化脚本。该资源上线以来已有425人学习浏览,说明适合初学者对照参考。通过该资源可以完整获得一个超市管理系统的源码与数据库脚本,并可在IDEA中配合Navicat、Git、Maven进行环境搭建,掌握从数据库设计、后端业务编写到前端页面渲染的整个流程,能够快速启动项目并据此二次扩展。项目前端使用layui框架,页面风格统一,逻辑清晰,还提供了必要的静态资源与样式配置。

1. 拿到这套 SpringBoot 超市仓库管理系统源码,先看清它在解决什么

开学季超市仓库里最典型的乱象是:Excel 记流水账,库存永远是昨天的数据,月底盘点对不上。这套基于 Java SpringBoot + MyBatis 的超市(仓库)管理系统源码,把收货、入库、销售出库、退供应商四件事统一进一套事务模型,前端用 layui 出后台界面,登录鉴权交给 Shiro,数据库侧提供 SQL Server 脚本并兼容 MySQL 5.0,项目构建走 Maven,配置好就能跑。拆这份源码的价值不在于页面有多炫,而在于商品表、库存表、出入库流水表和用户角色表之间的关系被理成了标准的仓库作业模型。适合做 Java 课设、毕业设计二次开发,也适合中小团队拿它当仓库后台脚手架。要看懂这套系统,核心是抓住一个点:库存表与流水表之间靠什么保证数据一致,以及 MyBatis 在写入链路里如何配合事务。

2. 工程骨架与 SQL Server 数据模型:先从源码里对齐构建环境

2.1 Maven 工程结构与 SpringBoot 静态资源目录

拿到源码后先看目录。这是一个典型的 SpringBoot + MyBatis 前后端半分离项目,前端资源没有拆成独立工程,而是放在后端代码里由 SpringBoot 统一托管。常规做法是src/main/resources/static放 layui 那套页面资源,页面通过 Ajax 调用接口,Controller 负责页面跳转和 JSON 返回。

supermarket/ ├── mvnw.cmd # Maven Wrapper,Windows 下不需要预装 Maven ├── pom.xml # 依赖与打包配置 ├── src/main/java/.../controller # 页面路由与 JSON 接口 ├── src/main/java/.../service # 事务层,出入库核心逻辑 ├── src/main/java/.../mapper # MyBatis Mapper 接口 ├── src/main/resources/mapper # Mapper XML,真正写 SQL 的地方 ├── src/main/resources/static # layui.css、laydate.css、dtree.css 等 ├── src/main/resources/application.yml # 数据源与 MyBatis 配置 └── src/main/resources/sql # SQL Server 初始化脚本

这里容易踩坑的是staticmapper两个目录。static里那堆layui.csslayer.csslaydate.csspublic.cssdtree.css,说明页面是基于 layui 后台模板改出来的,如果页面出现样式 404,先确认 SpringBoot 是否把 static 映射到根路径。mapper目录是 MyBatis 的 SQL 存放点,application.yml里的mapper-locations必须写对,否则启动时直接报Invalid bound statement (not found)。源码自带mvnw.cmd,说明构建环境已经用 Maven Wrapper 封装过,团队里有人没装 Maven 也能用它拉依赖执行命令,这个细节在课设答辩时值得单独提一下。

2.2 SQL Server 核心表:商品、库存、出入库单怎么关联

仓库系统最重要的不是查询快,而是“一张入库单提交下去,库存表和流水表同时变化”。这套源码的表结构围绕五个业务对象设计,关系如下。

表名作用关键字段核心关联
product商品档案id, sku, name, spec, unit, purchase_price, sale_price被库存和明细引用
supplier供应商id, name, contact_phone, address入库单关联供货商
stock实时库存id, product_id, warehouse_id, quantity, safe_stock商品+仓库唯一
stock_in_master入库单主表id, order_no, supplier_id, oper_user, create_time一对多明细
stock_in_item入库明细id, master_id, product_id, quantity, price回写 stock 数量
sys_user / sys_role / user_role账号与角色id, username, password, role_codeShiro 权限过滤

设计上,stock.quantity是系统中唯一允许保存“当前数量”的字段,stock_in_itemstock_out_item只记录每一次变动明细。业务上不要在产品表里加库存字段,否则改一次价格就会污染历史流水,库存对账时全乱。safe_stock字段是给低库存报表用的阈值,后面第 4 章的告警查询就靠它和quantity做比较。

2.3 建表脚本与从 SQL Server 切到 MySQL 5.0 的关键差异

源码包里给出的是 SQL Server 脚本,默认库名建议建为supermarket。我实际部署时也遇到只有 MySQL 5.0 环境的情况,所以把建表语句做了兼容调整,核心部分如下。

CREATE TABLE product ( id BIGINT IDENTITY(1,1) PRIMARY KEY, sku NVARCHAR(32) NOT NULL UNIQUE, name NVARCHAR(64) NOT NULL, spec NVARCHAR(64), unit NVARCHAR(8), purchase_price DECIMAL(10,2) DEFAULT 0, sale_price DECIMAL(10,2) DEFAULT 0, status TINYINT DEFAULT 1 ); CREATE TABLE stock ( id BIGINT IDENTITY(1,1) PRIMARY KEY, product_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL DEFAULT 1, quantity INT DEFAULT 0, safe_stock INT DEFAULT 10, CONSTRAINT uk_stock_product UNIQUE (product_id, warehouse_id) );

ID 列使用IDENTITY(1,1)是 SQL Server 的自增写法,换到 MySQL 5.0 时要改成AUTO_INCREMENTNVARCHAR改成VARCHARTINYINT两边通用。切换数据源时,pom.xml里把 SQL Server 驱动换成 MySQL 驱动,URL 写成jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8,注意 MySQL 5.0 的驱动类名是com.mysql.jdbc.Driver,不要用 8.x 的com.mysql.cj.jdbc.Driver,否则会报 ClassNotFound 或 SSL 握手失败。

提示:数据库名如果改成了别的名字,application.yml里的DatabaseName或 URL 路径必须同步改,否则启动后连的是另一个空库,页面能打开却查不到任何数据。

3. SpringBoot + MyBatis 的出入库核心链路:从表结构到可执行代码

3.1 实体类、Mapper 接口与 XML 文件的映射关系

上面建好的表,到 Java 代码里就是实体类。源码的 Mapper 层采用“接口定义 + XML 写 SQL”的方式,接口方法名必须与 XML 的id一致,这是 MyBatis 约定大于配置的核心。

@Data public class Stock { private Long id; private Long productId; private Long warehouseId; private Integer quantity; private Integer safeStock; }
public interface StockMapper { Stock selectByProductAndWarehouse(@Param("productId") Long productId, @Param("warehouseId") Long warehouseId); int decreaseStockByLock(@Param("delta") Integer delta, @Param("productId") Long productId, @Param("warehouseId") Long warehouseId); }

@Data由 Lombok 生成 getter 和 setter,让实体类保持干净。@Param注解在 MyBatis 多参数方法里必须写,否则 XML 中拿不到参数名,启动或调用时报There is no getter for property named 'productId'。Mapper 接口本身不写 SQL,真正逻辑在src/main/resources/mapper/StockMapper.xml,SpringBoot 启动时通过@MapperScan扫描接口并绑定代理对象,Service 层直接注入即可。

3.2 入库事务:一张入库单同时写主表、明细和库存

入库是仓库系统里最容易出现数据不一致的环节。假设先插明细再改库存,中途断电或抛异常,就会出现库存加了但明细没落库。源码在 Service 层使用@Transactional把三个动作框进同一事务,这是整条链路的可靠性根基。

@Service public class StockInService { @Autowired private StockInMapper stockInMapper; @Autowired private StockMapper stockMapper; @Transactional(rollbackFor = Exception.class) public void doStockIn(StockInMaster master, List<StockInItem> items) { stockInMapper.insertMaster(master); for (StockInItem item : items) { stockInMapper.insertItem(item); Stock stock = stockMapper.selectByProductAndWarehouse( item.getProductId(), master.getWarehouseId()); int newQty = stock.getQuantity() + item.getQuantity(); stockMapper.decreaseStockByLock(item.getQuantity(), item.getProductId(), master.getWarehouseId()); } } }

等等,上面的写法有问题:查询旧值再加回写,并发下会互相覆盖。更稳妥的做法是直接写更新语句,让数据库在单行上完成原子加减。下面这个 XML 方法更安全。

<update id="decreaseStockByLock"> UPDATE stock SET quantity = quantity - #{delta} WHERE product_id = #{productId} AND warehouse_id = #{warehouseId} AND quantity &gt;= #{delta} </update>

这里把“查出旧值、计算新值、写回”三步压缩成一条 UPDATE,数据库对同一行的更新会自动加锁,天然避免并发覆盖。WHERE里带quantity >= #{delta}可以防止库存扣成负数。&gt;=是 XML 文件中>=的标准转义,写错会导致 XML 解析直接失败。事务中如果主表、明细、库存任何一个步骤失败,rollbackFor = Exception.class都会让整单回滚,保证三张表最终要么全部成功,要么全部不动。

3.3 MyBatis 动态 SQL 与缓存配置的边界

实际业务里,入库单明细数量是不固定的,所以 XML 中大量使用<if><foreach>做动态 SQL。以查询商品为例,按名称、SKU、分类多条件筛选时,常见写法是这样。

<select id="selectProductByCondition" resultType="com.supermarket.entity.Product"> SELECT id, sku, name, spec, unit, purchase_price, sale_price, status FROM product <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="sku != null and sku != ''"> AND sku = #{sku} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY id DESC </select>

<where>标签会自动去掉第一个条件前的AND,比手动拼接WHERE 1=1干净得多。参数nameskustatus由前端传参直接映射。注意LIKE拼接时使用CONCAT而不是字符串加法,这样在 SQL Server 和 MySQL 下都能跑。

关于 MyBatis 一二级缓存:一级缓存默认开在 SqlSession 生命周期内,同一个事务里重复查同一条 SQL 会直接读缓存;二级缓存默认关闭,开启需要在 XML 写<cache/>。出入库这种写多读多、且对实时一致性要求高的场景,我不建议开启二级缓存,否则一个线程扣了库存,另一线程在同一 SQL 上可能读到旧缓存值。保持默认配置,让每次库存查询直接落库,才是这里正确的取舍。

4. 基于 Shiro 的后台权限控制与低库存报表:从表数据到页面

4.1 用户、角色、权限三张表在 Shiro 中如何落地

超市管理系统一定不止一个操作员:仓库管理员、收银员、店长看到的功能应当不同。源码使用sys_usersys_roleuser_role三张表完成 RBAC 设计,角色表结构如下。

字段类型说明
idBIGINT主键
role_nameNVARCHAR(32)角色名称,用于界面显示
role_codeNVARCHAR(32)角色标识,用于 URL 拦截
statusTINYINT启用状态,1 启用 0 停用
remarkNVARCHAR(255)备注

用户登录时,Shiro 的 Realm 会调用doGetAuthenticationInfo验证账号密码,然后再从用户表 join 角色表查出角色标识放回 Subject。

@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); List<String> roles = userMapper.selectRolesByUsername(username); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addRoles(roles); return info; }

这里的selectRolesByUsername在 XML 里通过sys_useruser_rolesys_role三表 join 查出role_code列表。角色标识建议用adminstockcashier这类英文业务代号,不要用中文直接入库,否则后续在过滤链里写roles[仓库管理员]时,一旦页面编码或库表排序规则不一致,角色匹配就会失效。

4.2 URL 过滤链和页面路由的权限控制

权限不散落在每个 Controller 方法里,而是统一在ShiroConfigfilterChainDefinitionMap中声明。中间层配置我一般这样加。

@Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean(); factory.setSecurityManager(securityManager); Map<String, String> filter = new LinkedHashMap<>(); filter.put("/login", "anon"); filter.put("/logout", "logout"); filter.put("/admin/**", "roles[admin]"); filter.put("/stock/**", "roles[admin,stock]"); filter.put("/**", "authc"); factory.setFilterChainDefinitionMap(filter); factory.setLoginUrl("/login"); return factory; }

LinkedHashMap保证过滤规则从上到下短路匹配,/**必须放在最后,否则前面写的具体规则全部失效。roles[admin,stock]表示 admin 和 stock 两个角色都能访问,多个角色用逗号分隔。前端 layui 页面如果请求被拦截返回 401,需要在 Ajax 层做统一跳转处理,不能只弹错误信息,否则用户看到接口报错却不知道要重新登录,这种问题在联调阶段最常见。

4.3 低库存告警与 MyBatis 分页报表的组合写法

报表页要列出“当前库存低于安全库存”的商品,不能靠 Java 内存过滤,因为数据量大了以后一次全表加载会压垮数据库。正确做法是让 SQL Server 完成过滤和聚合。

SELECT p.sku, p.name, p.unit, s.quantity, s.safe_stock, (s.safe_stock - s.quantity) AS shortage_qty FROM stock s JOIN product p ON s.product_id = p.id WHERE s.quantity < s.safe_stock ORDER BY shortage_qty DESC

shortage_qty是计算列,不能直接在 WHERE 中引用,所以过滤条件还是要写s.quantity < s.safe_stock。分页部分,源码如果集成了 MyBatis-Plus,就在配置类里注册PaginationInnerInterceptor,Controller 接收currentsize参数,返回IPage<ProductStockVO>

@GetMapping("/stock/alarm") public Result pageAlarm(@RequestParam(defaultValue = "1") long current, @RequestParam(defaultValue = "10") long size) { Page<ProductStockVO> page = new Page<>(current, size); IPage<ProductStockVO> result = stockMapper.selectAlarmPage(page); return Result.success(result); }

这里传入的Page对象会被 MyBatis-Plus 拦截器解析成带有LIMIT的分页语句,同时自动生成 COUNT 查询。如果发现分页总数对、但当前页数据重复,通常是 join 后分页主键不唯一造成的,解决办法是分页查询 SQL 和 COUNT 查询 SQL 分开写,避免 MyBatis-Plus 自动生成的 COUNT 包含多余字段。

5. 本地跑通源码的三个关键配置与排查实践

5.1 application.yml 里的数据源与 MyBatis 核心参数

把源码导入 IDEA 后,最先要改的是application.yml。SpringBoot 内置 Tomcat 8.5 可以直接用,不需要单独装外部容器,配置文件里的关键项如下。

spring: datasource: driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver url: jdbc:sqlserver://localhost:1433;DatabaseName=supermarket;encrypt=false username: sa password: root123 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.supermarket.entity configuration: map-underscore-to-camel-case: true

map-underscore-to-camel-case不配置的话,SQL Server 返回的product_idcreate_time这类字段没法自动映射到实体的productIdcreateTime,页面表格全是 null。这个参数配上后可以省掉大量手写resultMap的工作。encrypt=false是连接 SQL Server 2019 及以上版本时的常见配置,不开加密连接会出现 TLS 握手失败。

5.2 启动顺序与库存核对流程

源码自带mvnw.cmd,在 Windows 下可以直接用它构建,不要求电脑上预装 Maven。我一般这样执行:

mvnw.cmd clean package -DskipTests java -jar target/supermarket.jar

先打包再运行的好处是,编译错误和运行时错误能分阶段暴露,日志定位更清晰。项目启动后先做三件事验证系统是否真实可用:

  1. 打开登录页输入 admin 账户,能进入后台说明 Shiro 过滤链没有误拦静态资源。
  2. 手动录入一张入库单,然后去数据库中查询stock_in_masterstock_in_itemstock三张表,确认数量同步增加。
  3. 人为把safe_stock调大,返回低库存报表页刷新,确认告警商品能出现在列表第一页。

5.3 从 SQL Server 切换到 MySQL 5.0 时的三个差异点

如果课设环境要求用 MySQL 5.0 重新建库,差异集中在自增主键、字段类型、驱动配置三处。SQL Server 的IDENTITY(1,1)要替换成AUTO_INCREMENT,驱动换成com.mysql.jdbc.Driver,URL 改成jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=UTF-8。MySQL 5.0 对DATETIME默认值处理比 SQL Server 严格,建表时日期字段尽量不要写DEFAULT CURRENT_TIMESTAMP,改在 Java 代码里用new Date()生成时间,MyBatis XML 配合<if test="createTime != null">动态插入,兼容性更稳。切库完成后,重新执行上面核对步骤里的入库操作,然后在 SQL Server Management Studio 或 Navicat 里把主表、明细表、库存表三张表再查一遍,确认数量变动一致后再进入下一步功能验收。

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

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

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

立即咨询