SpringBoot+MyBatis-Plus构建企业级仓库管理系统(WMS)全栈实践

📅 2026/7/20 10:58:07 👁️ 阅读次数 📝 编程学习
SpringBoot+MyBatis-Plus构建企业级仓库管理系统(WMS)全栈实践

在实际企业级应用开发中,仓库管理系统(Warehouse Management System, WMS)是连接采购、销售、库存和财务的核心业务系统。一个设计良好的WMS不仅能实现基础的出入库管理,更需要处理复杂的库存批次、多仓库调拨、库存预警和报表分析。对于Java开发者而言,使用SpringBoot快速构建一个稳定、可扩展的WMS后端服务,是检验全栈工程能力的一个典型场景。本文将以一个基于SpringBoot 2.7.x、MyBatis-Plus、Thymeleaf和MySQL的仓库管理系统为例,从零开始,详细拆解其技术选型、环境搭建、核心模块实现、数据库设计、前后端交互以及生产环境部署的完整链路。无论你是希望学习SpringBoot整合主流技术栈,还是需要为课程设计或中小型企业寻找一个可落地的WMS解决方案,本文都将提供一个清晰、可复现的实践指南。

1. 理解仓库管理系统的核心业务与技术栈选型

在动手编码之前,必须明确系统要解决的核心业务问题,这直接决定了数据库表结构设计和接口的复杂度。一个基础的仓库管理系统通常包含以下几个核心模块:用户与权限管理、商品/物料主数据管理、仓库与库位管理、采购入库、销售出库、库存调拨、库存盘点以及多维度的报表统计。

1.1 核心业务实体与关系

系统的核心是“库存”,它由多个实体关系共同维护:

  • 商品(Product):库存的客体,具有SKU、名称、规格等属性。
  • 仓库(Warehouse)与库位(Storage):库存的位置,支持多级管理。
  • 库存记录(Inventory):核心实体,记录某个商品在某个库位的实时数量。
  • 单据(Order):驱动库存变化的业务凭证,如采购单、销售单、调拨单。每张单据会关联多条单据明细(OrderItem)

库存数量的变化遵循一个基本原则:任何实物移动都必须有单据凭证。入库增加库存,出库减少库存,调拨则是一个仓库减少,另一个仓库增加。

1.2 技术栈选型与职责划分

根据输入材料中提到的技术关键词,我们采用以下成熟稳定的技术组合:

  • SpringBoot 2.7.18:作为项目基石,提供自动配置、内嵌Web容器和便捷的依赖管理,极大简化了传统SSM框架的繁琐配置。
  • Maven:项目构建与依赖管理工具。
  • MyBatis-Plus:在MyBatis基础上增强,提供通用的CRUD操作、分页插件、条件构造器等,能显著减少单表操作的SQL编写工作量。
  • MySQL 8.0:关系型数据库,用于存储业务数据。其事务特性(ACID)对保证库存数据的一致性至关重要。
  • Thymeleaf:服务器端Java模板引擎,用于渲染后端返回的HTML页面。它语法自然,能与SpringBoot无缝集成,适合快速开发管理后台。
  • Ajax:异步JavaScript和XML技术,用于实现页面的局部刷新,提升用户体验,如表单提交、数据搜索等操作无需重载整个页面。
  • Lombok:通过注解自动生成Java Bean的getter、setter、构造方法等代码,保持代码简洁。

这个技术栈的特点是“约定大于配置”,开发者可以更专注于业务逻辑而非框架整合。

2. 项目初始化与环境准备

一个清晰的起点是成功的一半。我们将严格按照企业级项目的标准来初始化工程结构。

2.1 使用Spring Initializr创建项目

