feat: 完善库存锁定幂等与批次扣减策略
This commit is contained in:
149
src/Api/TakeoutSaaS.AdminApi/Controllers/InventoryController.cs
Normal file
149
src/Api/TakeoutSaaS.AdminApi/Controllers/InventoryController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -218,6 +218,14 @@
|
||||
"product-media:update",
|
||||
"product-pricing:read",
|
||||
"product-pricing:update",
|
||||
"inventory:read",
|
||||
"inventory:adjust",
|
||||
"inventory:lock",
|
||||
"inventory:release",
|
||||
"inventory:deduct",
|
||||
"inventory:batch:read",
|
||||
"inventory:batch:update",
|
||||
"inventory:lock:expire",
|
||||
"order:create",
|
||||
"order:read",
|
||||
"order:update",
|
||||
@@ -274,6 +282,14 @@
|
||||
"product-media:update",
|
||||
"product-pricing:read",
|
||||
"product-pricing:update",
|
||||
"inventory:read",
|
||||
"inventory:adjust",
|
||||
"inventory:lock",
|
||||
"inventory:release",
|
||||
"inventory:deduct",
|
||||
"inventory:batch:read",
|
||||
"inventory:batch:update",
|
||||
"inventory:lock:expire",
|
||||
"order:create",
|
||||
"order:read",
|
||||
"order:update",
|
||||
@@ -374,6 +390,14 @@
|
||||
"product-media:update",
|
||||
"product-pricing:read",
|
||||
"product-pricing:update",
|
||||
"inventory:read",
|
||||
"inventory:adjust",
|
||||
"inventory:lock",
|
||||
"inventory:release",
|
||||
"inventory:deduct",
|
||||
"inventory:batch:read",
|
||||
"inventory:batch:update",
|
||||
"inventory:lock:expire",
|
||||
"order:create",
|
||||
"order:read",
|
||||
"order:update",
|
||||
|
||||
Reference in New Issue
Block a user