wms-module

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

WMS Base NestJS Module (倉儲管理模組)

WMS Base NestJS Module(仓储管理模块)

Overview

概述

@rytass/wms-base-nestjs-module
提供 NestJS 倉儲管理系統的基礎模組,支援儲位樹狀結構、物料管理、庫存異動追蹤、訂單管理及倉庫地圖功能。
@rytass/wms-base-nestjs-module
为NestJS提供仓储管理系统的基础模块,支持储位树状结构、物料管理、库存异动追踪、订单管理及仓库地图功能。

Quick Start

快速开始

安裝

安装

bash
npm install @rytass/wms-base-nestjs-module
Peer Dependencies:
  • @nestjs/common
    ^9.0.0 || ^10.0.0
  • @nestjs/typeorm
    ^9.0.0 || ^10.0.0
  • typeorm
    ^0.3.0
bash
npm install @rytass/wms-base-nestjs-module
对等依赖:
  • @nestjs/common
    ^9.0.0 || ^10.0.0
  • @nestjs/typeorm
    ^9.0.0 || ^10.0.0
  • typeorm
    ^0.3.0

基本設定

基本设置

typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WMSBaseModule } from '@rytass/wms-base-nestjs-module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      // ... database config
    }),
    WMSBaseModule.forRoot({
      allowNegativeStock: false, // 預設: false, 禁止負庫存
    }),
  ],
})
export class AppModule {}
typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WMSBaseModule } from '@rytass/wms-base-nestjs-module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      // ... 数据库配置
    }),
    WMSBaseModule.forRoot({
      allowNegativeStock: false, // 默认: false,禁止负库存
    }),
  ],
})
export class AppModule {}

非同步設定

异步设置

typescript
import { WMSBaseModule, WMSBaseModuleAsyncOptions } from '@rytass/wms-base-nestjs-module';
import { ConfigService } from '@nestjs/config';

@Module({
  imports: [
    // useFactory 方式
    WMSBaseModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        allowNegativeStock: config.get('WMS_ALLOW_NEGATIVE_STOCK', false),
      }),
    }),

    // 或 useClass 方式(使用自訂 Factory)
    // WMSBaseModule.forRootAsync({
    //   useClass: WMSConfigFactory,
    // }),

    // 或 useExisting 方式(重用現有 Factory)
    // WMSBaseModule.forRootAsync({
    //   useExisting: ExistingWMSConfigFactory,
    // }),
  ],
})
export class AppModule {}
typescript
import { WMSBaseModule, WMSBaseModuleAsyncOptions } from '@rytass/wms-base-nestjs-module';
import { ConfigService } from '@nestjs/config';

@Module({
  imports: [
    // useFactory 方式
    WMSBaseModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        allowNegativeStock: config.get('WMS_ALLOW_NEGATIVE_STOCK', false),
      }),
    }),

    // 或 useClass 方式(使用自定义Factory)
    // WMSBaseModule.forRootAsync({
    //   useClass: WMSConfigFactory,
    // }),

    // 或 useExisting 方式(重用现有Factory)
    // WMSBaseModule.forRootAsync({
    //   useExisting: ExistingWMSConfigFactory,
    // }),
  ],
})
export class AppModule {}

Core Entities

核心实体

模組提供以下基礎 Entity,皆可透過繼承擴展:
模块提供以下基础Entity,均可通过继承扩展:

LocationEntity (儲位)

LocationEntity(储位)

typescript
import { LocationEntity } from '@rytass/wms-base-nestjs-module';

// 基礎結構 - Table: 'locations'
@Entity('locations')
@TableInheritance({ column: { type: 'varchar', name: 'entityName' } })
@Tree('materialized-path')
class LocationEntity {
  @PrimaryColumn({ type: 'varchar' })
  id: string;

  @TreeChildren()
  children: LocationEntity[];

  @TreeParent()
  parent: LocationEntity;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @DeleteDateColumn()
  deletedAt: Date | null;  // Soft delete
}
使用 TypeORM
@Tree('materialized-path')
實作樹狀結構
typescript
import { LocationEntity } from '@rytass/wms-base-nestjs-module';

// 基础结构 - Table: 'locations'
@Entity('locations')
@TableInheritance({ column: { type: 'varchar', name: 'entityName' } })
@Tree('materialized-path')
class LocationEntity {
  @PrimaryColumn({ type: 'varchar' })
  id: string;

  @TreeChildren()
  children: LocationEntity[];

  @TreeParent()
  parent: LocationEntity;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @DeleteDateColumn()
  deletedAt: Date | null;  // 软删除
}
使用TypeORM
@Tree('materialized-path')
实现树状结构

MaterialEntity (物料)

MaterialEntity(物料)

typescript
import { MaterialEntity } from '@rytass/wms-base-nestjs-module';

// Table: 'materials'
@Entity('materials')
@TableInheritance({ column: { type: 'varchar', name: 'entityName' } })
class MaterialEntity {
  @PrimaryColumn({ type: 'varchar' })
  id: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @DeleteDateColumn()
  deletedAt: Date | null;  // Soft delete

  @OneToMany(() => BatchEntity, batch => batch.material)
  batches: Relation<BatchEntity[]>;

  @OneToMany(() => StockEntity, stock => stock.material)
  stocks: Relation<StockEntity[]>;
}
typescript
import { MaterialEntity } from '@rytass/wms-base-nestjs-module';

// Table: 'materials'
@Entity('materials')
@TableInheritance({ column: { type: 'varchar', name: 'entityName' } })
class MaterialEntity {
  @PrimaryColumn({ type: 'varchar' })
  id: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @DeleteDateColumn()
  deletedAt: Date | null;  // 软删除

  @OneToMany(() => BatchEntity, batch => batch.material)
  batches: Relation<BatchEntity[]>;

  @OneToMany(() => StockEntity, stock => stock.material)
  stocks: Relation<StockEntity[]>;
}

BatchEntity (批次)

BatchEntity(批次)

typescript
import { BatchEntity } from '@rytass/wms-base-nestjs-module';