推荐使用IntelliJ IDEA内置的Spring Initializr,它比手动配置更高效。

  1. 打开IDEA,选择File -> New -> Project
  2. 选择Spring Initializr,选择合适的JDK版本(推荐JDK 8或11)。
  3. 填写项目元数据:
    • Group:com.yourcompany
    • Artifact:warehouse-management
    • Type:Maven
    • Java Version:811
    • Packaging:Jar
  4. Dependencies中,搜索并添加以下依赖:
    • Spring Web(构建Web应用)
    • Thymeleaf(模板引擎)
    • MyBatis Framework(或直接添加MyBatis-Plus的依赖,稍后手动配置)
    • MySQL Driver(数据库驱动)
    • Lombok(简化代码)
  5. 点击Finish,IDEA会自动生成项目并下载依赖。

2.2 手动添加关键依赖与配置

Spring Initializr可能没有MyBatis-Plus,我们需要在pom.xml中手动添加。同时,为了处理JSON和分页,我们还需要添加其他依赖。

<!-- 在 pom.xml 的 <dependencies> 节点内添加 --> <!-- MyBatis-Plus 启动器 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MyBatis-Plus 代码生成器 (可选,用于开发阶段快速生成代码) --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.3.1</version> <scope>test</scope> </dependency> <!-- HikariCP 连接池 (SpringBoot 2.x默认,确保存在) --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </dependency> <!-- Jackson 用于JSON处理 (通常Spring Web已包含) --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <!-- 参数校验 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>

2.3 数据库配置与初始化

src/main/resources/application.yml(或application.properties) 中配置数据库连接和MyBatis-Plus。

# application.yml spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/warehouse_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: yourpassword hikari: connection-timeout: 30000 maximum-pool-size: 20 minimum-idle: 5 thymeleaf: cache: false # 开发阶段关闭缓存,修改HTML立即生效 mode: HTML encoding: UTF-8 # MyBatis-Plus 配置 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL,生产环境关闭 map-underscore-to-camel-case: true # 自动将下划线字段映射为驼峰属性 global-config: db-config: id-type: auto # 主键策略,数据库自增 logic-delete-field: deleted # 全局逻辑删除字段名(如果使用) logic-delete-value: 1 # 逻辑已删除值 logic-not-delete-value: 0 # 逻辑未删除值 mapper-locations: classpath:mapper/*.xml # XML映射文件位置

使用MySQL客户端(如MySQL Workbench或命令行)创建数据库:

CREATE DATABASE IF NOT EXISTS `warehouse_db` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

3. 数据库设计与核心实体映射

数据库设计是系统的骨架。我们设计一个简化但核心功能完整的表结构。

3.1 核心表结构SQL

以下是关键表的创建语句,包含了必要的字段、索引和注释。

