feat: 添加统一的租户审核接口 POST /review
- 新增 ReviewTenantCommand 支持 approve/reason/renewMonths/operatingMode - 新增 ReviewTenantCommandHandler 统一处理通过/驳回逻辑 - 添加 TenantsController.Review 接口 - 审核完成后自动释放领取、记录审核日志 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -266,6 +266,33 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
|
|||||||
return ApiResponse<object>.Ok(null, "审核驳回");
|
return ApiResponse<object>.Ok(null, "审核驳回");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核租户(统一接口,支持通过/驳回)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">租户 ID(雪花算法)。</param>
|
||||||
|
/// <param name="command">审核命令。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>无内容。</returns>
|
||||||
|
[HttpPost("{id:long}/review")]
|
||||||
|
[PermissionAuthorize("tenant:review")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<object>> Review(
|
||||||
|
long id,
|
||||||
|
[FromBody] ReviewTenantCommand command,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 1. 确保路径参数与请求体一致
|
||||||
|
var updatedCommand = command with { TenantId = id.ToString() };
|
||||||
|
|
||||||
|
// 2. 执行命令
|
||||||
|
await mediator.Send(updatedCommand, cancellationToken);
|
||||||
|
|
||||||
|
// 3. 返回成功
|
||||||
|
return ApiResponse<object>.Ok(null, command.Approve ? "审核通过" : "审核驳回");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取租户审核领取信息。
|
/// 获取租户审核领取信息。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Domain.Common.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核租户命令(统一处理通过/驳回)。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ReviewTenantCommand : IRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID(雪花算法,字符串传输)。
|
||||||
|
/// </summary>
|
||||||
|
public required string TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否通过审核。
|
||||||
|
/// </summary>
|
||||||
|
public bool Approve { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 驳回原因(仅当 Approve=false 时有效)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Reason { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 续费时长(月)(仅当 Approve=true 时有效)。
|
||||||
|
/// </summary>
|
||||||
|
public int? RenewMonths { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经营模式(仅当 Approve=true 时有效)。
|
||||||
|
/// </summary>
|
||||||
|
public OperatingMode? OperatingMode { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||||
|
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||||
|
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核租户命令处理器(统一处理通过/驳回)。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReviewTenantCommandHandler(
|
||||||
|
ITenantRepository tenantRepository,
|
||||||
|
IIdentityUserRepository identityUserRepository,
|
||||||
|
ICurrentUserAccessor currentUserAccessor,
|
||||||
|
IIdGenerator idGenerator,
|
||||||
|
ILogger<ReviewTenantCommandHandler> logger)
|
||||||
|
: IRequestHandler<ReviewTenantCommand>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Handle(ReviewTenantCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 解析租户 ID
|
||||||
|
if (!long.TryParse(request.TenantId, out var tenantId) || tenantId <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "租户 ID 无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取租户(带跟踪)
|
||||||
|
var tenant = await tenantRepository.GetByIdForUpdateAsync(tenantId, cancellationToken);
|
||||||
|
if (tenant is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 校验租户状态(只有待审核状态才能审核)
|
||||||
|
if (tenant.Status != TenantStatus.PendingReview)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"租户当前状态为 {tenant.Status},无法审核");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 校验是否已领取审核
|
||||||
|
var existingClaim = await tenantRepository.GetActiveReviewClaimAsync(tenantId, cancellationToken);
|
||||||
|
if (existingClaim is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "请先领取审核");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingClaim.ClaimedBy != currentUserAccessor.UserId)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Forbidden, $"该审核已被 {existingClaim.ClaimedByName} 领取,无法操作");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 获取认证资料(带跟踪)
|
||||||
|
var verification = await tenantRepository.GetVerificationForUpdateAsync(tenantId, cancellationToken);
|
||||||
|
if (verification is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "租户认证资料不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 查询当前用户的显示名称
|
||||||
|
var actorName = "system";
|
||||||
|
if (currentUserAccessor.IsAuthenticated && currentUserAccessor.UserId != 0)
|
||||||
|
{
|
||||||
|
var user = await identityUserRepository.FindByIdAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
|
actorName = user?.DisplayName ?? $"用户{currentUserAccessor.UserId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 根据审核结果处理
|
||||||
|
if (request.Approve)
|
||||||
|
{
|
||||||
|
await HandleApprove(tenant, verification, existingClaim, actorName, request, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await HandleReject(tenant, verification, existingClaim, actorName, request, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 保存变更
|
||||||
|
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"租户 {TenantId} 审核{Result},审核人:{ReviewedBy}",
|
||||||
|
tenantId,
|
||||||
|
request.Approve ? "通过" : "驳回",
|
||||||
|
currentUserAccessor.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleApprove(
|
||||||
|
Domain.Tenants.Entities.Tenant tenant,
|
||||||
|
TenantVerificationProfile verification,
|
||||||
|
TenantReviewClaim claim,
|
||||||
|
string actorName,
|
||||||
|
ReviewTenantCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 更新租户状态
|
||||||
|
tenant.Status = TenantStatus.Active;
|
||||||
|
|
||||||
|
// 2. 更新经营模式
|
||||||
|
if (request.OperatingMode.HasValue)
|
||||||
|
{
|
||||||
|
tenant.OperatingMode = request.OperatingMode.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 更新订阅有效期(如果指定了续费时长)
|
||||||
|
if (request.RenewMonths.HasValue && request.RenewMonths.Value > 0)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var effectiveFrom = tenant.EffectiveFrom ?? now;
|
||||||
|
var effectiveTo = effectiveFrom.AddMonths(request.RenewMonths.Value);
|
||||||
|
tenant.EffectiveFrom = effectiveFrom;
|
||||||
|
tenant.EffectiveTo = effectiveTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 更新认证资料状态
|
||||||
|
verification.Status = TenantVerificationStatus.Approved;
|
||||||
|
verification.ReviewedAt = DateTime.UtcNow;
|
||||||
|
verification.ReviewedBy = currentUserAccessor.UserId;
|
||||||
|
verification.ReviewedByName = actorName;
|
||||||
|
|
||||||
|
// 5. 释放领取
|
||||||
|
claim.ReleasedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// 6. 添加审核日志
|
||||||
|
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||||
|
{
|
||||||
|
Id = idGenerator.NextId(),
|
||||||
|
TenantId = tenant.Id,
|
||||||
|
Action = TenantAuditAction.Approved,
|
||||||
|
Title = "审核通过",
|
||||||
|
Description = $"审核人:{actorName},续费时长:{request.RenewMonths ?? 0}个月",
|
||||||
|
OperatorId = currentUserAccessor.UserId,
|
||||||
|
OperatorName = actorName,
|
||||||
|
PreviousStatus = TenantStatus.PendingReview,
|
||||||
|
CurrentStatus = TenantStatus.Active
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleReject(
|
||||||
|
Domain.Tenants.Entities.Tenant tenant,
|
||||||
|
TenantVerificationProfile verification,
|
||||||
|
TenantReviewClaim claim,
|
||||||
|
string actorName,
|
||||||
|
ReviewTenantCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 校验驳回原因
|
||||||
|
var rejectReason = request.Reason?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(rejectReason))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "驳回原因不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 更新认证资料状态(租户状态保持 PendingReview,等待重新提交)
|
||||||
|
verification.Status = TenantVerificationStatus.Rejected;
|
||||||
|
verification.ReviewedAt = DateTime.UtcNow;
|
||||||
|
verification.ReviewedBy = currentUserAccessor.UserId;
|
||||||
|
verification.ReviewedByName = actorName;
|
||||||
|
verification.ReviewRemarks = rejectReason;
|
||||||
|
|
||||||
|
// 3. 释放领取
|
||||||
|
claim.ReleasedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// 4. 添加审核日志
|
||||||
|
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||||
|
{
|
||||||
|
Id = idGenerator.NextId(),
|
||||||
|
TenantId = tenant.Id,
|
||||||
|
Action = TenantAuditAction.Rejected,
|
||||||
|
Title = "审核驳回",
|
||||||
|
Description = $"审核人:{actorName},驳回原因:{rejectReason}",
|
||||||
|
OperatorId = currentUserAccessor.UserId,
|
||||||
|
OperatorName = actorName,
|
||||||
|
CurrentStatus = tenant.Status
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user