// Table: 'batches'
// 複合主鍵: id + materialId
@Entity('batches')
class BatchEntity {
  @PrimaryColumn('varchar')
  id: string;

  @PrimaryColumn('varchar')
  materialId: string;

  @ManyToOne(() => MaterialEntity, material => material.batches)
  @JoinColumn({ name: 'materialId', referencedColumnName: 'id' })
  material: Relation<MaterialEntity>;

  @OneToMany(() => StockEntity, stock => stock.batch)
  stocks: Relation<StockEntity[]>;
}
typescript
import { BatchEntity } from '@rytass/wms-base-nestjs-module';

// Table: 'batches'
// 复合主键: id + materialId
@Entity('batches')
class BatchEntity {
  @PrimaryColumn('varchar')
  id: string;

  @PrimaryColumn('varchar')
  materialId: string;

  @ManyToOne(() => MaterialEntity, material => material.batches)
  @JoinColumn({ name: 'materialId', referencedColumnName: 'id' })
  material: Relation<MaterialEntity>;

  @OneToMany(() => StockEntity, stock => stock.batch)
  stocks: Relation<StockEntity[]>;
}

StockEntity (庫存異動)

StockEntity(库存异动)

typescript
import { StockEntity } from '@rytass/wms-base-nestjs-module';

// Table: 'stocks'
@Entity('stocks')
@TableInheritance({ column: { type: 'varchar', name: 'entityName' } })
class StockEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'varchar' })
  materialId: string;

  @Column({ type: 'varchar' })
  batchId: string;

  @Column({ type: 'varchar' })
  locationId: string;

  @Column({ type: 'varchar' })
  orderId: string;

  @Column({ type: 'numeric' })
  quantity: number;     // 正數為入庫,負數為出庫

  @ManyToOne(() => MaterialEntity, material => material.stocks)
  @JoinColumn({ name: 'materialId', referencedColumnName: 'id' })
  material: Relation<MaterialEntity>;

  @ManyToOne(() => BatchEntity, batch => batch.stocks)
  @JoinColumn([
    { name: 'materialId', referencedColumnName: 'materialId' },
    { name: 'batchId', referencedColumnName: 'id' },
  ])
  batch: Relation<BatchEntity>;

  @ManyToOne(() => OrderEntity, order => order.stocks)
  @JoinColumn({ name: 'orderId', referencedColumnName: 'id' })
  order: Relation<OrderEntity>;

  @CreateDateColumn()
  createdAt: Date;
}
typescript
import { StockEntity } from '@rytass/wms-base-nestjs-module';

// Table: 'stocks'
@Entity('stocks')
@TableInheritance({ column: { type: 'varchar', name: 'entityName' } })
class StockEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'varchar' })
  materialId: string;

  @Column({ type: 'varchar' })
  batchId: string;

  @Column({ type: 'varchar' })
  locationId: string;

  @Column({ type: 'varchar' })
  orderId: string;

  @Column({ type: 'numeric' })
  quantity: number;     // 正数为入库,负数为出库

  @ManyToOne(() => MaterialEntity, material => material.stocks)
  @JoinColumn({ name: 'materialId', referencedColumnName: 'id' })
  material: Relation<MaterialEntity>;

  @ManyToOne(() => BatchEntity, batch => batch.stocks)
  @JoinColumn([
    { name: 'materialId', referencedColumnName: 'materialId' },
    { name: 'batchId', referencedColumnName: 'id' },
  ])
  batch: Relation<BatchEntity>;

  @ManyToOne(() => OrderEntity, order => order.stocks)
  @JoinColumn({ name: 'orderId', referencedColumnName: 'id' })
  order: Relation<OrderEntity>;

  @CreateDateColumn()
  createdAt: Date;
}

OrderEntity (訂單)

OrderEntity(订单)

typescript
import { OrderEntity } from '@rytass/wms-base-nestjs-module';

// Table: 'orders'
@Entity('orders')
@TableInheritance({ column: { type: 'varchar', name: 'entityName' } })
class OrderEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @OneToMany(() => StockEntity, stock => stock.order)
  stocks: Relation<StockEntity>[];
}
typescript
import { OrderEntity } from '@rytass/wms-base-nestjs-module';

// Table: 'orders'
@Entity('orders')
@TableInheritance({ column: { type: 'varchar', name: 'entityName' } })
class OrderEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @OneToMany(() => StockEntity, stock => stock.order)
  stocks: Relation<StockEntity>[];
}

WarehouseMapEntity (倉庫地圖)

WarehouseMapEntity(仓库地图)

注意:
WarehouseMapService
有從 index.ts 導出,可直接 import。但
WarehouseMapEntity
MapRangeType
MapRangeColor
MapData
等類型目前未從 index.ts 導出,需直接從原始碼路徑 import 或自行定義。
typescript
// WarehouseMapService 可直接 import
import { WarehouseMapService } from '@rytass/wms-base-nestjs-module';

// WarehouseMapEntity 需從原始碼路徑 import(若必要)

// Table: 'warehouse_maps'
@Entity('warehouse_maps')
class WarehouseMapEntity {
  @PrimaryColumn('varchar')  // 對應 locationId
  id: string;

  @Column({ type: 'jsonb' })
  mapData: MapData;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}
注意:
WarehouseMapService
已从index.ts导出,可直接import。但
WarehouseMapEntity
MapRangeType
MapRangeColor
MapData
等类型目前未从index.ts导出,需直接从源码路径import或自行定义。
typescript
// WarehouseMapService 可直接import
import { WarehouseMapService } from '@rytass/wms-base-nestjs-module';

// WarehouseMapEntity 需从源码路径import(若必要)

// Table: 'warehouse_maps'
@Entity('warehouse_maps')
class WarehouseMapEntity {
  @PrimaryColumn('varchar')  // 对应locationId
  id: string;

