10.1 中央库存系统概述
中央库存系统是新零售SaaS平台的核心组成部分,它负责统一管理企业所有渠道的库存信息,实现库存的实时同步、精准控制和智能调配。在多渠道零售环境下,中央库存系统确保了库存数据的一致性和准确性,为企业提供了全局的库存视图和决策支持。
10.1.1 中央库存系统的重要性
在传统的零售模式中,不同渠道往往维护独立的库存系统,这导致了以下问题:
- 库存数据不一致:各渠道库存数据无法实时同步,容易出现超卖或缺货情况
- 库存可见性差:无法获得全局库存视图,影响采购和销售决策
- 库存周转率低:无法实现库存在不同渠道间的灵活调配
- 客户体验差:因库存信息不准确导致的订单取消和延迟交付
中央库存系统通过统一的库存管理,解决了这些问题:
/**
* 中央库存系统核心价值
*/
public class CentralInventoryValue {
/**
* 库存统一管理的核心价值
*/
public enum CoreValue {
REAL_TIME_SYNC("实时同步", "所有渠道库存实时同步更新"),
GLOBAL_VISIBILITY("全局可见", "提供企业级库存全局视图"),
INTELLIGENT_ALLOCATION("智能调配", "基于算法的库存智能分配"),
ACCURATE_CONTROL("精准控制", "精确的库存数量和状态管理"),
UNIFIED_RULES("统一规则", "标准化的库存管理规则和流程");
private final String name;
private final String description;
CoreValue(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() { return name; }
public String getDescription() { return description; }
}
/**
* 业务收益指标
*/
public static class BusinessBenefits {
private double inventoryTurnoverImprovement; // 库存周转率提升
private double stockoutReduction; // 缺货率降低
private double oversellReduction; // 超卖率降低
private double customerSatisfactionIncrease; // 客户满意度提升
private double operationalEfficiencyGain; // 运营效率提升
public BusinessBenefits() {
// 基于行业数据的预期收益
this.inventoryTurnoverImprovement = 0.25; // 25%提升
this.stockoutReduction = 0.40; // 40%降低
this.oversellReduction = 0.90; // 90%降低
this.customerSatisfactionIncrease = 0.15; // 15%提升
this.operationalEfficiencyGain = 0.30; // 30%提升
}
public Map<String, String> getBenefitsSummary() {
Map<String, String> benefits = new HashMap<>();
benefits.put("库存周转率", String.format("提升%.0f%%", inventoryTurnoverImprovement * 100));
benefits.put("缺货率", String.format("降低%.0f%%", stockoutReduction * 100));
benefits.put("超卖率", String.format("降低%.0f%%", oversellReduction * 100));
benefits.put("客户满意度", String.format("提升%.0f%%", customerSatisfactionIncrease * 100));
benefits.put("运营效率", String.format("提升%.0f%%", operationalEfficiencyGain * 100));
return benefits;
}
}
}
10.1.2 系统架构设计原则
中央库存系统的设计遵循以下核心原则:
/**
* 中央库存系统设计原则
*/
public class InventorySystemPrinciples {
/**
* 设计原则枚举
*/
public enum DesignPrinciple {
SINGLE_SOURCE_OF_TRUTH("单一数据源", "库存数据的唯一权威来源"),
REAL_TIME_CONSISTENCY("实时一致性", "确保所有渠道库存数据实时一致"),
HIGH_AVAILABILITY("高可用性", "系统7x24小时稳定运行"),
SCALABILITY("可扩展性", "支持业务规模的快速增长"),
FAULT_TOLERANCE("容错性", "具备故障自动恢复能力"),
PERFORMANCE_OPTIMIZATION("性能优化", "高并发下的快速响应"),
SECURITY_COMPLIANCE("安全合规", "数据安全和合规性保障");
private final String name;
private final String description;
DesignPrinciple(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() { return name; }
public String getDescription() { return description; }
}
/**
* 架构质量属性
*/
public static class QualityAttributes {
private Map<String, QualityRequirement> requirements;
public QualityAttributes() {
this.requirements = new HashMap<>();
initializeRequirements();
}
private void initializeRequirements() {
requirements.put("可用性", new QualityRequirement(
"99.9%", "系统年度可用性不低于99.9%", "关键业务连续性保障"
));
requirements.put("响应时间", new QualityRequirement(
"<100ms", "库存查询响应时间小于100毫秒", "用户体验优化"
));
requirements.put("并发处理", new QualityRequirement(
"10000 TPS", "支持每秒10000次事务处理", "高并发场景支持"
));
requirements.put("数据一致性", new QualityRequirement(
"强一致性", "库存数据强一致性保证", "业务准确性要求"
));
requirements.put("扩展性", new QualityRequirement(
"水平扩展", "支持水平扩展到1000+节点", "业务增长支撑"
));
}
public QualityRequirement getRequirement(String attribute) {
return requirements.get(attribute);
}
public Map<String, QualityRequirement> getAllRequirements() {
return new HashMap<>(requirements);
}
}
public static class QualityRequirement {
private String target; // 目标值
private String description; // 描述
private String rationale; // 理由
public QualityRequirement(String target, String description, String rationale) {
this.target = target;
this.description = description;
this.rationale = rationale;
}
// getters
public String getTarget() { return target; }
public String getDescription() { return description; }
public String getRationale() { return rationale; }
}
}
10.2 库存数据模型设计
库存数据模型是中央库存系统的基础,它定义了库存信息的结构、关系和约束。一个良好的数据模型能够支持复杂的业务场景,同时保证数据的完整性和一致性。
10.2.1 核心库存实体
/**
* 库存核心实体定义
*/
@Entity
@Table(name = "inventory_item")
public class InventoryItem extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "sku_id", nullable = false)
private String skuId; // SKU标识
@Column(name = "warehouse_id", nullable = false)
private String warehouseId; // 仓库标识
@Column(name = "location_id")
private String locationId; // 库位标识
@Column(name = "available_quantity", nullable = false)
private Integer availableQuantity; // 可用库存
@Column(name = "reserved_quantity", nullable = false)
private Integer reservedQuantity; // 预留库存
@Column(name = "allocated_quantity", nullable = false)
private Integer allocatedQuantity; // 已分配库存
@Column(name = "in_transit_quantity", nullable = false)
private Integer inTransitQuantity; // 在途库存
@Column(name = "damaged_quantity", nullable = false)
private Integer damagedQuantity; // 损坏库存
@Column(name = "total_quantity", nullable = false)
private Integer totalQuantity; // 总库存
@Column(name = "safety_stock")
private Integer safetyStock; // 安全库存
@Column(name = "reorder_point")
private Integer reorderPoint; // 补货点
@Column(name = "max_stock")
private Integer maxStock; // 最大库存
@Column(name = "unit_cost")
private BigDecimal unitCost; // 单位成本
@Column(name = "last_updated")
private LocalDateTime lastUpdated; // 最后更新时间
@Column(name = "version", nullable = false)
@Version
private Long version; // 乐观锁版本号
@Enumerated(EnumType.STRING)
@Column(name = "status")
private InventoryStatus status; // 库存状态
@Column(name = "batch_number")
private String batchNumber; // 批次号
@Column(name = "expiry_date")
private LocalDate expiryDate; // 过期日期
@Column(name = "supplier_id")
private String supplierId; // 供应商ID
// 构造函数
public InventoryItem() {}
public InventoryItem(String skuId, String warehouseId) {
this.skuId = skuId;
this.warehouseId = warehouseId;
this.availableQuantity = 0;
this.reservedQuantity = 0;
this.allocatedQuantity = 0;
this.inTransitQuantity = 0;
this.damagedQuantity = 0;
this.totalQuantity = 0;
this.status = InventoryStatus.ACTIVE;
this.lastUpdated = LocalDateTime.now();
}
/**
* 计算可销售库存
*/
public Integer getAvailableForSale() {
return availableQuantity - reservedQuantity;
}
/**
* 检查是否需要补货
*/
public boolean needsReorder() {
return reorderPoint != null && availableQuantity <= reorderPoint;
}
/**
* 检查是否库存充足
*/
public boolean hasSufficientStock(Integer requiredQuantity) {
return getAvailableForSale() >= requiredQuantity;
}
/**
* 更新总库存
*/
public void updateTotalQuantity() {
this.totalQuantity = availableQuantity + reservedQuantity +
allocatedQuantity + inTransitQuantity + damagedQuantity;
this.lastUpdated = LocalDateTime.now();
}
/**
* 预留库存
*/
public boolean reserveStock(Integer quantity) {
if (hasSufficientStock(quantity)) {
this.availableQuantity -= quantity;
this.reservedQuantity += quantity;
updateTotalQuantity();
return true;
}
return false;
}
/**
* 释放预留库存
*/
public void releaseReservedStock(Integer quantity) {
int releaseQuantity = Math.min(quantity, this.reservedQuantity);
this.reservedQuantity -= releaseQuantity;
this.availableQuantity += releaseQuantity;
updateTotalQuantity();
}
/**
* 分配库存
*/
public boolean allocateStock(Integer quantity) {
if (this.reservedQuantity >= quantity) {
this.reservedQuantity -= quantity;
this.allocatedQuantity += quantity;
updateTotalQuantity();
return true;
}
return false;
}
/**
* 出库
*/
public boolean shipStock(Integer quantity) {
if (this.allocatedQuantity >= quantity) {
this.allocatedQuantity -= quantity;
updateTotalQuantity();
return true;
}
return false;
}
/**
* 入库
*/
public void receiveStock(Integer quantity) {
this.availableQuantity += quantity;
updateTotalQuantity();
}
// getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getSkuId() { return skuId; }
public void setSkuId(String skuId) { this.skuId = skuId; }
public String getWarehouseId() { return warehouseId; }
public void setWarehouseId(String warehouseId) { this.warehouseId = warehouseId; }
public String getLocationId() { return locationId; }
public void setLocationId(String locationId) { this.locationId = locationId; }
public Integer getAvailableQuantity() { return availableQuantity; }
public void setAvailableQuantity(Integer availableQuantity) {
this.availableQuantity = availableQuantity;
updateTotalQuantity();
}
public Integer getReservedQuantity() { return reservedQuantity; }
public void setReservedQuantity(Integer reservedQuantity) {
this.reservedQuantity = reservedQuantity;
updateTotalQuantity();
}
public Integer getAllocatedQuantity() { return allocatedQuantity; }
public void setAllocatedQuantity(Integer allocatedQuantity) {
this.allocatedQuantity = allocatedQuantity;
updateTotalQuantity();
}
public Integer getInTransitQuantity() { return inTransitQuantity; }
public void setInTransitQuantity(Integer inTransitQuantity) {
this.inTransitQuantity = inTransitQuantity;
updateTotalQuantity();
}
public Integer getDamagedQuantity() { return damagedQuantity; }
public void setDamagedQuantity(Integer damagedQuantity) {
this.damagedQuantity = damagedQuantity;
updateTotalQuantity();
}
public Integer getTotalQuantity() { return totalQuantity; }
public Integer getSafetyStock() { return safetyStock; }
public void setSafetyStock(Integer safetyStock) { this.safetyStock = safetyStock; }
public Integer getReorderPoint() { return reorderPoint; }
public void setReorderPoint(Integer reorderPoint) { this.reorderPoint = reorderPoint; }
public Integer getMaxStock() { return maxStock; }
public void setMaxStock(Integer maxStock) { this.maxStock = maxStock; }
public BigDecimal getUnitCost() { return unitCost; }
public void setUnitCost(BigDecimal unitCost) { this.unitCost = unitCost; }
public LocalDateTime getLastUpdated() { return lastUpdated; }
public void setLastUpdated(LocalDateTime lastUpdated) { this.lastUpdated = lastUpdated; }
public Long getVersion() { return version; }
public void setVersion(Long version) { this.version = version; }
public InventoryStatus getStatus() { return status; }
public void setStatus(InventoryStatus status) { this.status = status; }
public String getBatchNumber() { return batchNumber; }
public void setBatchNumber(String batchNumber) { this.batchNumber = batchNumber; }
public LocalDate getExpiryDate() { return expiryDate; }
public void setExpiryDate(LocalDate expiryDate) { this.expiryDate = expiryDate; }
public String getSupplierId() { return supplierId; }
public void setSupplierId(String supplierId) { this.supplierId = supplierId; }
}
/**
* 库存状态枚举
*/
public enum InventoryStatus {
ACTIVE("正常"),
INACTIVE("停用"),
QUARANTINE("隔离"),
EXPIRED("过期"),
DAMAGED("损坏"),
RESERVED("预留"),
ALLOCATED("已分配"),
IN_TRANSIT("在途");
private final String description;
InventoryStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
10.7 API接口设计
中央库存系统需要提供完整的REST API接口,支持各种库存操作:
/**
* 库存管理API控制器
*/
@RestController
@RequestMapping("/api/v1/inventory")
@Validated
public class InventoryController {
private final InventoryService inventoryService;
private final InventoryAnalyticsService analyticsService;
private final IntelligentReplenishmentService replenishmentService;
private final InventoryAlertService alertService;
public InventoryController(InventoryService inventoryService,
InventoryAnalyticsService analyticsService,
IntelligentReplenishmentService replenishmentService,
InventoryAlertService alertService) {
this.inventoryService = inventoryService;
this.analyticsService = analyticsService;
this.replenishmentService = replenishmentService;
this.alertService = alertService;
}
/**
* 查询库存信息
*/
@GetMapping("/items")
public ResponseEntity<PageResult<InventoryItemDTO>> getInventoryItems(
@RequestParam(required = false) String warehouseId,
@RequestParam(required = false) String skuId,
@RequestParam(required = false) InventoryStatus status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
InventoryQueryRequest request = new InventoryQueryRequest(
warehouseId, skuId, status, page, size
);
PageResult<InventoryItem> result = inventoryService.queryInventory(request);
PageResult<InventoryItemDTO> dtoResult = result.map(this::convertToDTO);
return ResponseEntity.ok(dtoResult);
}
/**
* 获取单个库存项详情
*/
@GetMapping("/items/{warehouseId}/{skuId}")
public ResponseEntity<InventoryItemDTO> getInventoryItem(
@PathVariable String warehouseId,
@PathVariable String skuId) {
Optional<InventoryItem> inventory = inventoryService.getInventory(skuId, warehouseId);
return inventory
.map(item -> ResponseEntity.ok(convertToDTO(item)))
.orElse(ResponseEntity.notFound().build());
}
/**
* 检查库存可用性
*/
@GetMapping("/availability")
public ResponseEntity<InventoryAvailabilityDTO> checkAvailability(
@RequestParam String skuId,
@RequestParam String warehouseId,
@RequestParam int quantity) {
boolean available = inventoryService.checkAvailability(skuId, warehouseId, quantity);
InventoryItem inventory = inventoryService.getInventory(skuId, warehouseId)
.orElseThrow(() -> new InventoryNotFoundException("库存不存在"));
InventoryAvailabilityDTO dto = new InventoryAvailabilityDTO(
skuId,
warehouseId,
quantity,
available,
inventory.getAvailableQuantity(),
inventory.getReservedQuantity()
);
return ResponseEntity.ok(dto);
}
/**
* 预留库存
*/
@PostMapping("/reserve")
public ResponseEntity<InventoryReservationDTO> reserveInventory(
@Valid @RequestBody InventoryReservationRequest request) {
try {
String reservationId = inventoryService.reserveInventory(
request.getSkuId(),
request.getWarehouseId(),
request.getQuantity(),
request.getReservationReason(),
request.getExpiryTime()
);
InventoryReservationDTO dto = new InventoryReservationDTO(
reservationId,
request.getSkuId(),
request.getWarehouseId(),
request.getQuantity(),
LocalDateTime.now(),
request.getExpiryTime(),
"RESERVED"
);
return ResponseEntity.ok(dto);
} catch (InsufficientInventoryException e) {
return ResponseEntity.badRequest()
.body(new InventoryReservationDTO(null, request.getSkuId(),
request.getWarehouseId(), request.getQuantity(),
LocalDateTime.now(), null, "FAILED: " + e.getMessage()));
}
}
/**
* 释放预留库存
*/
@PostMapping("/release")
public ResponseEntity<Void> releaseReservation(
@Valid @RequestBody InventoryReleaseRequest request) {
inventoryService.releaseReservation(
request.getSkuId(),
request.getWarehouseId(),
request.getQuantity(),
request.getReleaseReason()
);
return ResponseEntity.ok().build();
}
/**
* 分配库存
*/
@PostMapping("/allocate")
public ResponseEntity<InventoryAllocationDTO> allocateInventory(
@Valid @RequestBody InventoryAllocationRequest request) {
try {
String allocationId = inventoryService.allocateInventory(
request.getSkuId(),
request.getWarehouseId(),
request.getQuantity(),
request.getOrderId(),
request.getAllocationReason()
);
InventoryAllocationDTO dto = new InventoryAllocationDTO(
allocationId,
request.getSkuId(),
request.getWarehouseId(),
request.getQuantity(),
request.getOrderId(),
LocalDateTime.now(),
"ALLOCATED"
);
return ResponseEntity.ok(dto);
} catch (InsufficientInventoryException e) {
return ResponseEntity.badRequest()
.body(new InventoryAllocationDTO(null, request.getSkuId(),
request.getWarehouseId(), request.getQuantity(),
request.getOrderId(), LocalDateTime.now(),
"FAILED: " + e.getMessage()));
}
}
/**
* 库存出库
*/
@PostMapping("/outbound")
public ResponseEntity<InventoryTransactionDTO> outboundInventory(
@Valid @RequestBody InventoryOutboundRequest request) {
try {
String transactionId = inventoryService.outbound(
request.getSkuId(),
request.getWarehouseId(),
request.getQuantity(),
request.getUnitCost(),
request.getDocumentId(),
request.getDocumentType(),
request.getReason()
);
InventoryTransactionDTO dto = new InventoryTransactionDTO(
transactionId,
request.getSkuId(),
request.getWarehouseId(),
TransactionType.OUTBOUND,
request.getQuantity(),
request.getUnitCost(),
LocalDateTime.now(),
"COMPLETED"
);
return ResponseEntity.ok(dto);
} catch (InsufficientInventoryException e) {
return ResponseEntity.badRequest()
.body(new InventoryTransactionDTO(null, request.getSkuId(),
request.getWarehouseId(), TransactionType.OUTBOUND,
request.getQuantity(), request.getUnitCost(),
LocalDateTime.now(), "FAILED: " + e.getMessage()));
}
}
/**
* 库存入库
*/
@PostMapping("/inbound")
public ResponseEntity<InventoryTransactionDTO> inboundInventory(
@Valid @RequestBody InventoryInboundRequest request) {
String transactionId = inventoryService.inbound(
request.getSkuId(),
request.getWarehouseId(),
request.getQuantity(),
request.getUnitCost(),
request.getDocumentId(),
request.getDocumentType(),
request.getBatchNumber(),
request.getExpiryDate(),
request.getSupplierId(),
request.getReason()
);
InventoryTransactionDTO dto = new InventoryTransactionDTO(
transactionId,
request.getSkuId(),
request.getWarehouseId(),
TransactionType.INBOUND,
request.getQuantity(),
request.getUnitCost(),
LocalDateTime.now(),
"COMPLETED"
);
return ResponseEntity.ok(dto);
}
/**
* 库存调整
*/
@PostMapping("/adjust")
public ResponseEntity<InventoryTransactionDTO> adjustInventory(
@Valid @RequestBody InventoryAdjustmentRequest request) {
String transactionId = inventoryService.adjustInventory(
request.getSkuId(),
request.getWarehouseId(),
request.getQuantityChange(),
request.getUnitCost(),
request.getReason()
);
InventoryTransactionDTO dto = new InventoryTransactionDTO(
transactionId,
request.getSkuId(),
request.getWarehouseId(),
TransactionType.ADJUSTMENT,
request.getQuantityChange(),
request.getUnitCost(),
LocalDateTime.now(),
"COMPLETED"
);
return ResponseEntity.ok(dto);
}
/**
* 库存转移
*/
@PostMapping("/transfer")
public ResponseEntity<InventoryTransferDTO> transferInventory(
@Valid @RequestBody InventoryTransferRequest request) {
try {
String transferId = inventoryService.transferInventory(
request.getSkuId(),
request.getFromWarehouseId(),
request.getToWarehouseId(),
request.getQuantity(),
request.getReason()
);
InventoryTransferDTO dto = new InventoryTransferDTO(
transferId,
request.getSkuId(),
request.getFromWarehouseId(),
request.getToWarehouseId(),
request.getQuantity(),
LocalDateTime.now(),
"COMPLETED"
);
return ResponseEntity.ok(dto);
} catch (InsufficientInventoryException e) {
return ResponseEntity.badRequest()
.body(new InventoryTransferDTO(null, request.getSkuId(),
request.getFromWarehouseId(), request.getToWarehouseId(),
request.getQuantity(), LocalDateTime.now(),
"FAILED: " + e.getMessage()));
}
}
/**
* 获取库存事务历史
*/
@GetMapping("/transactions")
public ResponseEntity<PageResult<InventoryTransactionDTO>> getTransactionHistory(
@RequestParam(required = false) String skuId,
@RequestParam(required = false) String warehouseId,
@RequestParam(required = false) TransactionType transactionType,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
PageResult<InventoryTransaction> result = inventoryService.getTransactionHistory(
skuId, warehouseId, transactionType, startTime, endTime, page, size
);
PageResult<InventoryTransactionDTO> dtoResult = result.map(this::convertTransactionToDTO);
return ResponseEntity.ok(dtoResult);
}
/**
* 获取库存概览统计
*/
@GetMapping("/stats/overview")
public ResponseEntity<InventoryOverviewStatsDTO> getInventoryOverview(
@RequestParam String warehouseId) {
InventoryOverviewStats stats = analyticsService.getInventoryOverview(warehouseId);
InventoryOverviewStatsDTO dto = convertOverviewStatsToDTO(stats);
return ResponseEntity.ok(dto);
}
/**
* 获取库存周转分析
*/
@GetMapping("/analytics/turnover")
public ResponseEntity<InventoryTurnoverAnalysisDTO> getTurnoverAnalysis(
@RequestParam String warehouseId,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
InventoryTurnoverAnalysis analysis = analyticsService.getInventoryTurnoverAnalysis(
warehouseId, startDate, endDate
);
InventoryTurnoverAnalysisDTO dto = convertTurnoverAnalysisToDTO(analysis);
return ResponseEntity.ok(dto);
}
/**
* 获取ABC分析
*/
@GetMapping("/analytics/abc")
public ResponseEntity<ABCAnalysisResultDTO> getABCAnalysis(
@RequestParam String warehouseId,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
ABCAnalysisResult analysis = analyticsService.getABCAnalysis(warehouseId, startDate, endDate);
ABCAnalysisResultDTO dto = convertABCAnalysisToDTO(analysis);
return ResponseEntity.ok(dto);
}
/**
* 获取库存异常分析
*/
@GetMapping("/analytics/anomalies")
public ResponseEntity<InventoryAnomalyAnalysisDTO> getAnomalyAnalysis(
@RequestParam String warehouseId) {
InventoryAnomalyAnalysis analysis = analyticsService.getInventoryAnomalyAnalysis(warehouseId);
InventoryAnomalyAnalysisDTO dto = convertAnomalyAnalysisToDTO(analysis);
return ResponseEntity.ok(dto);
}
/**
* 获取补货建议
*/
@GetMapping("/replenishment/recommendations")
public ResponseEntity<ReplenishmentAnalysisResultDTO> getReplenishmentRecommendations(
@RequestParam String warehouseId) {
ReplenishmentAnalysisResult result = replenishmentService.analyzeWarehouseReplenishment(warehouseId);
ReplenishmentAnalysisResultDTO dto = convertReplenishmentResultToDTO(result);
return ResponseEntity.ok(dto);
}
/**
* 获取库存预警
*/
@GetMapping("/alerts")
public ResponseEntity<List<InventoryAlertDTO>> getInventoryAlerts(
@RequestParam String warehouseId,
@RequestParam(required = false) AlertType alertType,
@RequestParam(required = false) AlertLevel alertLevel) {
List<InventoryAlert> alerts = alertService.checkInventoryAlerts(warehouseId, alertType, alertLevel);
List<InventoryAlertDTO> dtoList = alerts.stream()
.map(this::convertAlertToDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(dtoList);
}
// DTO转换方法
private InventoryItemDTO convertToDTO(InventoryItem item) {
return new InventoryItemDTO(
item.getSkuId(),
item.getWarehouseId(),
item.getLocationId(),
item.getAvailableQuantity(),
item.getReservedQuantity(),
item.getAllocatedQuantity(),
item.getInTransitQuantity(),
item.getDamagedQuantity(),
item.getTotalQuantity(),
item.getSafetyStock(),
item.getReorderPoint(),
item.getMaxStock(),
item.getUnitCost(),
item.getLastUpdated(),
item.getVersion(),
item.getStatus(),
item.getBatchNumber(),
item.getExpiryDate(),
item.getSupplierId()
);
}
private InventoryTransactionDTO convertTransactionToDTO(InventoryTransaction transaction) {
return new InventoryTransactionDTO(
transaction.getTransactionId(),
transaction.getSkuId(),
transaction.getWarehouseId(),
transaction.getTransactionType(),
transaction.getQuantityChange(),
transaction.getUnitCost(),
transaction.getTransactionTime(),
transaction.getTransactionStatus().name()
);
}
private InventoryAlertDTO convertAlertToDTO(InventoryAlert alert) {
return new InventoryAlertDTO(
alert.getAlertId(),
alert.getSkuId(),
alert.getWarehouseId(),
alert.getAlertType(),
alert.getAlertLevel(),
alert.getMessage(),
alert.getCurrentQuantity(),
alert.getThresholdQuantity(),
alert.getCreatedTime(),
alert.isResolved()
);
}
// 其他DTO转换方法...
private InventoryOverviewStatsDTO convertOverviewStatsToDTO(InventoryOverviewStats stats) {
// 实现转换逻辑
return new InventoryOverviewStatsDTO(/* 参数 */);
}
private InventoryTurnoverAnalysisDTO convertTurnoverAnalysisToDTO(InventoryTurnoverAnalysis analysis) {
// 实现转换逻辑
return new InventoryTurnoverAnalysisDTO(/* 参数 */);
}
private ABCAnalysisResultDTO convertABCAnalysisToDTO(ABCAnalysisResult analysis) {
// 实现转换逻辑
return new ABCAnalysisResultDTO(/* 参数 */);
}
private InventoryAnomalyAnalysisDTO convertAnomalyAnalysisToDTO(InventoryAnomalyAnalysis analysis) {
// 实现转换逻辑
return new InventoryAnomalyAnalysisDTO(/* 参数 */);
}
private ReplenishmentAnalysisResultDTO convertReplenishmentResultToDTO(ReplenishmentAnalysisResult result) {
// 实现转换逻辑
return new ReplenishmentAnalysisResultDTO(/* 参数 */);
}
}
/**
* 全局异常处理器
*/
@ControllerAdvice
public class InventoryExceptionHandler {
@ExceptionHandler(InventoryNotFoundException.class)
public ResponseEntity<ErrorResponse> handleInventoryNotFound(InventoryNotFoundException e) {
ErrorResponse error = new ErrorResponse(
"INVENTORY_NOT_FOUND",
e.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(InsufficientInventoryException.class)
public ResponseEntity<ErrorResponse> handleInsufficientInventory(InsufficientInventoryException e) {
ErrorResponse error = new ErrorResponse(
"INSUFFICIENT_INVENTORY",
e.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@ExceptionHandler(InventoryLockException.class)
public ResponseEntity<ErrorResponse> handleInventoryLock(InventoryLockException e) {
ErrorResponse error = new ErrorResponse(
"INVENTORY_LOCK_FAILED",
e.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidation(ValidationException e) {
ErrorResponse error = new ErrorResponse(
"VALIDATION_ERROR",
e.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception e) {
ErrorResponse error = new ErrorResponse(
"INTERNAL_ERROR",
"系统内部错误,请稍后重试",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
/**
* 错误响应
*/
public class ErrorResponse {
private final String code;
private final String message;
private final LocalDateTime timestamp;
public ErrorResponse(String code, String message, LocalDateTime timestamp) {
this.code = code;
this.message = message;
this.timestamp = timestamp;
}
// getters
public String getCode() { return code; }
public String getMessage() { return message; }
public LocalDateTime getTimestamp() { return timestamp; }
}
10.8 性能优化策略
中央库存系统作为高并发系统,需要采用多种性能优化策略:
10.8.1 缓存策略
/**
* 库存缓存服务
*/
@Service
public class InventoryCacheService {
private final RedisTemplate<String, Object> redisTemplate;
private final InventoryRepository inventoryRepository;
private static final String INVENTORY_CACHE_PREFIX = "inventory:";
private static final String AVAILABILITY_CACHE_PREFIX = "availability:";
private static final int CACHE_TTL_SECONDS = 300; // 5分钟
public InventoryCacheService(RedisTemplate<String, Object> redisTemplate,
InventoryRepository inventoryRepository) {
this.redisTemplate = redisTemplate;
this.inventoryRepository = inventoryRepository;
}
/**
* 获取缓存的库存信息
*/
public Optional<InventoryItem> getCachedInventory(String skuId, String warehouseId) {
String key = INVENTORY_CACHE_PREFIX + skuId + ":" + warehouseId;
InventoryItem cached = (InventoryItem) redisTemplate.opsForValue().get(key);
return Optional.ofNullable(cached);
}
/**
* 缓存库存信息
*/
public void cacheInventory(InventoryItem inventory) {
String key = INVENTORY_CACHE_PREFIX + inventory.getSkuId() + ":" + inventory.getWarehouseId();
redisTemplate.opsForValue().set(key, inventory, CACHE_TTL_SECONDS, TimeUnit.SECONDS);
}
/**
* 删除库存缓存
*/
public void evictInventoryCache(String skuId, String warehouseId) {
String key = INVENTORY_CACHE_PREFIX + skuId + ":" + warehouseId;
redisTemplate.delete(key);
}
/**
* 批量预热库存缓存
*/
@Async
public void warmUpInventoryCache(String warehouseId) {
List<InventoryItem> inventories = inventoryRepository.findByWarehouseId(warehouseId);
for (InventoryItem inventory : inventories) {
cacheInventory(inventory);
}
}
/**
* 缓存库存可用性检查结果
*/
public void cacheAvailabilityCheck(String skuId, String warehouseId, int quantity, boolean available) {
String key = AVAILABILITY_CACHE_PREFIX + skuId + ":" + warehouseId + ":" + quantity;
redisTemplate.opsForValue().set(key, available, 60, TimeUnit.SECONDS); // 1分钟缓存
}
/**
* 获取缓存的可用性检查结果
*/
public Optional<Boolean> getCachedAvailabilityCheck(String skuId, String warehouseId, int quantity) {
String key = AVAILABILITY_CACHE_PREFIX + skuId + ":" + warehouseId + ":" + quantity;
Boolean cached = (Boolean) redisTemplate.opsForValue().get(key);
return Optional.ofNullable(cached);
}
}
10.8.2 数据库优化
-- 库存表索引优化
CREATE INDEX idx_inventory_warehouse_sku ON inventory_items(warehouse_id, sku_id);
CREATE INDEX idx_inventory_status ON inventory_items(status);
CREATE INDEX idx_inventory_updated ON inventory_items(last_updated);
CREATE INDEX idx_inventory_safety_stock ON inventory_items(available_quantity, safety_stock);
-- 事务表索引优化
CREATE INDEX idx_transaction_sku_warehouse ON inventory_transactions(sku_id, warehouse_id);
CREATE INDEX idx_transaction_time ON inventory_transactions(transaction_time);
CREATE INDEX idx_transaction_type ON inventory_transactions(transaction_type);
CREATE INDEX idx_transaction_document ON inventory_transactions(document_id, document_type);
-- 分区策略(按时间分区)
CREATE TABLE inventory_transactions_2024_01 PARTITION OF inventory_transactions
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE inventory_transactions_2024_02 PARTITION OF inventory_transactions
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
-- 库存汇总视图
CREATE MATERIALIZED VIEW inventory_summary AS
SELECT
warehouse_id,
COUNT(*) as total_skus,
SUM(total_quantity) as total_quantity,
SUM(available_quantity) as available_quantity,
SUM(reserved_quantity) as reserved_quantity,
SUM(unit_cost * total_quantity) as total_value,
COUNT(CASE WHEN available_quantity <= COALESCE(safety_stock, 10) THEN 1 END) as low_stock_count,
COUNT(CASE WHEN available_quantity <= 0 THEN 1 END) as out_of_stock_count
FROM inventory_items
WHERE status = 'ACTIVE'
GROUP BY warehouse_id;
-- 定期刷新汇总视图
CREATE OR REPLACE FUNCTION refresh_inventory_summary()
RETURNS void AS $$
BEGIN
REFRESH MATERIALIZED VIEW inventory_summary;
END;
$$ LANGUAGE plpgsql;
-- 创建定时任务刷新视图
SELECT cron.schedule('refresh-inventory-summary', '*/5 * * * *', 'SELECT refresh_inventory_summary();');
10.8.3 读写分离
/**
* 读写分离配置
*/
@Configuration
public class DatabaseConfig {
@Bean
@Primary
public DataSource routingDataSource() {
RoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("write", writeDataSource());
dataSourceMap.put("read", readDataSource());
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(writeDataSource());
return routingDataSource;
}
@Bean
public DataSource writeDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://write-db:5432/inventory");
config.setUsername("inventory_user");
config.setPassword("password");
config.setMaximumPoolSize(20);
return new HikariDataSource(config);
}
@Bean
public DataSource readDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://read-db:5432/inventory");
config.setUsername("inventory_user");
config.setPassword("password");
config.setMaximumPoolSize(30);
config.setReadOnly(true);
return new HikariDataSource(config);
}
}
/**
* 动态数据源路由
*/
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getDataSourceType();
}
}
/**
* 数据源上下文
*/
public class DatabaseContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
/**
* 读写分离注解
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}
/**
* 读写分离切面
*/
@Aspect
@Component
public class ReadOnlyAspect {
@Before("@annotation(readOnly)")
public void setReadDataSourceType(ReadOnly readOnly) {
DatabaseContextHolder.setDataSourceType("read");
}
@Before("@within(readOnly) && !@annotation(org.springframework.transaction.annotation.Transactional)")
public void setReadDataSourceTypeForClass(ReadOnly readOnly) {
DatabaseContextHolder.setDataSourceType("read");
}
@After("@annotation(readOnly) || @within(readOnly)")
public void resetDataSourceType() {
DatabaseContextHolder.clearDataSourceType();
}
}
10.9 本章总结
本章详细介绍了SaaS平台中央库存系统的设计与实现,涵盖了以下核心内容:
10.9.1 系统架构设计
中央库存系统采用了分层架构设计,包括:
- 数据层:基于关系型数据库的库存数据模型,支持多仓库、多SKU的复杂库存管理
- 服务层:核心业务逻辑实现,包括库存查询、预留、分配、出入库等操作
- 缓存层:基于Redis的分布式缓存,提升系统性能和响应速度
- API层:RESTful API接口,支持各种库存操作和查询需求
10.9.2 核心功能实现
系统实现了完整的库存管理功能:
-
基础库存管理
- 多维度库存数据模型(可用、预留、分配、在途、损坏等)
- 完整的库存事务记录和审计跟踪
- 灵活的库存状态管理和生命周期控制
-
库存操作功能
- 库存查询和可用性检查
- 库存预留和释放机制
- 库存分配和出入库操作
- 库存调整和转移功能
-
数据一致性保证
- 基于Redis的分布式锁机制
- 事件驱动的库存同步
- 库存数据一致性检查和修复
-
智能预警和补货
- 多层次库存预警系统
- 基于历史数据的智能补货策略
- 经济订货量(EOQ)和安全库存计算
-
统计分析功能
- 库存概览和实时统计
- 库存周转率分析
- ABC分析和异常检测
10.9.3 技术特点
系统在技术实现上具有以下特点:
-
高并发处理
- 分布式锁保证并发安全
- 缓存策略提升访问性能
- 读写分离优化数据库负载
-
数据一致性
- 事务管理确保操作原子性
- 事件驱动保证数据同步
- 定期一致性检查和修复
-
可扩展性
- 微服务架构支持水平扩展
- 数据库分区支持大数据量
- 插件化设计支持功能扩展
-
监控和运维
- 完整的日志记录和审计
- 性能监控和预警机制
- 自动化运维和故障恢复
10.9.4 实践建议
在实际项目中实施中央库存系统时,建议:
-
分阶段实施
- 先实现基础库存管理功能
- 逐步增加智能预警和分析功能
- 最后优化性能和扩展功能
-
数据质量管理
- 建立完善的数据校验机制
- 定期进行数据一致性检查
- 制定数据清理和修复流程
-
性能优化
- 合理设计缓存策略
- 优化数据库查询和索引
- 监控系统性能指标
-
业务集成
- 与订单系统紧密集成
- 支持多渠道库存共享
- 提供灵活的API接口
10.9.5 发展趋势
中央库存系统的发展趋势包括:
-
智能化升级
- AI驱动的需求预测
- 机器学习优化补货策略
- 智能库存配置建议
-
实时化处理
- 流式数据处理
- 实时库存同步
- 即时预警和响应
-
云原生架构
- 容器化部署
- 服务网格管理
- 弹性伸缩能力
中央库存系统作为SaaS平台的核心组件,其设计和实现质量直接影响整个平台的稳定性和性能。通过本章的学习,读者应该能够理解和掌握构建高可用、高性能库存管理系统的关键技术和最佳实践。
10.2.2 库存事务记录
为了确保库存数据的可追溯性和审计能力,系统需要记录所有的库存变更事务:
/**
* 库存事务记录
*/
@Entity
@Table(name = "inventory_transaction")
public class InventoryTransaction extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "transaction_id", unique = true, nullable = false)
private String transactionId; // 事务ID
@Column(name = "sku_id", nullable = false)
private String skuId; // SKU标识
@Column(name = "warehouse_id", nullable = false)
private String warehouseId; // 仓库标识
@Column(name = "location_id")
private String locationId; // 库位标识
@Enumerated(EnumType.STRING)
@Column(name = "transaction_type", nullable = false)
private TransactionType transactionType; // 事务类型
@Column(name = "quantity_change", nullable = false)
private Integer quantityChange; // 数量变更
@Column(name = "quantity_before")
private Integer quantityBefore; // 变更前数量
@Column(name = "quantity_after")
private Integer quantityAfter; // 变更后数量
@Column(name = "unit_cost")
private BigDecimal unitCost; // 单位成本
@Column(name = "total_cost")
private BigDecimal totalCost; // 总成本
@Column(name = "reference_id")
private String referenceId; // 关联单据ID
@Column(name = "reference_type")
private String referenceType; // 关联单据类型
@Column(name = "batch_number")
private String batchNumber; // 批次号
@Column(name = "supplier_id")
private String supplierId; // 供应商ID
@Column(name = "customer_id")
private String customerId; // 客户ID
@Column(name = "reason")
private String reason; // 变更原因
@Column(name = "notes")
private String notes; // 备注
@Column(name = "transaction_time", nullable = false)
private LocalDateTime transactionTime; // 事务时间
@Column(name = "processed_by")
private String processedBy; // 处理人
@Enumerated(EnumType.STRING)
@Column(name = "status")
private TransactionStatus status; // 事务状态
// 构造函数
public InventoryTransaction() {
this.transactionId = generateTransactionId();
this.transactionTime = LocalDateTime.now();
this.status = TransactionStatus.PENDING;
}
public InventoryTransaction(String skuId, String warehouseId,
TransactionType transactionType, Integer quantityChange) {
this();
this.skuId = skuId;
this.warehouseId = warehouseId;
this.transactionType = transactionType;
this.quantityChange = quantityChange;
}
/**
* 生成事务ID
*/
private String generateTransactionId() {
return "TXN" + System.currentTimeMillis() +
String.format("%04d", new Random().nextInt(10000));
}
/**
* 计算总成本
*/
public void calculateTotalCost() {
if (unitCost != null && quantityChange != null) {
this.totalCost = unitCost.multiply(new BigDecimal(Math.abs(quantityChange)));
}
}
/**
* 标记事务完成
*/
public void markCompleted() {
this.status = TransactionStatus.COMPLETED;
}
/**
* 标记事务失败
*/
public void markFailed(String reason) {
this.status = TransactionStatus.FAILED;
this.reason = reason;
}
// getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTransactionId() { return transactionId; }
public void setTransactionId(String transactionId) { this.transactionId = transactionId; }
public String getSkuId() { return skuId; }
public void setSkuId(String skuId) { this.skuId = skuId; }
public String getWarehouseId() { return warehouseId; }
public void setWarehouseId(String warehouseId) { this.warehouseId = warehouseId; }
public String getLocationId() { return locationId; }
public void setLocationId(String locationId) { this.locationId = locationId; }
public TransactionType getTransactionType() { return transactionType; }
public void setTransactionType(TransactionType transactionType) { this.transactionType = transactionType; }
public Integer getQuantityChange() { return quantityChange; }
public void setQuantityChange(Integer quantityChange) {
this.quantityChange = quantityChange;
calculateTotalCost();
}
public Integer getQuantityBefore() { return quantityBefore; }
public void setQuantityBefore(Integer quantityBefore) { this.quantityBefore = quantityBefore; }
public Integer getQuantityAfter() { return quantityAfter; }
public void setQuantityAfter(Integer quantityAfter) { this.quantityAfter = quantityAfter; }
public BigDecimal getUnitCost() { return unitCost; }
public void setUnitCost(BigDecimal unitCost) {
this.unitCost = unitCost;
calculateTotalCost();
}
public BigDecimal getTotalCost() { return totalCost; }
public void setTotalCost(BigDecimal totalCost) { this.totalCost = totalCost; }
public String getReferenceId() { return referenceId; }
public void setReferenceId(String referenceId) { this.referenceId = referenceId; }
public String getReferenceType() { return referenceType; }
public void setReferenceType(String referenceType) { this.referenceType = referenceType; }
public String getBatchNumber() { return batchNumber; }
public void setBatchNumber(String batchNumber) { this.batchNumber = batchNumber; }
public String getSupplierId() { return supplierId; }
public void setSupplierId(String supplierId) { this.supplierId = supplierId; }
public String getCustomerId() { return customerId; }
public void setCustomerId(String customerId) { this.customerId = customerId; }
public String getReason() { return reason; }
public void setReason(String reason) { this.reason = reason; }
public String getNotes() { return notes; }
public void setNotes(String notes) { this.notes = notes; }
public LocalDateTime getTransactionTime() { return transactionTime; }
public void setTransactionTime(LocalDateTime transactionTime) { this.transactionTime = transactionTime; }
public String getProcessedBy() { return processedBy; }
public void setProcessedBy(String processedBy) { this.processedBy = processedBy; }
public TransactionStatus getStatus() { return status; }
public void setStatus(TransactionStatus status) { this.status = status; }
}
/**
* 事务类型枚举
*/
public enum TransactionType {
INBOUND("入库", 1),
OUTBOUND("出库", -1),
TRANSFER_IN("调拨入库", 1),
TRANSFER_OUT("调拨出库", -1),
ADJUSTMENT_INCREASE("盘盈", 1),
ADJUSTMENT_DECREASE("盘亏", -1),
RESERVE("预留", 0),
RELEASE_RESERVE("释放预留", 0),
ALLOCATE("分配", 0),
DAMAGE("损坏", -1),
RETURN("退货", 1),
CONSUME("消耗", -1);
private final String description;
private final int quantityMultiplier; // 数量乘数,用于计算库存变化方向
TransactionType(String description, int quantityMultiplier) {
this.description = description;
this.quantityMultiplier = quantityMultiplier;
}
public String getDescription() {
return description;
}
public int getQuantityMultiplier() {
return quantityMultiplier;
}
/**
* 是否为入库类型
*/
public boolean isInbound() {
return quantityMultiplier > 0;
}
/**
* 是否为出库类型
*/
public boolean isOutbound() {
return quantityMultiplier < 0;
}
/**
* 是否为状态变更类型
*/
public boolean isStatusChange() {
return quantityMultiplier == 0;
}
}
/**
* 事务状态枚举
*/
public enum TransactionStatus {
PENDING("待处理"),
PROCESSING("处理中"),
COMPLETED("已完成"),
FAILED("失败"),
CANCELLED("已取消"),
REVERSED("已冲销");
private final String description;
TransactionStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
10.3 库存服务核心功能
中央库存系统的核心功能包括库存查询、库存更新、库存预留、库存分配等。这些功能需要保证高性能、高并发和数据一致性。
10.3.1 库存服务接口设计
/**
* 库存服务接口
*/
public interface InventoryService {
/**
* 查询库存信息
*/
InventoryItem getInventory(String skuId, String warehouseId);
/**
* 批量查询库存信息
*/
List<InventoryItem> getInventories(List<String> skuIds, String warehouseId);
/**
* 查询SKU在所有仓库的库存
*/
List<InventoryItem> getInventoriesBySku(String skuId);
/**
* 查询仓库所有库存
*/
List<InventoryItem> getInventoriesByWarehouse(String warehouseId);
/**
* 检查库存可用性
*/
boolean checkAvailability(String skuId, String warehouseId, Integer quantity);
/**
* 批量检查库存可用性
*/
Map<String, Boolean> checkAvailabilities(Map<String, Integer> skuQuantities, String warehouseId);
/**
* 预留库存
*/
ReservationResult reserveInventory(InventoryReservationRequest request);
/**
* 批量预留库存
*/
List<ReservationResult> reserveInventories(List<InventoryReservationRequest> requests);
/**
* 释放预留库存
*/
boolean releaseReservation(String reservationId);
/**
* 分配库存
*/
AllocationResult allocateInventory(InventoryAllocationRequest request);
/**
* 批量分配库存
*/
List<AllocationResult> allocateInventories(List<InventoryAllocationRequest> requests);
/**
* 出库
*/
ShipmentResult shipInventory(InventoryShipmentRequest request);
/**
* 入库
*/
ReceiptResult receiveInventory(InventoryReceiptRequest request);
/**
* 库存调整
*/
AdjustmentResult adjustInventory(InventoryAdjustmentRequest request);
/**
* 库存转移
*/
TransferResult transferInventory(InventoryTransferRequest request);
/**
* 获取库存事务历史
*/
List<InventoryTransaction> getTransactionHistory(String skuId, String warehouseId,
LocalDateTime startTime, LocalDateTime endTime);
/**
* 获取低库存预警
*/
List<InventoryAlert> getLowStockAlerts(String warehouseId);
/**
* 获取库存统计信息
*/
InventoryStatistics getInventoryStatistics(String warehouseId, LocalDate date);
}
10.3.2 库存服务实现
/**
* 库存服务实现
*/
@Service
@Transactional
public class InventoryServiceImpl implements InventoryService {
private final InventoryRepository inventoryRepository;
private final InventoryTransactionRepository transactionRepository;
private final InventoryLockService lockService;
private final InventoryEventPublisher eventPublisher;
private final InventoryValidator validator;
private final RedisTemplate<String, Object> redisTemplate;
public InventoryServiceImpl(InventoryRepository inventoryRepository,
InventoryTransactionRepository transactionRepository,
InventoryLockService lockService,
InventoryEventPublisher eventPublisher,
InventoryValidator validator,
RedisTemplate<String, Object> redisTemplate) {
this.inventoryRepository = inventoryRepository;
this.transactionRepository = transactionRepository;
this.lockService = lockService;
this.eventPublisher = eventPublisher;
this.validator = validator;
this.redisTemplate = redisTemplate;
}
@Override
@Cacheable(value = "inventory", key = "#skuId + ':' + #warehouseId")
public InventoryItem getInventory(String skuId, String warehouseId) {
return inventoryRepository.findBySkuIdAndWarehouseId(skuId, warehouseId)
.orElse(null);
}
@Override
public List<InventoryItem> getInventories(List<String> skuIds, String warehouseId) {
return inventoryRepository.findBySkuIdInAndWarehouseId(skuIds, warehouseId);
}
@Override
public List<InventoryItem> getInventoriesBySku(String skuId) {
return inventoryRepository.findBySkuId(skuId);
}
@Override
public List<InventoryItem> getInventoriesByWarehouse(String warehouseId) {
return inventoryRepository.findByWarehouseId(warehouseId);
}
@Override
public boolean checkAvailability(String skuId, String warehouseId, Integer quantity) {
InventoryItem inventory = getInventory(skuId, warehouseId);
return inventory != null && inventory.hasSufficientStock(quantity);
}
@Override
public Map<String, Boolean> checkAvailabilities(Map<String, Integer> skuQuantities, String warehouseId) {
Map<String, Boolean> results = new HashMap<>();
List<String> skuIds = new ArrayList<>(skuQuantities.keySet());
List<InventoryItem> inventories = getInventories(skuIds, warehouseId);
Map<String, InventoryItem> inventoryMap = inventories.stream()
.collect(Collectors.toMap(InventoryItem::getSkuId, Function.identity()));
for (Map.Entry<String, Integer> entry : skuQuantities.entrySet()) {
String skuId = entry.getKey();
Integer quantity = entry.getValue();
InventoryItem inventory = inventoryMap.get(skuId);
boolean available = inventory != null && inventory.hasSufficientStock(quantity);
results.put(skuId, available);
}
return results;
}
@Override
public ReservationResult reserveInventory(InventoryReservationRequest request) {
String lockKey = generateLockKey(request.getSkuId(), request.getWarehouseId());
return lockService.executeWithLock(lockKey, () -> {
// 验证请求
validator.validateReservationRequest(request);
// 获取库存
InventoryItem inventory = getInventory(request.getSkuId(), request.getWarehouseId());
if (inventory == null) {
return ReservationResult.failure("库存不存在");
}
// 检查库存可用性
if (!inventory.hasSufficientStock(request.getQuantity())) {
return ReservationResult.failure("库存不足");
}
// 预留库存
boolean success = inventory.reserveStock(request.getQuantity());
if (!success) {
return ReservationResult.failure("预留失败");
}
// 保存库存变更
inventoryRepository.save(inventory);
// 记录事务
InventoryTransaction transaction = createTransaction(
request.getSkuId(), request.getWarehouseId(),
TransactionType.RESERVE, request.getQuantity(),
request.getReferenceId(), request.getReferenceType()
);
transactionRepository.save(transaction);
// 清除缓存
evictInventoryCache(request.getSkuId(), request.getWarehouseId());
// 发布事件
eventPublisher.publishInventoryReserved(inventory, request.getQuantity());
return ReservationResult.success(transaction.getTransactionId());
});
}
@Override
public List<ReservationResult> reserveInventories(List<InventoryReservationRequest> requests) {
List<ReservationResult> results = new ArrayList<>();
for (InventoryReservationRequest request : requests) {
try {
ReservationResult result = reserveInventory(request);
results.add(result);
// 如果预留失败,回滚之前的预留
if (!result.isSuccess()) {
rollbackReservations(results);
break;
}
} catch (Exception e) {
results.add(ReservationResult.failure("预留异常: " + e.getMessage()));
rollbackReservations(results);
break;
}
}
return results;
}
@Override
public boolean releaseReservation(String reservationId) {
InventoryTransaction transaction = transactionRepository.findByTransactionId(reservationId)
.orElse(null);
if (transaction == null || transaction.getTransactionType() != TransactionType.RESERVE) {
return false;
}
String lockKey = generateLockKey(transaction.getSkuId(), transaction.getWarehouseId());
return lockService.executeWithLock(lockKey, () -> {
InventoryItem inventory = getInventory(transaction.getSkuId(), transaction.getWarehouseId());
if (inventory == null) {
return false;
}
// 释放预留库存
inventory.releaseReservedStock(transaction.getQuantityChange());
inventoryRepository.save(inventory);
// 记录释放事务
InventoryTransaction releaseTransaction = createTransaction(
transaction.getSkuId(), transaction.getWarehouseId(),
TransactionType.RELEASE_RESERVE, transaction.getQuantityChange(),
transaction.getReferenceId(), transaction.getReferenceType()
);
transactionRepository.save(releaseTransaction);
// 清除缓存
evictInventoryCache(transaction.getSkuId(), transaction.getWarehouseId());
// 发布事件
eventPublisher.publishReservationReleased(inventory, transaction.getQuantityChange());
return true;
});
}
@Override
public AllocationResult allocateInventory(InventoryAllocationRequest request) {
String lockKey = generateLockKey(request.getSkuId(), request.getWarehouseId());
return lockService.executeWithLock(lockKey, () -> {
// 验证请求
validator.validateAllocationRequest(request);
// 获取库存
InventoryItem inventory = getInventory(request.getSkuId(), request.getWarehouseId());
if (inventory == null) {
return AllocationResult.failure("库存不存在");
}
// 分配库存
boolean success = inventory.allocateStock(request.getQuantity());
if (!success) {
return AllocationResult.failure("分配失败,预留库存不足");
}
// 保存库存变更
inventoryRepository.save(inventory);
// 记录事务
InventoryTransaction transaction = createTransaction(
request.getSkuId(), request.getWarehouseId(),
TransactionType.ALLOCATE, request.getQuantity(),
request.getReferenceId(), request.getReferenceType()
);
transactionRepository.save(transaction);
// 清除缓存
evictInventoryCache(request.getSkuId(), request.getWarehouseId());
// 发布事件
eventPublisher.publishInventoryAllocated(inventory, request.getQuantity());
return AllocationResult.success(transaction.getTransactionId());
});
}
@Override
public List<AllocationResult> allocateInventories(List<InventoryAllocationRequest> requests) {
List<AllocationResult> results = new ArrayList<>();
for (InventoryAllocationRequest request : requests) {
try {
AllocationResult result = allocateInventory(request);
results.add(result);
// 如果分配失败,回滚之前的分配
if (!result.isSuccess()) {
rollbackAllocations(results);
break;
}
} catch (Exception e) {
results.add(AllocationResult.failure("分配异常: " + e.getMessage()));
rollbackAllocations(results);
break;
}
}
return results;
}
@Override
public ShipmentResult shipInventory(InventoryShipmentRequest request) {
String lockKey = generateLockKey(request.getSkuId(), request.getWarehouseId());
return lockService.executeWithLock(lockKey, () -> {
// 验证请求
validator.validateShipmentRequest(request);
// 获取库存
InventoryItem inventory = getInventory(request.getSkuId(), request.getWarehouseId());
if (inventory == null) {
return ShipmentResult.failure("库存不存在");
}
// 出库
boolean success = inventory.shipStock(request.getQuantity());
if (!success) {
return ShipmentResult.failure("出库失败,已分配库存不足");
}
// 保存库存变更
inventoryRepository.save(inventory);
// 记录事务
InventoryTransaction transaction = createTransaction(
request.getSkuId(), request.getWarehouseId(),
TransactionType.OUTBOUND, request.getQuantity(),
request.getReferenceId(), request.getReferenceType()
);
transaction.setCustomerId(request.getCustomerId());
transactionRepository.save(transaction);
// 清除缓存
evictInventoryCache(request.getSkuId(), request.getWarehouseId());
// 发布事件
eventPublisher.publishInventoryShipped(inventory, request.getQuantity());
return ShipmentResult.success(transaction.getTransactionId());
});
}
@Override
public ReceiptResult receiveInventory(InventoryReceiptRequest request) {
String lockKey = generateLockKey(request.getSkuId(), request.getWarehouseId());
return lockService.executeWithLock(lockKey, () -> {
// 验证请求
validator.validateReceiptRequest(request);
// 获取或创建库存
InventoryItem inventory = getInventory(request.getSkuId(), request.getWarehouseId());
if (inventory == null) {
inventory = new InventoryItem(request.getSkuId(), request.getWarehouseId());
inventory.setLocationId(request.getLocationId());
inventory.setBatchNumber(request.getBatchNumber());
inventory.setSupplierId(request.getSupplierId());
inventory.setUnitCost(request.getUnitCost());
}
// 入库
inventory.receiveStock(request.getQuantity());
// 保存库存变更
inventoryRepository.save(inventory);
// 记录事务
InventoryTransaction transaction = createTransaction(
request.getSkuId(), request.getWarehouseId(),
TransactionType.INBOUND, request.getQuantity(),
request.getReferenceId(), request.getReferenceType()
);
transaction.setSupplierId(request.getSupplierId());
transaction.setBatchNumber(request.getBatchNumber());
transaction.setUnitCost(request.getUnitCost());
transactionRepository.save(transaction);
// 清除缓存
evictInventoryCache(request.getSkuId(), request.getWarehouseId());
// 发布事件
eventPublisher.publishInventoryReceived(inventory, request.getQuantity());
return ReceiptResult.success(transaction.getTransactionId());
});
}
@Override
public AdjustmentResult adjustInventory(InventoryAdjustmentRequest request) {
String lockKey = generateLockKey(request.getSkuId(), request.getWarehouseId());
return lockService.executeWithLock(lockKey, () -> {
// 验证请求
validator.validateAdjustmentRequest(request);
// 获取库存
InventoryItem inventory = getInventory(request.getSkuId(), request.getWarehouseId());
if (inventory == null) {
return AdjustmentResult.failure("库存不存在");
}
// 记录调整前数量
Integer quantityBefore = inventory.getAvailableQuantity();
// 调整库存
inventory.setAvailableQuantity(request.getNewQuantity());
// 保存库存变更
inventoryRepository.save(inventory);
// 计算调整数量
Integer adjustmentQuantity = request.getNewQuantity() - quantityBefore;
TransactionType transactionType = adjustmentQuantity > 0 ?
TransactionType.ADJUSTMENT_INCREASE : TransactionType.ADJUSTMENT_DECREASE;
// 记录事务
InventoryTransaction transaction = createTransaction(
request.getSkuId(), request.getWarehouseId(),
transactionType, Math.abs(adjustmentQuantity),
request.getReferenceId(), "ADJUSTMENT"
);
transaction.setQuantityBefore(quantityBefore);
transaction.setQuantityAfter(request.getNewQuantity());
transaction.setReason(request.getReason());
transactionRepository.save(transaction);
// 清除缓存
evictInventoryCache(request.getSkuId(), request.getWarehouseId());
// 发布事件
eventPublisher.publishInventoryAdjusted(inventory, adjustmentQuantity);
return AdjustmentResult.success(transaction.getTransactionId());
});
}
@Override
public TransferResult transferInventory(InventoryTransferRequest request) {
// 使用分布式锁确保转移操作的原子性
String lockKey1 = generateLockKey(request.getSkuId(), request.getFromWarehouseId());
String lockKey2 = generateLockKey(request.getSkuId(), request.getToWarehouseId());
// 按字典序排序锁键,避免死锁
List<String> lockKeys = Arrays.asList(lockKey1, lockKey2);
Collections.sort(lockKeys);
return lockService.executeWithLocks(lockKeys, () -> {
// 验证请求
validator.validateTransferRequest(request);
// 获取源仓库库存
InventoryItem fromInventory = getInventory(request.getSkuId(), request.getFromWarehouseId());
if (fromInventory == null || !fromInventory.hasSufficientStock(request.getQuantity())) {
return TransferResult.failure("源仓库库存不足");
}
// 获取或创建目标仓库库存
InventoryItem toInventory = getInventory(request.getSkuId(), request.getToWarehouseId());
if (toInventory == null) {
toInventory = new InventoryItem(request.getSkuId(), request.getToWarehouseId());
}
// 执行转移
fromInventory.setAvailableQuantity(fromInventory.getAvailableQuantity() - request.getQuantity());
toInventory.receiveStock(request.getQuantity());
// 保存库存变更
inventoryRepository.save(fromInventory);
inventoryRepository.save(toInventory);
// 记录转出事务
InventoryTransaction outTransaction = createTransaction(
request.getSkuId(), request.getFromWarehouseId(),
TransactionType.TRANSFER_OUT, request.getQuantity(),
request.getReferenceId(), "TRANSFER"
);
transactionRepository.save(outTransaction);
// 记录转入事务
InventoryTransaction inTransaction = createTransaction(
request.getSkuId(), request.getToWarehouseId(),
TransactionType.TRANSFER_IN, request.getQuantity(),
request.getReferenceId(), "TRANSFER"
);
transactionRepository.save(inTransaction);
// 清除缓存
evictInventoryCache(request.getSkuId(), request.getFromWarehouseId());
evictInventoryCache(request.getSkuId(), request.getToWarehouseId());
// 发布事件
eventPublisher.publishInventoryTransferred(fromInventory, toInventory, request.getQuantity());
return TransferResult.success(outTransaction.getTransactionId(), inTransaction.getTransactionId());
});
}
@Override
public List<InventoryTransaction> getTransactionHistory(String skuId, String warehouseId,
LocalDateTime startTime, LocalDateTime endTime) {
return transactionRepository.findBySkuIdAndWarehouseIdAndTransactionTimeBetween(
skuId, warehouseId, startTime, endTime
);
}
@Override
public List<InventoryAlert> getLowStockAlerts(String warehouseId) {
List<InventoryItem> lowStockItems = inventoryRepository.findLowStockItems(warehouseId);
return lowStockItems.stream()
.map(this::createLowStockAlert)
.collect(Collectors.toList());
}
@Override
public InventoryStatistics getInventoryStatistics(String warehouseId, LocalDate date) {
return inventoryRepository.calculateStatistics(warehouseId, date);
}
// 私有辅助方法
private String generateLockKey(String skuId, String warehouseId) {
return "inventory:lock:" + skuId + ":" + warehouseId;
}
private InventoryTransaction createTransaction(String skuId, String warehouseId,
TransactionType type, Integer quantity,
String referenceId, String referenceType) {
InventoryTransaction transaction = new InventoryTransaction(skuId, warehouseId, type, quantity);
transaction.setReferenceId(referenceId);
transaction.setReferenceType(referenceType);
transaction.setProcessedBy(getCurrentUserId());
return transaction;
}
private void evictInventoryCache(String skuId, String warehouseId) {
String cacheKey = skuId + ":" + warehouseId;
redisTemplate.delete("inventory::" + cacheKey);
}
private void rollbackReservations(List<ReservationResult> results) {
for (ReservationResult result : results) {
if (result.isSuccess()) {
releaseReservation(result.getReservationId());
}
}
}
private void rollbackAllocations(List<AllocationResult> results) {
// 实现分配回滚逻辑
// 这里需要根据具体业务需求实现
}
private InventoryAlert createLowStockAlert(InventoryItem inventory) {
return new InventoryAlert(
inventory.getSkuId(),
inventory.getWarehouseId(),
AlertType.LOW_STOCK,
"库存不足,当前库存: " + inventory.getAvailableQuantity() +
", 安全库存: " + inventory.getSafetyStock()
);
}
private String getCurrentUserId() {
// 从安全上下文获取当前用户ID
return "system"; // 简化实现
}
}
10.4 库存同步与一致性保证
在分布式环境下,确保库存数据的一致性是一个重大挑战。系统需要处理并发访问、网络分区、系统故障等各种情况,同时保证数据的准确性和业务的连续性。
10.4.1 分布式锁实现
/**
* 基于Redis的分布式锁服务
*/
@Service
public class InventoryLockService {
private final RedisTemplate<String, String> redisTemplate;
private final RedisScript<Boolean> lockScript;
private final RedisScript<Boolean> unlockScript;
private static final String LOCK_PREFIX = "inventory:lock:";
private static final int DEFAULT_LOCK_TIMEOUT = 30; // 30秒
private static final int DEFAULT_RETRY_TIMES = 3;
private static final int RETRY_INTERVAL = 100; // 100毫秒
public InventoryLockService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
// 加锁脚本
this.lockScript = new DefaultRedisScript<>(
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
" return redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) " +
"end",
Boolean.class
);
// 解锁脚本
this.unlockScript = new DefaultRedisScript<>(
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end",
Boolean.class
);
}
/**
* 获取锁并执行业务逻辑
*/
public <T> T executeWithLock(String lockKey, Supplier<T> supplier) {
return executeWithLock(lockKey, DEFAULT_LOCK_TIMEOUT, supplier);
}
/**
* 获取锁并执行业务逻辑(指定超时时间)
*/
public <T> T executeWithLock(String lockKey, int timeoutSeconds, Supplier<T> supplier) {
String lockValue = generateLockValue();
String fullLockKey = LOCK_PREFIX + lockKey;
boolean acquired = false;
try {
// 尝试获取锁
acquired = acquireLock(fullLockKey, lockValue, timeoutSeconds);
if (!acquired) {
throw new InventoryLockException("获取锁失败: " + lockKey);
}
// 执行业务逻辑
return supplier.get();
} finally {
if (acquired) {
releaseLock(fullLockKey, lockValue);
}
}
}
/**
* 获取多个锁并执行业务逻辑
*/
public <T> T executeWithLocks(List<String> lockKeys, Supplier<T> supplier) {
List<String> fullLockKeys = lockKeys.stream()
.map(key -> LOCK_PREFIX + key)
.collect(Collectors.toList());
Map<String, String> lockValues = new HashMap<>();
List<String> acquiredLocks = new ArrayList<>();
try {
// 按顺序获取所有锁
for (String fullLockKey : fullLockKeys) {
String lockValue = generateLockValue();
boolean acquired = acquireLock(fullLockKey, lockValue, DEFAULT_LOCK_TIMEOUT);
if (!acquired) {
throw new InventoryLockException("获取锁失败: " + fullLockKey);
}
lockValues.put(fullLockKey, lockValue);
acquiredLocks.add(fullLockKey);
}
// 执行业务逻辑
return supplier.get();
} finally {
// 释放所有已获取的锁
for (String lockKey : acquiredLocks) {
releaseLock(lockKey, lockValues.get(lockKey));
}
}
}
/**
* 获取锁
*/
private boolean acquireLock(String lockKey, String lockValue, int timeoutSeconds) {
for (int i = 0; i < DEFAULT_RETRY_TIMES; i++) {
Boolean result = redisTemplate.execute(lockScript,
Collections.singletonList(lockKey),
lockValue,
String.valueOf(timeoutSeconds)
);
if (Boolean.TRUE.equals(result)) {
return true;
}
// 等待后重试
try {
Thread.sleep(RETRY_INTERVAL);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
/**
* 释放锁
*/
private void releaseLock(String lockKey, String lockValue) {
try {
redisTemplate.execute(unlockScript,
Collections.singletonList(lockKey),
lockValue
);
} catch (Exception e) {
// 记录日志但不抛出异常
log.warn("释放锁失败: {}", lockKey, e);
}
}
/**
* 生成锁值
*/
private String generateLockValue() {
return Thread.currentThread().getId() + ":" + System.currentTimeMillis();
}
}
/**
* 库存锁异常
*/
public class InventoryLockException extends RuntimeException {
public InventoryLockException(String message) {
super(message);
}
public InventoryLockException(String message, Throwable cause) {
super(message, cause);
}
}
10.4.2 事件驱动的库存同步
为了确保库存数据在各个系统间的一致性,采用事件驱动架构进行库存同步:
/**
* 库存事件发布器
*/
@Component
public class InventoryEventPublisher {
private final ApplicationEventPublisher eventPublisher;
private final MessageProducer messageProducer;
public InventoryEventPublisher(ApplicationEventPublisher eventPublisher,
MessageProducer messageProducer) {
this.eventPublisher = eventPublisher;
this.messageProducer = messageProducer;
}
/**
* 发布库存预留事件
*/
public void publishInventoryReserved(InventoryItem inventory, Integer quantity) {
InventoryReservedEvent event = new InventoryReservedEvent(
inventory.getSkuId(),
inventory.getWarehouseId(),
quantity,
inventory.getAvailableQuantity()
);
// 本地事件
eventPublisher.publishEvent(event);
// 分布式事件
messageProducer.send("inventory.reserved", event);
}
/**
* 发布预留释放事件
*/
public void publishReservationReleased(InventoryItem inventory, Integer quantity) {
ReservationReleasedEvent event = new ReservationReleasedEvent(
inventory.getSkuId(),
inventory.getWarehouseId(),
quantity,
inventory.getAvailableQuantity()
);
eventPublisher.publishEvent(event);
messageProducer.send("inventory.reservation.released", event);
}
/**
* 发布库存分配事件
*/
public void publishInventoryAllocated(InventoryItem inventory, Integer quantity) {
InventoryAllocatedEvent event = new InventoryAllocatedEvent(
inventory.getSkuId(),
inventory.getWarehouseId(),
quantity,
inventory.getAllocatedQuantity()
);
eventPublisher.publishEvent(event);
messageProducer.send("inventory.allocated", event);
}
/**
* 发布库存出库事件
*/
public void publishInventoryShipped(InventoryItem inventory, Integer quantity) {
InventoryShippedEvent event = new InventoryShippedEvent(
inventory.getSkuId(),
inventory.getWarehouseId(),
quantity,
inventory.getAvailableQuantity()
);
eventPublisher.publishEvent(event);
messageProducer.send("inventory.shipped", event);
}
/**
* 发布库存入库事件
*/
public void publishInventoryReceived(InventoryItem inventory, Integer quantity) {
InventoryReceivedEvent event = new InventoryReceivedEvent(
inventory.getSkuId(),
inventory.getWarehouseId(),
quantity,
inventory.getAvailableQuantity()
);
eventPublisher.publishEvent(event);
messageProducer.send("inventory.received", event);
}
/**
* 发布库存调整事件
*/
public void publishInventoryAdjusted(InventoryItem inventory, Integer adjustmentQuantity) {
InventoryAdjustedEvent event = new InventoryAdjustedEvent(
inventory.getSkuId(),
inventory.getWarehouseId(),
adjustmentQuantity,
inventory.getAvailableQuantity()
);
eventPublisher.publishEvent(event);
messageProducer.send("inventory.adjusted", event);
}
/**
* 发布库存转移事件
*/
public void publishInventoryTransferred(InventoryItem fromInventory,
InventoryItem toInventory,
Integer quantity) {
InventoryTransferredEvent event = new InventoryTransferredEvent(
fromInventory.getSkuId(),
fromInventory.getWarehouseId(),
toInventory.getWarehouseId(),
quantity
);
eventPublisher.publishEvent(event);
messageProducer.send("inventory.transferred", event);
}
}
/**
* 库存事件基类
*/
public abstract class InventoryEvent {
private final String skuId;
private final String warehouseId;
private final Integer quantity;
private final LocalDateTime timestamp;
public InventoryEvent(String skuId, String warehouseId, Integer quantity) {
this.skuId = skuId;
this.warehouseId = warehouseId;
this.quantity = quantity;
this.timestamp = LocalDateTime.now();
}
// getters
public String getSkuId() { return skuId; }
public String getWarehouseId() { return warehouseId; }
public Integer getQuantity() { return quantity; }
public LocalDateTime getTimestamp() { return timestamp; }
}
/**
* 库存预留事件
*/
public class InventoryReservedEvent extends InventoryEvent {
private final Integer availableQuantity;
public InventoryReservedEvent(String skuId, String warehouseId,
Integer quantity, Integer availableQuantity) {
super(skuId, warehouseId, quantity);
this.availableQuantity = availableQuantity;
}
public Integer getAvailableQuantity() { return availableQuantity; }
}
/**
* 预留释放事件
*/
public class ReservationReleasedEvent extends InventoryEvent {
private final Integer availableQuantity;
public ReservationReleasedEvent(String skuId, String warehouseId,
Integer quantity, Integer availableQuantity) {
super(skuId, warehouseId, quantity);
this.availableQuantity = availableQuantity;
}
public Integer getAvailableQuantity() { return availableQuantity; }
}
/**
* 库存分配事件
*/
public class InventoryAllocatedEvent extends InventoryEvent {
private final Integer allocatedQuantity;
public InventoryAllocatedEvent(String skuId, String warehouseId,
Integer quantity, Integer allocatedQuantity) {
super(skuId, warehouseId, quantity);
this.allocatedQuantity = allocatedQuantity;
}
public Integer getAllocatedQuantity() { return allocatedQuantity; }
}
/**
* 库存出库事件
*/
public class InventoryShippedEvent extends InventoryEvent {
private final Integer remainingQuantity;
public InventoryShippedEvent(String skuId, String warehouseId,
Integer quantity, Integer remainingQuantity) {
super(skuId, warehouseId, quantity);
this.remainingQuantity = remainingQuantity;
}
public Integer getRemainingQuantity() { return remainingQuantity; }
}
/**
* 库存入库事件
*/
public class InventoryReceivedEvent extends InventoryEvent {
private final Integer totalQuantity;
public InventoryReceivedEvent(String skuId, String warehouseId,
Integer quantity, Integer totalQuantity) {
super(skuId, warehouseId, quantity);
this.totalQuantity = totalQuantity;
}
public Integer getTotalQuantity() { return totalQuantity; }
}
/**
* 库存调整事件
*/
public class InventoryAdjustedEvent extends InventoryEvent {
private final Integer currentQuantity;
public InventoryAdjustedEvent(String skuId, String warehouseId,
Integer adjustmentQuantity, Integer currentQuantity) {
super(skuId, warehouseId, adjustmentQuantity);
this.currentQuantity = currentQuantity;
}
public Integer getCurrentQuantity() { return currentQuantity; }
}
/**
* 库存转移事件
*/
public class InventoryTransferredEvent extends InventoryEvent {
private final String toWarehouseId;
public InventoryTransferredEvent(String skuId, String fromWarehouseId,
String toWarehouseId, Integer quantity) {
super(skuId, fromWarehouseId, quantity);
this.toWarehouseId = toWarehouseId;
}
public String getToWarehouseId() { return toWarehouseId; }
}
10.4.3 库存数据一致性检查
定期进行库存数据一致性检查,确保数据的准确性:
/**
* 库存一致性检查服务
*/
@Service
public class InventoryConsistencyService {
private final InventoryRepository inventoryRepository;
private final InventoryTransactionRepository transactionRepository;
private final InventoryAuditService auditService;
public InventoryConsistencyService(InventoryRepository inventoryRepository,
InventoryTransactionRepository transactionRepository,
InventoryAuditService auditService) {
this.inventoryRepository = inventoryRepository;
this.transactionRepository = transactionRepository;
this.auditService = auditService;
}
/**
* 执行库存一致性检查
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void performConsistencyCheck() {
log.info("开始执行库存一致性检查");
List<String> warehouses = inventoryRepository.findAllWarehouseIds();
for (String warehouseId : warehouses) {
checkWarehouseConsistency(warehouseId);
}
log.info("库存一致性检查完成");
}
/**
* 检查单个仓库的一致性
*/
public ConsistencyCheckResult checkWarehouseConsistency(String warehouseId) {
List<InventoryItem> inventories = inventoryRepository.findByWarehouseId(warehouseId);
List<ConsistencyIssue> issues = new ArrayList<>();
for (InventoryItem inventory : inventories) {
ConsistencyIssue issue = checkInventoryConsistency(inventory);
if (issue != null) {
issues.add(issue);
}
}
ConsistencyCheckResult result = new ConsistencyCheckResult(warehouseId, issues);
if (!issues.isEmpty()) {
auditService.recordConsistencyIssues(result);
notifyConsistencyIssues(result);
}
return result;
}
/**
* 检查单个库存项的一致性
*/
private ConsistencyIssue checkInventoryConsistency(InventoryItem inventory) {
// 检查总库存计算是否正确
Integer calculatedTotal = inventory.getAvailableQuantity() +
inventory.getReservedQuantity() +
inventory.getAllocatedQuantity() +
inventory.getInTransitQuantity() +
inventory.getDamagedQuantity();
if (!calculatedTotal.equals(inventory.getTotalQuantity())) {
return new ConsistencyIssue(
inventory.getSkuId(),
inventory.getWarehouseId(),
ConsistencyIssueType.TOTAL_QUANTITY_MISMATCH,
"总库存计算不匹配",
Map.of(
"expected", calculatedTotal,
"actual", inventory.getTotalQuantity()
)
);
}
// 检查负库存
if (inventory.getAvailableQuantity() < 0) {
return new ConsistencyIssue(
inventory.getSkuId(),
inventory.getWarehouseId(),
ConsistencyIssueType.NEGATIVE_INVENTORY,
"可用库存为负数",
Map.of("availableQuantity", inventory.getAvailableQuantity())
);
}
// 检查事务记录一致性
LocalDateTime checkTime = LocalDateTime.now().minusDays(1);
List<InventoryTransaction> recentTransactions =
transactionRepository.findBySkuIdAndWarehouseIdAndTransactionTimeAfter(
inventory.getSkuId(), inventory.getWarehouseId(), checkTime
);
Integer transactionSum = recentTransactions.stream()
.mapToInt(t -> t.getQuantityChange() * t.getTransactionType().getQuantityMultiplier())
.sum();
// 这里可以添加更复杂的事务一致性检查逻辑
return null; // 没有发现问题
}
/**
* 修复库存一致性问题
*/
public void fixConsistencyIssue(ConsistencyIssue issue) {
switch (issue.getType()) {
case TOTAL_QUANTITY_MISMATCH:
fixTotalQuantityMismatch(issue);
break;
case NEGATIVE_INVENTORY:
fixNegativeInventory(issue);
break;
default:
log.warn("未知的一致性问题类型: {}", issue.getType());
}
}
private void fixTotalQuantityMismatch(ConsistencyIssue issue) {
InventoryItem inventory = inventoryRepository
.findBySkuIdAndWarehouseId(issue.getSkuId(), issue.getWarehouseId())
.orElse(null);
if (inventory != null) {
inventory.updateTotalQuantity();
inventoryRepository.save(inventory);
auditService.recordConsistencyFix(issue, "修复总库存计算");
}
}
private void fixNegativeInventory(ConsistencyIssue issue) {
// 负库存需要人工处理,这里只记录
auditService.recordConsistencyIssue(issue, "负库存需要人工处理");
}
private void notifyConsistencyIssues(ConsistencyCheckResult result) {
// 发送通知给相关人员
// 这里可以集成邮件、短信、钉钉等通知方式
}
}
/**
* 一致性检查结果
*/
public class ConsistencyCheckResult {
private final String warehouseId;
private final List<ConsistencyIssue> issues;
private final LocalDateTime checkTime;
public ConsistencyCheckResult(String warehouseId, List<ConsistencyIssue> issues) {
this.warehouseId = warehouseId;
this.issues = issues;
this.checkTime = LocalDateTime.now();
}
public boolean hasIssues() {
return !issues.isEmpty();
}
// getters
public String getWarehouseId() { return warehouseId; }
public List<ConsistencyIssue> getIssues() { return issues; }
public LocalDateTime getCheckTime() { return checkTime; }
}
/**
* 一致性问题
*/
public class ConsistencyIssue {
private final String skuId;
private final String warehouseId;
private final ConsistencyIssueType type;
private final String description;
private final Map<String, Object> details;
private final LocalDateTime discoveredTime;
public ConsistencyIssue(String skuId, String warehouseId,
ConsistencyIssueType type, String description,
Map<String, Object> details) {
this.skuId = skuId;
this.warehouseId = warehouseId;
this.type = type;
this.description = description;
this.details = details;
this.discoveredTime = LocalDateTime.now();
}
// getters
public String getSkuId() { return skuId; }
public String getWarehouseId() { return warehouseId; }
public ConsistencyIssueType getType() { return type; }
public String getDescription() { return description; }
public Map<String, Object> getDetails() { return details; }
public LocalDateTime getDiscoveredTime() { return discoveredTime; }
}
/**
* 一致性问题类型
*/
public enum ConsistencyIssueType {
TOTAL_QUANTITY_MISMATCH("总库存计算不匹配"),
NEGATIVE_INVENTORY("负库存"),
TRANSACTION_MISMATCH("事务记录不匹配"),
RESERVED_QUANTITY_OVERFLOW("预留库存溢出"),
ALLOCATED_QUANTITY_OVERFLOW("分配库存溢出");
private final String description;
ConsistencyIssueType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
10.5 库存预警与补货策略
智能的库存预警和补货策略是中央库存系统的重要功能,它能够帮助企业优化库存水平,减少缺货风险,提高资金周转率。
10.5.1 库存预警系统
/**
* 库存预警服务
*/
@Service
public class InventoryAlertService {
private final InventoryRepository inventoryRepository;
private final AlertConfigRepository alertConfigRepository;
private final NotificationService notificationService;
private final InventoryForecastService forecastService;
public InventoryAlertService(InventoryRepository inventoryRepository,
AlertConfigRepository alertConfigRepository,
NotificationService notificationService,
InventoryForecastService forecastService) {
this.inventoryRepository = inventoryRepository;
this.alertConfigRepository = alertConfigRepository;
this.notificationService = notificationService;
this.forecastService = forecastService;
}
/**
* 执行库存预警检查
*/
@Scheduled(fixedRate = 300000) // 每5分钟执行一次
public void performAlertCheck() {
List<String> warehouses = inventoryRepository.findAllWarehouseIds();
for (String warehouseId : warehouses) {
checkWarehouseAlerts(warehouseId);
}
}
/**
* 检查仓库预警
*/
public void checkWarehouseAlerts(String warehouseId) {
List<InventoryItem> inventories = inventoryRepository.findByWarehouseId(warehouseId);
List<AlertConfig> alertConfigs = alertConfigRepository.findByWarehouseId(warehouseId);
Map<String, AlertConfig> configMap = alertConfigs.stream()
.collect(Collectors.toMap(AlertConfig::getSkuId, Function.identity()));
for (InventoryItem inventory : inventories) {
AlertConfig config = configMap.get(inventory.getSkuId());
if (config == null) {
config = getDefaultAlertConfig();
}
checkInventoryAlerts(inventory, config);
}
}
/**
* 检查单个库存项的预警
*/
private void checkInventoryAlerts(InventoryItem inventory, AlertConfig config) {
List<InventoryAlert> alerts = new ArrayList<>();
// 低库存预警
if (isLowStock(inventory, config)) {
alerts.add(createAlert(inventory, AlertType.LOW_STOCK,
"库存低于安全库存线", AlertLevel.WARNING));
}
// 缺货预警
if (isOutOfStock(inventory)) {
alerts.add(createAlert(inventory, AlertType.OUT_OF_STOCK,
"库存为零", AlertLevel.CRITICAL));
}
// 过期预警
if (isNearExpiry(inventory, config)) {
alerts.add(createAlert(inventory, AlertType.NEAR_EXPIRY,
"商品即将过期", AlertLevel.WARNING));
}
// 滞销预警
if (isSlowMoving(inventory, config)) {
alerts.add(createAlert(inventory, AlertType.SLOW_MOVING,
"商品滞销", AlertLevel.INFO));
}
// 超储预警
if (isOverStock(inventory, config)) {
alerts.add(createAlert(inventory, AlertType.OVER_STOCK,
"库存过多", AlertLevel.WARNING));
}
// 发送预警通知
for (InventoryAlert alert : alerts) {
sendAlert(alert);
}
}
/**
* 检查是否低库存
*/
private boolean isLowStock(InventoryItem inventory, AlertConfig config) {
Integer safetyStock = inventory.getSafetyStock();
if (safetyStock == null) {
safetyStock = config.getDefaultSafetyStock();
}
return inventory.getAvailableQuantity() <= safetyStock;
}
/**
* 检查是否缺货
*/
private boolean isOutOfStock(InventoryItem inventory) {
return inventory.getAvailableQuantity() <= 0;
}
/**
* 检查是否即将过期
*/
private boolean isNearExpiry(InventoryItem inventory, AlertConfig config) {
if (inventory.getExpiryDate() == null) {
return false;
}
LocalDate alertDate = inventory.getExpiryDate().minusDays(config.getExpiryAlertDays());
return LocalDate.now().isAfter(alertDate);
}
/**
* 检查是否滞销
*/
private boolean isSlowMoving(InventoryItem inventory, AlertConfig config) {
// 获取最近销售数据
LocalDateTime since = LocalDateTime.now().minusDays(config.getSlowMovingDays());
Integer soldQuantity = inventoryRepository.getSoldQuantitySince(
inventory.getSkuId(), inventory.getWarehouseId(), since
);
return soldQuantity < config.getMinSalesThreshold();
}
/**
* 检查是否超储
*/
private boolean isOverStock(InventoryItem inventory, AlertConfig config) {
Integer maxStock = inventory.getMaxStock();
if (maxStock == null) {
maxStock = config.getDefaultMaxStock();
}
return inventory.getTotalQuantity() > maxStock;
}
/**
* 创建预警
*/
private InventoryAlert createAlert(InventoryItem inventory, AlertType type,
String message, AlertLevel level) {
return new InventoryAlert(
inventory.getSkuId(),
inventory.getWarehouseId(),
type,
level,
message,
inventory.getAvailableQuantity(),
inventory.getTotalQuantity()
);
}
/**
* 发送预警通知
*/
private void sendAlert(InventoryAlert alert) {
// 检查是否已经发送过相同预警
if (isDuplicateAlert(alert)) {
return;
}
// 根据预警级别选择通知方式
switch (alert.getLevel()) {
case CRITICAL:
notificationService.sendUrgentNotification(alert);
break;
case WARNING:
notificationService.sendWarningNotification(alert);
break;
case INFO:
notificationService.sendInfoNotification(alert);
break;
}
// 记录预警历史
recordAlertHistory(alert);
}
private boolean isDuplicateAlert(InventoryAlert alert) {
// 检查最近是否发送过相同类型的预警
return false; // 简化实现
}
private void recordAlertHistory(InventoryAlert alert) {
// 记录预警历史
}
private AlertConfig getDefaultAlertConfig() {
return new AlertConfig(
null, // skuId
null, // warehouseId
10, // defaultSafetyStock
1000, // defaultMaxStock
7, // expiryAlertDays
30, // slowMovingDays
5 // minSalesThreshold
);
}
}
/**
* 库存预警
*/
public class InventoryAlert {
private final String skuId;
private final String warehouseId;
private final AlertType type;
private final AlertLevel level;
private final String message;
private final Integer currentQuantity;
private final Integer totalQuantity;
private final LocalDateTime alertTime;
public InventoryAlert(String skuId, String warehouseId, AlertType type,
AlertLevel level, String message,
Integer currentQuantity, Integer totalQuantity) {
this.skuId = skuId;
this.warehouseId = warehouseId;
this.type = type;
this.level = level;
this.message = message;
this.currentQuantity = currentQuantity;
this.totalQuantity = totalQuantity;
this.alertTime = LocalDateTime.now();
}
// getters
public String getSkuId() { return skuId; }
public String getWarehouseId() { return warehouseId; }
public AlertType getType() { return type; }
public AlertLevel getLevel() { return level; }
public String getMessage() { return message; }
public Integer getCurrentQuantity() { return currentQuantity; }
public Integer getTotalQuantity() { return totalQuantity; }
public LocalDateTime getAlertTime() { return alertTime; }
}
/**
* 预警类型
*/
public enum AlertType {
LOW_STOCK("低库存"),
OUT_OF_STOCK("缺货"),
NEAR_EXPIRY("即将过期"),
SLOW_MOVING("滞销"),
OVER_STOCK("超储"),
DAMAGED("损坏");
private final String description;
AlertType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* 预警级别
*/
public enum AlertLevel {
INFO("信息", 1),
WARNING("警告", 2),
CRITICAL("严重", 3);
private final String description;
private final int priority;
AlertLevel(String description, int priority) {
this.description = description;
this.priority = priority;
}
public String getDescription() {
return description;
}
public int getPriority() {
return priority;
}
}
/**
* 预警配置
*/
@Entity
@Table(name = "alert_config")
public class AlertConfig extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "sku_id")
private String skuId; // SKU ID(为空表示默认配置)
@Column(name = "warehouse_id")
private String warehouseId; // 仓库ID(为空表示全局配置)
@Column(name = "default_safety_stock")
private Integer defaultSafetyStock; // 默认安全库存
@Column(name = "default_max_stock")
private Integer defaultMaxStock; // 默认最大库存
@Column(name = "expiry_alert_days")
private Integer expiryAlertDays; // 过期预警天数
@Column(name = "slow_moving_days")
private Integer slowMovingDays; // 滞销判断天数
@Column(name = "min_sales_threshold")
private Integer minSalesThreshold; // 最小销售阈值
@Column(name = "enabled")
private Boolean enabled; // 是否启用
// 构造函数
public AlertConfig() {
this.enabled = true;
}
public AlertConfig(String skuId, String warehouseId,
Integer defaultSafetyStock, Integer defaultMaxStock,
Integer expiryAlertDays, Integer slowMovingDays,
Integer minSalesThreshold) {
this();
this.skuId = skuId;
this.warehouseId = warehouseId;
this.defaultSafetyStock = defaultSafetyStock;
this.defaultMaxStock = defaultMaxStock;
this.expiryAlertDays = expiryAlertDays;
this.slowMovingDays = slowMovingDays;
this.minSalesThreshold = minSalesThreshold;
}
// getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getSkuId() { return skuId; }
public void setSkuId(String skuId) { this.skuId = skuId; }
public String getWarehouseId() { return warehouseId; }
public void setWarehouseId(String warehouseId) { this.warehouseId = warehouseId; }
public Integer getDefaultSafetyStock() { return defaultSafetyStock; }
public void setDefaultSafetyStock(Integer defaultSafetyStock) { this.defaultSafetyStock = defaultSafetyStock; }
public Integer getDefaultMaxStock() { return defaultMaxStock; }
public void setDefaultMaxStock(Integer defaultMaxStock) { this.defaultMaxStock = defaultMaxStock; }
public Integer getExpiryAlertDays() { return expiryAlertDays; }
public void setExpiryAlertDays(Integer expiryAlertDays) { this.expiryAlertDays = expiryAlertDays; }
public Integer getSlowMovingDays() { return slowMovingDays; }
public void setSlowMovingDays(Integer slowMovingDays) { this.slowMovingDays = slowMovingDays; }
public Integer getMinSalesThreshold() { return minSalesThreshold; }
public void setMinSalesThreshold(Integer minSalesThreshold) { this.minSalesThreshold = minSalesThreshold; }
public Boolean getEnabled() { return enabled; }
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
}
10.5.2 智能补货策略
基于历史销售数据和预测算法的智能补货系统:
/**
* 智能补货服务
*/
@Service
public class IntelligentReplenishmentService {
private final InventoryRepository inventoryRepository;
private final SalesDataRepository salesDataRepository;
private final SupplierRepository supplierRepository;
private final PurchaseOrderService purchaseOrderService;
private final InventoryForecastService forecastService;
public IntelligentReplenishmentService(InventoryRepository inventoryRepository,
SalesDataRepository salesDataRepository,
SupplierRepository supplierRepository,
PurchaseOrderService purchaseOrderService,
InventoryForecastService forecastService) {
this.inventoryRepository = inventoryRepository;
this.salesDataRepository = salesDataRepository;
this.supplierRepository = supplierRepository;
this.purchaseOrderService = purchaseOrderService;
this.forecastService = forecastService;
}
/**
* 执行智能补货分析
*/
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行
public void performReplenishmentAnalysis() {
List<String> warehouses = inventoryRepository.findAllWarehouseIds();
for (String warehouseId : warehouses) {
analyzeWarehouseReplenishment(warehouseId);
}
}
/**
* 分析仓库补货需求
*/
public ReplenishmentAnalysisResult analyzeWarehouseReplenishment(String warehouseId) {
List<InventoryItem> inventories = inventoryRepository.findByWarehouseId(warehouseId);
List<ReplenishmentRecommendation> recommendations = new ArrayList<>();
for (InventoryItem inventory : inventories) {
ReplenishmentRecommendation recommendation = analyzeItemReplenishment(inventory);
if (recommendation != null) {
recommendations.add(recommendation);
}
}
return new ReplenishmentAnalysisResult(warehouseId, recommendations);
}
/**
* 分析单个商品的补货需求
*/
private ReplenishmentRecommendation analyzeItemReplenishment(InventoryItem inventory) {
// 获取历史销售数据
LocalDateTime since = LocalDateTime.now().minusDays(90);
List<SalesData> salesHistory = salesDataRepository.findBySkuIdAndWarehouseIdAndDateAfter(
inventory.getSkuId(), inventory.getWarehouseId(), since
);
if (salesHistory.isEmpty()) {
return null; // 没有销售历史,不进行补货分析
}
// 计算销售统计
SalesStatistics stats = calculateSalesStatistics(salesHistory);
// 预测未来需求
DemandForecast forecast = forecastService.forecastDemand(
inventory.getSkuId(), inventory.getWarehouseId(), 30
);
// 计算补货建议
ReplenishmentCalculation calculation = calculateReplenishment(
inventory, stats, forecast
);
if (calculation.getRecommendedQuantity() > 0) {
return new ReplenishmentRecommendation(
inventory.getSkuId(),
inventory.getWarehouseId(),
calculation.getRecommendedQuantity(),
calculation.getUrgencyLevel(),
calculation.getReason(),
calculation.getExpectedDeliveryDate(),
calculation.getEstimatedCost()
);
}
return null;
}
/**
* 计算销售统计
*/
private SalesStatistics calculateSalesStatistics(List<SalesData> salesHistory) {
double totalSales = salesHistory.stream()
.mapToDouble(SalesData::getQuantity)
.sum();
double averageDailySales = totalSales / salesHistory.size();
double variance = salesHistory.stream()
.mapToDouble(s -> Math.pow(s.getQuantity() - averageDailySales, 2))
.average()
.orElse(0.0);
double standardDeviation = Math.sqrt(variance);
// 计算趋势
double trend = calculateTrend(salesHistory);
return new SalesStatistics(
totalSales,
averageDailySales,
standardDeviation,
trend
);
}
/**
* 计算销售趋势
*/
private double calculateTrend(List<SalesData> salesHistory) {
if (salesHistory.size() < 2) {
return 0.0;
}
// 简单线性回归计算趋势
int n = salesHistory.size();
double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
for (int i = 0; i < n; i++) {
double x = i + 1; // 时间序列
double y = salesHistory.get(i).getQuantity();
sumX += x;
sumY += y;
sumXY += x * y;
sumX2 += x * x;
}
// 计算斜率(趋势)
double slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
return slope;
}
/**
* 计算补货建议
*/
private ReplenishmentCalculation calculateReplenishment(InventoryItem inventory,
SalesStatistics stats,
DemandForecast forecast) {
// 计算安全库存
double safetyStock = calculateSafetyStock(stats, inventory.getLeadTimeDays());
// 计算再订货点
double reorderPoint = (stats.getAverageDailySales() * inventory.getLeadTimeDays()) + safetyStock;
// 计算经济订货量(EOQ)
double eoq = calculateEOQ(stats.getAverageDailySales() * 365, inventory.getOrderingCost(), inventory.getHoldingCost());
// 当前可用库存
int currentStock = inventory.getAvailableQuantity();
// 判断是否需要补货
if (currentStock <= reorderPoint) {
// 计算建议订货量
double recommendedQuantity = Math.max(eoq, reorderPoint - currentStock + (stats.getAverageDailySales() * 7));
// 考虑预测趋势调整
if (stats.getTrend() > 0) {
recommendedQuantity *= (1 + stats.getTrend() * 0.1); // 上升趋势增加10%
} else if (stats.getTrend() < 0) {
recommendedQuantity *= (1 + stats.getTrend() * 0.05); // 下降趋势减少5%
}
// 确定紧急程度
UrgencyLevel urgency = determineUrgency(currentStock, reorderPoint, stats.getAverageDailySales());
// 计算预期交货日期
LocalDate expectedDeliveryDate = LocalDate.now().plusDays(inventory.getLeadTimeDays());
// 估算成本
BigDecimal estimatedCost = inventory.getUnitCost().multiply(BigDecimal.valueOf(recommendedQuantity));
String reason = String.format(
"当前库存(%d)低于再订货点(%.0f),建议补货。平均日销量:%.1f,安全库存:%.0f",
currentStock, reorderPoint, stats.getAverageDailySales(), safetyStock
);
return new ReplenishmentCalculation(
(int) Math.ceil(recommendedQuantity),
urgency,
reason,
expectedDeliveryDate,
estimatedCost
);
}
return new ReplenishmentCalculation(0, UrgencyLevel.NONE, "库存充足,无需补货", null, BigDecimal.ZERO);
}
/**
* 计算安全库存
*/
private double calculateSafetyStock(SalesStatistics stats, int leadTimeDays) {
// 安全库存 = Z值 * 标准差 * sqrt(提前期)
double zValue = 1.65; // 95%服务水平
return zValue * stats.getStandardDeviation() * Math.sqrt(leadTimeDays);
}
/**
* 计算经济订货量(EOQ)
*/
private double calculateEOQ(double annualDemand, BigDecimal orderingCost, BigDecimal holdingCost) {
if (holdingCost.compareTo(BigDecimal.ZERO) == 0) {
return annualDemand / 12; // 默认月订货量
}
double eoq = Math.sqrt((2 * annualDemand * orderingCost.doubleValue()) / holdingCost.doubleValue());
return eoq;
}
/**
* 确定补货紧急程度
*/
private UrgencyLevel determineUrgency(int currentStock, double reorderPoint, double averageDailySales) {
double daysOfStock = currentStock / averageDailySales;
if (daysOfStock <= 3) {
return UrgencyLevel.CRITICAL;
} else if (daysOfStock <= 7) {
return UrgencyLevel.HIGH;
} else if (currentStock <= reorderPoint * 1.2) {
return UrgencyLevel.MEDIUM;
} else {
return UrgencyLevel.LOW;
}
}
/**
* 自动创建采购订单
*/
public void autoCreatePurchaseOrders(String warehouseId) {
ReplenishmentAnalysisResult result = analyzeWarehouseReplenishment(warehouseId);
// 按供应商分组
Map<String, List<ReplenishmentRecommendation>> supplierGroups = result.getRecommendations().stream()
.filter(r -> r.getUrgencyLevel().getPriority() >= UrgencyLevel.MEDIUM.getPriority())
.collect(Collectors.groupingBy(this::getPreferredSupplier));
// 为每个供应商创建采购订单
for (Map.Entry<String, List<ReplenishmentRecommendation>> entry : supplierGroups.entrySet()) {
String supplierId = entry.getKey();
List<ReplenishmentRecommendation> items = entry.getValue();
purchaseOrderService.createPurchaseOrder(supplierId, warehouseId, items);
}
}
private String getPreferredSupplier(ReplenishmentRecommendation recommendation) {
// 获取商品的首选供应商
return supplierRepository.findPreferredSupplierBySkuId(recommendation.getSkuId())
.map(Supplier::getId)
.orElse("DEFAULT_SUPPLIER");
}
}
/**
* 销售统计
*/
public class SalesStatistics {
private final double totalSales;
private final double averageDailySales;
private final double standardDeviation;
private final double trend;
public SalesStatistics(double totalSales, double averageDailySales,
double standardDeviation, double trend) {
this.totalSales = totalSales;
this.averageDailySales = averageDailySales;
this.standardDeviation = standardDeviation;
this.trend = trend;
}
// getters
public double getTotalSales() { return totalSales; }
public double getAverageDailySales() { return averageDailySales; }
public double getStandardDeviation() { return standardDeviation; }
public double getTrend() { return trend; }
}
/**
* 补货计算结果
*/
public class ReplenishmentCalculation {
private final int recommendedQuantity;
private final UrgencyLevel urgencyLevel;
private final String reason;
private final LocalDate expectedDeliveryDate;
private final BigDecimal estimatedCost;
public ReplenishmentCalculation(int recommendedQuantity, UrgencyLevel urgencyLevel,
String reason, LocalDate expectedDeliveryDate,
BigDecimal estimatedCost) {
this.recommendedQuantity = recommendedQuantity;
this.urgencyLevel = urgencyLevel;
this.reason = reason;
this.expectedDeliveryDate = expectedDeliveryDate;
this.estimatedCost = estimatedCost;
}
// getters
public int getRecommendedQuantity() { return recommendedQuantity; }
public UrgencyLevel getUrgencyLevel() { return urgencyLevel; }
public String getReason() { return reason; }
public LocalDate getExpectedDeliveryDate() { return expectedDeliveryDate; }
public BigDecimal getEstimatedCost() { return estimatedCost; }
}
/**
* 补货建议
*/
public class ReplenishmentRecommendation {
private final String skuId;
private final String warehouseId;
private final int recommendedQuantity;
private final UrgencyLevel urgencyLevel;
private final String reason;
private final LocalDate expectedDeliveryDate;
private final BigDecimal estimatedCost;
private final LocalDateTime createdTime;
public ReplenishmentRecommendation(String skuId, String warehouseId,
int recommendedQuantity, UrgencyLevel urgencyLevel,
String reason, LocalDate expectedDeliveryDate,
BigDecimal estimatedCost) {
this.skuId = skuId;
this.warehouseId = warehouseId;
this.recommendedQuantity = recommendedQuantity;
this.urgencyLevel = urgencyLevel;
this.reason = reason;
this.expectedDeliveryDate = expectedDeliveryDate;
this.estimatedCost = estimatedCost;
this.createdTime = LocalDateTime.now();
}
// getters
public String getSkuId() { return skuId; }
public String getWarehouseId() { return warehouseId; }
public int getRecommendedQuantity() { return recommendedQuantity; }
public UrgencyLevel getUrgencyLevel() { return urgencyLevel; }
public String getReason() { return reason; }
public LocalDate getExpectedDeliveryDate() { return expectedDeliveryDate; }
public BigDecimal getEstimatedCost() { return estimatedCost; }
public LocalDateTime getCreatedTime() { return createdTime; }
}
/**
* 补货分析结果
*/
public class ReplenishmentAnalysisResult {
private final String warehouseId;
private final List<ReplenishmentRecommendation> recommendations;
private final LocalDateTime analysisTime;
public ReplenishmentAnalysisResult(String warehouseId, List<ReplenishmentRecommendation> recommendations) {
this.warehouseId = warehouseId;
this.recommendations = recommendations;
this.analysisTime = LocalDateTime.now();
}
public int getCriticalCount() {
return (int) recommendations.stream()
.filter(r -> r.getUrgencyLevel() == UrgencyLevel.CRITICAL)
.count();
}
public BigDecimal getTotalEstimatedCost() {
return recommendations.stream()
.map(ReplenishmentRecommendation::getEstimatedCost)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// getters
public String getWarehouseId() { return warehouseId; }
public List<ReplenishmentRecommendation> getRecommendations() { return recommendations; }
public LocalDateTime getAnalysisTime() { return analysisTime; }
}
/**
* 紧急程度
*/
public enum UrgencyLevel {
NONE("无需补货", 0),
LOW("低", 1),
MEDIUM("中", 2),
HIGH("高", 3),
CRITICAL("紧急", 4);
private final String description;
private final int priority;
UrgencyLevel(String description, int priority) {
this.description = description;
this.priority = priority;
}
public String getDescription() {
return description;
}
public int getPriority() {
return priority;
}
}
10.6 库存统计与分析
为了支持业务决策,中央库存系统需要提供全面的统计分析功能:
/**
* 库存统计分析服务
*/
@Service
public class InventoryAnalyticsService {
private final InventoryRepository inventoryRepository;
private final InventoryTransactionRepository transactionRepository;
private final SalesDataRepository salesDataRepository;
public InventoryAnalyticsService(InventoryRepository inventoryRepository,
InventoryTransactionRepository transactionRepository,
SalesDataRepository salesDataRepository) {
this.inventoryRepository = inventoryRepository;
this.transactionRepository = transactionRepository;
this.salesDataRepository = salesDataRepository;
}
/**
* 获取库存概览统计
*/
public InventoryOverviewStats getInventoryOverview(String warehouseId) {
List<InventoryItem> inventories = inventoryRepository.findByWarehouseId(warehouseId);
int totalSkus = inventories.size();
long totalQuantity = inventories.stream()
.mapToLong(InventoryItem::getTotalQuantity)
.sum();
long availableQuantity = inventories.stream()
.mapToLong(InventoryItem::getAvailableQuantity)
.sum();
long reservedQuantity = inventories.stream()
.mapToLong(InventoryItem::getReservedQuantity)
.sum();
BigDecimal totalValue = inventories.stream()
.map(inv -> inv.getUnitCost().multiply(BigDecimal.valueOf(inv.getTotalQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 低库存商品数量
long lowStockCount = inventories.stream()
.filter(inv -> inv.getAvailableQuantity() <= (inv.getSafetyStock() != null ? inv.getSafetyStock() : 10))
.count();
// 缺货商品数量
long outOfStockCount = inventories.stream()
.filter(inv -> inv.getAvailableQuantity() <= 0)
.count();
return new InventoryOverviewStats(
warehouseId,
totalSkus,
totalQuantity,
availableQuantity,
reservedQuantity,
totalValue,
lowStockCount,
outOfStockCount
);
}
/**
* 获取库存周转分析
*/
public InventoryTurnoverAnalysis getInventoryTurnoverAnalysis(String warehouseId,
LocalDate startDate,
LocalDate endDate) {
List<InventoryItem> inventories = inventoryRepository.findByWarehouseId(warehouseId);
List<InventoryTurnoverItem> turnoverItems = new ArrayList<>();
for (InventoryItem inventory : inventories) {
// 获取期间销售数据
List<SalesData> salesData = salesDataRepository.findBySkuIdAndWarehouseIdAndDateBetween(
inventory.getSkuId(), warehouseId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59)
);
double totalSold = salesData.stream()
.mapToDouble(SalesData::getQuantity)
.sum();
// 计算平均库存
double averageInventory = calculateAverageInventory(inventory, startDate, endDate);
// 计算周转率
double turnoverRate = averageInventory > 0 ? totalSold / averageInventory : 0;
// 计算周转天数
long periodDays = ChronoUnit.DAYS.between(startDate, endDate);
double turnoverDays = turnoverRate > 0 ? periodDays / turnoverRate : periodDays;
turnoverItems.add(new InventoryTurnoverItem(
inventory.getSkuId(),
totalSold,
averageInventory,
turnoverRate,
turnoverDays
));
}
return new InventoryTurnoverAnalysis(warehouseId, startDate, endDate, turnoverItems);
}
/**
* 计算平均库存
*/
private double calculateAverageInventory(InventoryItem inventory, LocalDate startDate, LocalDate endDate) {
// 获取期间内的库存变动记录
List<InventoryTransaction> transactions = transactionRepository
.findBySkuIdAndWarehouseIdAndTransactionTimeBetween(
inventory.getSkuId(),
inventory.getWarehouseId(),
startDate.atStartOfDay(),
endDate.atTime(23, 59, 59)
);
if (transactions.isEmpty()) {
return inventory.getTotalQuantity();
}
// 简化计算:期初库存 + 期末库存 / 2
int currentQuantity = inventory.getTotalQuantity();
int totalChange = transactions.stream()
.mapToInt(t -> t.getQuantityChange() * t.getTransactionType().getQuantityMultiplier())
.sum();
int startQuantity = currentQuantity - totalChange;
return (startQuantity + currentQuantity) / 2.0;
}
/**
* 获取ABC分析
*/
public ABCAnalysisResult getABCAnalysis(String warehouseId, LocalDate startDate, LocalDate endDate) {
List<InventoryItem> inventories = inventoryRepository.findByWarehouseId(warehouseId);
List<ABCAnalysisItem> analysisItems = new ArrayList<>();
for (InventoryItem inventory : inventories) {
// 获取销售数据
List<SalesData> salesData = salesDataRepository.findBySkuIdAndWarehouseIdAndDateBetween(
inventory.getSkuId(), warehouseId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59)
);
double totalSalesQuantity = salesData.stream()
.mapToDouble(SalesData::getQuantity)
.sum();
BigDecimal totalSalesValue = salesData.stream()
.map(s -> BigDecimal.valueOf(s.getQuantity()).multiply(s.getUnitPrice()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
analysisItems.add(new ABCAnalysisItem(
inventory.getSkuId(),
totalSalesQuantity,
totalSalesValue,
inventory.getTotalQuantity(),
inventory.getUnitCost().multiply(BigDecimal.valueOf(inventory.getTotalQuantity()))
));
}
// 按销售金额排序
analysisItems.sort((a, b) -> b.getSalesValue().compareTo(a.getSalesValue()));
// 计算累计百分比并分类
BigDecimal totalSalesValue = analysisItems.stream()
.map(ABCAnalysisItem::getSalesValue)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal cumulativeValue = BigDecimal.ZERO;
for (ABCAnalysisItem item : analysisItems) {
cumulativeValue = cumulativeValue.add(item.getSalesValue());
double cumulativePercentage = cumulativeValue.divide(totalSalesValue, 4, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100)).doubleValue();
if (cumulativePercentage <= 80) {
item.setCategory(ABCCategory.A);
} else if (cumulativePercentage <= 95) {
item.setCategory(ABCCategory.B);
} else {
item.setCategory(ABCCategory.C);
}
}
return new ABCAnalysisResult(warehouseId, startDate, endDate, analysisItems);
}
/**
* 获取库存异常分析
*/
public InventoryAnomalyAnalysis getInventoryAnomalyAnalysis(String warehouseId) {
List<InventoryItem> inventories = inventoryRepository.findByWarehouseId(warehouseId);
List<InventoryAnomaly> anomalies = new ArrayList<>();
LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30);
for (InventoryItem inventory : inventories) {
// 检查长期无销售
List<SalesData> recentSales = salesDataRepository.findBySkuIdAndWarehouseIdAndDateAfter(
inventory.getSkuId(), warehouseId, thirtyDaysAgo
);
if (recentSales.isEmpty() && inventory.getTotalQuantity() > 0) {
anomalies.add(new InventoryAnomaly(
inventory.getSkuId(),
AnomalyType.NO_SALES_30_DAYS,
"30天内无销售记录",
inventory.getTotalQuantity(),
inventory.getUnitCost().multiply(BigDecimal.valueOf(inventory.getTotalQuantity()))
));
}
// 检查库存过多
if (inventory.getMaxStock() != null && inventory.getTotalQuantity() > inventory.getMaxStock()) {
anomalies.add(new InventoryAnomaly(
inventory.getSkuId(),
AnomalyType.OVER_STOCK,
String.format("库存(%d)超过最大库存(%d)", inventory.getTotalQuantity(), inventory.getMaxStock()),
inventory.getTotalQuantity() - inventory.getMaxStock(),
inventory.getUnitCost().multiply(BigDecimal.valueOf(inventory.getTotalQuantity() - inventory.getMaxStock()))
));
}
// 检查即将过期
if (inventory.getExpiryDate() != null) {
long daysToExpiry = ChronoUnit.DAYS.between(LocalDate.now(), inventory.getExpiryDate());
if (daysToExpiry <= 7 && daysToExpiry > 0) {
anomalies.add(new InventoryAnomaly(
inventory.getSkuId(),
AnomalyType.NEAR_EXPIRY,
String.format("商品将在%d天后过期", daysToExpiry),
inventory.getTotalQuantity(),
inventory.getUnitCost().multiply(BigDecimal.valueOf(inventory.getTotalQuantity()))
));
}
}
}
return new InventoryAnomalyAnalysis(warehouseId, anomalies);
}
}
/**
* 库存概览统计
*/
public class InventoryOverviewStats {
private final String warehouseId;
private final int totalSkus;
private final long totalQuantity;
private final long availableQuantity;
private final long reservedQuantity;
private final BigDecimal totalValue;
private final long lowStockCount;
private final long outOfStockCount;
private final LocalDateTime generatedTime;
public InventoryOverviewStats(String warehouseId, int totalSkus, long totalQuantity,
long availableQuantity, long reservedQuantity, BigDecimal totalValue,
long lowStockCount, long outOfStockCount) {
this.warehouseId = warehouseId;
this.totalSkus = totalSkus;
this.totalQuantity = totalQuantity;
this.availableQuantity = availableQuantity;
this.reservedQuantity = reservedQuantity;
this.totalValue = totalValue;
this.lowStockCount = lowStockCount;
this.outOfStockCount = outOfStockCount;
this.generatedTime = LocalDateTime.now();
}
// getters
public String getWarehouseId() { return warehouseId; }
public int getTotalSkus() { return totalSkus; }
public long getTotalQuantity() { return totalQuantity; }
public long getAvailableQuantity() { return availableQuantity; }
public long getReservedQuantity() { return reservedQuantity; }
public BigDecimal getTotalValue() { return totalValue; }
public long getLowStockCount() { return lowStockCount; }
public long getOutOfStockCount() { return outOfStockCount; }
public LocalDateTime getGeneratedTime() { return generatedTime; }
}
/**
* 库存周转分析
*/
public class InventoryTurnoverAnalysis {
private final String warehouseId;
private final LocalDate startDate;
private final LocalDate endDate;
private final List<InventoryTurnoverItem> items;
public InventoryTurnoverAnalysis(String warehouseId, LocalDate startDate,
LocalDate endDate, List<InventoryTurnoverItem> items) {
this.warehouseId = warehouseId;
this.startDate = startDate;
this.endDate = endDate;
this.items = items;
}
public double getAverageTurnoverRate() {
return items.stream()
.mapToDouble(InventoryTurnoverItem::getTurnoverRate)
.average()
.orElse(0.0);
}
public double getAverageTurnoverDays() {
return items.stream()
.mapToDouble(InventoryTurnoverItem::getTurnoverDays)
.average()
.orElse(0.0);
}
// getters
public String getWarehouseId() { return warehouseId; }
public LocalDate getStartDate() { return startDate; }
public LocalDate getEndDate() { return endDate; }
public List<InventoryTurnoverItem> getItems() { return items; }
}
/**
* 库存周转项目
*/
public class InventoryTurnoverItem {
private final String skuId;
private final double totalSold;
private final double averageInventory;
private final double turnoverRate;
private final double turnoverDays;
public InventoryTurnoverItem(String skuId, double totalSold, double averageInventory,
double turnoverRate, double turnoverDays) {
this.skuId = skuId;
this.totalSold = totalSold;
this.averageInventory = averageInventory;
this.turnoverRate = turnoverRate;
this.turnoverDays = turnoverDays;
}
// getters
public String getSkuId() { return skuId; }
public double getTotalSold() { return totalSold; }
public double getAverageInventory() { return averageInventory; }
public double getTurnoverRate() { return turnoverRate; }
public double getTurnoverDays() { return turnoverDays; }
}
/**
* ABC分析结果
*/
public class ABCAnalysisResult {
private final String warehouseId;
private final LocalDate startDate;
private final LocalDate endDate;
private final List<ABCAnalysisItem> items;
public ABCAnalysisResult(String warehouseId, LocalDate startDate,
LocalDate endDate, List<ABCAnalysisItem> items) {
this.warehouseId = warehouseId;
this.startDate = startDate;
this.endDate = endDate;
this.items = items;
}
public Map<ABCCategory, Long> getCategoryCount() {
return items.stream()
.collect(Collectors.groupingBy(
ABCAnalysisItem::getCategory,
Collectors.counting()
));
}
public Map<ABCCategory, BigDecimal> getCategoryValue() {
return items.stream()
.collect(Collectors.groupingBy(
ABCAnalysisItem::getCategory,
Collectors.reducing(
BigDecimal.ZERO,
ABCAnalysisItem::getSalesValue,
BigDecimal::add
)
));
}
// getters
public String getWarehouseId() { return warehouseId; }
public LocalDate getStartDate() { return startDate; }
public LocalDate getEndDate() { return endDate; }
public List<ABCAnalysisItem> getItems() { return items; }
}
/**
* ABC分析项目
*/
public class ABCAnalysisItem {
private final String skuId;
private final double salesQuantity;
private final BigDecimal salesValue;
private final int inventoryQuantity;
private final BigDecimal inventoryValue;
private ABCCategory category;
public ABCAnalysisItem(String skuId, double salesQuantity, BigDecimal salesValue,
int inventoryQuantity, BigDecimal inventoryValue) {
this.skuId = skuId;
this.salesQuantity = salesQuantity;
this.salesValue = salesValue;
this.inventoryQuantity = inventoryQuantity;
this.inventoryValue = inventoryValue;
}
// getters and setters
public String getSkuId() { return skuId; }
public double getSalesQuantity() { return salesQuantity; }
public BigDecimal getSalesValue() { return salesValue; }
public int getInventoryQuantity() { return inventoryQuantity; }
public BigDecimal getInventoryValue() { return inventoryValue; }
public ABCCategory getCategory() { return category; }
public void setCategory(ABCCategory category) { this.category = category; }
}
/**
* ABC分类
*/
public enum ABCCategory {
A("A类商品", "高价值商品,占销售额80%"),
B("B类商品", "中等价值商品,占销售额15%"),
C("C类商品", "低价值商品,占销售额5%");
private final String name;
private final String description;
ABCCategory(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() { return name; }
public String getDescription() { return description; }
}
/**
* 库存异常分析
*/
public class InventoryAnomalyAnalysis {
private final String warehouseId;
private final List<InventoryAnomaly> anomalies;
private final LocalDateTime analysisTime;
public InventoryAnomalyAnalysis(String warehouseId, List<InventoryAnomaly> anomalies) {
this.warehouseId = warehouseId;
this.anomalies = anomalies;
this.analysisTime = LocalDateTime.now();
}
public Map<AnomalyType, Long> getAnomalyCount() {
return anomalies.stream()
.collect(Collectors.groupingBy(
InventoryAnomaly::getType,
Collectors.counting()
));
}
public BigDecimal getTotalAnomalyValue() {
return anomalies.stream()
.map(InventoryAnomaly::getValue)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// getters
public String getWarehouseId() { return warehouseId; }
public List<InventoryAnomaly> getAnomalies() { return anomalies; }
public LocalDateTime getAnalysisTime() { return analysisTime; }
}
/**
* 库存异常
*/
public class InventoryAnomaly {
private final String skuId;
private final AnomalyType type;
private final String description;
private final int quantity;
private final BigDecimal value;
public InventoryAnomaly(String skuId, AnomalyType type, String description,
int quantity, BigDecimal value) {
this.skuId = skuId;
this.type = type;
this.description = description;
this.quantity = quantity;
this.value = value;
}
// getters
public String getSkuId() { return skuId; }
public AnomalyType getType() { return type; }
public String getDescription() { return description; }
public int getQuantity() { return quantity; }
public BigDecimal getValue() { return value; }
}
/**
* 异常类型
*/
public enum AnomalyType {
NO_SALES_30_DAYS("30天无销售"),
OVER_STOCK("库存过多"),
NEAR_EXPIRY("即将过期"),
DAMAGED_STOCK("损坏库存"),
NEGATIVE_STOCK("负库存");
private final String description;
AnomalyType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}

1921

被折叠的 条评论
为什么被折叠?



