4、库存服务(Stock Service)设计与实现:库存核心接口定义(入库、出库、查询)、数据库表设计(stock、stock_detail)、MyBatis-Plus集成与代码生成

好,咱们进入库存服务的核心环节了。说实话,库存服务是整个库存系统的灵魂。你想想看,订单可以取消,商品可以下架,但库存数据一旦出错,那可真是一笔糊涂账。我在项目中见过太多因为库存接口设计不合理,导致超卖、数据不一致的惨案。今天咱们就把这块彻底讲透。

4.1 库存核心接口定义

接口设计,说白了就是定义清楚「别人怎么用你的库存」。我个人习惯先把接口契约定死,再动手写代码。这样后面联调的时候,能少踩很多坑。

4.1.1 入库接口

入库操作,不只是加库存那么简单。它背后往往关联着采购单、退货单、调拨单。我建议接口设计成幂等的,防止网络重试导致库存翻倍。

POST /api/stock/inbound
Request Body:
{
  "skuId": "SKU001",        // 商品SKU ID
  "warehouseId": "WH001",   // 仓库ID
  "quantity": 100,          // 入库数量
  "batchNo": "B20240301",   // 批次号(可选)
  "sourceType": "PURCHASE", // 来源类型:PURCHASE/RETURN/TRANSFER
  "sourceOrderNo": "PO20240301001", // 来源单号
  "operator": "张三"        // 操作人
}

Response:
{
  "code": 200,
  "message": "success",
  "data": {
    "stockId": 12345,
    "beforeQuantity": 500,
    "afterQuantity": 600,
    "inboundTime": "2024-03-01 10:30:00"
  }
}
我的经验:入库接口一定要返回「操作前后的库存量」。这样调用方可以自己做校验,也能方便地记录操作日志。我曾经遇到过一个系统,入库接口只返回成功失败,结果对账时发现库存对不上,查了三天才发现是并发问题。

4.1.2 出库接口

出库比入库复杂,因为它涉及「库存扣减」和「库存预占」两个概念。电商场景下,用户下单时先预占库存,支付成功后才真正扣减。这样能防止「下了单却发不出货」的尴尬。

POST /api/stock/outbound
Request Body:
{
  "skuId": "SKU001",
  "warehouseId": "WH001",
  "quantity": 10,
  "orderNo": "ORD20240301001",
  "outboundType": "SALE",   // 出库类型:SALE/RETURN/TRANSFER
  "operator": "李四"
}

Response:
{
  "code": 200,
  "message": "success",
  "data": {
    "stockId": 12345,
    "beforeQuantity": 600,
    "afterQuantity": 590,
    "outboundTime": "2024-03-01 11:00:00"
  }
}
注意:出库接口必须做「库存不足」的校验。如果库存不够,直接返回错误码,不要做部分扣减。我曾经见过一个系统,库存不够时扣了负数,结果整个报表都乱了。

4.1.3 库存查询接口

查询接口看似简单,但实际场景中查询维度很多。我建议提供灵活的查询方式,支持按SKU、仓库、批次等条件组合查询。

GET /api/stock/query?skuId=SKU001&warehouseId=WH001&batchNo=B20240301

Response:
{
  "code": 200,
  "message": "success",
  "data": {
    "skuId": "SKU001",
    "warehouseId": "WH001",
    "availableQuantity": 590,   // 可用库存
    "lockedQuantity": 20,       // 预占库存
    "totalQuantity": 610,       // 总库存
    "batchInfo": [
      {
        "batchNo": "B20240301",
        "quantity": 590,
        "inboundTime": "2024-03-01 10:30:00"
      }
    ]
  }
}

4.2 数据库表设计

表设计这块,我踩过不少坑。最核心的原则是:库存主表和流水表分离。主表存当前库存快照,流水表存每一次变动记录。这样既能保证查询性能,又能追溯历史。

4.2.1 stock 表(库存主表)

字段名 类型 说明
id bigint 主键ID
sku_id varchar(64) 商品SKU ID
warehouse_id varchar(64) 仓库ID
available_quantity int 可用库存
locked_quantity int 预占库存
total_quantity int 总库存
version int 乐观锁版本号
create_time datetime 创建时间
update_time datetime 更新时间
核心设计思路:我用了「可用库存」和「预占库存」两个字段,而不是只用一个总库存。这样能清晰区分「真正能卖的」和「已经被下单但还没支付的」。version字段用于乐观锁,防止并发扣减。

4.2.2 stock_detail 表(库存流水表)

字段名 类型 说明
id bigint 主键ID
stock_id bigint 库存主表ID
sku_id varchar(64) 商品SKU ID
warehouse_id varchar(64) 仓库ID
change_type varchar(32) 变动类型:INBOUND/OUTBOUND/LOCK/UNLOCK
change_quantity int 变动数量(正数增加,负数减少)
before_quantity int 变动前库存
after_quantity int 变动后库存
source_order_no varchar(64) 来源单号
operator varchar(32) 操作人
remark varchar(255) 备注
create_time datetime 创建时间
为什么要有 before_quantity 和 after_quantity? 这两个字段能帮你做数据对账。如果哪天发现库存对不上,直接查流水表,看每一步的变动前后值,问题一目了然。我在项目中靠这个字段救过好几次火。

4.3 MyBatis-Plus 集成与代码生成