  @Column({ type: 'jsonb' })
  mapData: MapData;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

Services

服务

LocationService (儲位服務)

LocationService(储位服务)

typescript
import { LocationService, LocationEntity } from '@rytass/wms-base-nestjs-module';

@Injectable()
export class WarehouseService {
  constructor(private readonly locationService: LocationService) {}

  // 建立儲位
  async createLocation() {
    // 建立根儲位
    const warehouse = await this.locationService.create({
      id: 'WAREHOUSE-A',
    });

    // 建立子儲位
    const zone = await this.locationService.create({
      id: 'ZONE-A1',
      parentId: 'WAREHOUSE-A',
    });

    return zone;
  }

  // 封存儲位 (Soft Delete)
  // 注意: 只有庫存為 0 的儲位才能封存
  async archiveLocation(id: string) {
    await this.locationService.archive(id);
    // 會連同所有子儲位一起封存
  }

  // 解除封存
  async unArchiveLocation(id: string) {
    const location = await this.locationService.unArchive(id);
    return location;
  }
}
typescript
import { LocationService, LocationEntity } from '@rytass/wms-base-nestjs-module';

@Injectable()
export class WarehouseService {
  constructor(private readonly locationService: LocationService) {}

  // 创建储位
  async createLocation() {
    // 创建根储位
    const warehouse = await this.locationService.create({
      id: 'WAREHOUSE-A',
    });

    // 创建子储位
    const zone = await this.locationService.create({
      id: 'ZONE-A1',
      parentId: 'WAREHOUSE-A',
    });

    return zone;
  }

  // 归档储位 (软删除)
  // 注意: 只有库存为0的储位才能归档
  async archiveLocation(id: string) {
    await this.locationService.archive(id);
    // 会连同所有子储位一起归档
  }

  // 取消归档
  async unArchiveLocation(id: string) {
    const location = await this.locationService.unArchive(id);
    return location;
  }
}

MaterialService (物料服務)

MaterialService(物料服务)

typescript
import { MaterialService, MaterialEntity } from '@rytass/wms-base-nestjs-module';

@Injectable()
export class ProductService {
  constructor(private readonly materialService: MaterialService) {}

  // 建立物料
  async createMaterial() {
    const material = await this.materialService.create({
      id: 'SKU-001',
    });

    return material;
  }
}
typescript
import { MaterialService, MaterialEntity } from '@rytass/wms-base-nestjs-module';

@Injectable()
export class ProductService {
  constructor(private readonly materialService: MaterialService) {}

  // 创建物料
  async createMaterial() {
    const material = await this.materialService.create({
      id: 'SKU-001',
    });

    return material;
  }
}

StockService (庫存服務)

StockService(库存服务)

typescript
import {
  StockService,
  StockFindDto,
  StockFindAllDto,
  StockSorter,
} from '@rytass/wms-base-nestjs-module';

@Injectable()
export class InventoryService {
  constructor(private readonly stockService: StockService) {}

  // 查詢庫存數量 (回傳加總數量)
  // find() 可選傳入 manager 參數供交易使用
  async getStockQuantity(locationId: string, materialId: string): Promise<number> {
    return this.stockService.find({
      locationIds: [locationId],
      materialIds: [materialId],
      exactLocationMatch: true, // 只查詢該儲位,不包含子儲位
    });
  }

  // 查詢儲位樹下的總庫存 (包含所有子儲位)
  async getTotalStock(locationId: string): Promise<number> {
    return this.stockService.find({
      locationIds: [locationId],
      // exactLocationMatch: false (預設) - 包含所有子儲位
    });
  }

  // 查詢庫存異動記錄
  async getTransactionLogs(options: StockFindAllDto) {
    return this.stockService.findTransactions({
      locationIds: ['WAREHOUSE-A'],
      materialIds: ['SKU-001'],
      batchIds: ['BATCH-001'],
      offset: 0,
      limit: 20,  // 最大 100
      sorter: StockSorter.CREATED_AT_DESC,
    });
    // 回傳: StockCollectionDto { transactionLogs, total, offset, limit }
  }
}
StockFindDto 參數:
參數類型說明
locationIds
string[]
儲位 ID 列表
materialIds
string[]
物料 ID 列表
batchIds
string[]
批次 ID 列表
exactLocationMatch
boolean
true
: 只查詢指定儲位;
false
(預設): 包含子儲位
StockFindAllDto 額外參數:
參數類型預設值說明
offset
number
0
分頁偏移
limit
number
20
每頁筆數 (最大 100)
sorter
StockSorter
CREATED_AT_DESC
排序方式
StockCollectionDto 回傳結構:
typescript
interface StockCollectionDto {
  transactionLogs: StockTransactionLogDto[];
  total: number;
  offset: number;
  limit: number;
}

// 泛型版本,排除關聯欄位
type StockTransactionLogDto<Stock extends StockEntity = StockEntity> = Omit<
  Stock,
  'material' | 'batch' | 'location' | 'order'
>;

// 實際包含欄位(以預設 StockEntity 為例):
// { id, materialId, batchId, locationId, orderId, quantity, createdAt }
// 注意: 原始碼 Omit 了 'location',但 StockEntity 實際上沒有 location relation
// (只有 locationId 欄位),這是防禦性設計
typescript
import {
  StockService,
  StockFindDto,
  StockFindAllDto,
  StockSorter,
} from '@rytass/wms-base-nestjs-module';

@Injectable()
export class InventoryService {
  constructor(private readonly stockService: StockService) {}

  // 查询库存数量 (返回汇总数量)
  // find() 可选传入manager参数供事务使用
  async getStockQuantity(locationId: string, materialId: string): Promise<number> {
    return this.stockService.find({
      locationIds: [locationId],
      materialIds: [materialId],
      exactLocationMatch: true, // 只查询该储位,不包含子储位
    });
  }

  // 查询储位树下的总库存 (包含所有子储位)
  async getTotalStock(locationId: string): Promise<number> {
    return this.stockService.find({
      locationIds: [locationId],
      // exactLocationMatch: false (默认) - 包含所有子储位
    });
  }