-- 用户表 CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(加密后)', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `phone` varchar(20) DEFAULT NULL COMMENT '电话', `role` varchar(20) DEFAULT 'operator' COMMENT '角色:admin, manager, operator', `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-启用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; -- 商品表 CREATE TABLE `product` ( `id` bigint NOT NULL AUTO_INCREMENT, `product_code` varchar(50) NOT NULL COMMENT '商品编码/SKU', `product_name` varchar(200) NOT NULL COMMENT '商品名称', `spec` varchar(200) DEFAULT NULL COMMENT '规格型号', `unit` varchar(20) DEFAULT NULL COMMENT '单位(如:个,箱)', `category` varchar(100) DEFAULT NULL COMMENT '分类', `remark` varchar(500) DEFAULT NULL COMMENT '备注', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_product_code` (`product_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表'; -- 仓库表 CREATE TABLE `warehouse` ( `id` bigint NOT NULL AUTO_INCREMENT, `warehouse_code` varchar(50) NOT NULL COMMENT '仓库编码', `warehouse_name` varchar(200) NOT NULL COMMENT '仓库名称', `location` varchar(500) DEFAULT NULL COMMENT '仓库地址', `manager` varchar(50) DEFAULT NULL COMMENT '负责人', `status` tinyint DEFAULT '1' COMMENT '状态:0-停用,1-启用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_warehouse_code` (`warehouse_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='仓库表'; -- 库存表 (核心) CREATE TABLE `inventory` ( `id` bigint NOT NULL AUTO_INCREMENT, `product_id` bigint NOT NULL COMMENT '商品ID', `warehouse_id` bigint NOT NULL COMMENT '仓库ID', `quantity` decimal(15,3) NOT NULL DEFAULT '0.000' COMMENT '当前数量', `lock_quantity` decimal(15,3) NOT NULL DEFAULT '0.000' COMMENT '锁定数量(如已下单未出库)', `alert_quantity` decimal(15,3) DEFAULT '0.000' COMMENT '库存预警数量', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_product_warehouse` (`product_id`,`warehouse_id`), -- 唯一约束,一个商品在一个仓库只有一条记录 KEY `idx_warehouse_id` (`warehouse_id`), CONSTRAINT `fk_inventory_product` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`), CONSTRAINT `fk_inventory_warehouse` FOREIGN KEY (`warehouse_id`) REFERENCES `warehouse` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表'; -- 单据主表 CREATE TABLE `warehouse_order` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(50) NOT NULL COMMENT '单据编号', `order_type` varchar(20) NOT NULL COMMENT '单据类型:purchase-采购入库,sale-销售出库,transfer-调拨', `from_warehouse_id` bigint DEFAULT NULL COMMENT '源仓库ID(调拨、出库时有值)', `to_warehouse_id` bigint DEFAULT NULL COMMENT '目标仓库ID(调拨、入库时有值)', `total_amount` decimal(15,2) DEFAULT NULL COMMENT '总金额', `status` varchar(20) DEFAULT 'draft' COMMENT '状态:draft-草稿,confirmed-已确认,completed-已完成,cancelled-已取消', `create_user_id` bigint DEFAULT NULL COMMENT '制单人', `confirm_user_id` bigint DEFAULT NULL COMMENT '确认人', `remark` varchar(500) DEFAULT NULL COMMENT '备注', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `confirm_time` datetime DEFAULT NULL, `complete_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_order_type` (`order_type`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='仓库单据主表'; -- 单据明细表 CREATE TABLE `order_item` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL COMMENT '单据ID', `product_id` bigint NOT NULL COMMENT '商品ID', `quantity` decimal(15,3) NOT NULL COMMENT '数量', `unit_price` decimal(15,2) DEFAULT NULL COMMENT '单价', `total_price` decimal(15,2) DEFAULT NULL COMMENT '总价', `remark` varchar(200) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`), CONSTRAINT `fk_item_order` FOREIGN KEY (`order_id`) REFERENCES `warehouse_order` (`id`), CONSTRAINT `fk_item_product` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='单据明细表';

3.2 实体类与MyBatis-Plus映射

使用Lombok和MyBatis-Plus注解创建对应的Java实体类。以Inventory实体为例:

package com.yourcompany.warehouse.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; @Data @TableName("inventory") // 指定表名 public class Inventory { @TableId(type = IdType.AUTO) // 主键自增 private Long id; private Long productId; private Long warehouseId; private BigDecimal quantity; // 使用BigDecimal处理精确小数 private BigDecimal lockQuantity; private BigDecimal alertQuantity; private LocalDateTime updateTime; }

注意:金额和数量字段务必使用BigDecimal类型,避免使用floatdouble导致精度丢失,这是金融和仓储系统的基本要求。

4. 核心业务逻辑实现与分层架构

我们采用经典的三层架构:Controller(控制层)、Service(业务逻辑层)、Mapper(数据访问层)。这里以库存查询创建采购入库单两个核心流程为例。

4.1 数据访问层:Mapper与XML

MyBatis-Plus提供了强大的BaseMapper,对于单表CRUD,只需继承它即可获得通用方法。对于复杂联表查询,可以编写XML映射文件。

首先,创建InventoryMapper接口:

package com.yourcompany.warehouse.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.yourcompany.warehouse.entity.Inventory; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; import java.util.Map; public interface InventoryMapper extends BaseMapper<Inventory> { // 复杂查询:查询某个仓库下所有商品的库存及商品信息 @Select("SELECT i.*, p.product_code, p.product_name, p.spec, p.unit " + "FROM inventory i " + "LEFT JOIN product p ON i.product_id = p.id " + "WHERE i.warehouse_id = #{warehouseId}") List<Map<String, Object>> selectInventoryDetailByWarehouse(@Param("warehouseId") Long warehouseId); }

4.2 业务逻辑层:Service与事务管理

业务层负责处理核心逻辑,如库存数量的增减必须在一个事务内完成,保证数据一致性。

创建InventoryService接口及其实现:

package com.yourcompany.warehouse.service; import com.baomidou.mybatisplus.extension.service.IService; import com.yourcompany.warehouse.entity.Inventory; import java.math.BigDecimal; public interface InventoryService extends IService<Inventory> { /** * 增加库存(采购入库、调拨入库) * @param productId 商品ID * @param warehouseId 仓库ID * @param quantity 增加的数量 * @return 是否成功 */ boolean increaseInventory(Long productId, Long warehouseId, BigDecimal quantity); /** * 减少库存(销售出库、调拨出库) * @param productId 商品ID * @param warehouseId 仓库ID * @param quantity 减少的数量 * @return 是否成功(库存不足则失败) */ boolean decreaseInventory(Long productId, Long warehouseId, BigDecimal quantity); }
package com.yourcompany.warehouse.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.yourcompany.warehouse.entity.Inventory; import com.yourcompany.warehouse.mapper.InventoryMapper; import com.yourcompany.warehouse.service.InventoryService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; @Slf4j @Service public class InventoryServiceImpl extends ServiceImpl<InventoryMapper, Inventory> implements InventoryService { @Override @Transactional(rollbackFor = Exception.class) // 声明式事务,异常回滚 public boolean increaseInventory(Long productId, Long warehouseId, BigDecimal quantity) { if (quantity.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("入库数量必须大于0"); } LambdaQueryWrapper<Inventory> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Inventory::getProductId, productId) .eq(Inventory::getWarehouseId, warehouseId); Inventory inventory = this.getOne(wrapper); if (inventory == null) { // 如果该商品在该仓库尚无库存记录,则创建一条 inventory = new Inventory(); inventory.setProductId(productId); inventory.setWarehouseId(warehouseId); inventory.setQuantity(quantity); inventory.setLockQuantity(BigDecimal.ZERO); inventory.setAlertQuantity(BigDecimal.ZERO); inventory.setUpdateTime(LocalDateTime.now()); return this.save(inventory); } else { // 已有记录,则增加数量 BigDecimal newQuantity = inventory.getQuantity().add(quantity); inventory.setQuantity(newQuantity); inventory.setUpdateTime(LocalDateTime.now()); return this.updateById(inventory); } } @Override @Transactional(rollbackFor = Exception.class) public boolean decreaseInventory(Long productId, Long warehouseId, BigDecimal quantity) { if (quantity.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("出库数量必须大于0"); } LambdaQueryWrapper<Inventory> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Inventory::getProductId, productId) .eq(Inventory::getWarehouseId, warehouseId); Inventory inventory = this.getOne(wrapper, false); // 这里不加锁,高并发场景下需用悲观锁或乐观锁 if (inventory == null) { log.error("商品ID:{}在仓库ID:{}中无库存记录", productId, warehouseId); return false; } BigDecimal availableQuantity = inventory.getQuantity().subtract(inventory.getLockQuantity()); if (availableQuantity.compareTo(quantity) < 0) { log.error("商品ID:{}在仓库ID:{}中可用库存不足。可用:{}, 需求:{}", productId, warehouseId, availableQuantity, quantity); return false; } BigDecimal newQuantity = inventory.getQuantity().subtract(quantity); inventory.setQuantity(newQuantity); inventory.setUpdateTime(LocalDateTime.now()); return this.updateById(inventory); } }

关键点@Transactional注解确保了库存增减操作的原子性。在高并发场景下,上述代码存在超卖风险(多个线程同时读取到相同的可用库存并都认为足够)。生产环境必须使用悲观锁(SELECT ... FOR UPDATE)或乐观锁(版本号字段)来解决。

4.3 控制层:RESTful API与页面跳转

Controller层接收前端请求,调用Service,并返回结果。我们同时支持返回JSON(供Ajax调用)和跳转Thymeleaf页面。

package com.yourcompany.warehouse.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yourcompany.warehouse.entity.WarehouseOrder; import com.yourcompany.warehouse.service.WarehouseOrderService; import com.yourcompany.warehouse.vo.OrderQueryVO; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.HashMap; import java.util.Map; @Slf4j @Controller @RequestMapping("/order") public class WarehouseOrderController { @Autowired private WarehouseOrderService orderService; // 跳转到采购单列表页面 @GetMapping("/purchase/list") public String purchaseListPage(Model model) { // 可以在这里初始化一些页面数据,如仓库下拉列表 return "order/purchase_list"; // 对应 src/main/resources/templates/order/purchase_list.html } // 处理Ajax请求,返回采购单分页数据 (JSON格式) @PostMapping("/purchase/data") @ResponseBody // 表示返回JSON,而不是视图名 public Map<String, Object> getPurchaseOrderData(@RequestBody OrderQueryVO queryVO) { Page<WarehouseOrder> page = orderService.queryPurchaseOrderPage(queryVO); Map<String, Object> result = new HashMap<>(); result.put("code", 0); result.put("msg", "success"); result.put("count", page.getTotal()); result.put("data", page.getRecords()); return result; } // 创建采购单 (接收JSON请求体) @PostMapping("/purchase/create") @ResponseBody public Map<String, Object> createPurchaseOrder(@Valid @RequestBody WarehouseOrder order) { Map<String, Object> result = new HashMap<>(); try { boolean success = orderService.createPurchaseOrder(order); if (success) { result.put("code", 200); result.put("msg", "采购单创建成功"); } else { result.put("code", 500); result.put("msg", "采购单创建失败,请检查库存或数据"); } } catch (Exception e) { log.error("创建采购单异常", e); result.put("code", 500); result.put("msg", "系统错误: " + e.getMessage()); } return result; } }

5. 前端页面与Thymeleaf、Ajax集成

Thymeleaf模板与Ajax的结合,能构建出交互良好的管理后台。

5.1 Thymeleaf页面基础结构

src/main/resources/templates/order/purchase_list.html中:

<!DOCTYPE html> <html lang="zh" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>采购单管理</title> <!-- 引入Bootstrap等CSS --> <link rel="stylesheet" th:href="@{/webjars/bootstrap/5.1.3/css/bootstrap.min.css}"> <!-- 引入Layui等前端组件库CSS (示例) --> <link rel="stylesheet" th:href="@{/static/layui/css/layui.css}"> </head> <body> <div class="container-fluid"> <h2>采购单列表</h2> <!-- 查询条件表单 --> <form class="form-inline layui-form" id="searchForm"> <div class="form-group"> <label>单据编号:</label> <input type="text" name="orderNo" class="form-control" placeholder="请输入单据编号"> </div> <div class="form-group"> <label>状态:</label> <select name="status" class="form-control"> <option value="">全部</option> <option value="draft">草稿</option> <option value="confirmed">已确认</option> </select> </div> <button type="button" class="btn btn-primary" onclick="loadTableData()">搜索</button> <button type="button" class="btn btn-success" onclick="showCreateModal()">新建采购单</button> </form> <!-- 数据表格 --> <table id="orderTable" class="layui-table" lay-filter="orderTable"></table> </div> <!-- 引入jQuery、Layui等JS --> <script th:src="@{/webjars/jquery/3.6.0/jquery.min.js}"></script> <script th:src="@{/static/layui/layui.js}"></script> <script> // 使用Layui的table模块 layui.use('table', function(){ var table = layui.table; // 渲染表格 window.loadTableData = function() { var formData = $('#searchForm').serializeJSON(); // 需要引入serializeJSON插件或手动拼接 table.render({ elem: '#orderTable', url: '/order/purchase/data', // Ajax请求地址 method: 'post', contentType: 'application/json', where: JSON.stringify(formData), // 传递查询条件 page: true, // 开启分页 cols: [[ {field: 'orderNo', title: '单据编号', width: 180}, {field: 'orderType', title: '类型', width: 100}, {field: 'totalAmount', title: '总金额', width: 120}, {field: 'status', title: '状态', width: 100, templet: function(d){ var statusMap = {'draft':'草稿','confirmed':'已确认','completed':'已完成'}; return statusMap[d.status] || d.status; }}, {field: 'createTime', title: '创建时间', width: 180}, {title: '操作', width: 150, toolbar: '#operateBar'} ]] }); }; // 初始加载数据 loadTableData(); }); // 显示创建采购单的模态框 window.showCreateModal = function() { // 这里可以通过layer.open打开一个包含表单的弹出层 layer.open({ type: 2, title: '新建采购单', area: ['800px', '600px'], content: '/order/purchase/create/page' // 另一个Controller方法,返回创建页面的HTML片段 }); }; </script> <!-- 操作列模板 --> <script type="text/html" id="operateBar"> <a class="layui-btn layui-btn-xs" lay-event="detail">查看</a> {{# if(d.status === 'draft'){ }} <a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="confirm">确认</a> <a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="delete">删除</a> {{# } }} </script> </body> </html>

5.2 Ajax提交与后端交互

创建采购单的Ajax请求示例:

// 在创建采购单的模态框表单提交事件中 function submitPurchaseOrder() { var formData = collectFormData(); // 收集表单数据 $.ajax({ url: '/order/purchase/create', type: 'POST', contentType: 'application/json', data: JSON.stringify(formData), success: function(res) { if (res.code === 200) { layer.msg('创建成功'); layer.closeAll(); // 关闭弹出层 loadTableData(); // 刷新列表 } else { layer.msg('创建失败:' + res.msg); } }, error: function() { layer.msg('网络请求失败'); } }); }

6. 项目运行、验证与常见问题排查

6.1 启动与访问

  1. 找到主启动类WarehouseManagementApplication(通常位于src/main/java/com/yourcompany/warehouse下),运行其main方法。
  2. 控制台看到Started ... in X seconds表示启动成功。
  3. 打开浏览器,访问http://localhost:8080。你需要编写一个简单的IndexController来跳转到登录页或仪表盘。
  4. 按照业务逻辑,依次测试:用户登录、商品管理、仓库管理、创建采购单、审核采购单、查询库存变化。

6.2 关键验证点

  • 数据库连接:启动时无DataSource相关错误。
  • 页面渲染:能正常访问.html页面,Thymeleaf标签被正确解析。
  • Ajax交互:浏览器开发者工具(F12)的Network标签中,能看到Ajax请求成功(状态码200)并返回预期的JSON数据。
  • 业务逻辑:创建采购单后,对应商品的库存数量应增加。创建销售单后,库存应减少,且不能减为负数。
  • 事务测试:在入库Service方法中故意抛出异常,查看数据库数据是否回滚。

6.3 常见问题与排查路径

在开发过程中,你几乎一定会遇到以下问题。请按此清单排查:

问题现象可能原因检查方式处理建议
启动报错:Failed to configure a DataSource1.application.yml中数据库配置错误。
2. MySQL服务未启动。
3. 依赖未正确引入。
1. 检查yml文件缩进和冒号后的空格。
2. 检查MySQL服务状态和端口。
3. 检查pom.xml中mysql-connector-java依赖。
1. 确保配置格式正确。
2. 启动MySQL服务。
3. 执行mvn clean compile
访问页面报4041. Controller请求路径映射错误。
2. 静态资源未放行。
3. Thymeleaf模板文件位置错误。
1. 检查Controller的@RequestMapping和方法上的@GetMapping/@PostMapping
2. 检查SpringBoot静态资源默认目录 (static,public)。
3. 检查HTML文件是否在templates目录下。
1. 使用IDEA的“Find Usages”功能检查路径。
2. 自定义WebMvcConfigurer放行资源。
3. 确保路径与Controller返回的字符串一致。
Ajax请求报405 (Method Not Allowed)Controller方法请求方式不匹配(如前端用POST,后端是GET)。查看浏览器Network面板中请求的Method,并与后端@RequestMappingmethod属性或@PostMapping等注解对比。统一前后端请求方式。
页面显示${...}原样文本Thymeleaf未正确解析。1. 检查HTML文件开头是否有xmlns:th="http://www.thymeleaf.org"
2. 检查是否错误地访问了.html静态文件而非经过Controller跳转。
1. 确保命名空间正确。
2. 页面必须通过Controller的return "pageName";来访问。
MyBatis-Plus查询不到数据1. 实体类字段名与数据库列名映射失败。
2. 查询条件构造错误。
3. 数据本身不存在。
1. 开启SQL日志 (mybatis-plus.configuration.log-impl),查看实际执行的SQL。
2. 检查@TableField注解或全局的驼峰转换配置。
3. 直接在数据库客户端执行打印出的SQL。
1. 确认map-underscore-to-camel-case: true已配置。
2. 使用LambdaQueryWrapper避免字段名拼写错误。
事务不生效1. 方法不是public
2. 异常被捕获未抛出。
3. 方法在同一个类内部调用,未经过代理。
1. 检查方法访问修饰符。
2. 检查@TransactionalrollbackFor属性。
3. 检查调用方式(应通过Spring代理对象调用)。
1. 确保方法是public
2. 设置@Transactional(rollbackFor = Exception.class)
3. 将事务方法放到另一个Service中,通过@Autowired注入调用。

7. 生产环境部署与优化建议

将学习项目推向生产环境,需要考虑更多因素。

7.1 应用打包与部署

  1. 打包:使用Maven命令mvn clean package -DskipTests生成可执行的JAR文件(位于target/目录下)。
  2. 分离配置:将application.yml中的敏感信息(数据库密码)和与环境相关的配置(服务器端口)提取到外部的application-prod.yml,并通过启动参数--spring.config.location指定。
    java -jar warehouse-management.jar --spring.config.location=file:/path/to/application-prod.yml
  3. 进程管理:使用systemd(Linux) 或nssm(Windows) 将JAR包注册为系统服务,实现开机自启和故障重启。

7.2 数据库优化

  • 索引:为高频查询条件(如order_no,status,create_time)和关联字段(外键)建立索引。
  • 连接池:调整HikariCP参数,如maximum-pool-size(根据数据库和服务器的CPU、内存调整,通常建议在10-50之间)。
  • 慢查询日志:开启MySQL慢查询日志,定期分析并优化执行时间长的SQL。

7.3 应用性能与安全

  • 关闭开发配置:生产环境务必关闭mybatis-plus.configuration.log-impl的SQL打印和thymeleaf.cache
  • API限流与鉴权:为关键业务接口(如库存扣减)添加限流(如使用Sentinel),防止恶意请求。实现完整的基于角色的访问控制(RBAC)。
  • 输入校验:在Controller层使用@Valid注解配合JSR-303校验注解(如@NotBlank,@Min),并在全局异常处理器中处理MethodArgumentNotValidException,返回友好的错误信息。
  • 全局异常处理:使用@ControllerAdvice@ExceptionHandler统一处理业务异常和系统异常,避免将堆栈信息直接暴露给前端。
  • 库存并发控制:将decreaseInventory方法中的查询和更新操作放在一个数据库事务中,并使用悲观锁SELECT ... FOR UPDATE,或为inventory表增加version字段实现乐观锁,这是防止超卖的关键。

7.4 监控与日志

  • 日志:配置logback-spring.xml,将日志按级别(INFO, ERROR)输出到不同文件,并设置合理的滚动策略和保存天数。
  • 健康检查:Spring Boot Actuator 提供/actuator/health端点,可用于监控应用状态。
  • APM:考虑集成SkyWalking、Pinpoint等应用性能监控工具,追踪请求链路和SQL性能。

通过以上步骤,你不仅能够搭建并运行一个基础的SpringBoot仓库管理系统,更能理解从数据库设计、业务编码到生产部署的完整闭环。这个项目可以作为你深入理解SpringBoot生态和复杂业务系统设计的起点,后续可以在此基础上扩展更复杂的特性,如多级审批流、序列号管理、财务结算集成等。