ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于 Spring Boot + Vue3 实现仓库管理系统:从架构设计到核心功能落地

基于 Spring Boot + Vue3 实现仓库管理系统:从架构设计到核心功能落地

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系名片!

一、项目背景与目标

仓库管理系统(WMS,Warehouse Management System)是电商、制造、零售等行业的基础设施,负责商品入库、出库、库存盘点、库位管理等核心业务。传统单体系统在业务增长后往往面临扩展困难、前后端耦合严重、部署效率低等问题。

本文基于Spring Boot 3 + Vue 3前后端分离架构,实现一套可运行的仓库管理系统,覆盖商品管理、入库管理、出库管理、库存查询与预警、用户权限控制等核心模块,并给出关键代码与部署方案。

阅读本文后,你将掌握:前后端分离项目的工程结构划分、基于 JWT 的认证授权、库存扣减的并发控制思路,以及 Docker 化部署的完整流程。

二、技术选型与整体架构

2.1 技术栈

层次技术选型说明
后端框架Spring Boot 3.2基于 JDK 17,提供自动装配与内嵌容器
持久层MyBatis-Plus 3.5简化 CRUD,支持分页与逻辑删除
数据库MySQL 8.0存储业务数据,使用 InnoDB 事务
缓存Redis 7缓存库存热点数据、存储 Token 黑名单
认证Spring Security + JWT无状态认证,支持角色权限控制
前端框架Vue 3 + TypeScript组合式 API,类型安全
UI 组件库Element Plus后台管理系统常用组件
构建工具Vite 5快速冷启动与热更新
状态管理Pinia替代 Vuex,更轻量的状态管理

2.2 系统架构

flowchart LR A[浏览器 Vue3 SPA] -->|HTTP/JSON + JWT| B[Nginx 静态资源与反向代理] B --> C[Spring Boot 应用] C --> D[(MySQL 8.0)] C --> E[(Redis 7)] C --> F[文件存储 OSS/本地]

前端通过 Nginx 托管静态资源,并将/api前缀请求反向代理到后端服务;后端采用分层结构:Controller → Service → Mapper,统一异常处理与统一响应体封装。

三、数据库设计

核心表设计遵循「主数据 + 单据 + 流水」的思路,库存数量不直接冗余在商品表,而是通过库存流水汇总,保证可追溯。

