ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot + MyBatis 仓库管理系统源码实战:从环境搭建到事务库存联动

Spring Boot + MyBatis 仓库管理系统源码实战:从环境搭建到事务库存联动 简介本资源为基于JavaMySQL的Web企业仓库存储管理系统课程设计项目采用Spring Boot、Maven、MyBatis与B/S结构开发适合计算机相关专业学生、Java初学者及需要完成课程设计或毕业设计的开发者参考学习。系统覆盖客户、仓库、产品三类基本信息管理以及用户权限、入库出库记录、库存和系统日志等核心模块功能完整贴近企业仓储业务场景。压缩包共221个文件约6.95MB包含42个Java源码文件、28个HTML页面、24个JavaScript脚本、9个XML配置及SQL建表脚本、properties配置等另有大量gif、jpg、png图片与字体资源便于直接运行与二次开发。目前已有568人学习下载读者可获取完整项目源码、数据库脚本与前端静态资源快速理解Spring Boot整合MyBatis的增删改查实现、权限控制与日志记录思路适合作为课程设计参考或Java Web入门练手项目。1. 从一份能跑起来的仓库管理系统源码说起它到底解决了什么问题很多同学做课程设计时最头疼的不是写不出代码而是拼不出一套能跑通、能演示、能讲清楚业务闭环的完整系统。这份基于 Spring Boot Java Maven MySQL MyBatis 的企业仓库存储管理系统编号 100010160就是冲着这个痛点来的。它把客户、仓库、产品、用户、入库、出库、库存、日志八个模块全部打通前端用 Layui 搭界面后端走标准 B/S 结构导入 IDEA 配好数据库就能启动。适合正在找 JavaWeb 项目完整案例、需要一套能改能扩的课程设计底稿或者想拿它练手 MyBatis 增删改查和权限控制的同学。下面我按实际拆包和部署的顺序把这份资源从环境到代码到踩坑讲透。2. 环境搭建与数据库初始化从 MySQL 建库到 IDEA 导入2.1 技术栈选型为什么是 Spring Boot MyBatis 而不是 SSM 手写配置这套系统没有用传统的 SSM 三层 XML 配置而是走了 Spring Boot 的自动装配路线。原因很直接课程设计周期短XML 里配 SqlSessionFactory、MapperScannerConfigurer、事务管理器这些样板代码一旦写错启动就报No qualifying bean或者Invalid bound statement排查成本极高。Spring Boot 把数据源、MyBatis 的SqlSessionFactory、事务管理都通过application.yml和 starter 自动装配了你只需要在配置文件里写清楚数据库连接和 Mapper 扫描路径。MyBatis 在这里的角色是半自动 ORM。仓库管理系统里的库存表、入库记录表、出库记录表之间存在关联查询比如查某个产品的当前库存需要 join 库存表和产品表用 MyBatis 手写 SQL 比 JPA 的自动映射更可控字段名和表名对不上时改 XML 或注解就行不用去猜 Hibernate 的命名策略。对于课程设计来说这种可控性比“全自动”更重要因为答辩时老师大概率会问你 SQL 是怎么写的。Maven 负责依赖管理pom.xml里主要锁定了spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java和druid连接池。Layui 和 xadmin 这些前端资源放在src/main/resources/static下通过mvnw.cmd或mvn spring-boot:run启动后直接访问不需要额外配 Nginx。2.2 数据库建库建表与初始数据导入拿到源码后第一件事不是急着启动而是先把 MySQL 里的库和表建好。项目一般会附带一个.sql文件里面包含建表语句和初始数据。常见做法是在 Navicat 或者 MySQL Workbench 里新建一个数据库字符集选utf8mb4排序规则选utf8mb4_general_ci然后执行 SQL 文件。-- 创建数据库字符集必须用 utf8mb4否则中文产品名会乱码 CREATE DATABASE warehouse_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 切换到该数据库 USE warehouse_db; -- 示例产品信息表结构实际以源码附带的 sql 为准 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(100) NOT NULL COMMENT 产品名称, product_code VARCHAR(50) UNIQUE COMMENT 产品编号, category VARCHAR(50) COMMENT 产品分类, unit VARCHAR(20) COMMENT 计量单位, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 库存表关联产品 ID CREATE TABLE inventory ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, quantity INT DEFAULT 0 COMMENT 当前库存数量, warehouse_id INT COMMENT 所在仓库, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (product_id) REFERENCES product(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时注意两个参数ENGINEInnoDB是为了支持外键和事务仓库管理里的入库和出库操作往往需要同时更新记录表和库存表没有事务会出现“记录写了但库存没变”的数据不一致。utf8mb4而不是utf8是因为 Layui 前端提交的中文备注里可能包含生僻字或特殊符号utf8三字节存不下。初始数据里通常会有默认管理员账号常见的是admin/123456登录后第一件事就是改密码。如果 SQL 文件里没有插入默认用户就需要手动往user表里插一条密码字段可能是明文也可能是 MD5看源码里登录逻辑怎么写的。2.3 IDEA 导入与 application.yml 关键配置IDEA 里选择File - Open指向项目根目录等 Maven 依赖下载完。如果mvnw.cmd存在说明项目自带了 Maven Wrapper不需要本地装 Maven直接在终端跑./mvnw clean install就行。依赖下载慢的话在settings.xml里换国内镜像源这个不展开。配置文件在src/main/resources/application.yml核心就几行spring: datasource: url: jdbc:mysql://localhost:3306/warehouse_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.warehouse.entity configuration: map-underscore-to-camel-case: true server: port: 8080serverTimezoneAsia/Shanghai不加的话MySQL 8.x 驱动会报时区错误启动直接失败。useSSLfalse在本地开发环境关掉避免证书警告。map-underscore-to-camel-case: true让数据库的product_name自动映射到 Java 的productName省掉大量resultMap配置。mapper-locations指向 XML 文件位置如果 Mapper 接口和 XML 不在同一包路径下这个配置必须写对否则运行时报Invalid bound statement (not found)。启动类上一般会有MapperScan(com.warehouse.mapper)注解扫描 Mapper 接口。如果启动报Consider defining a bean of type xxxMapper先检查这个注解的包路径对不对。3. 核心模块的代码结构与增删改查实现3.1 客户、仓库、产品三个基础模块的 Controller-Service-Mapper 分层这三个模块的代码结构几乎一致拿产品模块举例。Controller 层接收前端 Layui 表格的请求返回 JSON 格式的数据。Layui 的 table 组件默认要求返回{code: 0, msg: , count: 总数, data: [列表]}这种格式所以 Controller 里通常有一个RequestMapping(/list)的方法接收page和limit参数做分页。RestController RequestMapping(/product) public class ProductController { Autowired private ProductService productService; // Layui 表格数据接口返回格式必须匹配 layui table 的约定 GetMapping(/list) public MapString, Object list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer limit, String productName) { // 分页插件计算 offset PageHelper.startPage(page, limit); ListProduct list productService.selectByCondition(productName); PageInfoProduct pageInfo new PageInfo(list); MapString, Object result new HashMap(); result.put(code, 0); result.put(msg, ); result.put(count, pageInfo.getTotal()); result.put(data, pageInfo.getList()); return result; } PostMapping(/add) public MapString, Object add(RequestBody Product product) { MapString, Object result new HashMap(); try { productService.insert(product); result.put(success, true); result.put(msg, 添加成功); } catch (Exception e) { result.put(success, false); result.put(msg, 添加失败 e.getMessage()); } return result; } }这里用了 PageHelper 做分页PageHelper.startPage(page, limit)必须紧跟在查询方法之前中间不能插入其他数据库操作否则分页会作用到错误的 SQL 上。RequestBody接收前端提交的 JSON 数据如果前端用的是表单序列化而不是 JSON这里要改成ModelAttribute或者直接接HttpServletRequest取参数。Service 层做业务校验比如产品编号不能重复、必填字段不能为空。Mapper 层对应 XML 文件里的 SQLselectByCondition用if testproductName ! null and productName ! 做动态查询。这套分层的好处是职责清晰改查询条件只动 XML改业务规则只动 ServiceController 基本不用动。3.2 入库出库与库存联动事务和并发扣减的处理入库和出库是这套系统里唯一涉及多表联动的地方。入库操作要同时做两件事往inbound_record表插一条记录同时更新inventory表对应产品的数量。出库则相反先检查库存够不够够的话扣减库存并写出库记录。Service public class InventoryService { Autowired private InventoryMapper inventoryMapper; Autowired private InboundRecordMapper inboundRecordMapper; // Transactional 保证入库记录和库存更新要么都成功要么都回滚 Transactional(rollbackFor Exception.class) public void inbound(InboundRecord record) { // 1. 插入入库记录 inboundRecordMapper.insert(record); // 2. 更新库存如果该产品还没有库存记录则新建 Inventory inventory inventoryMapper.selectByProductId(record.getProductId()); if (inventory null) { inventory new Inventory(); inventory.setProductId(record.getProductId()); inventory.setQuantity(record.getQuantity()); inventoryMapper.insert(inventory); } else { inventoryMapper.addQuantity(record.getProductId(), record.getQuantity()); } } Transactional(rollbackFor Exception.class) public void outbound(OutboundRecord record) { // 先查当前库存不足则抛异常触发回滚 Inventory inventory inventoryMapper.selectByProductId(record.getProductId()); if (inventory null || inventory.getQuantity() record.getQuantity()) { throw new RuntimeException(库存不足当前库存 (inventory null ? 0 : inventory.getQuantity())); } inventoryMapper.reduceQuantity(record.getProductId(), record.getQuantity()); outboundRecordMapper.insert(record); } }Transactional(rollbackFor Exception.class)里的rollbackFor必须写因为 Spring 默认只对RuntimeException回滚如果抛的是受检异常事务不会回滚库存扣了但记录没写数据就乱了。出库时的库存检查在并发场景下会有问题两个请求同时查到库存为 10都认为够扣结果扣成负数。课程设计级别通常不要求处理高并发但如果想稳一点可以在reduceQuantity的 SQL 里加条件AND quantity #{quantity}根据返回的影响行数判断是否扣减成功。!-- 带条件的库存扣减防止并发超卖 -- update idreduceQuantity UPDATE inventory SET quantity quantity - #{quantity} WHERE product_id #{productId} AND quantity #{quantity} /update这个 SQL 返回的int影响行数如果是 0说明库存不够Service 里据此抛异常回滚。这是常见做法比在 Java 里先查再扣更可靠。3.3 用户管理与系统日志权限控制和操作留痕用户管理模块包含用户的增删改查和权限修改。权限字段一般是一个role或者type字段区分管理员和普通操作员。登录拦截用 Spring 的HandlerInterceptor或者简单的 Session 判断在WebMvcConfigurer里注册拦截器排除登录页和静态资源。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /css/**, /js/**, /layui/**, /images/**); } }excludePathPatterns里必须把 Layui 的静态资源路径排除掉否则登录页的样式加载不出来页面变成裸 HTML。登录拦截器里从 Session 取用户信息取不到就重定向到登录页。系统日志模块记录用户的操作行为比如谁在什么时间删除了哪条入库记录。实现方式通常是在关键操作的方法里插入一条日志记录或者用 AOP 切面统一处理。日志表字段包括操作人、操作类型、操作对象、操作时间、IP 地址。查看日志的页面一般只做查询和展示不做修改删除保证留痕的可靠性。4. 避坑与常见问题排查4.1 启动报 Invalid bound statement (not found)现象项目启动不报错但访问列表接口时返回 500控制台提示Invalid bound statement (not found): com.warehouse.mapper.ProductMapper.selectByCondition。原因MyBatis 没有找到 Mapper 接口对应的 XML 文件。常见情况有三种XML 文件没有放在resources/mapper目录下application.yml里的mapper-locations路径写错Maven 打包时没有把src/main/java下的 XML 复制到target/classes。解决先确认 XML 文件位置和mapper-locations配置一致。如果 XML 放在 Java 包路径下需要在pom.xml的build里加resources配置把src/main/java下的**/*.xml也纳入资源目录。改完后执行mvn clean再启动。4.2 中文乱码从数据库到前端的三处字符集现象产品名称在数据库里看是正常的但页面上显示成问号或者乱码。原因字符集在三个环节可能出问题——数据库建库时用了latin1或utf8JDBC 连接串没有指定characterEncodingutf8前端页面没有声明meta charsetutf-8。解决建库时用utf8mb4连接串加useUnicodetruecharacterEncodingutf8HTML 页面 head 里加 charset 声明。三处都对齐后乱码基本消失。如果历史数据已经乱码需要导出后重新以正确字符集导入。4.3 出库时库存扣成负数现象出库记录写入了但库存数量变成负数或者库存表和出库记录对不上。原因出库逻辑里先查库存再扣减两个操作之间没有事务保护或者事务注解没生效。另一个常见原因是Transactional加在了 Controller 层而不是 Service 层Spring 的事务代理对 Controller 里的方法不生效。解决把事务注解加在 Service 的 public 方法上确保rollbackFor Exception.class。库存扣减的 SQL 加上AND quantity #{quantity}条件根据影响行数判断是否成功。出库前在 Service 里做库存校验不足时抛异常。4.4 Layui 表格分页不生效或总数显示为 0现象表格能显示数据但分页栏显示“共 0 条”翻页后数据不变。原因Controller 返回的 JSON 格式和 Layui table 要求的格式不匹配。Layui 要求count字段是总记录数data是当前页数据code为 0 表示成功。如果用了 PageHelper 但返回的是PageInfo对象本身字段名对不上。解决手动组装返回的 Map把pageInfo.getTotal()赋给countpageInfo.getList()赋给data。确认PageHelper.startPage()调用后紧接着就是查询方法中间没有其他数据库操作。4.5 MySQL 8 驱动时区报错导致启动失败现象启动时抛java.sql.SQLException: The server time zone value ?D1ú±ê×?ê±?? is unrecognized。原因MySQL 8 的驱动要求明确指定时区不指定就取系统默认中文系统下会报错。解决连接串加serverTimezoneAsia/Shanghai。如果还报错检查 MySQL 服务端的time_zone设置或者升级驱动版本。这个坑在 Windows 中文环境下尤其常见Linux 服务器上反而少一些。5. 二次开发与功能扩展的实操建议5.1 把库存预警做成定时任务或触发器现有系统只做了库存的增删改查没有预警机制。实际仓库管理里库存低于安全阈值时需要提醒。最简单的做法是在inventory表加一个min_quantity字段然后在查询库存的接口里加一个判断quantity min_quantity时返回一个标记前端用红色字体显示。如果想做得更自动可以用 Spring 的Scheduled注解写一个定时任务每天固定时间扫描库存表把低于阈值的记录写进系统日志或者发邮件。定时任务需要在启动类上加EnableScheduling然后在 Service 里写Component public class InventoryWarningTask { Autowired private InventoryMapper inventoryMapper; // 每天上午 9 点执行一次库存预警扫描 Scheduled(cron 0 0 9 * * ?) public void checkInventory() { ListInventory warningList inventoryMapper.selectBelowMinQuantity(); for (Inventory inv : warningList) { // 写入日志表操作类型标记为“库存预警” System.out.println(库存预警产品ID inv.getProductId() 当前库存 inv.getQuantity()); } } }cron表达式0 0 9 * * ?表示每天 9 点整触发。selectBelowMinQuantity的 SQL 是SELECT * FROM inventory WHERE quantity min_quantity。这个扩展不复杂但能让课程设计的业务完整度上一个台阶。5.2 用 AOP 统一记录系统日志现有日志模块可能是在每个操作里手动插入日志代码重复且容易漏。改成 AOP 切面更干净。定义一个Log注解在需要记录的方法上标注切面里拦截注解取当前登录用户和操作类型写入日志表。Aspect Component public class LogAspect { Autowired private SysLogMapper sysLogMapper; // 拦截所有标注了 Log 注解的方法 AfterReturning(pointcut annotation(com.warehouse.annotation.Log), returning result) public void recordLog(JoinPoint joinPoint, Object result) { SysLog log new SysLog(); // 从 Session 或 ThreadLocal 取当前用户 log.setOperator(UserContext.getCurrentUser()); log.setMethod(joinPoint.getSignature().getName()); log.setCreateTime(new Date()); sysLogMapper.insert(log); } }AfterReturning表示方法正常返回后记录如果方法抛异常则不记录。UserContext可以用ThreadLocal在拦截器里存当前登录用户Service 层直接取。这样加日志只需要在方法上加一个Log注解不用改业务代码。5.3 验证系统是否跑通的三个检查点部署完成后按这个顺序验证第一用默认管理员账号登录能进主页说明拦截器和用户表没问题第二新增一个产品然后在产品列表里能查到说明产品模块的增删改查链路通了第三对刚建的产品做一次入库和一次出库入库后查库存增加出库后查库存减少且入库出库记录都能在对应列表里看到说明事务和库存联动正常。三个检查点都过这套系统就算完整跑起来了。我自己的习惯是每次改完 Mapper XML 或者事务注解都强制走一遍“入库→查库存→出库→查库存”这个闭环因为血泪经验告诉我事务失效和 SQL 条件写错这两个坑光看代码是看不出来的只有跑一遍数据流才能暴露。从那以后我每次部署这类仓库管理系统都先把这三个检查点过一遍再动其他代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表