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. 灰度发布:使用网关权重路由,逐步切流


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

相关推荐
Coder个人博客2 小时前
05_apollo_tools子模块整体软件架构深入分析文档
架构
源远流长jerry3 小时前
软件定义网络 SDN 核心技术深度解析:从概念到实践
linux·网络·架构
二等饼干~za8986683 小时前
豆包GEO优化源码开发全解析:技术架构、实现逻辑与实操指南
数据库·sql·重构·架构·mybatis·音视频
盘古信息IMS3 小时前
2026年注塑MES系统选型新思维:从技术架构到行业适配的全方位评估框架
大数据·架构
roman_日积跬步-终至千里3 小时前
【软件系统架构师-综合题(2)】项目管理题目
架构
ai生成式引擎优化技术3 小时前
TSPR-WEB-LLM-HIC 生产级架构升级方案
架构
heimeiyingwang4 小时前
【架构实战】灰度发布架构设计与实现
架构
cyber_两只龙宝4 小时前
【Docker】Dockerfile构建镜像实验全流程详解
linux·运维·docker·云原生