feat(admin-api): 实现 TD-001 和 TD-002 后端接口

TD-001 - PUT /api/admin/v1/tenants/{tenantId}:
- 新增 UpdateTenantCommand + UpdateTenantCommandHandler
- Controller 新增 Update 端点(tenant:update 权限)
- 校验:租户存在、name 非空、name/contactPhone 冲突返回 409
- 仓储扩展:ITenantRepository.ExistsByNameAsync

TD-002 - GET /api/admin/v1/tenants/{tenantId}/quota-usage-history:
- 新增 CQRS Query/Handler/DTO/Validator
- 支持分页(Page>=1, PageSize 1~100)
- 支持时间范围和 QuotaType 过滤
- 新增 tenant_quota_usage_histories 表(含迁移)
- 写入点:CheckTenantQuotaCommandHandler + PurchaseQuotaPackageCommandHandler

构建验证:dotnet build 通过
数据库迁移:已应用 20251218121053_AddTenantQuotaUsageHistories
This commit is contained in:
2025-12-18 20:19:33 +08:00
parent 40e914dc92
commit 907c9938ae
20 changed files with 8072 additions and 0 deletions

View File

@@ -84,6 +84,35 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
return ApiResponse<TenantDetailDto>.Ok(result);
}
/// <summary>
/// 更新租户基础信息。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="body">更新命令。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>更新结果。</returns>
[HttpPut("{tenantId:long}")]
[PermissionAuthorize("tenant:update")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
public async Task<ApiResponse<object>> Update(
long tenantId,
[FromBody, Required] UpdateTenantCommand body,
CancellationToken cancellationToken)
{
// 1. 校验路由与请求体租户标识一致
if (body.TenantId != 0 && body.TenantId != tenantId)
{
return ApiResponse<object>.Error(StatusCodes.Status400BadRequest, "路由 tenantId 与请求体 tenantId 不一致");
}
// 2. (空行后) 绑定租户标识并执行更新(若不存在或冲突则抛出业务异常,由全局异常处理转换为 404/409
var command = body with { TenantId = tenantId };
await mediator.Send(command, cancellationToken);
// 3. (空行后) 返回成功结果
return ApiResponse<object>.Ok(null);
}
/// <summary>
/// 提交或更新实名认证资料。
/// </summary>
@@ -382,4 +411,29 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
var result = await mediator.Send(command, cancellationToken);
return ApiResponse<QuotaCheckResultDto>.Ok(result);
}
/// <summary>
/// 分页查询租户配额使用历史。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="query">查询条件。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>配额使用历史分页结果。</returns>
[HttpGet("{tenantId:long}/quota-usage-history")]
[PermissionAuthorize("tenant:quota:read")]
[ProducesResponseType(typeof(ApiResponse<PagedResult<QuotaUsageHistoryDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<PagedResult<QuotaUsageHistoryDto>>> GetQuotaUsageHistory(
long tenantId,
[FromQuery] GetTenantQuotaUsageHistoryQuery query,
CancellationToken cancellationToken)
{
// 1. 绑定租户标识
query = query with { TenantId = tenantId };
// 2. (空行后) 查询配额使用历史
var result = await mediator.Send(query, cancellationToken);
// 3. (空行后) 返回分页结果
return ApiResponse<PagedResult<QuotaUsageHistoryDto>>.Ok(result);
}
}