feat: 套餐管理与配额校验能力

This commit is contained in:
2025-12-03 20:17:55 +08:00
parent ea33e6fefe
commit 19137f3cf7
25 changed files with 996 additions and 4 deletions

View File

@@ -0,0 +1,90 @@
using System.ComponentModel.DataAnnotations;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using TakeoutSaaS.Application.App.Tenants.Commands;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Module.Authorization.Attributes;
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}/tenant-packages")]
public sealed class TenantPackagesController(IMediator mediator) : BaseApiController
{
/// <summary>
/// 分页查询租户套餐。
/// </summary>
[HttpGet]
[PermissionAuthorize("tenant-package:read")]
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantPackageDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<PagedResult<TenantPackageDto>>> Search([FromQuery] SearchTenantPackagesQuery query, CancellationToken cancellationToken)
{
var result = await mediator.Send(query, cancellationToken);
return ApiResponse<PagedResult<TenantPackageDto>>.Ok(result);
}
/// <summary>
/// 查看套餐详情。
/// </summary>
[HttpGet("{tenantPackageId:long}")]
[PermissionAuthorize("tenant-package:read")]
[ProducesResponseType(typeof(ApiResponse<TenantPackageDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<TenantPackageDto>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<TenantPackageDto>> Detail(long tenantPackageId, CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetTenantPackageByIdQuery { TenantPackageId = tenantPackageId }, cancellationToken);
return result is null
? ApiResponse<TenantPackageDto>.Error(StatusCodes.Status404NotFound, "套餐不存在")
: ApiResponse<TenantPackageDto>.Ok(result);
}
/// <summary>
/// 创建套餐。
/// </summary>
[HttpPost]
[PermissionAuthorize("tenant-package:create")]
[ProducesResponseType(typeof(ApiResponse<TenantPackageDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantPackageDto>> Create([FromBody, Required] CreateTenantPackageCommand command, CancellationToken cancellationToken)
{
var result = await mediator.Send(command, cancellationToken);
return ApiResponse<TenantPackageDto>.Ok(result);
}
/// <summary>
/// 更新套餐。
/// </summary>
[HttpPut("{tenantPackageId:long}")]
[PermissionAuthorize("tenant-package:update")]
[ProducesResponseType(typeof(ApiResponse<TenantPackageDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<TenantPackageDto>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<TenantPackageDto>> Update(long tenantPackageId, [FromBody, Required] UpdateTenantPackageCommand command, CancellationToken cancellationToken)
{
command = command with { TenantPackageId = tenantPackageId };
var result = await mediator.Send(command, cancellationToken);
return result is null
? ApiResponse<TenantPackageDto>.Error(StatusCodes.Status404NotFound, "套餐不存在")
: ApiResponse<TenantPackageDto>.Ok(result);
}
/// <summary>
/// 删除套餐。
/// </summary>
[HttpDelete("{tenantPackageId:long}")]
[PermissionAuthorize("tenant-package:delete")]
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
public async Task<ApiResponse<bool>> Delete(long tenantPackageId, CancellationToken cancellationToken)
{
var command = new DeleteTenantPackageCommand { TenantPackageId = tenantPackageId };
var result = await mediator.Send(command, cancellationToken);
return ApiResponse<bool>.Ok(result);
}
}

View File

@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -134,4 +135,21 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
var result = await mediator.Send(query, cancellationToken);
return ApiResponse<PagedResult<TenantAuditLogDto>>.Ok(result);
}
/// <summary>
/// 配额校验并占用额度(门店/账号/短信/配送)。
/// </summary>
/// <remarks>需在请求头携带 X-Tenant-Id 对应的租户。</remarks>
[HttpPost("{tenantId:long}/quotas/check")]
[PermissionAuthorize("tenant:quota:check")]
[ProducesResponseType(typeof(ApiResponse<QuotaCheckResultDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<QuotaCheckResultDto>> CheckQuota(
long tenantId,
[FromBody, Required] CheckTenantQuotaCommand body,
CancellationToken cancellationToken)
{
var command = body with { TenantId = tenantId };
var result = await mediator.Send(command, cancellationToken);
return ApiResponse<QuotaCheckResultDto>.Ok(result);
}
}