  // 查询库存异动记录
  async getTransactionLogs(options: StockFindAllDto) {
    return this.stockService.findTransactions({
      locationIds: ['WAREHOUSE-A'],
      materialIds: ['SKU-001'],
      batchIds: ['BATCH-001'],
      offset: 0,
      limit: 20,  // 最大100
      sorter: StockSorter.CREATED_AT_DESC,
    });
    // 返回: StockCollectionDto { transactionLogs, total, offset, limit }
  }
}
StockFindDto 参数:
参数类型说明
locationIds
string[]
储位ID列表
materialIds
string[]
物料ID列表
batchIds
string[]
批次ID列表
exactLocationMatch
boolean
true
: 只查询指定储位;
false
(默认): 包含子储位
StockFindAllDto 额外参数:
参数类型默认值说明
offset
number
0
分页偏移
limit
number
20
每页笔数 (最大100)
sorter
StockSorter
CREATED_AT_DESC
排序方式
StockCollectionDto 返回结构:
typescript
interface StockCollectionDto {
  transactionLogs: StockTransactionLogDto[];
  total: number;
  offset: number;
  limit: number;
}

// 泛型版本,排除关联字段
type StockTransactionLogDto<Stock extends StockEntity = StockEntity> = Omit<
  Stock,
  'material' | 'batch' | 'location' | 'order'
>;

// 实际包含字段(以默认StockEntity为例):
// { id, materialId, batchId, locationId, orderId, quantity, createdAt }
// 注意: 源码Omit了'location',但StockEntity实际上没有location relation
// (只有locationId字段),这是防御性设计

OrderService (訂單服務)

OrderService(订单服务)

typescript
import {
  OrderService,
  OrderEntity,
  OrderCreateDto,
  BatchCreateDto,
} from '@rytass/wms-base-nestjs-module';
import { Entity, Column } from 'typeorm';

// 擴展訂單 Entity
@Entity('custom_orders')
export class CustomOrderEntity extends OrderEntity {
  @Column()
  orderNumber: string;

  @Column()
  type: 'INBOUND' | 'OUTBOUND';
}

@Injectable()
export class OrderManagementService {
  constructor(private readonly orderService: OrderService) {}

  // 建立入庫訂單
  async createInboundOrder() {
    const order = await this.orderService.createOrder(CustomOrderEntity, {
      order: {
        orderNumber: 'IN-2024-001',
        type: 'INBOUND',
      },
      batches: [
        {
          id: 'BATCH-001',
          materialId: 'SKU-001',
          locationId: 'ZONE-A1',
          quantity: 100, // 正數: 入庫
        },
      ],
    });

    return order;
  }

  // 建立出庫訂單
  async createOutboundOrder() {
    const order = await this.orderService.createOrder(CustomOrderEntity, {
      order: {
        orderNumber: 'OUT-2024-001',
        type: 'OUTBOUND',
      },
      batches: [
        {
          id: 'BATCH-001',
          materialId: 'SKU-001',
          locationId: 'ZONE-A1',
          quantity: -50, // 負數: 出庫
        },
      ],
    });

    return order;
    // 若 allowNegativeStock: false 且庫存不足,會拋出 StockQuantityNotEnoughError
  }

  // 檢查是否可建立庫存異動
  async canCreateStock(batch: BatchCreateDto): Promise<boolean> {
    return this.orderService.canCreateStock(batch);
  }
}
BatchCreateDto:
typescript
interface BatchCreateDto {
  id: string;
  materialId: string;
  locationId: string;
  quantity: number;
}
OrderCreateDto:
typescript
type OrderDto<O extends OrderEntity = OrderEntity> = DeepPartial<Omit<O, 'stocks'>>;

type OrderCreateDto<O extends OrderEntity = OrderEntity> = {
  order: OrderDto<O>;
  batches: BatchCreateDto[];
};
typescript
import {
  OrderService,
  OrderEntity,
  OrderCreateDto,
  BatchCreateDto,
} from '@rytass/wms-base-nestjs-module';
import { Entity, Column } from 'typeorm';

// 扩展订单Entity
@Entity('custom_orders')
export class CustomOrderEntity extends OrderEntity {
  @Column()
  orderNumber: string;

  @Column()
  type: 'INBOUND' | 'OUTBOUND';
}

@Injectable()
export class OrderManagementService {
  constructor(private readonly orderService: OrderService) {}

  // 创建入库订单
  async createInboundOrder() {
    const order = await this.orderService.createOrder(CustomOrderEntity, {
      order: {
        orderNumber: 'IN-2024-001',
        type: 'INBOUND',
      },
      batches: [
        {
          id: 'BATCH-001',
          materialId: 'SKU-001',
          locationId: 'ZONE-A1',
          quantity: 100, // 正数: 入库
        },
      ],
    });

    return order;
  }

  // 创建出库订单
  async createOutboundOrder() {
    const order = await this.orderService.createOrder(CustomOrderEntity, {
      order: {
        orderNumber: 'OUT-2024-001',
        type: 'OUTBOUND',
      },
      batches: [
        {
          id: 'BATCH-001',
          materialId: 'SKU-001',
          locationId: 'ZONE-A1',
          quantity: -50, // 负数: 出库
        },
      ],
    });

    return order;
    // 若allowNegativeStock: false且库存不足,会抛出StockQuantityNotEnoughError
  }

