WMS系统演进——从单体到微服务


WMS系统演进------从单体到微服务

实战指南:基于Java + Vue + MySQL的完整搭建教程


目录

  1. 架构演进背景与目标
  2. 单体架构阶段:快速启动
  3. 演进策略:识别服务边界
  4. 微服务架构阶段:服务拆分
  5. 基础设施搭建
  6. 完整代码实现
  7. 部署与运维

一、架构演进背景与目标

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%

关键设计原则

  1. 数据库先行隔离:微服务拆分前,先完成数据库schema隔离
  2. 接口契约优先:使用OpenAPI规范,版本化管理
  3. 异步化解耦:核心流程同步,非核心流程异步
  4. 最终一致性:库存类数据允许秒级延迟,保证最终一致
  5. 灰度发布:使用网关权重路由,逐步切流


本文基于实际生产环境经验编写,代码有部分删减。

相关推荐
Kstheme4 小时前
把任何 GitHub 仓库变成系统设计课:这个开源项目做到了
架构
禅思院4 小时前
路由性能高可用架构实战方案
前端·架构·前端框架
贵慜_Derek1 天前
《从零实现 Agent 系统》连载 32|闭集 IE 与小模型:分类、意图与字段抽取
人工智能·架构·agent
江米小枣tonylua2 天前
译:设计生产级 RAG 架构
架构
怕浪猫2 天前
领域特定语言(Domain-Specific Language, DSL)
设计模式·程序员·架构
怕浪猫2 天前
哪些软件对 Chrome DevTools Protocol 频繁使用
人工智能·架构·前端框架
Jack202 天前
HarmonyOS APP事件驱动大揭秘
架构
米丘2 天前
微前端之 Web Components 完全指南
微服务·html
秋播2 天前
国内本地WSL2编译rancher源码
云原生
Colin草率地做慢慢地改2 天前
关于QuickStore这个项目的重构(2)- 数据库建表文件
后端·面试·架构