feat: 完善库存锁定幂等与批次扣减策略
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Inventory.Commands;
|
||||
using TakeoutSaaS.Application.App.Inventory.Dto;
|
||||
using TakeoutSaaS.Domain.Inventory.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Inventory.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 库存锁定处理器。
|
||||
/// </summary>
|
||||
public sealed class LockInventoryCommandHandler(
|
||||
IInventoryRepository inventoryRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<LockInventoryCommandHandler> logger)
|
||||
: IRequestHandler<LockInventoryCommand, InventoryItemDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<InventoryItemDto> Handle(LockInventoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取库存
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var item = await inventoryRepository.GetForUpdateAsync(tenantId, request.StoreId, request.ProductSkuId, cancellationToken);
|
||||
if (item is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "库存不存在");
|
||||
}
|
||||
|
||||
// 1.1 幂等处理
|
||||
var existingLock = await inventoryRepository.FindLockByKeyAsync(tenantId, request.IdempotencyKey, cancellationToken);
|
||||
if (existingLock is not null)
|
||||
{
|
||||
return InventoryMapping.ToDto(item);
|
||||
}
|
||||
|
||||
// 2. 校验可用量
|
||||
var now = DateTime.UtcNow;
|
||||
var isPresale = request.IsPresaleOrder || item.IsPresale;
|
||||
if (isPresale)
|
||||
{
|
||||
if (item.PresaleStartTime.HasValue && now < item.PresaleStartTime.Value)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "预售尚未开始");
|
||||
}
|
||||
|
||||
if (item.PresaleEndTime.HasValue && now > item.PresaleEndTime.Value)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "预售已结束");
|
||||
}
|
||||
}
|
||||
|
||||
var available = isPresale
|
||||
? (item.PresaleCapacity ?? item.QuantityOnHand) - item.PresaleLocked
|
||||
: item.QuantityOnHand - item.QuantityReserved;
|
||||
if (available < request.Quantity)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "库存不足,无法锁定");
|
||||
}
|
||||
|
||||
// 3. 执行锁定
|
||||
if (isPresale)
|
||||
{
|
||||
item.PresaleLocked += request.Quantity;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.QuantityReserved += request.Quantity;
|
||||
}
|
||||
|
||||
item.IsSoldOut = item.QuantityOnHand - item.QuantityReserved - item.PresaleLocked <= (item.SafetyStock ?? 0);
|
||||
await inventoryRepository.UpdateItemAsync(item, cancellationToken);
|
||||
var lockRecord = new Domain.Inventory.Entities.InventoryLockRecord
|
||||
{
|
||||
TenantId = tenantId,
|
||||
StoreId = request.StoreId,
|
||||
ProductSkuId = request.ProductSkuId,
|
||||
Quantity = request.Quantity,
|
||||
IsPresale = isPresale,
|
||||
IdempotencyKey = request.IdempotencyKey,
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
Status = Domain.Inventory.Enums.InventoryLockStatus.Locked
|
||||
};
|
||||
|
||||
await inventoryRepository.AddLockAsync(lockRecord, cancellationToken);
|
||||
await inventoryRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("锁定库存 门店 {StoreId} SKU {ProductSkuId} 数量 {Quantity}", request.StoreId, request.ProductSkuId, request.Quantity);
|
||||
return InventoryMapping.ToDto(item);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user