  // 检查是否可创建库存异动
  async canCreateStock(batch: BatchCreateDto): Promise<boolean> {
    return this.orderService.canCreateStock(batch);
  }
}
BatchCreateDto:
typescript
interface BatchCreateDto {
  id: string;
  materialId: string;
  locationId: string;
  quantity: number;
}
OrderCreateDto:
typescript
type OrderDto<O extends OrderEntity = OrderEntity> = DeepPartial<Omit<O, 'stocks'>>;

type OrderCreateDto<O extends OrderEntity = OrderEntity> = {
  order: OrderDto<O>;

  batches: BatchCreateDto[];
};

WarehouseMapService (倉庫地圖服務)

WarehouseMapService(仓库地图服务)

注意:
WarehouseMapService
有從 index.ts 導出,可直接 import。但
MapRangeType
MapRangeColor
目前未從 index.ts 導出,需自行定義或直接使用字串值。
typescript
import { WarehouseMapService } from '@rytass/wms-base-nestjs-module';
// MapRangeType/MapRangeColor 可直接使用字串值或自行定義 enum

// 可自行定義 enum 或使用字串常數
enum MapRangeType {
  RECTANGLE = 'RECTANGLE',
  POLYGON = 'POLYGON',
}

enum MapRangeColor {
  RED = 'RED',
  YELLOW = 'YELLOW',
  GREEN = 'GREEN',
  BLUE = 'BLUE',
  BLACK = 'BLACK',
}

@Injectable()
export class MapService {
  constructor(private readonly warehouseMapService: WarehouseMapService) {}

  // 更新倉庫地圖
  async updateMap(locationId: string) {
    const map = await this.warehouseMapService.updateMap(
      locationId,
      // backgrounds: 背景圖片
      [
        {
          id: 'bg-1',
          filename: 'warehouse-floor.png',
          x: 0,
          y: 0,
          width: 1000,
          height: 800,
        },
      ],
      // ranges: 區域標記
      [
        // 矩形區域
        {
          id: 'zone-a1',
          type: MapRangeType.RECTANGLE,
          color: MapRangeColor.GREEN,
          x: 100,
          y: 100,
          width: 200,
          height: 150,
        },
        // 多邊形區域
        {
          id: 'zone-special',
          type: MapRangeType.POLYGON,
          color: MapRangeColor.YELLOW,
          points: [
            { x: 400, y: 100 },
            { x: 500, y: 100 },
            { x: 550, y: 200 },
            { x: 400, y: 200 },
          ],
        },
      ],
    );

    return map;
  }

  // 取得地圖資料
  async getMap(locationId: string) {
    return this.warehouseMapService.getMapById(locationId);
    // 若不存在回傳: { id, backgrounds: [], ranges: [] }
  }

  // 刪除地圖
  async deleteMap(locationId: string) {
    await this.warehouseMapService.deleteMapById(locationId);
  }
}
注意:
WarehouseMapService
已从index.ts导出,可直接import。但
MapRangeType
MapRangeColor
目前未从index.ts导出,需自行定义或直接使用字符串值。
typescript
import { WarehouseMapService } from '@rytass/wms-base-nestjs-module';
// MapRangeType/MapRangeColor 可直接使用字符串值或自行定义enum

// 可自行定义enum或使用字符串常量
enum MapRangeType {
  RECTANGLE = 'RECTANGLE',
  POLYGON = 'POLYGON',
}

enum MapRangeColor {
  RED = 'RED',
  YELLOW = 'YELLOW',
  GREEN = 'GREEN',
  BLUE = 'BLUE',
  BLACK = 'BLACK',
}

@Injectable()
export class MapService {
  constructor(private readonly warehouseMapService: WarehouseMapService) {}

  // 更新仓库地图
  async updateMap(locationId: string) {
    const map = await this.warehouseMapService.updateMap(
      locationId,
      // backgrounds: 背景图片
      [
        {
          id: 'bg-1',
          filename: 'warehouse-floor.png',
          x: 0,
          y: 0,
          width: 1000,
          height: 800,
        },
      ],
      // ranges: 区域标记
      [
        // 矩形区域
        {
          id: 'zone-a1',
          type: MapRangeType.RECTANGLE,
          color: MapRangeColor.GREEN,
          x: 100,
          y: 100,
          width: 200,
          height: 150,
        },
        // 多边形区域
        {
          id: 'zone-special',
          type: MapRangeType.POLYGON,
          color: MapRangeColor.YELLOW,
          points: [
            { x: 400, y: 100 },
            { x: 500, y: 100 },
            { x: 550, y: 200 },
            { x: 400, y: 200 },
          ],
        },
      ],
    );

    return map;
  }

  // 获取地图数据
  async getMap(locationId: string) {
    return this.warehouseMapService.getMapById(locationId);
    // 若不存在返回: { id, backgrounds: [], ranges: [] }
  }

  // 删除地图
  async deleteMap(locationId: string) {
    await this.warehouseMapService.deleteMapById(locationId);
  }
}

Custom Entities (擴展 Entity)

自定义实体(扩展Entity)

透過
forRoot
forRootAsync
傳入自訂 Entity:
typescript
import {
  WMSBaseModule,
  LocationEntity,
  MaterialEntity,
  StockEntity,
  BatchEntity,
  OrderEntity,
} from '@rytass/wms-base-nestjs-module';
import { Entity, Column } from 'typeorm';

// 擴展儲位
@Entity('custom_locations')
export class CustomLocationEntity extends LocationEntity {
  @Column()
  name: string;

  @Column({ nullable: true })
  description: string;
}

// 擴展物料
@Entity('custom_materials')
export class CustomMaterialEntity extends MaterialEntity {
  @Column()
  name: string;

  @Column()
  sku: string;
}

// 模組設定
@Module({
  imports: [
    WMSBaseModule.forRoot({
      locationEntity: CustomLocationEntity,
      materialEntity: CustomMaterialEntity,
      stockEntity: StockEntity,  // 使用預設
      batchEntity: BatchEntity,  // 使用預設
      orderEntity: OrderEntity,  // 使用預設
      allowNegativeStock: false,
    }),
  ],
})
export class AppModule {}
通过
forRoot
forRootAsync
传入自定义Entity:
typescript
import {
  WMSBaseModule,
  LocationEntity,
  MaterialEntity,
  StockEntity,
  BatchEntity,
  OrderEntity,
} from '@rytass/wms-base-nestjs-module';
import { Entity, Column } from 'typeorm';

// 扩展储位
@Entity('custom_locations')
export class CustomLocationEntity extends LocationEntity {
  @Column()
  name: string;

  @Column({ nullable: true })
  description: string;
}

// 扩展物料
@Entity('custom_materials')
export class CustomMaterialEntity extends MaterialEntity {
  @Column()
  name: string;