-- 商品表 CREATE TABLE `product` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `code` VARCHAR(64) NOT NULL COMMENT '商品编码', `name` VARCHAR(128) NOT NULL COMMENT '商品名称', `category_id` BIGINT DEFAULT NULL COMMENT '分类ID', `unit` VARCHAR(16) DEFAULT '件' COMMENT '单位', `price` DECIMAL(12,2) DEFAULT 0.00 COMMENT '参考单价', `status` TINYINT DEFAULT 1 COMMENT '1启用 0停用', `deleted` TINYINT DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_code` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 库存表(按商品 + 库位维度) CREATE TABLE stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, location_code VARCHAR(64) DEFAULT NULL COMMENT '库位编码', quantity INT NOT NULL DEFAULT 0 COMMENT '可用库存', locked_quantity INT NOT NULL DEFAULT 0 COMMENT '锁定库存', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_product_wh_loc (product_id,warehouse_id,location_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 出入库单据表 CREATE TABLE stock_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '单号', type TINYINT NOT NULL COMMENT '1入库 2出库', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审核 1已完成 2已取消', operator_id BIGINT NOT NULL, remark VARCHAR(255) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 单据明细表 CREATE TABLE stock_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL, KEY idx_order_id (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

四、后端核心实现

4.1 工程结构

wms-server ├── wms-common # 通用工具、统一响应、异常 ├── wms-security # Spring Security + JWT ├── wms-module-product # 商品模块 ├── wms-module-stock # 库存与出入库模块 └── wms-boot # 启动模块与配置

4.2 统一响应与全局异常处理

@Data public class R<T> implements Serializable { private int code; private String message; private T data; public static &lt;T&gt; R&lt;T&gt; ok(T data) { R&lt;T&gt; r = new R&lt;&gt;(); r.code = 200; r.message = "success"; r.data = data; return r; } public static &lt;T&gt; R&lt;T&gt; fail(int code, String message) { R&lt;T&gt; r = new R&lt;&gt;(); r.code = code; r.message = message; return r; } }
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BizException.class) public R&lt;Void&gt; handleBiz(BizException e) { return R.fail(e.getCode(), e.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) public R&lt;Void&gt; handleValid(MethodArgumentNotValidException e) { String msg = e.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining("; ")); return R.fail(400, msg); } @ExceptionHandler(Exception.class) public R&lt;Void&gt; handleOther(Exception e) { log.error("系统异常", e); return R.fail(500, "系统繁忙,请稍后重试"); } }

4.3 JWT 认证过滤器

@Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtils jwtUtils; private final UserDetailsService userDetailsService; private final StringRedisTemplate redisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (StringUtils.hasText(header) &amp;&amp; header.startsWith("Bearer ")) { String token = header.substring(7); // 校验是否已登出 if (Boolean.TRUE.equals(redisTemplate.hasKey("jwt:blacklist:" + token))) { chain.doFilter(request, response); return; } try { String username = jwtUtils.getUsername(token); if (username != null &amp;&amp; SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails user = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(auth); } } catch (JwtException e) { log.warn("JWT 校验失败: {}", e.getMessage()); } } chain.doFilter(request, response); } }

4.4 库存扣减:乐观锁 + 重试

出库场景下并发扣减库存是典型问题。这里采用「乐观锁版本号 + 有限重试」方案,避免直接使用悲观锁带来的吞吐下降。

@Service @RequiredArgsConstructor public class StockService { private final StockMapper stockMapper; private static final int MAX_RETRY = 3; @Transactional(rollbackFor = Exception.class) public void deduct(Long productId, Long warehouseId, int quantity) { for (int i = 0; i &lt; MAX_RETRY; i++) { Stock stock = stockMapper.selectByProductAndWarehouse(productId, warehouseId); if (stock == null) { throw new BizException(404, "库存记录不存在"); } if (stock.getQuantity() - stock.getLockedQuantity() &lt; quantity) { throw new BizException(400, "库存不足,当前可用:" + (stock.getQuantity() - stock.getLockedQuantity())); } int rows = stockMapper.deductWithVersion( stock.getId(), quantity, stock.getVersion()); if (rows &gt; 0) { return; } log.warn("库存扣减冲突,第 {} 次重试, productId={}", i + 1, productId); } throw new BizException(409, "库存扣减失败,请稍后重试"); } }
<update id="deductWithVersion"> UPDATE stock SET quantity = quantity - #{quantity}, version = version + 1 WHERE id = #{id} AND version = #{version} AND quantity - locked_quantity >= #{quantity} </update>

五、前端核心实现

5.1 请求封装与 Token 注入

import axios, { AxiosInstance } from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/user' import router from '@/router' const request: AxiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000 }) request.interceptors.request.use((config) => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = Bearer ${userStore.token} } return config }) request.interceptors.response.use( (response) => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, (error) => { if (error.response?.status === 401) { useUserStore().logout() router.push('/login') } ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default request

5.2 库存列表页(组合式 API)

<template> <el-card> <el-form :inline="true" :model="query"> <el-form-item label="商品名称"> <el-input v-model="query.keyword" placeholder="编码或名称" clearable /> </el-form-item> <el-form-item> <el-button type="primary" @click="loadData">查询</el-button> </el-form-item> </el-form> &lt;el-table :data="list" v-loading="loading" border&gt; &lt;el-table-column prop="productCode" label="商品编码" width="160" /&gt; &lt;el-table-column prop="productName" label="商品名称" /&gt; &lt;el-table-column prop="warehouseName" label="仓库" width="140" /&gt; &lt;el-table-column prop="quantity" label="可用库存" width="120"&gt; &lt;template #default="{ row }"&gt; &lt;el-tag :type="row.quantity &lt; row.warnLine ? 'danger' : 'success'"&gt; {{ row.quantity }} &lt;/el-tag&gt; &lt;/template&gt; &lt;/el-table-column&gt; &lt;el-table-column prop="updateTime" label="更新时间" width="180" /&gt; &lt;/el-table&gt; &lt;el-pagination class="pager" v-model:current-page="query.page" v-model:page-size="query.size" :total="total" layout="total, prev, pager, next" @current-change="loadData" /&gt; </el-card> </template> <script setup lang="ts"> import { onMounted, reactive, ref } from 'vue' import request from '@/utils/request' interface StockVO { productCode: string productName: string warehouseName: string quantity: number warnLine: number updateTime: string } const loading = ref(false) const list = ref<StockVO[]>([]) const total = ref(0) const query = reactive({ keyword: '', page: 1, size: 10 }) async function loadData() { loading.value = true try { const data = await request.get('/api/stock/page', { params: query }) list.value = data.records total.value = data.total } finally { loading.value = false } } onMounted(loadData) </script> <style scoped> .pager { margin-top: 16px; justify-content: flex-end; } </style>

六、关键业务难点与解决方案

  • 库存一致性:出库采用乐观锁重试,入库直接累加;跨仓库调拨使用事务包裹两条库存更新,保证原子性。
  • 单据幂等:提交单据时以order_no唯一索引兜底,前端按钮加防重复提交锁。
  • 权限控制:基于 RBAC 模型,接口使用@PreAuthorize("hasAuthority('stock:out')")注解校验。
  • 库存预警:定时任务每日扫描低于预警线的库存,写入消息表并推送站内通知。
  • 操作审计:通过 AOP 切面记录关键操作日志,包含操作人、IP、请求参数与耗时。

七、部署方案

7.1 Docker Compose 编排

version: "3.8" services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: wms ports: - "3306:3306" volumes: - ./sql:/docker-entrypoint-initdb.d - mysql-data:/var/lib/mysql redis: image: redis:7-alpine ports: - "6379:6379" server: build: ./wms-server depends_on: - mysql - redis environment: SPRING_PROFILES_ACTIVE: prod ports: - "8080:8080" web: build: ./wms-web depends_on: - server ports: - "80:80" volumes: mysql-data:

7.2 Nginx 配置

server { listen 80; server_name wms.example.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://server:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

八、总结与后续优化方向

本文从架构设计、数据库建模、后端认证与库存并发控制、前端工程化到容器化部署,完整梳理了一套基于 Spring Boot 3 + Vue 3 的仓库管理系统实现路径。核心思路是:主数据与流水分离、并发扣减用乐观锁、认证无状态化、部署容器化。

后续可继续扩展的方向包括:

  1. 引入消息队列(RocketMQ / RabbitMQ)解耦库存变更事件,支撑异步对账。
  2. 增加批次与效期管理,支持先进先出(FIFO)出库策略。
  3. 接入报表引擎,输出库存周转率、呆滞库存等经营指标。
  4. 前端引入虚拟滚动与表格列配置,提升大数据量下的交互体验。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进