Files
TakeoutSaaS.TenantApi/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs

386 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using TakeoutSaaS.Application.App.Tenants.Commands;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Application.Identity.Contracts;
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>
/// <returns>注册的租户信息。</returns>
[HttpPost]
[PermissionAuthorize("tenant:create")]
[ProducesResponseType(typeof(ApiResponse<TenantDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantDto>> Register([FromBody] RegisterTenantCommand command, CancellationToken cancellationToken)
{
// 1. 注册租户并初始化套餐
var result = await mediator.Send(command, cancellationToken);
// 2. 返回注册结果
return ApiResponse<TenantDto>.Ok(result);
}
/// <summary>
/// 后台手动新增租户并直接入驻(创建租户 + 认证 + 订阅 + 管理员账号)。
/// </summary>
/// <returns>新增后的租户详情。</returns>
[HttpPost("manual")]
[PermissionAuthorize("tenant:create")]
[ProducesResponseType(typeof(ApiResponse<TenantDetailDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantDetailDto>> CreateManually([FromBody] CreateTenantManuallyCommand command, CancellationToken cancellationToken)
{
// 1. 后台手动新增租户(直接可用)
var result = await mediator.Send(command, cancellationToken);
// 2. 返回创建结果
return ApiResponse<TenantDetailDto>.Ok(result);
}
/// <summary>
/// 分页查询租户。
/// </summary>
/// <returns>租户分页结果。</returns>
[HttpGet]
[PermissionAuthorize("tenant:read")]
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<PagedResult<TenantDto>>> Search([FromQuery] SearchTenantsQuery query, CancellationToken cancellationToken)
{
// 1. 查询租户分页
var result = await mediator.Send(query, cancellationToken);
// 2. 返回分页数据
return ApiResponse<PagedResult<TenantDto>>.Ok(result);
}
/// <summary>
/// 查看租户详情。
/// </summary>
/// <returns>租户详情。</returns>
[HttpGet("{tenantId:long}")]
[PermissionAuthorize("tenant:read")]
[ProducesResponseType(typeof(ApiResponse<TenantDetailDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantDetailDto>> Detail(long tenantId, CancellationToken cancellationToken)
{
// 1. 查询租户详情
var result = await mediator.Send(new GetTenantByIdQuery(tenantId), cancellationToken);
// 2. 返回租户信息
return ApiResponse<TenantDetailDto>.Ok(result);
}
/// <summary>
/// 提交或更新实名认证资料。
/// </summary>
/// <returns>提交的实名认证信息。</returns>
[HttpPost("{tenantId:long}/verification")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse<TenantVerificationDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantVerificationDto>> SubmitVerification(
long tenantId,
[FromBody] SubmitTenantVerificationCommand body,
CancellationToken cancellationToken)
{
// 1. 合并路由中的租户标识
var command = body with { TenantId = tenantId };
// 2. 提交或更新认证资料
var result = await mediator.Send(command, cancellationToken);
// 3. 返回认证结果
return ApiResponse<TenantVerificationDto>.Ok(result);
}
/// <summary>
/// 审核租户。
/// </summary>
/// <returns>审核后的租户信息。</returns>
[HttpPost("{tenantId:long}/review")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse<TenantDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantDto>> Review(long tenantId, [FromBody] ReviewTenantCommand body, CancellationToken cancellationToken)
{
// 1. 绑定租户标识
var command = body with { TenantId = tenantId };
// 2. 执行审核
var result = await mediator.Send(command, cancellationToken);
// 3. 返回审核结果
return ApiResponse<TenantDto>.Ok(result);
}
/// <summary>
/// 查询当前租户审核领取信息。
/// </summary>
/// <returns>领取信息,未领取返回 null。</returns>
[HttpGet("{tenantId:long}/review/claim")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse<TenantReviewClaimDto?>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantReviewClaimDto?>> GetReviewClaim(long tenantId, CancellationToken cancellationToken)
{
// 1. 查询领取信息
var result = await mediator.Send(new GetTenantReviewClaimQuery(tenantId), cancellationToken);
// 2. 返回领取信息
return ApiResponse<TenantReviewClaimDto?>.Ok(result);
}
/// <summary>
/// 领取租户入驻审核(领取后仅领取人可操作审核)。
/// </summary>
/// <returns>领取结果。</returns>
[HttpPost("{tenantId:long}/review/claim")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse<TenantReviewClaimDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantReviewClaimDto>> ClaimReview(long tenantId, CancellationToken cancellationToken)
{
// 1. 执行领取
var result = await mediator.Send(new ClaimTenantReviewCommand { TenantId = tenantId }, cancellationToken);
// 2. 返回领取结果
return ApiResponse<TenantReviewClaimDto>.Ok(result);
}
/// <summary>
/// 强制接管租户入驻审核(仅超级管理员可用)。
/// </summary>
/// <returns>接管后的领取信息。</returns>
[HttpPost("{tenantId:long}/review/force-claim")]
[PermissionAuthorize("tenant:review:force-claim")]
[ProducesResponseType(typeof(ApiResponse<TenantReviewClaimDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantReviewClaimDto>> ForceClaimReview(long tenantId, CancellationToken cancellationToken)
{
// 1. 执行强制接管
var result = await mediator.Send(new ForceClaimTenantReviewCommand { TenantId = tenantId }, cancellationToken);
// 2. 返回接管结果
return ApiResponse<TenantReviewClaimDto>.Ok(result);
}
/// <summary>
/// 释放租户入驻审核领取(仅领取人可释放)。
/// </summary>
/// <returns>释放后的领取信息,未领取返回 null。</returns>
[HttpPost("{tenantId:long}/review/release")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse<TenantReviewClaimDto?>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantReviewClaimDto?>> ReleaseReview(long tenantId, CancellationToken cancellationToken)
{
// 1. 执行释放
var result = await mediator.Send(new ReleaseTenantReviewClaimCommand { TenantId = tenantId }, cancellationToken);
// 2. 返回释放结果
return ApiResponse<TenantReviewClaimDto?>.Ok(result);
}
/// <summary>
/// 冻结租户(暂停服务)。
/// </summary>
/// <returns>冻结后的租户信息。</returns>
[HttpPost("{tenantId:long}/freeze")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse<TenantDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantDto>> Freeze(
long tenantId,
[FromBody] FreezeTenantCommand body,
CancellationToken cancellationToken)
{
// 1. 合并路由参数
var command = body with { TenantId = tenantId };
// 2. 执行冻结
var result = await mediator.Send(command, cancellationToken);
// 3. 返回冻结结果
return ApiResponse<TenantDto>.Ok(result);
}
/// <summary>
/// 解冻租户(恢复服务)。
/// </summary>
/// <returns>解冻后的租户信息。</returns>
[HttpPost("{tenantId:long}/unfreeze")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse<TenantDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantDto>> Unfreeze(
long tenantId,
[FromBody] UnfreezeTenantCommand body,
CancellationToken cancellationToken)
{
// 1. 合并路由参数
var command = body with { TenantId = tenantId };
// 2. 执行解冻
var result = await mediator.Send(command, cancellationToken);
// 3. 返回解冻结果
return ApiResponse<TenantDto>.Ok(result);
}
/// <summary>
/// 创建或续费租户订阅。
/// </summary>
/// <returns>创建或续费的订阅信息。</returns>
[HttpPost("{tenantId:long}/subscriptions")]
[PermissionAuthorize("tenant:subscription")]
[ProducesResponseType(typeof(ApiResponse<TenantSubscriptionDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantSubscriptionDto>> CreateSubscription(
long tenantId,
[FromBody] CreateTenantSubscriptionCommand body,
CancellationToken cancellationToken)
{
// 1. 绑定租户并创建或续费订阅
var command = body with { TenantId = tenantId };
// 2. 返回订阅结果
var result = await mediator.Send(command, cancellationToken);
return ApiResponse<TenantSubscriptionDto>.Ok(result);
}
/// <summary>
/// 延期/赠送租户订阅时长(按当前订阅套餐续费)。
/// </summary>
/// <returns>续费后的订阅信息。</returns>
[HttpPost("{tenantId:long}/subscriptions/extend")]
[PermissionAuthorize("tenant:subscription")]
[ProducesResponseType(typeof(ApiResponse<TenantSubscriptionDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantSubscriptionDto>> ExtendSubscription(
long tenantId,
[FromBody] ExtendTenantSubscriptionCommand body,
CancellationToken cancellationToken)
{
// 1. 合并租户标识
var command = body with { TenantId = tenantId };
// 2. 执行延期/赠送
var result = await mediator.Send(command, cancellationToken);
// 3. 返回订阅结果
return ApiResponse<TenantSubscriptionDto>.Ok(result);
}
/// <summary>
/// 套餐升降配。
/// </summary>
/// <returns>更新后的订阅信息。</returns>
[HttpPut("{tenantId:long}/subscriptions/{subscriptionId:long}/plan")]
[PermissionAuthorize("tenant:subscription")]
[ProducesResponseType(typeof(ApiResponse<TenantSubscriptionDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TenantSubscriptionDto>> ChangePlan(
long tenantId,
long subscriptionId,
[FromBody] ChangeTenantSubscriptionPlanCommand body,
CancellationToken cancellationToken)
{
// 1. 绑定租户与订阅标识
var command = body with { TenantId = tenantId, TenantSubscriptionId = subscriptionId };
// 2. 执行升降配
var result = await mediator.Send(command, cancellationToken);
// 3. 返回调整后的订阅
return ApiResponse<TenantSubscriptionDto>.Ok(result);
}
/// <summary>
/// 查询审核日志。
/// </summary>
/// <returns>租户审核日志分页结果。</returns>
[HttpGet("{tenantId:long}/audits")]
[PermissionAuthorize("tenant:read")]
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantAuditLogDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<PagedResult<TenantAuditLogDto>>> AuditLogs(
long tenantId,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
CancellationToken cancellationToken = default)
{
// 1. 构造审核日志查询
var query = new GetTenantAuditLogsQuery(tenantId, page, pageSize);
// 2. 查询并返回分页结果
var result = await mediator.Send(query, cancellationToken);
return ApiResponse<PagedResult<TenantAuditLogDto>>.Ok(result);
}
/// <summary>
/// 伪装登录租户(仅平台超级管理员可用)。
/// </summary>
/// <returns>目标租户主管理员的令牌对。</returns>
[HttpPost("{tenantId:long}/impersonate")]
[PermissionAuthorize("tenant:read")]
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TokenResponse>> Impersonate(long tenantId, CancellationToken cancellationToken)
{
// 1. 执行伪装登录
var result = await mediator.Send(new ImpersonateTenantCommand { TenantId = tenantId }, cancellationToken);
// 2. 返回令牌
return ApiResponse<TokenResponse>.Ok(result);
}
/// <summary>
/// 生成租户主管理员重置密码链接(仅平台超级管理员可用)。
/// </summary>
/// <remarks>链接默认 24 小时有效且仅可使用一次。</remarks>
/// <returns>重置密码链接。</returns>
[HttpPost("{tenantId:long}/admin/reset-link")]
[PermissionAuthorize("tenant:read")]
[ProducesResponseType(typeof(ApiResponse<string>), StatusCodes.Status200OK)]
public async Task<ApiResponse<string>> CreateAdminResetLink(long tenantId, CancellationToken cancellationToken)
{
// 1. 生成一次性令牌
var token = await mediator.Send(new CreateTenantAdminResetLinkTokenCommand { TenantId = tenantId }, cancellationToken);
// 2. (空行后) 解析前端来源(优先 Origin避免拼成 AdminApi 域名)
var origin = Request.Headers.Origin.ToString();
if (string.IsNullOrWhiteSpace(origin))
{
origin = $"{Request.Scheme}://{Request.Host}";
}
origin = origin.TrimEnd('/');
var resetUrl = $"{origin}/#/auth/reset-password?token={Uri.EscapeDataString(token)}";
// 3. (空行后) 返回链接
return ApiResponse<string>.Ok(data: resetUrl);
}
/// <summary>
/// 配额校验并占用额度(门店/账号/短信/配送)。
/// </summary>
/// <remarks>需在请求头携带 X-Tenant-Id 对应的租户。</remarks>
/// <returns>配额校验结果。</returns>
[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)
{
// 1. 绑定租户标识
var command = body with { TenantId = tenantId };
// 2. 校验并占用配额
var result = await mediator.Send(command, cancellationToken);
return ApiResponse<QuotaCheckResultDto>.Ok(result);
}
}