MyBatis-Plus 这东西,说白了就是 MyBatis 的增强版。它帮你省掉了大部分 CRUD 的模板代码。我个人习惯用它来生成 Entity、Mapper、Service 这些基础代码,然后自己再写业务逻辑。

4.3.1 引入依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3</version>
</dependency>

<!-- 代码生成器 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.3</version>
</dependency>

<!-- 模板引擎 -->
<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.3</version>
</dependency>

4.3.2 代码生成器配置

我一般会写一个独立的生成器类,放在 test 目录下。这样不会污染主代码。配置好数据库连接、表名、包名,一键生成。

public class CodeGenerator {
    public static void main(String[] args) {
        FastAutoGenerator.create("jdbc:mysql://localhost:3306/stock_db", 
                                  "root", "password")
            .globalConfig(builder -> {
                builder.author("张三") // 设置作者
                       .outputDir("D://code//stock-service//src//main//java"); // 输出目录
            })
            .packageConfig(builder -> {
                builder.parent("com.example.stock")
                       .entity("entity")
                       .mapper("mapper")
                       .service("service")
                       .serviceImpl("service.impl")
                       .controller("controller");
            })
            .strategyConfig(builder -> {
                builder.addInclude("stock", "stock_detail") // 设置需要生成的表名
                       .entityBuilder()
                       .enableLombok() // 启用 Lombok
                       .enableTableFieldAnnotation() // 启用字段注解
                       .controllerBuilder()
                       .enableRestStyle(); // 启用 REST 风格
            })
            .execute();
    }
}
生成后的代码结构:
  • entity/Stock.java - 库存实体类
  • entity/StockDetail.java - 库存流水实体类
  • mapper/StockMapper.java - 库存 Mapper 接口
  • mapper/StockDetailMapper.java - 库存流水 Mapper 接口
  • service/IStockService.java - 库存服务接口
  • service/impl/StockServiceImpl.java - 库存服务实现
  • controller/StockController.java - 库存控制器

4.3.3 核心业务代码示例

生成完基础代码后,咱们得自己写核心业务逻辑。比如入库操作的 Service 实现:

@Service
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> 
                               implements IStockService {

    @Override
    @Transactional(rollbackFor = Exception.class)
    public StockInboundResponse inbound(StockInboundRequest request) {
        // 1. 查询当前库存(带乐观锁)
        Stock stock = this.lambdaQuery()
            .eq(Stock::getSkuId, request.getSkuId())
            .eq(Stock::getWarehouseId, request.getWarehouseId())
            .one();
        
        if (stock == null) {
            // 首次入库,创建库存记录
            stock = new Stock();
            stock.setSkuId(request.getSkuId());
            stock.setWarehouseId(request.getWarehouseId());
            stock.setAvailableQuantity(0);
            stock.setLockedQuantity(0);
            stock.setTotalQuantity(0);
            stock.setVersion(1);
        }
        
        // 2. 更新库存(乐观锁更新)
        int beforeQuantity = stock.getAvailableQuantity();
        stock.setAvailableQuantity(beforeQuantity + request.getQuantity());
        stock.setTotalQuantity(stock.getTotalQuantity() + request.getQuantity());
        stock.setVersion(stock.getVersion() + 1);
        
        boolean updated = this.update(
            new LambdaUpdateWrapper<Stock>()
                .eq(Stock::getId, stock.getId())
                .eq(Stock::getVersion, stock.getVersion() - 1) // 乐观锁条件
        );
        
        if (!updated) {
            throw new RuntimeException("库存更新失败,请重试");
        }
        
        // 3. 记录流水
        StockDetail detail = new StockDetail();
        detail.setStockId(stock.getId());
        detail.setSkuId(request.getSkuId());
        detail.setWarehouseId(request.getWarehouseId());
        detail.setChangeType("INBOUND");
        detail.setChangeQuantity(request.getQuantity());
        detail.setBeforeQuantity(beforeQuantity);
        detail.setAfterQuantity(beforeQuantity + request.getQuantity());
        detail.setSourceOrderNo(request.getSourceOrderNo());
        detail.setOperator(request.getOperator());
        detail.setRemark("入库操作");
        stockDetailMapper.insert(detail);
        
        // 4. 返回结果
        StockInboundResponse response = new StockInboundResponse();
        response.setStockId(stock.getId());
        response.setBeforeQuantity(beforeQuantity);
        response.setAfterQuantity(beforeQuantity + request.getQuantity());
        return response;
    }
}
注意:乐观锁更新失败时,一定要让调用方重试。不要直接吞掉异常。我在生产环境遇到过并发高峰时乐观锁冲突率高达 30%,重试机制帮了大忙。

4.4 核心流程示意图

下面这张图,是我梳理的库存服务核心流程。你一看就明白入库、出库、查询是怎么串联起来的。

库存服务核心流程图 客户端 StockController StockService StockMapper 入库操作 出库操作 查询操作 MySQL 数据库 stock / stock_detail 图例说明: 客户端请求 控制器层 服务层 持久层 数据库

嗯,到这里库存服务的核心设计就讲完了。接口定义、表设计、代码生成,这三块是库存服务的基石。你在实际项目中,可以根据业务场景再灵活调整。比如有些场景需要支持批次管理,那就在 stock 表里加个 batch_no 字段。核心思路不变,细节自己把控就好。

专注资料整理