  @Column()
  sku: string;
}

// 模块设置
@Module({
  imports: [
    WMSBaseModule.forRoot({
      locationEntity: CustomLocationEntity,
      materialEntity: CustomMaterialEntity,
      stockEntity: StockEntity,  // 使用默认
      batchEntity: BatchEntity,  // 使用默认
      orderEntity: OrderEntity,  // 使用默认
      allowNegativeStock: false,
    }),
  ],
})
export class AppModule {}

Data Types

数据类型

MapData

MapData

typescript
interface MapData {
  id: string;
  backgrounds: MapBackground[];
  ranges: (MapRectangleRange | MapPolygonRange)[];
}

interface MapBackground {
  id: string;
  filename: string;
  x: number;
  y: number;
  height: number;
  width: number;
}

interface MapRange {
  id: string;
  type: MapRangeType;
  color: string;
}

interface MapRectangleRange extends MapRange {
  x: number;
  y: number;
  width: number;
  height: number;
}

interface MapPolygonRange extends MapRange {
  points: MapPolygonRangePoint[];
}

interface MapPolygonRangePoint {
  x: number;
  y: number;
}
typescript
interface MapData {
  id: string;
  backgrounds: MapBackground[];
  ranges: (MapRectangleRange | MapPolygonRange)[];
}

interface MapBackground {
  id: string;
  filename: string;
  x: number;
  y: number;
  height: number;
  width: number;
}

interface MapRange {
  id: string;
  type: MapRangeType;
  color: string;
}

interface MapRectangleRange extends MapRange {
  x: number;
  y: number;
  width: number;
  height: number;
}

interface MapPolygonRange extends MapRange {
  points: MapPolygonRangePoint[];
}

interface MapPolygonRangePoint {
  x: number;
  y: number;
}

Enums

枚举

typescript
enum MapRangeType {
  RECTANGLE = 'RECTANGLE',
  POLYGON = 'POLYGON',
}

enum MapRangeColor {
  RED = 'RED',
  YELLOW = 'YELLOW',
  GREEN = 'GREEN',
  BLUE = 'BLUE',
  BLACK = 'BLACK',
}

enum StockSorter {
  CREATED_AT_DESC = 'CREATED_AT_DESC',
  CREATED_AT_ASC = 'CREATED_AT_ASC',
}
typescript
enum MapRangeType {
  RECTANGLE = 'RECTANGLE',
  POLYGON = 'POLYGON',
}

enum MapRangeColor {
  RED = 'RED',
  YELLOW = 'YELLOW',
  GREEN = 'GREEN',
  BLUE = 'BLUE',
  BLACK = 'BLACK',
}

enum StockSorter {
  CREATED_AT_DESC = 'CREATED_AT_DESC',
  CREATED_AT_ASC = 'CREATED_AT_ASC',
}

Error Handling

错误处理

typescript
import {
  LocationNotFoundError,
  LocationCannotArchiveError,
  LocationAlreadyExistedError,
  StockQuantityNotEnoughError,
} from '@rytass/wms-base-nestjs-module';
錯誤代碼表:
CodeErrorDescription
100LocationNotFoundError儲位不存在
101LocationCannotArchiveError儲位無法封存 (庫存不為 0)
102LocationAlreadyExistedError儲位已存在
200StockQuantityNotEnoughError庫存數量不足
typescript
@Injectable()
export class SafeLocationService {
  constructor(private readonly locationService: LocationService) {}

  async safeArchive(id: string) {
    try {
      await this.locationService.archive(id);
    } catch (error) {
      if (error instanceof LocationCannotArchiveError) {
        throw new Error('無法封存儲位,請先清空庫存');
      }
      if (error instanceof LocationNotFoundError) {
        throw new Error('儲位不存在');
      }
      throw error;
    }
  }
}
typescript
import {
  LocationNotFoundError,
  LocationCannotArchiveError,
  LocationAlreadyExistedError,
  StockQuantityNotEnoughError,
} from '@rytass/wms-base-nestjs-module';
错误代码表:
代码错误类型描述
100LocationNotFoundError储位不存在
101LocationCannotArchiveError储位无法归档 (库存不为0)
102LocationAlreadyExistedError储位已存在
200StockQuantityNotEnoughError库存数量不足
typescript
@Injectable()
export class SafeLocationService {
  constructor(private readonly locationService: LocationService) {}

  async safeArchive(id: string) {
    try {
      await this.locationService.archive(id);
    } catch (error) {
      if (error instanceof LocationCannotArchiveError) {
        throw new Error('无法归档储位,请先清空库存');
      }
      if (error instanceof LocationNotFoundError) {
        throw new Error('储位不存在');
      }
      throw error;
    }
  }
}

Configuration Options

配置选项

typescript
interface WMSBaseModuleOptions {
  // 自訂 Entity (皆為選填)
  stockEntity?: new () => StockEntity;
  locationEntity?: new () => LocationEntity;
  materialEntity?: new () => MaterialEntity;
  batchEntity?: new () => BatchEntity;
  orderEntity?: new () => OrderEntity;
  warehouseMapEntity?: new () => WarehouseMapEntity;

  // 選項
  allowNegativeStock?: boolean;  // 預設: false
}

// Async Options
interface WMSBaseModuleAsyncOptions {
  imports?: ModuleMetadata['imports'];
  inject?: InjectionToken[];
  useFactory?: (...args: any[]) => Promise<WMSBaseModuleOptions> | WMSBaseModuleOptions;
  useClass?: Type<WMSBaseModuleOptionsFactory>;
  useExisting?: Type<WMSBaseModuleOptionsFactory>;
}

// Options Factory Interface
interface WMSBaseModuleOptionsFactory {
  createWMSBaseModuleOptions(): Promise<WMSBaseModuleOptions> | WMSBaseModuleOptions;
}
typescript
interface WMSBaseModuleOptions {
  // 自定义Entity(均为可选)
  stockEntity?: new () => StockEntity;
  locationEntity?: new () => LocationEntity;
  materialEntity?: new () => MaterialEntity;
  batchEntity?: new () => BatchEntity;
  orderEntity?: new () => OrderEntity;
  warehouseMapEntity?: new () => WarehouseMapEntity;

