feat: 完善库存锁定幂等与批次扣减策略

This commit is contained in:
2025-12-04 11:31:26 +08:00
parent cd8862b223
commit 7e6125c687
35 changed files with 1670 additions and 0 deletions

View File

@@ -1,3 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Inventory.Entities;
@@ -41,4 +43,10 @@ public sealed class InventoryBatch : MultiTenantEntityBase
/// 剩余数量。
/// </summary>
public int RemainingQuantity { get; set; }
/// <summary>
/// 并发控制字段。
/// </summary>
[Timestamp]
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
}

View File

@@ -1,4 +1,7 @@
using System;
using System.ComponentModel.DataAnnotations;
using TakeoutSaaS.Shared.Abstractions.Entities;
using TakeoutSaaS.Domain.Inventory.Enums;
namespace TakeoutSaaS.Domain.Inventory.Entities;
@@ -46,4 +49,50 @@ public sealed class InventoryItem : MultiTenantEntityBase
/// 过期日期。
/// </summary>
public DateTime? ExpireDate { get; set; }
/// <summary>
/// 是否预售商品。
/// </summary>
public bool IsPresale { get; set; }
/// <summary>
/// 预售开始时间UTC
/// </summary>
public DateTime? PresaleStartTime { get; set; }
/// <summary>
/// 预售结束时间UTC
/// </summary>
public DateTime? PresaleEndTime { get; set; }
/// <summary>
/// 预售名额(上限)。
/// </summary>
public int? PresaleCapacity { get; set; }
/// <summary>
/// 当前预售已锁定数量。
/// </summary>
public int PresaleLocked { get; set; }
/// <summary>
/// 单品限购(覆盖商品级 MaxQuantityPerOrder
/// </summary>
public int? MaxQuantityPerOrder { get; set; }
/// <summary>
/// 是否标记售罄。
/// </summary>
public bool IsSoldOut { get; set; }
/// <summary>
/// 批次扣减策略。
/// </summary>
public InventoryBatchConsumeStrategy BatchConsumeStrategy { get; set; } = InventoryBatchConsumeStrategy.Fifo;
/// <summary>
/// 并发控制字段。
/// </summary>
[Timestamp]
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
}

View File

@@ -0,0 +1,52 @@
using System.ComponentModel.DataAnnotations;
using TakeoutSaaS.Domain.Inventory.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Inventory.Entities;
/// <summary>
/// 库存锁定记录。
/// </summary>
public sealed class InventoryLockRecord : MultiTenantEntityBase
{
/// <summary>
/// 门店 ID。
/// </summary>
public long StoreId { get; set; }
/// <summary>
/// SKU ID。
/// </summary>
public long ProductSkuId { get; set; }
/// <summary>
/// 锁定数量。
/// </summary>
public int Quantity { get; set; }
/// <summary>
/// 是否预售锁定。
/// </summary>
public bool IsPresale { get; set; }
/// <summary>
/// 幂等键。
/// </summary>
public string IdempotencyKey { get; set; } = string.Empty;
/// <summary>
/// 过期时间UTC
/// </summary>
public DateTime? ExpiresAt { get; set; }
/// <summary>
/// 锁定状态。
/// </summary>
public InventoryLockStatus Status { get; set; } = InventoryLockStatus.Locked;
/// <summary>
/// 并发控制字段。
/// </summary>
[Timestamp]
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
}

View File

@@ -0,0 +1,17 @@
namespace TakeoutSaaS.Domain.Inventory.Enums;
/// <summary>
/// 批次扣减策略。
/// </summary>
public enum InventoryBatchConsumeStrategy
{
/// <summary>
/// 先进先出。
/// </summary>
Fifo = 0,
/// <summary>
/// 先到期先出。
/// </summary>
Fefo = 1
}

View File

@@ -0,0 +1,22 @@
namespace TakeoutSaaS.Domain.Inventory.Enums;
/// <summary>
/// 库存锁定状态。
/// </summary>
public enum InventoryLockStatus
{
/// <summary>
/// 已锁定。
/// </summary>
Locked = 0,
/// <summary>
/// 已释放。
/// </summary>
Released = 1,
/// <summary>
/// 已扣减。
/// </summary>
Deducted = 2
}

View File

@@ -0,0 +1,95 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TakeoutSaaS.Domain.Inventory.Entities;
using TakeoutSaaS.Domain.Inventory.Enums;
namespace TakeoutSaaS.Domain.Inventory.Repositories;
/// <summary>
/// 库存仓储契约。
/// </summary>
public interface IInventoryRepository
{
/// <summary>
/// 依据标识查询库存。
/// </summary>
Task<InventoryItem?> FindByIdAsync(long inventoryItemId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 按门店与 SKU 查询库存(只读)。
/// </summary>
Task<InventoryItem?> FindBySkuAsync(long tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default);
/// <summary>
/// 按门店与 SKU 查询库存(跟踪用于更新)。
/// </summary>
Task<InventoryItem?> GetForUpdateAsync(long tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default);
/// <summary>
/// 新增库存记录。
/// </summary>
Task AddItemAsync(InventoryItem item, CancellationToken cancellationToken = default);
/// <summary>
/// 更新库存记录。
/// </summary>
Task UpdateItemAsync(InventoryItem item, CancellationToken cancellationToken = default);
/// <summary>
/// 新增库存调整记录。
/// </summary>
Task AddAdjustmentAsync(InventoryAdjustment adjustment, CancellationToken cancellationToken = default);
/// <summary>
/// 新增锁定记录。
/// </summary>
Task AddLockAsync(InventoryLockRecord lockRecord, CancellationToken cancellationToken = default);
/// <summary>
/// 按幂等键查询锁记录。
/// </summary>
Task<InventoryLockRecord?> FindLockByKeyAsync(long tenantId, string idempotencyKey, CancellationToken cancellationToken = default);
/// <summary>
/// 更新锁状态。
/// </summary>
Task MarkLockStatusAsync(InventoryLockRecord lockRecord, InventoryLockStatus status, CancellationToken cancellationToken = default);
/// <summary>
/// 查询过期锁定。
/// </summary>
Task<IReadOnlyList<InventoryLockRecord>> FindExpiredLocksAsync(long tenantId, DateTime utcNow, CancellationToken cancellationToken = default);
/// <summary>
/// 查询批次列表。
/// </summary>
Task<IReadOnlyList<InventoryBatch>> GetBatchesAsync(long tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default);
/// <summary>
/// 批次扣减读取(带排序策略)。
/// </summary>
Task<IReadOnlyList<InventoryBatch>> GetBatchesForConsumeAsync(long tenantId, long storeId, long productSkuId, InventoryBatchConsumeStrategy strategy, CancellationToken cancellationToken = default);
/// <summary>
/// 查询批次(跟踪用于更新)。
/// </summary>
Task<InventoryBatch?> GetBatchForUpdateAsync(long tenantId, long storeId, long productSkuId, string batchNumber, CancellationToken cancellationToken = default);
/// <summary>
/// 新增批次。
/// </summary>
Task AddBatchAsync(InventoryBatch batch, CancellationToken cancellationToken = default);
/// <summary>
/// 更新批次。
/// </summary>
Task UpdateBatchAsync(InventoryBatch batch, CancellationToken cancellationToken = default);
/// <summary>
/// 持久化变更。
/// </summary>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}