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; namespace TakeoutSaaS.Application.App.Inventory.Handlers; /// /// 库存锁定处理器。 /// public sealed class LockInventoryCommandHandler( IInventoryRepository inventoryRepository, ILogger logger) : IRequestHandler { /// public async Task Handle(LockInventoryCommand request, CancellationToken cancellationToken) { // 1. 读取库存 var item = await inventoryRepository.GetForUpdateAsync(null, request.StoreId, request.ProductSkuId, cancellationToken); if (item is null) { throw new BusinessException(ErrorCodes.NotFound, "库存不存在"); } var tenantId = item.TenantId; // 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); } }