  // 选项
  allowNegativeStock?: boolean;  // 默认: false
}

// 异步选项
interface WMSBaseModuleAsyncOptions {
  imports?: ModuleMetadata['imports'];
  inject?: InjectionToken[];
  useFactory?: (...args: any[]) => Promise<WMSBaseModuleOptions> | WMSBaseModuleOptions;
  useClass?: Type<WMSBaseModuleOptionsFactory>;
  useExisting?: Type<WMSBaseModuleOptionsFactory>;
}

// 选项工厂接口
interface WMSBaseModuleOptionsFactory {
  createWMSBaseModuleOptions(): Promise<WMSBaseModuleOptions> | WMSBaseModuleOptions;
}

Symbol Tokens

符号令牌

可用於依賴注入的 Symbol Tokens:
typescript
import {
  // Repository Tokens
  RESOLVED_TREE_LOCATION_REPO,   // TreeRepository<LocationEntity>
  RESOLVED_MATERIAL_REPO,        // Repository<MaterialEntity>
  RESOLVED_BATCH_REPO,           // Repository<BatchEntity>
  RESOLVED_ORDER_REPO,           // Repository<OrderEntity>
  RESOLVED_STOCK_REPO,           // Repository<StockEntity>
  RESOLVED_WAREHOUSE_MAP_REPO,   // Repository<WarehouseMapEntity>

  // Entity Provider Tokens
  PROVIDE_LOCATION_ENTITY,
  PROVIDE_MATERIAL_ENTITY,
  PROVIDE_BATCH_ENTITY,
  PROVIDE_ORDER_ENTITY,
  PROVIDE_STOCK_ENTITY,
  PROVIDE_WAREHOUSE_MAP_ENTITY,

  // Options Tokens
  WMS_MODULE_OPTIONS,            // WMSBaseModuleOptions
  ALLOW_NEGATIVE_STOCK,          // boolean
} from '@rytass/wms-base-nestjs-module';

// 使用範例
@Injectable()
export class CustomService {
  constructor(
    @Inject(RESOLVED_TREE_LOCATION_REPO)
    private readonly locationRepo: TreeRepository<LocationEntity>,

    @Inject(RESOLVED_STOCK_REPO)
    private readonly stockRepo: Repository<StockEntity>,

    @Inject(ALLOW_NEGATIVE_STOCK)
    private readonly allowNegativeStock: boolean,
  ) {}
}
可用于依赖注入的Symbol Tokens:
typescript
import {
  // 仓库令牌
  RESOLVED_TREE_LOCATION_REPO,   // TreeRepository<LocationEntity>
  RESOLVED_MATERIAL_REPO,        // Repository<MaterialEntity>
  RESOLVED_BATCH_REPO,           // Repository<BatchEntity>
  RESOLVED_ORDER_REPO,           // Repository<OrderEntity>
  RESOLVED_STOCK_REPO,           // Repository<StockEntity>
  RESOLVED_WAREHOUSE_MAP_REPO,   // Repository<WarehouseMapEntity>

  // Entity提供器令牌
  PROVIDE_LOCATION_ENTITY,
  PROVIDE_MATERIAL_ENTITY,
  PROVIDE_BATCH_ENTITY,
  PROVIDE_ORDER_ENTITY,
  PROVIDE_STOCK_ENTITY,
  PROVIDE_WAREHOUSE_MAP_ENTITY,

  // 选项令牌
  WMS_MODULE_OPTIONS,            // WMSBaseModuleOptions
  ALLOW_NEGATIVE_STOCK,          // boolean
} from '@rytass/wms-base-nestjs-module';

// 使用示例
@Injectable()
export class CustomService {
  constructor(
    @Inject(RESOLVED_TREE_LOCATION_REPO)
    private readonly locationRepo: TreeRepository<LocationEntity>,

    @Inject(RESOLVED_STOCK_REPO)
    private readonly stockRepo: Repository<StockEntity>,

    @Inject(ALLOW_NEGATIVE_STOCK)
    private readonly allowNegativeStock: boolean,
  ) {}
}

Complete Example

完整示例

typescript
import { Module, Injectable } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
  WMSBaseModule,
  LocationService,
  MaterialService,
  StockService,
  OrderService,
  OrderEntity,
  StockSorter,
} from '@rytass/wms-base-nestjs-module';
import { Entity, Column } from 'typeorm';

// 自訂訂單
@Entity('warehouse_orders')
export class WarehouseOrderEntity extends OrderEntity {
  @Column()
  orderNumber: string;

  @Column()
  type: 'INBOUND' | 'OUTBOUND' | 'TRANSFER';

  @Column({ nullable: true })
  note: string;
}

// 倉儲服務
@Injectable()
export class WarehouseManagementService {
  constructor(
    private readonly locationService: LocationService,
    private readonly materialService: MaterialService,
    private readonly stockService: StockService,
    private readonly orderService: OrderService,
  ) {}

  // 初始化倉庫結構
  async initializeWarehouse() {
    const warehouse = await this.locationService.create({ id: 'WH-001' });
    const zoneA = await this.locationService.create({ id: 'WH-001-A', parentId: 'WH-001' });
    const zoneB = await this.locationService.create({ id: 'WH-001-B', parentId: 'WH-001' });

    return { warehouse, zoneA, zoneB };
  }

  // 入庫作業
  async inbound(materialId: string, locationId: string, quantity: number, batchId: string) {
    // 確保物料存在
    await this.materialService.create({ id: materialId });

    // 建立入庫訂單
    const order = await this.orderService.createOrder(WarehouseOrderEntity, {
      order: {
        orderNumber: `IN-${Date.now()}`,
        type: 'INBOUND',
      },
      batches: [{
        id: batchId,
        materialId,
        locationId,
        quantity, // 正數
      }],
    });

    return order;
  }

