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

@@ -0,0 +1,149 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using TakeoutSaaS.Application.App.Inventory.Commands;
using TakeoutSaaS.Application.App.Inventory.Dto;
using TakeoutSaaS.Application.App.Inventory.Queries;
using TakeoutSaaS.Module.Authorization.Attributes;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Web.Api;
namespace TakeoutSaaS.AdminApi.Controllers;
/// <summary>
/// 库存管理。
/// </summary>
[ApiVersion("1.0")]
[Authorize]
[Route("api/admin/v{version:apiVersion}/stores/{storeId:long}/inventory")]
public sealed class InventoryController(IMediator mediator) : BaseApiController
{
/// <summary>
/// 查询库存。
/// </summary>
[HttpGet("{productSkuId:long}")]
[PermissionAuthorize("inventory:read")]
[ProducesResponseType(typeof(ApiResponse<InventoryItemDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<InventoryItemDto>> Get(long storeId, long productSkuId, CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetInventoryItemQuery { StoreId = storeId, ProductSkuId = productSkuId }, cancellationToken);
return result is null
? ApiResponse<InventoryItemDto>.Error(ErrorCodes.NotFound, "库存不存在")
: ApiResponse<InventoryItemDto>.Ok(result);
}
/// <summary>
/// 调整库存(入库/盘点/报损)。
/// </summary>
[HttpPost("adjust")]
[PermissionAuthorize("inventory:adjust")]
[ProducesResponseType(typeof(ApiResponse<InventoryItemDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<InventoryItemDto>> Adjust(long storeId, [FromBody] AdjustInventoryCommand command, CancellationToken cancellationToken)
{
if (command.StoreId == 0)
{
command = command with { StoreId = storeId };
}
var result = await mediator.Send(command, cancellationToken);
return ApiResponse<InventoryItemDto>.Ok(result);
}
/// <summary>
/// 锁定库存(下单占用)。
/// </summary>
[HttpPost("lock")]
[PermissionAuthorize("inventory:lock")]
[ProducesResponseType(typeof(ApiResponse<InventoryItemDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<InventoryItemDto>> Lock(long storeId, [FromBody] LockInventoryCommand command, CancellationToken cancellationToken)
{
if (command.StoreId == 0)
{
command = command with { StoreId = storeId };
}
var result = await mediator.Send(command, cancellationToken);
return ApiResponse<InventoryItemDto>.Ok(result);
}
/// <summary>
/// 释放库存(取消订单等)。
/// </summary>
[HttpPost("release")]
[PermissionAuthorize("inventory:release")]
[ProducesResponseType(typeof(ApiResponse<InventoryItemDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<InventoryItemDto>> Release(long storeId, [FromBody] ReleaseInventoryCommand command, CancellationToken cancellationToken)
{
if (command.StoreId == 0)
{
command = command with { StoreId = storeId };
}
var result = await mediator.Send(command, cancellationToken);
return ApiResponse<InventoryItemDto>.Ok(result);
}
/// <summary>
/// 扣减库存(支付或履约成功)。
/// </summary>
[HttpPost("deduct")]
[PermissionAuthorize("inventory:deduct")]
[ProducesResponseType(typeof(ApiResponse<InventoryItemDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<InventoryItemDto>> Deduct(long storeId, [FromBody] DeductInventoryCommand command, CancellationToken cancellationToken)
{
if (command.StoreId == 0)
{
command = command with { StoreId = storeId };
}
var result = await mediator.Send(command, cancellationToken);
return ApiResponse<InventoryItemDto>.Ok(result);
}
/// <summary>
/// 查询批次列表。
/// </summary>
[HttpGet("{productSkuId:long}/batches")]
[PermissionAuthorize("inventory:batch:read")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<InventoryBatchDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<InventoryBatchDto>>> GetBatches(long storeId, long productSkuId, CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetInventoryBatchesQuery
{
StoreId = storeId,
ProductSkuId = productSkuId
}, cancellationToken);
return ApiResponse<IReadOnlyList<InventoryBatchDto>>.Ok(result);
}
/// <summary>
/// 新增或更新批次。
/// </summary>
[HttpPost("{productSkuId:long}/batches")]
[PermissionAuthorize("inventory:batch:update")]
[ProducesResponseType(typeof(ApiResponse<InventoryBatchDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<InventoryBatchDto>> UpsertBatch(long storeId, long productSkuId, [FromBody] UpsertInventoryBatchCommand command, CancellationToken cancellationToken)
{
if (command.StoreId == 0 || command.ProductSkuId == 0)
{
command = command with { StoreId = storeId, ProductSkuId = productSkuId };
}
var result = await mediator.Send(command, cancellationToken);
return ApiResponse<InventoryBatchDto>.Ok(result);
}
/// <summary>
/// 释放过期锁定。
/// </summary>
[HttpPost("locks/expire")]
[PermissionAuthorize("inventory:release")]
[ProducesResponseType(typeof(ApiResponse<int>), StatusCodes.Status200OK)]
public async Task<ApiResponse<int>> ReleaseExpiredLocks(CancellationToken cancellationToken)
{
var count = await mediator.Send(new ReleaseExpiredInventoryLocksCommand(), cancellationToken);
return ApiResponse<int>.Ok(count);
}
}