资讯动态

SpringBoot+MyBatis实现超市仓库管理系统:库存批次与事务控制

发布时间:2026/9/16 12:07:59 来源:尧图企业网站定制
简介这套基于SpringBootMyBatis的超市仓库管理系统完整源码面向Java后端学习者、课程设计及毕业设计人员。系统采用SpringBoot、Shiro安全框架与MybatisPlus持久层技术涵盖商品管理、库存管理、出入库等常见业务模块可帮助读者演练企业级分层开发与权限控制思路。压缩包共392个文件约10.3MB以Java源文件、GIF演示动图、HTML页面、JS脚本与SQL脚本为主另含Maven配置及项目说明文档结构清晰便于导入IDEA运行调试。目前已有425人浏览学习适合正在完成超市/仓库类课设、或希望快速上手SpringBoot整合MyBatis的开发者。通过源码阅读与运行可掌握Shiro认证授权流程、MybatisPlus条件构造器使用、前端布局与交互实现并参照SQL脚本完成数据库设计部署为独立开发类似管理后台提供扎实参考。1. 基于SpringBootMyBatis的超市仓库管理系统核心在于把库存账目在SQL Server里算清楚你在很多仓库管理系统demo里会看到stock表只有一个quantity字段出库就update减一入库就update加一。这在教学项目里成立在真实超市里站不住脚同一款酱油有不同进货批次食品有保质期多门店可能共用总仓退货、报损、盘点差异都会改变库存口径每一笔变动都要能追溯。基于Java SpringBoot MyBatis SQL Server数据库的超市仓库管理系统解决的就是这类账要能对得上的问题通常覆盖商品档案、供应商管理、采购入库、销售出库、库存查询与盘点、保质期预警等模块并附带可运行的源码和SQL Server数据库脚本。它适合两类人一类是学完SpringBoot和MyBatis想找个完整业务项目练手的开发者另一类是给社区超市或小连锁门店做信息化建设的小型技术团队。2. SpringBoot负责流程编排MyBatis负责把SQL写明白两者在仓库系统里的分工要清楚2.1 请求在Controller、Service、Mapper之间的流转一个采购入库单从界面提交到SQL Server落库通常要过四层。Controller只做参数接收、格式校验和简单的权限过滤不直接拼SQL。Service层承担业务规则比如入库单审核后才能改库存、出库前检查库存是否充足、盘点单是否有差异还要控制整个事务的边界。Mapper层是MyBatis的接口定义所在XML文件里写具体SQL和结果映射SQL Server数据库脚本、存储过程都可以在这个环节被调用。比如采购入库时常见的Service方法签名是这样Service public class InboundService { private final InboundMapper inboundMapper; private final StockMapper stockMapper; Transactional(rollbackFor Exception.class) public void audit(InboundDTO dto) { inboundMapper.updateStatus(dto.getBillNo(), AUDITED); // 按明细更新库存与批次 dto.getItems().forEach(item - stockMapper.increaseBatch(item.getGoodsId(), item.getQuantity()) ); } }这里不把SQL逻辑堆在Service里而是通过Mapper接口调用。注意Transactional的用法入库开单和审核原本是两个动作审核时再把库存加上去而不是创建单据时就变动库存这是进销存系统里很常见的约定。2.2 动态SQL是复杂查询的落点用where和if组合库存筛选仓库管理里最常见的查询是“库存查询”页面筛选条件可能有商品名称、仓库、是否临期、供应商甚至商品分类。这些条件不是每次都齐全的。MyBatis相比Spring Data JPA的一个明显优势就是XML中的动态SQL能直接控制生成的语句。通常我会这样写select idselectStockPage resultTypecom.example.domain.StockView SELECT s.goods_id, g.goods_name, w.warehouse_name, s.quantity, s.avg_cost FROM stock s LEFT JOIN goods g ON s.goods_id g.goods_id LEFT JOIN warehouse w ON s.warehouse_id w.warehouse_id where if testgoodsName ! null and goodsName ! AND g.goods_name LIKE CONCAT(%, #{goodsName}, %) /if if testwarehouseId ! null AND s.warehouse_id #{warehouseId} /if if testexpireNotify AND EXISTS ( SELECT 1 FROM stock_batch sb WHERE sb.goods_id s.goods_id AND sb.expire_date DATEADD(DAY, 30, GETDATE()) ) /if /where ORDER BY s.last_update_time DESC /select几个要点说明。where标签会自动处理拼接位置上的AND不会出现WHERE AND这种语法错误。expireNotify 这个布尔条件通常来自表单里的“只看30天内到期”勾选DATEADD加上EXISTS子查询能避免把所有批次数据查出来再过滤。LIKE 里的 CONCAT 是 SQL Server 2012及以上版本支持的写法源码如果要在2008 R2上运行需要改成% #{goodsName} %。2.3 库存系统要不要开MyBatis缓存答案和面试常问的八股不一样MyBatis一级缓存默认开启二级缓存默认关闭这是两个默认值。但在Spring集成环境里一级缓存的生命周期通常和一个SqlSession绑定Spring每次请求都会创建新会话所以一级缓存在无事务的查询里基本发挥不了作用而二级缓存是跨SqlSession的。仓库库存表的查询频率高、更新频率也高开了二级缓存会出现“界面显示库存100SQL Server里实际只剩60”的脏读窗口销售、盘点、对账都不允许这种延迟。所以常见做法是明确关闭二级缓存让每次库存查询都读SQL Servermybatis: configuration: cache-enabled: false如果是商品分类、单位这类几乎不变化的字典表把数据放Redis或本地Caffeine都比MyBatis二级缓存更好控制失效时间。原因在于缓存一致性库存数据由多个入口变更单靠ORM框架的缓存策略很难处理全部失效点。2.4 三种ORM在仓库类项目里的取舍对比点MyBatis原生MyBatis-PlusSpring Data JPASQL可控性高XML完全手写中通用方法多复杂查询仍要手写低复杂报表容易生成低效查询学习成本中要理解映射和动态SQL低CRUD开箱即用中要理解懒加载和持久化上下文适合场景库存流水、对账这类SQL密集型管理后台基础CRUD简单领域模型维护仓库系统的对账、批次分配、库存流水都绕不开精确SQL所以原生MyBatis反而是最稳的选择。MyBatis-Plus适合把不需要性能调优的基础增删改加快速度但不应拿它替代对SQL的控制。3. 从商品档案到批次库存SQL Server里的表结构这么建模才扛得住超市业务3.1 建库脚本里的排序规则决定了中文数据能不能对齐常见的项目源码会附带一个建库脚本。很多人直接执行不看Collation其实SQL Server的数据库排序规则会影响中文比较和排序。建库时显式指定Chinese_PRC_CI_AS是最省事的做法它支持中文按拼音排序且不区分大小写符合中文字段筛选的习惯CREATE DATABASE SuperMarketDB COLLATE Chinese_PRC_CI_AS; GO USE SuperMarketDB; GO如果你的SQL Server是从2019或2022安装的默认排序规则已经是中文相关这条语句仍然可以执行一致性不会受影响。在SQL Server 2008 R2上也要用它老实例默认可能是拉丁或英文规则中文字段用LIKE查询时会返回意想不到的结果。3.2 商品表业务编码和主键分开字段不必多但要精CREATE TABLE goods ( goods_id INT IDENTITY(1,1) PRIMARY KEY, goods_no VARCHAR(32) NOT NULL UNIQUE, goods_name NVARCHAR(100) NOT NULL, category_id INT NULL, unit NVARCHAR(10) NOT NULL DEFAULT N件, shelf_life_days INT NULL, create_time DATETIME2 DEFAULT SYSDATETIME() );这张表的主键是自增INT不是业务编号。原因很实际goods_no要面向扫码枪、条码、人工录入它可能存在但会按门店规则变化主键一旦被别的表引用换编码就得级联改成本太高。字段含义字段类型作用goods_idINT IDENTITY主键内部关联使用goods_noVARCHAR(32)对外编码条码/手动录入shelf_life_daysINT NULLNULL表示非食品有值则参与保质期预警NVARCHAR不是多余的写法。MyBatis在插入中文时如果字段是VARCHAR且客户端代码页不对会出现乱码使用NVARCHAR能规避大多数中文乱码问题SQL Server里存储也按Unicode处理。3.3 批次表出库时先扣哪批货由这张表决定CREATE TABLE stock_batch ( batch_id INT IDENTITY PRIMARY KEY, goods_id INT NOT NULL, warehouse_id INT NOT NULL, batch_no VARCHAR(40) NOT NULL, production_date DATE NOT NULL, expire_date DATE NOT NULL, initial_qty DECIMAL(14,2) NOT NULL, remain_qty DECIMAL(14,2) NOT NULL, last_update_time DATETIME2 DEFAULT SYSDATETIME(), CONSTRAINT ck_batch_remain CHECK (remain_qty 0), CONSTRAINT uq_batch UNIQUE (goods_id, warehouse_id, batch_no) ); CREATE INDEX ix_batch_expire ON stock_batch(expire_date) INCLUDE (goods_id, remain_qty);remain_qty 0这个CHECK约束是数据库层面的底线防止应用层漏判导致负数库存。uq_batch确保同一商品同一仓库下批次号不重复采购单多录一次会被拦住。DECIMAL(14,2)是为了让称重商品比如散装糖果、蔬菜能入系统用整数会在这个环节翻车。索引ix_batch_expire是给“还有30天到期”这类查询用的INCLUDE把要展示的字段塞进索引叶子节点查询时不用回表取数据数据量大时速度差异明显。3.4 库存流水表所有库存变动都要留下痕迹CREATE TABLE stock_flow ( flow_id BIGINT IDENTITY(1,1) PRIMARY KEY, ref_type VARCHAR(20) NOT NULL, ref_no VARCHAR(40) NOT NULL, goods_id INT NOT NULL, batch_id INT NOT NULL, change_qty DECIMAL(14,2) NOT NULL, before_qty DECIMAL(14,2) NOT NULL, after_qty DECIMAL(14,2) NOT NULL, operator VARCHAR(40) NOT NULL, remark NVARCHAR(200) NULL, create_time DATETIME2 DEFAULT SYSDATETIME() ); CREATE INDEX ix_flow_goods ON stock_flow(goods_id, create_time DESC);ref_type记录业务来源如 INBOUND、OUTBOUND、CHECK、ADJUSTref_no关联入库单号或出库单号。这样在盘点差异出现时能追查某一商品每一笔变动的前置数量和变动后数量。市面上不少精简单据里省略了库存流水后续对账只能靠猜所以这张表建议保留。3.5 近效期预警DATEDIFF取剩余天数SELECT goods_id, batch_no, expire_date, DATEDIFF(DAY, GETDATE(), expire_date) AS days_left FROM stock_batch WHERE remain_qty 0 AND expire_date BETWEEN GETDATE() AND DATEADD(DAY, notifyDays, GETDATE()) ORDER BY expire_date, batch_no;notifyDays是预警提前量传30就查未来30天内到期且还有余量的批次。BETWEEN的两个边界都由GETDATE()生成业务上不允许比当前时间更早的到期日期入库所以不需要 GETDATE()的条件。DATEDIFF的DAY计算结果就是剩余天数界面直接显示即可。阈值如果做成门店参数可以在门店表里加个notify_days字段查询时带出。4. 出库扣减与并发一句UPDATE都不该写成先查再改4.1 库存扣减的错误示范与SQL Server里的原子操作业务最忙的时候出库和采购入库同时发生库存表同一行可能被两个事务同时操作。新手版本一般这么写Stock stock stockMapper.selectById(goodsId); if (stock.getQuantity() need) { throw new BusinessException(库存不足); } stockMapper.updateQuantity(goodsId, stock.getQuantity() - need);两个会话同时查到quantity100都判断“够”然后各自减10写回90。实际卖出20件库存只扣了10。解决办法是利用UPDATE的WHERE条件直接在数据库里完成判断和扣减Update(UPDATE stock SET quantity quantity - #{need} WHERE goods_id #{goodsId} AND quantity #{need}) int deduct(Param(goodsId) Long goodsId, Param(need) BigDecimal need);调用方检查影响行数int rows stockMapper.deduct(realGoodsId, need); if (rows 0) { throw new InsufficientStockException(库存不足); }逻辑说明quantity quantity - #{need}把计算放在SQL Server一侧quantity #{need}作为条件行锁只会锁定匹配到的这一行直到事务提交或回滚。影响行数0表示库存不足或商品不存在不用再查一次。注意不能写成“先查出数量再判断再update”因为判断和更新不是原子操作。4.2 批次先进先出无法在一条UPDATE里完成按批次出库需要先算出“从哪个批次开始扣”然后逐条UPDATE批次余量同时写库存流水。SQL Server的窗口函数可以完成批次分配通常的写法是SELECT batch_id, expire_date, initial_qty, remain_qty FROM ( SELECT batch_id, expire_date, initial_qty, remain_qty, SUM(remain_qty) OVER (ORDER BY expire_date, batch_id) AS cum_qty FROM stock_batch WHERE goods_id goodsId AND warehouse_id warehouseId AND remain_qty 0 ) t WHERE cum_qty - remain_qty need ORDER BY expire_date;这个查询的结果是“本次出库需要动到的所有批次”且按到期日排序。举个例子三个批次余量分别是10、20、30本次出库25第一行cum_qty1010-10025扣完第二行cum_qty3030-201025扣完第三行cum_qty6060-303025是最后一个要部分扣减的批次。然后在事务里循环扣掉每批的余量。如果同时处理多种商品可以把查询改成使用JOIN和APPLY子查询或每个商品单独调用这个查询。在事务里逐商品处理比一大段复杂SQL更易维护事务时间也短。4.3 Transactional三类失效场景Spring的Transactional在仓库系统里常见三种失效方法被同类this.xxx调用导致事务代理没生效方法是非public异常被try-catch吞掉。仓库系统最容易犯的是第三种扣库存失败后想“提示但继续”结果异常被吃了库存没有回滚。常见做法是Transactional(rollbackFor Exception.class) public void outbound(OutboundDTO dto) { // 1. 分配批次 // 2. 扣减 stock_batch.remain_qty // 3. 扣减 stock.quantity // 4. 写 stock_flow 流水 }rollbackFor Exception.class显式声明异常回滚边界默认情况下Spring只回滚RuntimeException受检异常如SQLException如果不声明事务不会自动回滚。在出库单场景任一步失败前面扣的批次余量都应该撤销所以这个参数值得写。不要在一个事务里做批量大循环插入比如一次审核2000行入库单明细。事务持有连接和锁时间越长越容易与其他写操作冲突可以分批提交或把事务放在审核动作上而不是逐行保存。4.4 库存汇总表与流水的最终一致性仓库系统有时候会看到“库存表单独维护流水表只记录不汇总”的设计。我一般保留stock表做实时展示用stock_flow做审计追溯避免每次展示汇总流水算库存。实时展示是单据发生时的最终结果流水是审计和复盘时重建历史的依据二者缺一不可。若发现不一致用第6章的核对SQL检查并重建。5. SQL Server连接与MyBatis适配跑源码前先把这几处配置对齐5.1 驱动类与连接串选哪个项目里连SQL Server数据库常见的驱动有两种微软官方mssql-jdbc和老牌的jtds。新项目使用官方mssql-jdbc较多如果源码里用的jtds通常是为了兼容旧的SQL Server 2000/2008环境。两个驱动的类名和URL都不通用驱动driver-class-nameurl 协议mssql-jdbccom.microsoft.sqlserver.jdbc.SQLServerDriverjdbc:sqlserver://jtdsnet.sourceforge.jtds.jdbc.Driverjdbc:jtds:sqlserver://我一般建议新建项目用官方驱动因为微软会持续修复安全与兼容问题。官方驱动9.x以后默认要求加密连接本地开发经常看到“The driver could not establish a secure connection to SQL Server by using Secure Sockets Layer (SSL) encryption”错误常见做法是连接串加两个参数spring: datasource: driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver url: jdbc:sqlserver://127.0.0.1:1433;DatabaseNameSuperMarketDB;encryptfalse;trustServerCertificatetrue username: sa password: 123456encryptfalse在开发和内网环境可以接受生产环境下应改为encrypttrue并配置证书。trustServerCertificatetrue用于跳过自签证书校验只在测试环境使用。数据库密码建议通过环境变量或配置中心注入不要写死在application.yml里。5.2 分页SQL Server不是MySQLLIMIT在这边是语法错误MySQL写惯了的人会顺手写LIMITSQL Server不支持。SQL Server 2012及以上版本主流写法是OFFSET FETCHSELECT goods_id, goods_no, goods_name FROM goods ORDER BY goods_id OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY;OFFSET #{offset}是跳过的行数FETCH NEXT #{pageSize}是取多少行。ORDER BY必须有否则分页结果顺序不稳定。源码如果用的是通用Mapper或PageHelper在application.yml里配pagehelper: helper-dialect: sqlserver2012 reasonable: truehelper-dialect指定sqlserver2012PageHelper会使用OFFSET FETCH语法reasonabletrue让页码越界时自动回退到最后一页。如果你的SQL Server是2008 R2OFFSET FETCH不可用只能写ROW_NUMBER()窗口函数。5.3 MyBatis类型映射的三个常用细节第一个是nvarcharMyBatis里的String参数传到SQL Server的NVARCHAR字段时尽量在XML里写jdbcTypeNVARCHAR或让全局配置开启mapUnderscoreToCamelCase避免中文乱码和隐式转换。第二个是datetime2SQL Server的datetime2精度比java.util.Date高用LocalDateTime配合mybatis-jsr310扩展比较省事。第三个是money类型映射成BigDecimal直接用Double会在精度上出问题超市账单不能有这种误差。5.4 SQL Server连不上的四张排查表报错片段主要原因处理Login failed for user saSQL Server只开Windows认证或密码错误用混合认证模式或改连接串用户The TCP/IP connection to the host has failed实例未开启TCP/IP或防火墙拦截打开SQL Server配置管理器启用TCP/IP并重启服务SQL Server does not allow remote connections远程连接未启用实例属性-连接-允许远程连接与服务器建立连接时出现与网络相关或特定于实例的错误命名实例名不对URL写完整实例名如jdbc:sqlserver://127.0.0.1\SQLEXPRESS:1433第一行“Login failed”是最常见的Windows下安装SQL Server时默认是Windows认证项目连接要用sa账户和密码必须在实例属性里启用SQL Server和Windows混合认证模式。6. 部署后如何验证仓库系统已经跑通一条核对SQL和四个操作路径6.1 先建库还是先建表顺序别反拿到源码通常会有SQL Server数据库脚本文件。常见命名是01_create_database.sql、02_create_tables.sql、03_init_data.sql。在SSMS或sqlcmd里按顺序执行第1个脚本负责建库并切换上下文后面两个实际建表和填初始数据。直接用SSMS打开02建表脚本会报“当前数据库不存在”因为建库脚本还没跑。命令行执行的时候注意-D参数指定库名sqlcmd -S 127.0.0.1,1433 -U sa -P 密码 -i 01_create_database.sql sqlcmd -S 127.0.0.1,1433 -d SuperMarketDB -U sa -P 密码 -i 02_create_tables.sql sqlcmd -S 127.0.0.1,1433 -d SuperMarketDB -U sa -P 密码 -i 03_init_data.sql本地装SQL Server 2019、2022 Developer或Express版都可以Developer功能全且免费。SQL Server 2008 R2老源码要注意分页语法问题本标题的库存流水和窗口函数部分在2008上跑不动至少用2012以后版本。6.2 登录和进销存主流程验证启动SpringBoot应用后按超市日常路径过一遍新增一个商品分类再新增商品新增供应商并维护期初余额做一张采购入库单并审核确认库存数量增加做一张销售出库单确认库存减少批次余量对应减少做一次盘点单盘点差异应生成ADJUST流水在库存查询页面勾选“近效期预警”对照批次表检查预警列表。每步都在SQL Server Management Studio里打开对应表确认能定位是哪一层的问题。页面有数据但库里没数据通常是事务没提交库里改了页面没刷新多半是查询缓存或前端没刷新。6.3 用一条核对SQL检查库存数与流水是否一致SELECT g.goods_id, g.goods_name, ISNULL(s.quantity, 0) AS current_stock, ISNULL(SUM(f.change_qty), 0) AS flow_total FROM goods g LEFT JOIN stock s ON g.goods_id s.goods_id LEFT JOIN stock_flow f ON g.goods_id f.goods_id GROUP BY g.goods_id, g.goods_name, s.quantity HAVING ISNULL(SUM(f.change_qty), 0) ISNULL(s.quantity, 0);如果流水加总等于库存数量说明账实一致。这条查询有结果则说明有单据变化但库存没同步或流水写了一半事务回滚不一致。修正后再查直到没有多于的行。6.4 最后的日志开关把MyBatis里的SQL打出来开发阶段配置MyBatis打印SQL页面上看到怪异数据时直接看底层语句mybatis: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case把goods_name映射成goodsName不配的话很多实体属性是空值。log-impl用StdOutImpl会把每条SQL和参数打到控制台对定位漏参数、条件写错、IN查询元素超限都有直接帮助。上线前把它关掉或调到debug级别避免打印敏感数据。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价