feat: 实现租户列表查询接口

- 获取租户列表:GET /api/admin/v1/tenants
- 用于填充租户下拉选择器

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 08:04:12 +00:00
parent 0bcad8ba7e
commit e15ab4f0be
6 changed files with 151 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TakeoutSaaS.Application.App.Tenants.Contracts;
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}/tenants")]
public sealed class TenantsController(IMediator mediator) : BaseApiController
{
/// <summary>
/// 获取租户列表(用于下拉选择器)。
/// </summary>
/// <param name="keyword">关键字(租户名称/编码)。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>租户列表。</returns>
[HttpGet]
[PermissionAuthorize("tenant:read")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<TenantListItemDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<TenantListItemDto>>> List(
[FromQuery] string? keyword,
CancellationToken cancellationToken)
{
// 1. 构造查询
var query = new ListTenantsQuery { Keyword = keyword };
// 2. 执行查询
var result = await mediator.Send(query, cancellationToken);
// 3. 返回租户列表
return ApiResponse<IReadOnlyList<TenantListItemDto>>.Ok(result);
}
}

View File

@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Serialization;
namespace TakeoutSaaS.Application.App.Tenants.Contracts;
/// <summary>
/// 租户列表项 DTO用于下拉选择器
/// </summary>
public sealed class TenantListItemDto
{
/// <summary>
/// 租户 ID雪花序列化为字符串
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long Id { get; init; }
/// <summary>
/// 租户编码。
/// </summary>
public string Code { get; init; } = string.Empty;
/// <summary>
/// 租户名称。
/// </summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// 租户简称。
/// </summary>
public string? ShortName { get; init; }
/// <summary>
/// 租户状态。
/// </summary>
public TenantStatus Status { get; init; }
}

View File

@@ -0,0 +1,30 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Contracts;
using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 获取租户列表查询处理器。
/// </summary>
public sealed class ListTenantsQueryHandler(ITenantRepository tenantRepository)
: IRequestHandler<ListTenantsQuery, IReadOnlyList<TenantListItemDto>>
{
/// <inheritdoc />
public async Task<IReadOnlyList<TenantListItemDto>> Handle(ListTenantsQuery request, CancellationToken cancellationToken)
{
// 1. 查询租户列表
var tenants = await tenantRepository.GetAllAsync(request.Keyword, cancellationToken);
// 2. 映射 DTO
return tenants.Select(t => new TenantListItemDto
{
Id = t.Id,
Code = t.Code,
Name = t.Name,
ShortName = t.ShortName,
Status = t.Status
}).ToArray();
}
}

View File

@@ -0,0 +1,15 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Contracts;
namespace TakeoutSaaS.Application.App.Tenants.Queries;
/// <summary>
/// 获取租户列表查询(用于下拉选择器)。
/// </summary>
public sealed record ListTenantsQuery : IRequest<IReadOnlyList<TenantListItemDto>>
{
/// <summary>
/// 关键字(租户名称/编码)。
/// </summary>
public string? Keyword { get; init; }
}

View File

@@ -22,4 +22,12 @@ public interface ITenantRepository
/// <param name="cancellationToken">取消标记。</param>
/// <returns>租户列表(仅返回找到的租户)。</returns>
Task<IReadOnlyList<Tenant>> FindByIdsAsync(IReadOnlyCollection<long> tenantIds, CancellationToken cancellationToken = default);
/// <summary>
/// 获取所有租户列表(用于下拉选择器)。
/// </summary>
/// <param name="keyword">关键字(租户名称/编码)。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>租户列表。</returns>
Task<IReadOnlyList<Tenant>> GetAllAsync(string? keyword, CancellationToken cancellationToken = default);
}

View File

@@ -34,4 +34,23 @@ public sealed class EfTenantRepository(TakeoutAdminDbContext context) : ITenantR
.Where(x => tenantIds.Contains(x.Id))
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<IReadOnlyList<Tenant>> GetAllAsync(string? keyword, CancellationToken cancellationToken = default)
{
// 1. 构建基础查询
var query = context.Tenants
.AsNoTracking()
.Where(x => x.DeletedAt == null);
// 2. 应用关键字过滤
if (!string.IsNullOrWhiteSpace(keyword))
{
var normalized = keyword.Trim();
query = query.Where(x => x.Name.Contains(normalized) || x.Code.Contains(normalized));
}
// 3. 返回列表
return await query.OrderBy(x => x.Code).ToListAsync(cancellationToken);
}
}