WMS系统演进------从单体到微服务
实战指南:基于Java + Vue + MySQL的完整搭建教程
目录
一、架构演进背景与目标

1.1 业务场景
WMS(Warehouse Management System,仓库管理系统)是供应链的核心系统,典型功能包括:
- 入库管理:采购入库、退货入库、调拨入库
- 出库管理:销售出库、领料出库、调拨出库
- 库存管理:实时库存、库存预警、盘点管理
- 库位管理:库位规划、货位分配、路径优化
- 报表统计:库存报表、作业效率分析
1.2 演进路线图
┌─────────────────────────────────────────────────────────────┐
│ Phase 1: 单体架构 (0-6个月) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Spring Boot + Vue + MySQL │ │
│ │ 快速交付,验证业务模型 │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↓ 业务增长 │
│ Phase 2: 单体 + 领域拆分 (6-12个月) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 代码层面拆分领域,数据库共享,为微服务做准备 │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↓ 团队扩张 │
│ Phase 3: 微服务架构 (12个月+) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 库存服务 │ 订单服务 │ 库位服务 │ 报表服务 │ 网关层 │ │
│ │ 独立部署,独立扩展,独立演进 │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
二、单体架构阶段:快速启动
2.1 技术选型
| 层级 | 技术栈 | 版本 |
|---|---|---|
| 后端 | Spring Boot | 3.2.x |
| ORM | MyBatis-Plus | 3.5.x |
| 数据库 | MySQL | 8.0 |
| 缓存 | Caffeine | 3.1.x |
| 前端 | Vue 3 + Element Plus | 3.4.x |
| 构建 | Maven | 3.9.x |
2.2 项目结构
wms-monolithic/
├── wms-server/ # 后端服务
│ ├── src/main/java/com/wms/
│ │ ├── controller/ # 控制器层
│ │ ├── service/ # 业务层
│ │ ├── mapper/ # 数据访问层
│ │ ├── entity/ # 实体类
│ │ ├── dto/ # 数据传输对象
│ │ └── config/ # 配置类
│ └── src/main/resources/
│ └── application.yml
├── wms-web/ # 前端项目
│ ├── src/
│ │ ├── views/ # 页面组件
│ │ ├── components/ # 公共组件
│ │ ├── api/ # API接口
│ │ └── store/ # 状态管理
│ └── package.json
└── sql/ # 数据库脚本
└── init.sql
2.3 后端核心代码
2.3.1 依赖配置 (pom.xml)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.wms</groupId>
<artifactId>wms-server</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- 缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.3.2 配置文件 (application.yml)
yaml
server:
port: 8080
spring:
application:
name: wms-monolithic
datasource:
url: jdbc:mysql://localhost:3306/wms_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: wms_user
password: wms_password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
# MyBatis Plus配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
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
# 日志配置
logging:
level:
com.wms.mapper: debug
2.3.3 数据库实体类
库存实体 (Inventory.java)
java
package com.wms.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("wms_inventory")
public class Inventory {
@TableId(type = IdType.AUTO)
private Long id;
// 商品SKU编码
private String skuCode;
// 商品名称
private String skuName;
// 仓库编码
private String warehouseCode;
// 库位编码
private String locationCode;
// 可用库存数量
private Integer availableQty;
// 锁定库存数量(已分配但未出库)
private Integer lockedQty;
// 在途库存数量
private Integer inTransitQty;
// 批次号
private String batchNo;
// 生产日期
private LocalDateTime productionDate;
// 过期日期
private LocalDateTime expiryDate;
// 库存状态:0-正常 1-冻结 2-破损
private Integer status;
// 创建时间
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
// 更新时间
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
// 逻辑删除
@TableLogic
private Integer deleted;
}
入库单实体 (InboundOrder.java)
java
package com.wms.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("wms_inbound_order")
public class InboundOrder {
@TableId(type = IdType.AUTO)
private Long id;
// 入库单号:RK202403300001
private String orderNo;
// 入库类型:1-采购入库 2-退货入库 3-调拨入库
private Integer inboundType;
// 关联单号(采购单号/退货单号/调拨单号)
private String sourceOrderNo;
// 仓库编码
private String warehouseCode;
// 供应商编码
private String supplierCode;
// 入库单状态:0-待入库 1-部分入库 2-已完成 3-已取消
private Integer status;
// 预计到货时间
private LocalDateTime expectTime;
// 实际入库时间
private LocalDateTime actualTime;
// 备注
private String remark;
// 创建人
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
}
入库单明细 (InboundOrderDetail.java)
java
package com.wms.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
@Data
@TableName("wms_inbound_order_detail")
public class InboundOrderDetail {
@TableId(type = IdType.AUTO)
private Long id;
// 入库单ID
private Long orderId;
// 商品SKU
private String skuCode;
// 预计入库数量
private Integer expectQty;
// 实际入库数量
private Integer actualQty;
// 入库库位
private String locationCode;
// 批次号
private String batchNo;
// 状态:0-待入库 1-已入库
private Integer status;
}
2.3.4 数据访问层
库存Mapper
java
package com.wms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.wms.entity.Inventory;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
public interface InventoryMapper extends BaseMapper<Inventory> {
/**
* 查询可用库存(带悲观锁)
*/
@Select("SELECT * FROM wms_inventory WHERE sku_code = #{skuCode} " +
"AND warehouse_code = #{warehouseCode} AND status = 0 " +
"FOR UPDATE")
List<Inventory> selectAvailableWithLock(@Param("skuCode") String skuCode,
@Param("warehouseCode") String warehouseCode);
/**
* 扣减可用库存,增加锁定库存
*/
@Update("UPDATE wms_inventory SET available_qty = available_qty - #{qty}, " +
"locked_qty = locked_qty + #{qty} " +
"WHERE sku_code = #{skuCode} AND warehouse_code = #{warehouseCode} " +
"AND available_qty >= #{qty}")
int lockStock(@Param("skuCode") String skuCode,
@Param("warehouseCode") String warehouseCode,
@Param("qty") Integer qty);
/**
* 查询库存预警列表
*/
@Select("SELECT * FROM wms_inventory WHERE available_qty <= 10")
List<Inventory> selectWarningList();
}
2.3.5 业务服务层
库存服务
java
package com.wms.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.wms.dto.StockLockDTO;
import com.wms.entity.Inventory;
import com.wms.vo.InventoryVO;
import java.util.List;
public interface InventoryService extends IService<Inventory> {
/**
* 查询实时库存
*/
InventoryVO getRealtimeStock(String skuCode, String warehouseCode);
/**
* 锁定库存(出库预占)
*/
boolean lockStock(StockLockDTO lockDTO);
/**
* 释放库存(取消预占)
*/
boolean releaseStock(String skuCode, String warehouseCode, Integer qty);
/**
* 确认出库(扣减锁定库存)
*/
boolean confirmOutbound(String skuCode, String warehouseCode, Integer qty);
/**
* 入库上架
*/
boolean inboundPutaway(String skuCode, String warehouseCode,
String locationCode, Integer qty);
/**
* 获取库存预警列表
*/
List<InventoryVO> getWarningList();
}
库存服务实现
java
package com.wms.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wms.dto.StockLockDTO;
import com.wms.entity.Inventory;
import com.wms.mapper.InventoryMapper;
import com.wms.service.InventoryService;
import com.wms.vo.InventoryVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class InventoryServiceImpl extends ServiceImpl<InventoryMapper, Inventory>
implements InventoryService {
@Override
@Cacheable(value = "inventory", key = "#skuCode + ':' + #warehouseCode")
public InventoryVO getRealtimeStock(String skuCode, String warehouseCode) {
Inventory inventory = lambdaQuery()
.eq(Inventory::getSkuCode, skuCode)
.eq(Inventory::getWarehouseCode, warehouseCode)
.eq(Inventory::getStatus, 0)
.one();
if (inventory == null) {
return null;
}
return convertToVO(inventory);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean lockStock(StockLockDTO lockDTO) {
// 使用乐观锁防止超卖
int affected = baseMapper.lockStock(
lockDTO.getSkuCode(),
lockDTO.getWarehouseCode(),
lockDTO.getQty()
);
if (affected == 0) {
log.warn("库存不足,锁定失败: {}", lockDTO);
throw new RuntimeException("库存不足,锁定失败");
}
log.info("库存锁定成功: {}", lockDTO);
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean releaseStock(String skuCode, String warehouseCode, Integer qty) {
lambdaUpdate()
.eq(Inventory::getSkuCode, skuCode)
.eq(Inventory::getWarehouseCode, warehouseCode)
.setSql("locked_qty = locked_qty - " + qty)
.setSql("available_qty = available_qty + " + qty)
.ge(Inventory::getLockedQty, qty) // 防止减到负数
.update();
log.info("库存释放成功: skuCode={}, qty={}", skuCode, qty);
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean confirmOutbound(String skuCode, String warehouseCode, Integer qty) {
lambdaUpdate()
.eq(Inventory::getSkuCode, skuCode)
.eq(Inventory::getWarehouseCode, warehouseCode)
.setSql("locked_qty = locked_qty - " + qty)
.ge(Inventory::getLockedQty, qty)
.update();
log.info("出库确认成功: skuCode={}, qty={}", skuCode, qty);
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean inboundPutaway(String skuCode, String warehouseCode,
String locationCode, Integer qty) {
// 查询是否已存在库存记录
Inventory exist = lambdaQuery()
.eq(Inventory::getSkuCode, skuCode)
.eq(Inventory::getWarehouseCode, warehouseCode)
.eq(Inventory::getLocationCode, locationCode)
.one();
if (exist != null) {
// 更新现有库存
lambdaUpdate()
.eq(Inventory::getId, exist.getId())
.setSql("available_qty = available_qty + " + qty)
.update();
} else {
// 新建库存记录
Inventory inventory = new Inventory();
inventory.setSkuCode(skuCode);
inventory.setWarehouseCode(warehouseCode);
inventory.setLocationCode(locationCode);
inventory.setAvailableQty(qty);
inventory.setLockedQty(0);
inventory.setInTransitQty(0);
inventory.setStatus(0);
save(inventory);
}
log.info("入库上架成功: skuCode={}, locationCode={}, qty={}",
skuCode, locationCode, qty);
return true;
}
@Override
public List<InventoryVO> getWarningList() {
List<Inventory> list = baseMapper.selectWarningList();
return list.stream().map(this::convertToVO).collect(Collectors.toList());
}
private InventoryVO convertToVO(Inventory inventory) {
InventoryVO vo = new InventoryVO();
vo.setSkuCode(inventory.getSkuCode());
vo.setSkuName(inventory.getSkuName());
vo.setWarehouseCode(inventory.getWarehouseCode());
vo.setLocationCode(inventory.getLocationCode());
vo.setAvailableQty(inventory.getAvailableQty());
vo.setLockedQty(inventory.getLockedQty());
vo.setTotalQty(inventory.getAvailableQty() + inventory.getLockedQty());
vo.setStatus(inventory.getStatus());
return vo;
}
}
2.3.6 控制器层
java
package com.wms.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.wms.common.Result;
import com.wms.dto.InboundOrderDTO;
import com.wms.dto.StockLockDTO;
import com.wms.entity.InboundOrder;
import com.wms.service.InboundOrderService;
import com.wms.service.InventoryService;
import com.wms.vo.InventoryVO;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class WmsController {
private final InventoryService inventoryService;
private final InboundOrderService inboundOrderService;
// ==================== 库存管理接口 ====================
@GetMapping("/inventory/{skuCode}")
public Result<InventoryVO> getStock(@PathVariable String skuCode,
@RequestParam String warehouseCode) {
InventoryVO vo = inventoryService.getRealtimeStock(skuCode, warehouseCode);
return Result.success(vo);
}
@PostMapping("/inventory/lock")
public Result<Boolean> lockStock(@RequestBody @Validated StockLockDTO dto) {
boolean success = inventoryService.lockStock(dto);
return Result.success(success);
}
@PostMapping("/inventory/release")
public Result<Boolean> releaseStock(@RequestBody StockLockDTO dto) {
boolean success = inventoryService.releaseStock(
dto.getSkuCode(), dto.getWarehouseCode(), dto.getQty());
return Result.success(success);
}
@GetMapping("/inventory/warning")
public Result<List<InventoryVO>> getWarningList() {
return Result.success(inventoryService.getWarningList());
}
// ==================== 入库管理接口 ====================
@PostMapping("/inbound")
public Result<Long> createInboundOrder(@RequestBody @Validated InboundOrderDTO dto) {
Long orderId = inboundOrderService.createOrder(dto);
return Result.success(orderId);
}
@PostMapping("/inbound/{orderId}/confirm")
public Result<Boolean> confirmInbound(@PathVariable Long orderId,
@RequestBody List<InboundDetailDTO> details) {
boolean success = inboundOrderService.confirmInbound(orderId, details);
return Result.success(success);
}
@GetMapping("/inbound")
public Result<Page<InboundOrder>> listInboundOrders(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) Integer status) {
Page<InboundOrder> result = inboundOrderService.lambdaQuery()
.eq(status != null, InboundOrder::getStatus, status)
.orderByDesc(InboundOrder::getCreateTime)
.page(new Page<>(page, size));
return Result.success(result);
}
}
2.4 前端核心代码 (Vue 3)
2.4.1 项目初始化
bash
# 创建Vue项目
npm create vue@latest wms-web
cd wms-web
# 安装依赖
npm install
npm install element-plus axios vue-router pinia
2.4.2 库存管理页面
vue
<!-- src/views/inventory/InventoryList.vue -->
<template>
<div class="inventory-container">
<el-card class="search-card">
<el-form :model="searchForm" inline>
<el-form-item label="SKU编码">
<el-input v-model="searchForm.skuCode" placeholder="请输入SKU编码" clearable />
</el-form-item>
<el-form-item label="仓库">
<el-select v-model="searchForm.warehouseCode" placeholder="选择仓库">
<el-option label="北京仓" value="BJ01" />
<el-option label="上海仓" value="SH01" />
<el-option label="广州仓" value="GZ01" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card">
<template #header>
<div class="card-header">
<span>库存列表</span>
<el-button type="warning" @click="showWarningList">库存预警</el-button>
</div>
</template>
<el-table :data="inventoryList" v-loading="loading" border>
<el-table-column prop="skuCode" label="SKU编码" width="120" />
<el-table-column prop="skuName" label="商品名称" min-width="200" />
<el-table-column prop="warehouseCode" label="仓库" width="100" />
<el-table-column prop="locationCode" label="库位" width="120" />
<el-table-column prop="availableQty" label="可用库存" width="100">
<template #default="{ row }">
<el-tag :type="row.availableQty <= 10 ? 'danger' : 'success'">
{{ row.availableQty }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="lockedQty" label="锁定库存" width="100" />
<el-table-column prop="totalQty" label="总库存" width="100" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleLock(row)">
锁定
</el-button>
<el-button type="success" size="small" @click="handleMove(row)">
移库
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:total="pagination.total"
layout="total, sizes, prev, pager, next"
@change="handlePageChange"
/>
</el-card>
<!-- 库存锁定弹窗 -->
<el-dialog v-model="lockDialogVisible" title="库存锁定" width="500px">
<el-form :model="lockForm" label-width="100px">
<el-form-item label="SKU编码">
<span>{{ lockForm.skuCode }}</span>
</el-form-item>
<el-form-item label="当前可用">
<span>{{ lockForm.availableQty }}</span>
</el-form-item>
<el-form-item label="锁定数量" required>
<el-input-number
v-model="lockForm.qty"
:min="1"
:max="lockForm.availableQty"
/>
</el-form-item>
<el-form-item label="出库单号">
<el-input v-model="lockForm.orderNo" placeholder="关联出库单号" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="lockDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmLock" :loading="submitting">
确认锁定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getInventoryList, lockStock } from '@/api/inventory'
const loading = ref(false)
const inventoryList = ref([])
const lockDialogVisible = ref(false)
const submitting = ref(false)
const searchForm = reactive({
skuCode: '',
warehouseCode: ''
})
const pagination = reactive({
page: 1,
size: 10,
total: 0
})
const lockForm = reactive({
skuCode: '',
availableQty: 0,
qty: 1,
orderNo: ''
})
const fetchData = async () => {
loading.value = true
try {
const res = await getInventoryList({
...searchForm,
page: pagination.page,
size: pagination.size
})
inventoryList.value = res.data.records
pagination.total = res.data.total
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.page = 1
fetchData()
}
const resetSearch = () => {
Object.keys(searchForm).forEach(key => searchForm[key] = '')
handleSearch()
}
const handleLock = (row) => {
lockForm.skuCode = row.skuCode
lockForm.availableQty = row.availableQty
lockForm.qty = 1
lockForm.orderNo = ''
lockDialogVisible.value = true
}
const confirmLock = async () => {
if (!lockForm.orderNo) {
ElMessage.warning('请输入出库单号')
return
}
submitting.value = true
try {
await lockStock({
skuCode: lockForm.skuCode,
warehouseCode: searchForm.warehouseCode,
qty: lockForm.qty,
orderNo: lockForm.orderNo
})
ElMessage.success('库存锁定成功')
lockDialogVisible.value = false
fetchData()
} finally {
submitting.value = false
}
}
onMounted(fetchData)
</script>
<style scoped>
.inventory-container {
padding: 20px;
}
.search-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
2.4.3 API封装
javascript
// src/api/inventory.js
import request from '@/utils/request'
export const getInventoryList = (params) => {
return request.get('/inventory', { params })
}
export const getStockDetail = (skuCode, warehouseCode) => {
return request.get(`/inventory/${skuCode}`, { params: { warehouseCode } })
}
export const lockStock = (data) => {
return request.post('/inventory/lock', data)
}
export const releaseStock = (data) => {
return request.post('/inventory/release', data)
}
export const getWarningList = () => {
return request.get('/inventory/warning')
}
javascript
// src/utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1',
timeout: 10000
})
// 请求拦截器
request.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
// 响应拦截器
request.interceptors.response.use(
response => {
const { code, message, data } = response.data
if (code === 200) {
return data
} else {
ElMessage.error(message || '请求失败')
return Promise.reject(new Error(message))
}
},
error => {
ElMessage.error(error.response?.data?.message || '网络错误')
return Promise.reject(error)
}
)
export default request
2.5 数据库初始化脚本
sql
-- sql/init.sql
CREATE DATABASE IF NOT EXISTS wms_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE wms_db;
-- 创建用户(生产环境请使用更复杂的密码)
CREATE USER IF NOT EXISTS 'wms_user'@'%' IDENTIFIED BY 'wms_password';
GRANT ALL PRIVILEGES ON wms_db.* TO 'wms_user'@'%';
FLUSH PRIVILEGES;
-- 库存表
CREATE TABLE wms_inventory (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
sku_code VARCHAR(50) NOT NULL COMMENT 'SKU编码',
sku_name VARCHAR(200) COMMENT '商品名称',
warehouse_code VARCHAR(20) NOT NULL COMMENT '仓库编码',
location_code VARCHAR(50) COMMENT '库位编码',
available_qty INT DEFAULT 0 COMMENT '可用库存',
locked_qty INT DEFAULT 0 COMMENT '锁定库存',
in_transit_qty INT DEFAULT 0 COMMENT '在途库存',
batch_no VARCHAR(50) COMMENT '批次号',
production_date DATETIME COMMENT '生产日期',
expiry_date DATETIME COMMENT '过期日期',
status TINYINT DEFAULT 0 COMMENT '状态:0-正常 1-冻结 2-破损',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0,
INDEX idx_sku_warehouse (sku_code, warehouse_code),
INDEX idx_location (warehouse_code, location_code)
) ENGINE=InnoDB COMMENT='库存表';
-- 入库单表
CREATE TABLE wms_inbound_order (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(50) NOT NULL UNIQUE COMMENT '入库单号',
inbound_type TINYINT NOT NULL COMMENT '入库类型:1-采购 2-退货 3-调拨',
source_order_no VARCHAR(50) COMMENT '关联单号',
warehouse_code VARCHAR(20) NOT NULL COMMENT '仓库编码',
supplier_code VARCHAR(50) COMMENT '供应商编码',
status TINYINT DEFAULT 0 COMMENT '状态:0-待入库 1-部分入库 2-已完成 3-已取消',
expect_time DATETIME COMMENT '预计到货时间',
actual_time DATETIME COMMENT '实际入库时间',
remark VARCHAR(500),
create_by VARCHAR(50),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0,
INDEX idx_order_no (order_no),
INDEX idx_status (status)
) ENGINE=InnoDB COMMENT='入库单表';
-- 入库单明细表
CREATE TABLE wms_inbound_order_detail (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT NOT NULL COMMENT '入库单ID',
sku_code VARCHAR(50) NOT NULL COMMENT 'SKU编码',
expect_qty INT NOT NULL COMMENT '预计数量',
actual_qty INT DEFAULT 0 COMMENT '实际数量',
location_code VARCHAR(50) COMMENT '入库库位',
batch_no VARCHAR(50) COMMENT '批次号',
status TINYINT DEFAULT 0 COMMENT '状态:0-待入库 1-已入库',
INDEX idx_order_id (order_id)
) ENGINE=InnoDB COMMENT='入库单明细表';
-- 出库单表
CREATE TABLE wms_outbound_order (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(50) NOT NULL UNIQUE COMMENT '出库单号',
outbound_type TINYINT NOT NULL COMMENT '出库类型:1-销售 2-领料 3-调拨',
source_order_no VARCHAR(50) COMMENT '关联单号',
warehouse_code VARCHAR(20) NOT NULL COMMENT '仓库编码',
status TINYINT DEFAULT 0 COMMENT '状态:0-待分配 1-已分配 2-拣货中 3-已出库 4-已取消',
priority TINYINT DEFAULT 5 COMMENT '优先级:1-10',
create_by VARCHAR(50),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0,
INDEX idx_order_no (order_no),
INDEX idx_status (status)
) ENGINE=InnoDB COMMENT='出库单表';
-- 插入测试数据
INSERT INTO wms_inventory (sku_code, sku_name, warehouse_code, location_code, available_qty) VALUES
('SKU001', 'iPhone 15 Pro', 'BJ01', 'A-01-01', 100),
('SKU002', 'MacBook Pro 14', 'BJ01', 'A-01-02', 50),
('SKU003', 'AirPods Pro', 'SH01', 'B-02-01', 200),
('SKU004', '测试商品-低库存', 'GZ01', 'C-01-01', 5);
三、演进策略:识别服务边界
3.1 领域驱动设计(DDD)分析
在演进前,需要通过DDD识别限界上下文(Bounded Context):
┌─────────────────────────────────────────────────────────────┐
│ WMS领域分析 │
├─────────────────────────────────────────────────────────────┤
│ 上下文 │ 核心业务 │
├─────────────────────────────────────────────────────────────┤
│ 库存上下文 (Inventory) │ 实时库存、库存锁定、库存移动 │
│ 入库上下文 (Inbound) │ 收货、质检、上架 │
│ 出库上下文 (Outbound) │ 分配、拣货、复核、发货 │
│ 库位上下文 (Location) │ 库位管理、路径规划、容量管理 │
│ 报表上下文 (Report) │ 库存报表、作业分析、绩效统计 │
└─────────────────────────────────────────────────────────────┘
3.2 演进 checklist
在拆分前,确保完成以下准备工作:
- 代码层面解耦:使用Maven多模块拆分,模块间通过接口依赖
- 数据库表隔离:不同领域的表拆分到不同schema,禁止跨表join
- 数据同步机制:确定最终一致性方案(消息队列/BCDC)
- 接口契约定义:使用OpenAPI规范定义服务间接口
- 监控体系搭建:分布式追踪、日志聚合、指标监控
四、微服务架构阶段:服务拆分
4.1 整体架构
┌─────────────────────────────────────────────────────────────┐
│ 接入层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Web App │ │ Mobile │ │ Open API │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
└─────────┼────────────────┼────────────────┼─────────────────┘
│ │ │
┌─────────┼────────────────┼────────────────┼─────────────────┐
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ API Gateway (Spring Cloud Gateway) │ │
│ │ - 路由转发 - 鉴权认证 - 限流熔断 - 日志记录 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────┼──────────────────────┐ │
│ │ ▼ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 库存服务 │ │ 入库服务 │ │ 出库服务 │ │ │
│ │ │:8081 │ │:8082 │ │:8083 │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │
│ │ ┌────┴─────────────┴─────────────┴────┐ │ │
│ │ │ 消息队列 (RabbitMQ/Redis) │ │ │
│ │ │ - 库存变更事件 - 订单状态变更事件 │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 库位服务 │ │ 报表服务 │ │ 基础服务 │ │ │
│ │ │:8084 │ │:8085 │ │:8086 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ 注册中心 (Nacos) / 配置中心 │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ MySQL │ │ Redis │ │ES │ │Prometheus│ │
│ │ (分库) │ │(缓存) │ │(搜索) │ │(监控) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
4.2 微服务项目结构
wms-microservices/
├── wms-parent/ # 父POM,统一管理依赖
├── wms-common/ # 公共工具类、常量、异常定义
├── wms-api/ # API契约定义(DTO、Feign接口)
├── wms-gateway/ # 网关服务
├── wms-auth/ # 认证授权服务
├── wms-inventory-service/ # 库存服务
│ ├── src/main/java/com/wms/inventory/
│ │ ├── controller/
│ │ ├── service/
│ │ ├── domain/ # 领域层(DDD)
│ │ ├── infrastructure/ # 基础设施层
│ │ └── InventoryApplication.java
│ └── pom.xml
├── wms-inbound-service/ # 入库服务
├── wms-outbound-service/ # 出库服务
├── wms-location-service/ # 库位服务
├── wms-report-service/ # 报表服务
└── docker-compose.yml # 本地开发环境
4.3 服务拆分详解
4.3.1 库存服务 (wms-inventory-service)
核心职责:实时库存计算、库存锁定/释放、库存预警
java
// InventoryApplication.java
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.wms.api")
public class InventoryApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryApplication.class, args);
}
}
领域事件发布(库存变更时触发)
java
package com.wms.inventory.domain.event;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
import java.time.LocalDateTime;
@Getter
public class StockChangedEvent extends ApplicationEvent {
private final String skuCode;
private final String warehouseCode;
private final Integer changeQty; // 变更数量(正数增加,负数减少)
private final Integer availableQty; // 变更后可用库存
private final String bizType; // 业务类型:INBOUND/OUTBOUND/LOCK/RELEASE
private final String bizNo; // 业务单号
private final LocalDateTime eventTime;
public StockChangedEvent(Object source, String skuCode, String warehouseCode,
Integer changeQty, Integer availableQty,
String bizType, String bizNo) {
super(source);
this.skuCode = skuCode;
this.warehouseCode = warehouseCode;
this.changeQty = changeQty;
this.availableQty = availableQty;
this.bizType = bizType;
this.bizNo = bizNo;
this.eventTime = LocalDateTime.now();
}
}
事件监听器(发送消息到MQ)
java
package com.wms.inventory.infrastructure.messaging;
import com.wms.inventory.domain.event.StockChangedEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class StockChangedEventListener {
private final RabbitTemplate rabbitTemplate;
@Async
@EventListener
public void handleStockChanged(StockChangedEvent event) {
log.info("库存变更事件: {}", event);
// 发送到消息队列,供报表服务、出库服务等消费
rabbitTemplate.convertAndSend(
"wms.exchange", // 交换机
"inventory.changed", // 路由键
event // 消息体
);
}
}
对外提供的Feign接口
java
package com.wms.api.inventory;
import com.wms.common.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(name = "wms-inventory-service", fallback = InventoryFeignClientFallback.class)
public interface InventoryFeignClient {
@GetMapping("/api/v1/inventory/{skuCode}")
Result<InventoryVO> getStock(@PathVariable("skuCode") String skuCode,
@RequestParam("warehouseCode") String warehouseCode);
@PostMapping("/api/v1/inventory/lock")
Result<Boolean> lockStock(@RequestBody StockLockDTO dto);
@PostMapping("/api/v1/inventory/confirm")
Result<Boolean> confirmOutbound(@RequestBody StockConfirmDTO dto);
}
4.3.2 入库服务 (wms-inbound-service)
核心职责:入库单管理、收货质检、上架作业
java
package com.wms.inbound.service.impl;
import com.wms.api.inventory.InventoryFeignClient;
import com.wms.api.inventory.StockLockDTO;
import com.wms.inbound.entity.InboundOrder;
import com.wms.inbound.entity.InboundOrderDetail;
import com.wms.inbound.mapper.InboundOrderMapper;
import com.wms.inbound.service.InboundOrderService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class InboundOrderServiceImpl implements InboundOrderService {
private final InboundOrderMapper inboundOrderMapper;
private final InventoryFeignClient inventoryFeignClient;
/**
* 确认入库并更新库存(分布式事务)
*/
@Override
@GlobalTransactional(name = "inbound-confirm", rollbackFor = Exception.class)
public boolean confirmInbound(Long orderId, List<InboundDetailDTO> details) {
// 1. 校验入库单状态
InboundOrder order = inboundOrderMapper.selectById(orderId);
if (order == null || order.getStatus() != 0) {
throw new RuntimeException("入库单状态异常");
}
// 2. 更新入库单明细
for (InboundDetailDTO detail : details) {
// 更新明细实际入库数量
inboundOrderMapper.updateActualQty(detail.getId(), detail.getActualQty(), detail.getLocationCode());
// 3. 调用库存服务增加库存
// 这里使用Feign调用,Seata会自动处理分布式事务
StockAddDTO addDTO = new StockAddDTO();
addDTO.setSkuCode(detail.getSkuCode());
addDTO.setWarehouseCode(order.getWarehouseCode());
addDTO.setLocationCode(detail.getLocationCode());
addDTO.setQty(detail.getActualQty());
addDTO.setBatchNo(detail.getBatchNo());
addDTO.setSourceOrderNo(order.getOrderNo());
Result<Boolean> result = inventoryFeignClient.addStock(addDTO);
if (!result.getData()) {
throw new RuntimeException("库存更新失败: " + detail.getSkuCode());
}
}
// 4. 更新入库单状态为已完成
order.setStatus(2);
order.setActualTime(LocalDateTime.now());
inboundOrderMapper.updateById(order);
log.info("入库确认完成: orderId={}", orderId);
return true;
}
}
4.3.3 出库服务 (wms-outbound-service)
核心职责:出库单管理、库存分配、拣货路径规划
java
package com.wms.outbound.service.impl;
import com.wms.api.inventory.InventoryFeignClient;
import com.wms.api.inventory.StockLockDTO;
import com.wms.api.location.LocationFeignClient;
import com.wms.outbound.entity.OutboundOrder;
import com.wms.outbound.service.AllocationService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class AllocationServiceImpl implements AllocationService {
private final InventoryFeignClient inventoryFeignClient;
private final LocationFeignClient locationFeignClient;
/**
* 智能分配库存(考虑库位路径优化)
*/
@Override
public List<AllocationResult> allocateStock(String skuCode, String warehouseCode, Integer requiredQty) {
// 1. 查询可用库存分布
List<InventoryDistribution> distributions =
inventoryFeignClient.getDistribution(skuCode, warehouseCode).getData();
// 2. 获取库位路径信息(从库位服务获取)
List<String> locationCodes = distributions.stream()
.map(InventoryDistribution::getLocationCode)
.collect(Collectors.toList());
PathOptimizationResult pathResult =
locationFeignClient.optimizePath(warehouseCode, locationCodes).getData();
// 3. 按路径顺序分配库存(先拣近的)
List<AllocationResult> results = new ArrayList<>();
int remainingQty = requiredQty;
for (String locationCode : pathResult.getOptimizedSequence()) {
if (remainingQty <= 0) break;
InventoryDistribution dist = distributions.stream()
.filter(d -> d.getLocationCode().equals(locationCode))
.findFirst()
.orElse(null);
if (dist != null && dist.getAvailableQty() > 0) {
int allocateQty = Math.min(remainingQty, dist.getAvailableQty());
// 4. 锁定库存
StockLockDTO lockDTO = new StockLockDTO();
lockDTO.setSkuCode(skuCode);
lockDTO.setWarehouseCode(warehouseCode);
lockDTO.setLocationCode(locationCode);
lockDTO.setQty(allocateQty);
boolean locked = inventoryFeignClient.lockStock(lockDTO).getData();
if (locked) {
results.add(new AllocationResult(locationCode, allocateQty));
remainingQty -= allocateQty;
}
}
}
if (remainingQty > 0) {
throw new RuntimeException("库存不足,分配失败,缺少: " + remainingQty);
}
return results;
}
}
4.4 服务间通信设计
| 通信方式 | 使用场景 | 实现技术 |
|---|---|---|
| 同步调用 | 实时性要求高,需要立即返回结果 | OpenFeign + 负载均衡 |
| 异步消息 | 最终一致性,解耦服务 | RabbitMQ/Kafka |
| 事件溯源 | 关键业务审计,状态重建 | 事件存储 + CQRS |
消息队列配置
java
package com.wms.common.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
// 库存变更交换机
@Bean
public TopicExchange inventoryExchange() {
return new TopicExchange("wms.inventory.exchange");
}
// 入库完成队列
@Bean
public Queue inboundCompletedQueue() {
return new Queue("wms.inbound.completed");
}
// 出库创建队列
@Bean
public Queue outboundCreatedQueue() {
return new Queue("wms.outbound.created");
}
// 绑定关系
@Bean
public Binding inboundBinding() {
return BindingBuilder.bind(inboundCompletedQueue())
.to(inventoryExchange())
.with("inbound.completed");
}
}
五、基础设施搭建
5.1 本地开发环境 (Docker Compose)
yaml
# docker-compose.yml
version: '3.8'
services:
# MySQL主库
mysql-master:
image: mysql:8.0
container_name: wms-mysql-master
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: wms_inventory
ports:
- "3306:3306"
volumes:
- mysql-master-data:/var/lib/mysql
- ./sql/init-master.sql:/docker-entrypoint-initdb.d/init.sql
command: --default-authentication-plugin=mysql_native_password
# MySQL从库(报表服务使用)
mysql-slave:
image: mysql:8.0
container_name: wms-mysql-slave
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: wms_report
ports:
- "3307:3306"
volumes:
- mysql-slave-data:/var/lib/mysql
# Redis(缓存 + 分布式锁)
redis:
image: redis:7-alpine
container_name: wms-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes
# RabbitMQ(消息队列)
rabbitmq:
image: rabbitmq:3-management
container_name: wms-rabbitmq
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: wms
RABBITMQ_DEFAULT_PASS: wms123
volumes:
- rabbitmq-data:/var/lib/rabbitmq
# Nacos(注册中心 + 配置中心)
nacos:
image: nacos/nacos-server:v2.3.0
container_name: wms-nacos
ports:
- "8848:8848"
- "9848:9848"
environment:
MODE: standalone
SPRING_DATASOURCE_PLATFORM: mysql
MYSQL_SERVICE_HOST: mysql-master
MYSQL_SERVICE_DB_NAME: nacos
MYSQL_SERVICE_USER: root
MYSQL_SERVICE_PASSWORD: root123
# Elasticsearch(搜索)
elasticsearch:
image: elasticsearch:8.11.0
container_name: wms-es
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
volumes:
- es-data:/usr/share/elasticsearch/data
volumes:
mysql-master-data:
mysql-slave-data:
redis-data:
rabbitmq-data:
es-data:
5.2 网关配置
yaml
# wms-gateway/src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: wms-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
# 库存服务路由
- id: inventory-service
uri: lb://wms-inventory-service
predicates:
- Path=/api/v1/inventory/**
filters:
- name: CircuitBreaker
args:
name: inventoryCircuitBreaker
fallbackUri: forward:/fallback/inventory
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
# 入库服务路由
- id: inbound-service
uri: lb://wms-inbound-service
predicates:
- Path=/api/v1/inbound/**
# 出库服务路由
- id: outbound-service
uri: lb://wms-outbound-service
predicates:
- Path=/api/v1/outbound/**
# 库位服务路由
- id: location-service
uri: lb://wms-location-service
predicates:
- Path=/api/v1/location/**
# 全局默认过滤器
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
- AddResponseHeader=X-Response-Time, ${now}
# 熔断配置
resilience4j:
circuitbreaker:
instances:
inventoryCircuitBreaker:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
六、完整代码实现
6.1 关键完整代码文件
6.1.1 分布式锁实现(基于Redis)
java
package com.wms.common.lock;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Component
@RequiredArgsConstructor
public class DistributedLock {
private final StringRedisTemplate redisTemplate;
// Lua脚本:原子性释放锁
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
/**
* 尝试获取锁
*/
public boolean tryLock(String lockKey, String requestId, long expireTime) {
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, expireTime, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
/**
* 释放锁
*/
public boolean unlock(String lockKey, String requestId) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(UNLOCK_SCRIPT);
script.setResultType(Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(lockKey), requestId);
return result != null && result == 1;
}
}
6.1.2 库存扣减服务(防超卖)
java
package com.wms.inventory.service.impl;
import com.wms.common.lock.DistributedLock;
import com.wms.inventory.entity.Inventory;
import com.wms.inventory.mapper.InventoryMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class InventoryDeductionService {
private final InventoryMapper inventoryMapper;
private final DistributedLock distributedLock;
/**
* 扣减库存(防止超卖的多层防护)
* 1. 分布式锁防止并发
* 2. 数据库乐观锁(version字段)防止并发
* 3. 数据库约束防止负数
*/
@Transactional(rollbackFor = Exception.class)
public boolean deductStock(String skuCode, String warehouseCode, Integer qty) {
String lockKey = String.format("stock:%s:%s", skuCode, warehouseCode);
String requestId = UUID.randomUUID().toString();
// 1. 获取分布式锁(等待10秒,锁持有30秒)
boolean locked = distributedLock.tryLock(lockKey, requestId, 30);
if (!locked) {
throw new RuntimeException("系统繁忙,请稍后重试");
}
try {
// 2. 查询库存(使用FOR UPDATE悲观锁,双重保险)
Inventory inventory = inventoryMapper.selectForUpdate(skuCode, warehouseCode);
if (inventory == null || inventory.getAvailableQty() < qty) {
throw new RuntimeException("库存不足");
}
// 3. 扣减库存(使用乐观锁)
int affected = inventoryMapper.deductWithVersion(
inventory.getId(), qty, inventory.getVersion());
if (affected == 0) {
throw new RuntimeException("库存并发冲突,请重试");
}
log.info("库存扣减成功: skuCode={}, qty={}, remaining={}",
skuCode, qty, inventory.getAvailableQty() - qty);
return true;
} finally {
// 4. 释放分布式锁
distributedLock.unlock(lockKey, requestId);
}
}
}
6.1.3 前端微服务适配
javascript
// src/api/request.js - 适配网关
import axios from 'axios'
import { ElMessage } from 'element-plus'
const service = axios.create({
baseURL: '/api/v1', // 通过网关代理
timeout: 30000 // 微服务调用可能较慢
})
// 请求拦截器添加租户信息(多租户支持)
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
const tenantId = localStorage.getItem('tenantId') || 'default'
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
config.headers['X-Tenant-Id'] = tenantId
return config
}
)
// 响应拦截器处理熔断降级
service.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 503) {
ElMessage.error('服务暂时不可用,请稍后重试')
} else if (error.code === 'ECONNABORTED') {
ElMessage.error('请求超时,请检查网络')
} else {
ElMessage.error(error.response?.data?.message || '请求失败')
}
return Promise.reject(error)
}
)
export default service
七、部署与运维
7.1 生产环境部署架构
┌─────────────────────────────────────────────────────────────┐
│ K8s Cluster │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Ingress Controller (Nginx) │ │
│ │ - SSL终止 - 路由分发 - 限流防护 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────┼──────────────────────┐ │
│ │ Namespace: wms-production │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Gateway │ │ Inventory│ │ Inbound │ │ │
│ │ │ 3 Replicas│ │ 5 Replicas│ │ 3 Replicas│ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Outbound │ │ Location │ │ Report │ │ │
│ │ │ 3 Replicas│ │ 2 Replicas│ │ 2 Replicas│ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Namespace: wms-data │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ MySQL │ │ Redis │ │RabbitMQ │ │ │
│ │ │ Operator │ │ Cluster │ │ Cluster │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
7.2 Kubernetes部署清单
yaml
# k8s/inventory-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: wms-inventory-service
namespace: wms-production
spec:
replicas: 3
selector:
matchLabels:
app: wms-inventory-service
template:
metadata:
labels:
app: wms-inventory-service
spec:
containers:
- name: inventory-service
image: registry.example.com/wms/inventory-service:1.0.0
ports:
- containerPort: 8081
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: MYSQL_HOST
valueFrom:
secretKeyRef:
name: wms-db-secret
key: host
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 10
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: wms-inventory-service
namespace: wms-production
spec:
selector:
app: wms-inventory-service
ports:
- port: 8081
targetPort: 8081
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: wms-inventory-service-hpa
namespace: wms-production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: wms-inventory-service
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
7.3 监控与告警
yaml
# prometheus-rules.yml
groups:
- name: wms-alerts
rules:
# 接口响应时间告警
- alert: WmsHighLatency
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "WMS接口响应时间过长"
# 库存服务错误率告警
- alert: InventoryServiceHighErrorRate
expr: rate(http_requests_total{service="wms-inventory-service",status=~"5.."}[5m]) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "库存服务错误率过高"
# 库存积压告警(业务指标)
- alert: InventoryBacklog
expr: wms_inventory_locked_qty / wms_inventory_total_qty > 0.8
for: 10m
labels:
severity: warning
annotations:
summary: "库存锁定比例过高,可能存在积压"
总结与演进建议
演进 checklist
| 阶段 | 关键动作 | 验收标准 |
|---|---|---|
| 单体优化 | 代码模块化、数据库索引优化、缓存引入 | 单机支持1000 TPS |
| 服务拆分 | 领域边界清晰、接口契约定义、数据同步机制 | 服务独立部署,故障隔离 |
| 治理能力 | 服务注册发现、配置中心、监控告警 | 可观测性覆盖100% |
| 高可用 | 多活架构、数据分片、自动扩缩容 | 99.99%可用性 |
| 智能化 | 预测性补货、智能库位、自动化作业 | 人效提升50% |
关键设计原则
- 数据库先行隔离:微服务拆分前,先完成数据库schema隔离
- 接口契约优先:使用OpenAPI规范,版本化管理
- 异步化解耦:核心流程同步,非核心流程异步
- 最终一致性:库存类数据允许秒级延迟,保证最终一致
- 灰度发布:使用网关权重路由,逐步切流
本文基于实际生产环境经验编写,代码有部分删减。