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;
///
/// 租户管理。
///
[ApiVersion("1.0")]
[Authorize]
[Route("api/admin/v{version:apiVersion}/tenants")]
public sealed class TenantsController(IMediator mediator) : BaseApiController
{
///
/// 注册租户并初始化套餐。
///
/// 注册的租户信息。
[HttpPost]
[PermissionAuthorize("tenant:create")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> Register([FromBody] RegisterTenantCommand command, CancellationToken cancellationToken)
{
// 1. 注册租户并初始化套餐
var result = await mediator.Send(command, cancellationToken);
// 2. 返回注册结果
return ApiResponse.Ok(result);
}
///
/// 后台手动新增租户并直接入驻(创建租户 + 认证 + 订阅 + 管理员账号)。
///
/// 新增后的租户详情。
[HttpPost("manual")]
[PermissionAuthorize("tenant:create")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> CreateManually([FromBody] CreateTenantManuallyCommand command, CancellationToken cancellationToken)
{
// 1. 后台手动新增租户(直接可用)
var result = await mediator.Send(command, cancellationToken);
// 2. 返回创建结果
return ApiResponse.Ok(result);
}
///
/// 分页查询租户。
///
/// 租户分页结果。
[HttpGet]
[PermissionAuthorize("tenant:read")]
[ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
public async Task>> Search([FromQuery] SearchTenantsQuery query, CancellationToken cancellationToken)
{
// 1. 查询租户分页
var result = await mediator.Send(query, cancellationToken);
// 2. 返回分页数据
return ApiResponse>.Ok(result);
}
///
/// 查看租户详情。
///
/// 租户详情。
[HttpGet("{tenantId:long}")]
[PermissionAuthorize("tenant:read")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> Detail(long tenantId, CancellationToken cancellationToken)
{
// 1. 查询租户详情
var result = await mediator.Send(new GetTenantByIdQuery(tenantId), cancellationToken);
// 2. 返回租户信息
return ApiResponse.Ok(result);
}
///
/// 提交或更新实名认证资料。
///
/// 提交的实名认证信息。
[HttpPost("{tenantId:long}/verification")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> 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.Ok(result);
}
///
/// 审核租户。
///
/// 审核后的租户信息。
[HttpPost("{tenantId:long}/review")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> 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.Ok(result);
}
///
/// 查询当前租户审核领取信息。
///
/// 领取信息,未领取返回 null。
[HttpGet("{tenantId:long}/review/claim")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> GetReviewClaim(long tenantId, CancellationToken cancellationToken)
{
// 1. 查询领取信息
var result = await mediator.Send(new GetTenantReviewClaimQuery(tenantId), cancellationToken);
// 2. 返回领取信息
return ApiResponse.Ok(result);
}
///
/// 领取租户入驻审核(领取后仅领取人可操作审核)。
///
/// 领取结果。
[HttpPost("{tenantId:long}/review/claim")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> ClaimReview(long tenantId, CancellationToken cancellationToken)
{
// 1. 执行领取
var result = await mediator.Send(new ClaimTenantReviewCommand { TenantId = tenantId }, cancellationToken);
// 2. 返回领取结果
return ApiResponse.Ok(result);
}
///
/// 强制接管租户入驻审核(仅超级管理员可用)。
///
/// 接管后的领取信息。
[HttpPost("{tenantId:long}/review/force-claim")]
[PermissionAuthorize("tenant:review:force-claim")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> ForceClaimReview(long tenantId, CancellationToken cancellationToken)
{
// 1. 执行强制接管
var result = await mediator.Send(new ForceClaimTenantReviewCommand { TenantId = tenantId }, cancellationToken);
// 2. 返回接管结果
return ApiResponse.Ok(result);
}
///
/// 释放租户入驻审核领取(仅领取人可释放)。
///
/// 释放后的领取信息,未领取返回 null。
[HttpPost("{tenantId:long}/review/release")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> ReleaseReview(long tenantId, CancellationToken cancellationToken)
{
// 1. 执行释放
var result = await mediator.Send(new ReleaseTenantReviewClaimCommand { TenantId = tenantId }, cancellationToken);
// 2. 返回释放结果
return ApiResponse.Ok(result);
}
///
/// 冻结租户(暂停服务)。
///
/// 冻结后的租户信息。
[HttpPost("{tenantId:long}/freeze")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> 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.Ok(result);
}
///
/// 解冻租户(恢复服务)。
///
/// 解冻后的租户信息。
[HttpPost("{tenantId:long}/unfreeze")]
[PermissionAuthorize("tenant:review")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> 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.Ok(result);
}
///
/// 创建或续费租户订阅。
///
/// 创建或续费的订阅信息。
[HttpPost("{tenantId:long}/subscriptions")]
[PermissionAuthorize("tenant:subscription")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> 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.Ok(result);
}
///
/// 延期/赠送租户订阅时长(按当前订阅套餐续费)。
///
/// 续费后的订阅信息。
[HttpPost("{tenantId:long}/subscriptions/extend")]
[PermissionAuthorize("tenant:subscription")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> 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.Ok(result);
}
///
/// 套餐升降配。
///
/// 更新后的订阅信息。
[HttpPut("{tenantId:long}/subscriptions/{subscriptionId:long}/plan")]
[PermissionAuthorize("tenant:subscription")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> 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.Ok(result);
}
///
/// 查询审核日志。
///
/// 租户审核日志分页结果。
[HttpGet("{tenantId:long}/audits")]
[PermissionAuthorize("tenant:read")]
[ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
public async Task>> 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>.Ok(result);
}
///
/// 伪装登录租户(仅平台超级管理员可用)。
///
/// 目标租户主管理员的令牌对。
[HttpPost("{tenantId:long}/impersonate")]
[PermissionAuthorize("tenant:read")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> Impersonate(long tenantId, CancellationToken cancellationToken)
{
// 1. 执行伪装登录
var result = await mediator.Send(new ImpersonateTenantCommand { TenantId = tenantId }, cancellationToken);
// 2. 返回令牌
return ApiResponse.Ok(result);
}
///
/// 生成租户主管理员重置密码链接(仅平台超级管理员可用)。
///
/// 链接默认 24 小时有效且仅可使用一次。
/// 重置密码链接。
[HttpPost("{tenantId:long}/admin/reset-link")]
[PermissionAuthorize("tenant:read")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> 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.Ok(data: resetUrl);
}
///
/// 配额校验并占用额度(门店/账号/短信/配送)。
///
/// 需在请求头携带 X-Tenant-Id 对应的租户。
/// 配额校验结果。
[HttpPost("{tenantId:long}/quotas/check")]
[PermissionAuthorize("tenant:quota:check")]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task> 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.Ok(result);
}
}