  // 出庫作業
  async outbound(materialId: string, locationId: string, quantity: number, batchId: string) {
    // 檢查庫存
    const stock = await this.stockService.find({
      locationIds: [locationId],
      materialIds: [materialId],
      batchIds: [batchId],
      exactLocationMatch: true,
    });

    if (stock < quantity) {
      throw new Error(`庫存不足: 現有 ${stock}, 需要 ${quantity}`);
    }

    // 建立出庫訂單
    const order = await this.orderService.createOrder(WarehouseOrderEntity, {
      order: {
        orderNumber: `OUT-${Date.now()}`,
        type: 'OUTBOUND',
      },
      batches: [{
        id: batchId,
        materialId,
        locationId,
        quantity: -quantity, // 負數
      }],
    });

    return order;
  }

  // 查詢庫存
  async getInventory(locationId: string) {
    const total = await this.stockService.find({ locationIds: [locationId] });
    const logs = await this.stockService.findTransactions({
      locationIds: [locationId],
      limit: 10,
      sorter: StockSorter.CREATED_AT_DESC,
    });

    return { total, recentLogs: logs.transactionLogs };
  }
}

// 模組
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      database: 'wms',
      entities: [WarehouseOrderEntity],
      synchronize: true,
    }),
    WMSBaseModule.forRoot({
      orderEntity: WarehouseOrderEntity,
      allowNegativeStock: false,
    }),
  ],
  providers: [WarehouseManagementService],
})
export class WarehouseModule {}
typescript
import { Module, Injectable } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
  WMSBaseModule,
  LocationService,
  MaterialService,
  StockService,
  OrderService,
  OrderEntity,
  StockSorter,
} from '@rytass/wms-base-nestjs-module';
import { Entity, Column } from 'typeorm';

// 自定义订单
@Entity('warehouse_orders')
export class WarehouseOrderEntity extends OrderEntity {
  @Column()
  orderNumber: string;

  @Column()
  type: 'INBOUND' | 'OUTBOUND' | 'TRANSFER';

  @Column({ nullable: true })
  note: string;
}

// 仓储服务
@Injectable()
export class WarehouseManagementService {
  constructor(
    private readonly locationService: LocationService,
    private readonly materialService: MaterialService,
    private readonly stockService: StockService,
    private readonly orderService: OrderService,
  ) {}

  // 初始化仓库结构
  async initializeWarehouse() {
    const warehouse = await this.locationService.create({ id: 'WH-001' });
    const zoneA = await this.locationService.create({ id: 'WH-001-A', parentId: 'WH-001' });
    const zoneB = await this.locationService.create({ id: 'WH-001-B', parentId: 'WH-001' });

    return { warehouse, zoneA, zoneB };
  }

  // 入库作业
  async inbound(materialId: string, locationId: string, quantity: number, batchId: string) {
    // 确保物料存在
    await this.materialService.create({ id: materialId });

    // 创建入库订单
    const order = await this.orderService.createOrder(WarehouseOrderEntity, {
      order: {
        orderNumber: `IN-${Date.now()}`,
        type: 'INBOUND',
      },
      batches: [{
        id: batchId,
        materialId,
        locationId,
        quantity, // 正数
      }],
    });

    return order;
  }

  // 出库作业
  async outbound(materialId: string, locationId: string, quantity: number, batchId: string) {
    // 查询库存
    const stock = await this.stockService.find({
      locationIds: [locationId],
      materialIds: [materialId],
      batchIds: [batchId],
      exactLocationMatch: true,
    });

    if (stock < quantity) {
      throw new Error(`库存不足: 现有 ${stock}, 需要 ${quantity}`);
    }

    // 创建出库订单
    const order = await this.orderService.createOrder(WarehouseOrderEntity, {
      order: {
        orderNumber: `OUT-${Date.now()}`,
        type: 'OUTBOUND',
      },
      batches: [{
        id: batchId,
        materialId,
        locationId,
        quantity: -quantity, // 负数
      }],
    });

    return order;
  }

  // 查询库存
  async getInventory(locationId: string) {
    const total = await this.stockService.find({ locationIds: [locationId] });
    const logs = await this.stockService.findTransactions({
      locationIds: [locationId],
      limit: 10,
      sorter: StockSorter.CREATED_AT_DESC,
    });

    return { total, recentLogs: logs.transactionLogs };
  }
}

// 模块
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      database: 'wms',
      entities: [WarehouseOrderEntity],
      synchronize: true,
    }),
    WMSBaseModule.forRoot({
      orderEntity: WarehouseOrderEntity,
      allowNegativeStock: false,
    }),
  ],
  providers: [WarehouseManagementService],
})
export class WarehouseModule {}

Troubleshooting

故障排除

負庫存錯誤

负库存错误

如果
allowNegativeStock: false
,出庫數量超過庫存會拋出
StockQuantityNotEnoughError
。 先查詢庫存再執行出庫操作。
如果
allowNegativeStock: false
,出库数量超过库存会抛出
StockQuantityNotEnoughError
。 先查询库存再执行出库操作。

交易失敗

交易失败

createOrder
使用資料庫交易,任何批次失敗都會回滾整個訂單。 確保所有批次資料正確再提交。
createOrder
使用数据库事务,任何批次失败都会回滚整个订单。 确保所有批次数据正确再提交。

倉位無法封存

储位无法归档

當嘗試封存一個仍有庫存的倉位時,會拋出
LocationCannotArchiveError
。 需先將庫存移出(出庫或調撥)後才能封存倉位。
当尝试归档一个仍有库存的储位时,会抛出
LocationCannotArchiveError
。 需先将库存移出(出库或调拨)后才能归档储位。

倉位已存在錯誤

储位已存在错误

建立儲位時,若 ID 已存在(含已封存的),會拋出
LocationAlreadyExistedError
。 可以先解除封存 (
unArchive
) 或使用不同的 ID。
创建储位时,若ID已存在(含已归档的),会抛出
LocationAlreadyExistedError
。 可以先取消归档(
unArchive
)或使用不同的ID。