feat: 新增租户审核领取和审核日志接口
- 新增 TenantReviewClaim 和 TenantAuditLog 实体 - 新增 TenantAuditAction 枚举 - 新增审核领取相关接口:GET/POST /review/claim, /review/force-claim, /review/release - 新增审核日志接口:GET /audits - 更新 ITenantRepository 和 EfTenantRepository - 更新 TakeoutAppDbContext 添加 DbSet Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -261,4 +261,133 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
|
|||||||
// 3. 返回成功
|
// 3. 返回成功
|
||||||
return ApiResponse<object>.Ok(null, "审核驳回");
|
return ApiResponse<object>.Ok(null, "审核驳回");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取租户审核领取信息。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">租户 ID(雪花算法)。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>审核领取信息,未领取返回 null。</returns>
|
||||||
|
[HttpGet("{id:long}/review/claim")]
|
||||||
|
[PermissionAuthorize("tenant:review")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<TenantReviewClaimDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<TenantReviewClaimDto?>> GetReviewClaim(
|
||||||
|
long id,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 1. 构造查询
|
||||||
|
var query = new GetTenantReviewClaimQuery(id);
|
||||||
|
|
||||||
|
// 2. 执行查询
|
||||||
|
var result = await mediator.Send(query, cancellationToken);
|
||||||
|
|
||||||
|
// 3. 返回结果
|
||||||
|
return ApiResponse<TenantReviewClaimDto?>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取租户审核。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">租户 ID(雪花算法)。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>审核领取信息。</returns>
|
||||||
|
[HttpPost("{id:long}/review/claim")]
|
||||||
|
[PermissionAuthorize("tenant:review")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<TenantReviewClaimDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||||
|
public async Task<ApiResponse<TenantReviewClaimDto>> ClaimReview(
|
||||||
|
long id,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 1. 构造命令
|
||||||
|
var command = new ClaimTenantReviewCommand(id);
|
||||||
|
|
||||||
|
// 2. 执行命令
|
||||||
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
|
||||||
|
// 3. 返回结果
|
||||||
|
return ApiResponse<TenantReviewClaimDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 强制接管租户审核(仅超级管理员)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">租户 ID(雪花算法)。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>审核领取信息。</returns>
|
||||||
|
[HttpPost("{id:long}/review/force-claim")]
|
||||||
|
[PermissionAuthorize("tenant:review:force-claim")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<TenantReviewClaimDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<ApiResponse<TenantReviewClaimDto>> ForceClaimReview(
|
||||||
|
long id,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 1. 构造命令
|
||||||
|
var command = new ForceClaimTenantReviewCommand(id);
|
||||||
|
|
||||||
|
// 2. 执行命令
|
||||||
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
|
||||||
|
// 3. 返回结果
|
||||||
|
return ApiResponse<TenantReviewClaimDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放租户审核领取。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">租户 ID(雪花算法)。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>无内容。</returns>
|
||||||
|
[HttpPost("{id:long}/review/release")]
|
||||||
|
[PermissionAuthorize("tenant:review")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status403Forbidden)]
|
||||||
|
public async Task<ApiResponse<object>> ReleaseReviewClaim(
|
||||||
|
long id,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 1. 构造命令
|
||||||
|
var command = new ReleaseTenantReviewClaimCommand(id);
|
||||||
|
|
||||||
|
// 2. 执行命令
|
||||||
|
await mediator.Send(command, cancellationToken);
|
||||||
|
|
||||||
|
// 3. 返回成功
|
||||||
|
return ApiResponse<object>.Ok(null, "已释放审核");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取租户审核日志列表(分页)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">租户 ID(雪花算法)。</param>
|
||||||
|
/// <param name="page">页码(从 1 开始)。</param>
|
||||||
|
/// <param name="pageSize">每页条数。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>审核日志分页列表。</returns>
|
||||||
|
[HttpGet("{id:long}/audits")]
|
||||||
|
[PermissionAuthorize("tenant:read")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantAuditLogDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<PagedResult<TenantAuditLogDto>>> GetAuditLogs(
|
||||||
|
long id,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 20,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 1. 构造查询
|
||||||
|
var query = new GetTenantAuditLogsQuery
|
||||||
|
{
|
||||||
|
TenantId = id,
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. 执行查询
|
||||||
|
var result = await mediator.Send(query, cancellationToken);
|
||||||
|
|
||||||
|
// 3. 返回审核日志分页列表
|
||||||
|
return ApiResponse<PagedResult<TenantAuditLogDto>>.Ok(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Tenants.Contracts;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取租户审核命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ClaimTenantReviewCommand(long TenantId) : IRequest<TenantReviewClaimDto>;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Tenants.Contracts;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 强制接管租户审核命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ForceClaimTenantReviewCommand(long TenantId) : IRequest<TenantReviewClaimDto>;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放租户审核领取命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ReleaseTenantReviewClaimCommand(long TenantId) : IRequest;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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 TenantAuditLogDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 日志 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作类型。
|
||||||
|
/// </summary>
|
||||||
|
public TenantAuditAction Action { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作标题。
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作人名称。
|
||||||
|
/// </summary>
|
||||||
|
public string? OperatorName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作前状态。
|
||||||
|
/// </summary>
|
||||||
|
public TenantStatus? PreviousStatus { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作后状态。
|
||||||
|
/// </summary>
|
||||||
|
public TenantStatus? CurrentStatus { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Tenants.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户审核领取信息 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TenantReviewClaimDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取人 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long ClaimedBy { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取人名称。
|
||||||
|
/// </summary>
|
||||||
|
public string ClaimedByName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime ClaimedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Tenants.Contracts;
|
||||||
|
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 ClaimTenantReviewCommandHandler(
|
||||||
|
ITenantRepository tenantRepository,
|
||||||
|
ICurrentUserAccessor currentUserAccessor,
|
||||||
|
IIdGenerator idGenerator,
|
||||||
|
ILogger<ClaimTenantReviewCommandHandler> logger)
|
||||||
|
: IRequestHandler<ClaimTenantReviewCommand, TenantReviewClaimDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<TenantReviewClaimDto> Handle(ClaimTenantReviewCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 校验租户是否存在
|
||||||
|
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken);
|
||||||
|
if (tenant is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 校验租户状态(只有待审核状态才能领取)
|
||||||
|
if (tenant.Status != TenantStatus.PendingReview)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "租户不在待审核状态,无法领取审核");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查是否已被领取
|
||||||
|
var existingClaim = await tenantRepository.GetActiveReviewClaimAsync(request.TenantId, cancellationToken);
|
||||||
|
if (existingClaim is not null)
|
||||||
|
{
|
||||||
|
// 4. 如果是自己领取的,直接返回
|
||||||
|
if (existingClaim.ClaimedBy == currentUserAccessor.UserId)
|
||||||
|
{
|
||||||
|
return ToDto(existingClaim);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 已被他人领取
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {existingClaim.ClaimedByName} 领取");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 创建新的领取记录
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var actorName = currentUserAccessor.IsAuthenticated
|
||||||
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
|
: "system";
|
||||||
|
|
||||||
|
var claim = new TenantReviewClaim
|
||||||
|
{
|
||||||
|
Id = idGenerator.NextId(),
|
||||||
|
TenantId = request.TenantId,
|
||||||
|
ClaimedBy = currentUserAccessor.UserId,
|
||||||
|
ClaimedByName = actorName,
|
||||||
|
ClaimedAt = now
|
||||||
|
};
|
||||||
|
|
||||||
|
// 7. 保存领取记录
|
||||||
|
await tenantRepository.AddReviewClaimAsync(claim, cancellationToken);
|
||||||
|
|
||||||
|
// 8. 添加审核日志
|
||||||
|
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||||
|
{
|
||||||
|
Id = idGenerator.NextId(),
|
||||||
|
TenantId = request.TenantId,
|
||||||
|
Action = TenantAuditAction.ReviewClaimed,
|
||||||
|
Title = "领取审核",
|
||||||
|
Description = $"领取人:{actorName}",
|
||||||
|
OperatorId = currentUserAccessor.UserId,
|
||||||
|
OperatorName = actorName,
|
||||||
|
CurrentStatus = tenant.Status
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
// 9. 保存变更
|
||||||
|
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation("租户 {TenantId} 审核已被 {UserId} 领取", request.TenantId, currentUserAccessor.UserId);
|
||||||
|
|
||||||
|
return ToDto(claim);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TenantReviewClaimDto ToDto(TenantReviewClaim claim)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Id = claim.Id,
|
||||||
|
TenantId = claim.TenantId,
|
||||||
|
ClaimedBy = claim.ClaimedBy,
|
||||||
|
ClaimedByName = claim.ClaimedByName,
|
||||||
|
ClaimedAt = claim.ClaimedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Tenants.Contracts;
|
||||||
|
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 ForceClaimTenantReviewCommandHandler(
|
||||||
|
ITenantRepository tenantRepository,
|
||||||
|
ICurrentUserAccessor currentUserAccessor,
|
||||||
|
IIdGenerator idGenerator,
|
||||||
|
ILogger<ForceClaimTenantReviewCommandHandler> logger)
|
||||||
|
: IRequestHandler<ForceClaimTenantReviewCommand, TenantReviewClaimDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<TenantReviewClaimDto> Handle(ForceClaimTenantReviewCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 校验租户是否存在
|
||||||
|
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken);
|
||||||
|
if (tenant is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 校验租户状态(只有待审核状态才能领取)
|
||||||
|
if (tenant.Status != TenantStatus.PendingReview)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "租户不在待审核状态,无法领取审核");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查是否已被领取,如果有则释放
|
||||||
|
var existingClaim = await tenantRepository.GetActiveReviewClaimAsync(request.TenantId, cancellationToken);
|
||||||
|
if (existingClaim is not null)
|
||||||
|
{
|
||||||
|
existingClaim.ReleasedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 创建新的领取记录
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var actorName = currentUserAccessor.IsAuthenticated
|
||||||
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
|
: "system";
|
||||||
|
|
||||||
|
var claim = new TenantReviewClaim
|
||||||
|
{
|
||||||
|
Id = idGenerator.NextId(),
|
||||||
|
TenantId = request.TenantId,
|
||||||
|
ClaimedBy = currentUserAccessor.UserId,
|
||||||
|
ClaimedByName = actorName,
|
||||||
|
ClaimedAt = now
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. 保存领取记录
|
||||||
|
await tenantRepository.AddReviewClaimAsync(claim, cancellationToken);
|
||||||
|
|
||||||
|
// 6. 添加审核日志
|
||||||
|
var previousClaimInfo = existingClaim is not null ? $",原领取人:{existingClaim.ClaimedByName}" : "";
|
||||||
|
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||||
|
{
|
||||||
|
Id = idGenerator.NextId(),
|
||||||
|
TenantId = request.TenantId,
|
||||||
|
Action = TenantAuditAction.ReviewForceClaimed,
|
||||||
|
Title = "强制接管审核",
|
||||||
|
Description = $"接管人:{actorName}{previousClaimInfo}",
|
||||||
|
OperatorId = currentUserAccessor.UserId,
|
||||||
|
OperatorName = actorName,
|
||||||
|
CurrentStatus = tenant.Status
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
// 7. 保存变更
|
||||||
|
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation("租户 {TenantId} 审核已被 {UserId} 强制接管", request.TenantId, currentUserAccessor.UserId);
|
||||||
|
|
||||||
|
return new TenantReviewClaimDto
|
||||||
|
{
|
||||||
|
Id = claim.Id,
|
||||||
|
TenantId = claim.TenantId,
|
||||||
|
ClaimedBy = claim.ClaimedBy,
|
||||||
|
ClaimedByName = claim.ClaimedByName,
|
||||||
|
ClaimedAt = claim.ClaimedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Tenants.Contracts;
|
||||||
|
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取租户审核日志查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetTenantAuditLogsQueryHandler(ITenantRepository tenantRepository)
|
||||||
|
: IRequestHandler<GetTenantAuditLogsQuery, PagedResult<TenantAuditLogDto>>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PagedResult<TenantAuditLogDto>> Handle(GetTenantAuditLogsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 查询审核日志
|
||||||
|
var (items, totalCount) = await tenantRepository.GetAuditLogsAsync(
|
||||||
|
request.TenantId,
|
||||||
|
request.Page,
|
||||||
|
request.PageSize,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
// 2. 转换为 DTO
|
||||||
|
var dtos = items.Select(log => new TenantAuditLogDto
|
||||||
|
{
|
||||||
|
Id = log.Id,
|
||||||
|
TenantId = log.TenantId,
|
||||||
|
Action = log.Action,
|
||||||
|
Title = log.Title,
|
||||||
|
Description = log.Description,
|
||||||
|
OperatorName = log.OperatorName,
|
||||||
|
PreviousStatus = log.PreviousStatus,
|
||||||
|
CurrentStatus = log.CurrentStatus,
|
||||||
|
CreatedAt = log.CreatedAt
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
// 3. 返回分页结果
|
||||||
|
return new PagedResult<TenantAuditLogDto>(dtos, totalCount, request.Page, request.PageSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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 GetTenantReviewClaimQueryHandler(ITenantRepository tenantRepository)
|
||||||
|
: IRequestHandler<GetTenantReviewClaimQuery, TenantReviewClaimDto?>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<TenantReviewClaimDto?> Handle(GetTenantReviewClaimQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 查询当前有效的审核领取记录
|
||||||
|
var claim = await tenantRepository.GetActiveReviewClaimAsync(request.TenantId, cancellationToken);
|
||||||
|
|
||||||
|
// 2. 未找到返回 null
|
||||||
|
if (claim is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 转换为 DTO
|
||||||
|
return new TenantReviewClaimDto
|
||||||
|
{
|
||||||
|
Id = claim.Id,
|
||||||
|
TenantId = claim.TenantId,
|
||||||
|
ClaimedBy = claim.ClaimedBy,
|
||||||
|
ClaimedByName = claim.ClaimedByName,
|
||||||
|
ClaimedAt = claim.ClaimedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||||
|
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 ReleaseTenantReviewClaimCommandHandler(
|
||||||
|
ITenantRepository tenantRepository,
|
||||||
|
ICurrentUserAccessor currentUserAccessor,
|
||||||
|
IIdGenerator idGenerator,
|
||||||
|
ILogger<ReleaseTenantReviewClaimCommandHandler> logger)
|
||||||
|
: IRequestHandler<ReleaseTenantReviewClaimCommand>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Handle(ReleaseTenantReviewClaimCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 校验租户是否存在
|
||||||
|
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken);
|
||||||
|
if (tenant is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否有领取记录
|
||||||
|
var existingClaim = await tenantRepository.GetActiveReviewClaimAsync(request.TenantId, cancellationToken);
|
||||||
|
if (existingClaim is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "该租户审核未被领取");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 校验是否是自己领取的
|
||||||
|
if (existingClaim.ClaimedBy != currentUserAccessor.UserId)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Forbidden, "只能释放自己领取的审核");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 释放领取
|
||||||
|
existingClaim.ReleasedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// 5. 添加审核日志
|
||||||
|
var actorName = currentUserAccessor.IsAuthenticated
|
||||||
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
|
: "system";
|
||||||
|
|
||||||
|
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||||
|
{
|
||||||
|
Id = idGenerator.NextId(),
|
||||||
|
TenantId = request.TenantId,
|
||||||
|
Action = TenantAuditAction.ReviewReleased,
|
||||||
|
Title = "释放审核",
|
||||||
|
Description = $"释放人:{actorName}",
|
||||||
|
OperatorId = currentUserAccessor.UserId,
|
||||||
|
OperatorName = actorName,
|
||||||
|
CurrentStatus = tenant.Status
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
// 6. 保存变更
|
||||||
|
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation("租户 {TenantId} 审核已被 {UserId} 释放", request.TenantId, currentUserAccessor.UserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Tenants.Contracts;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Tenants.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取租户审核日志查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record GetTenantAuditLogsQuery : IRequest<PagedResult<TenantAuditLogDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码(从 1 开始)。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; init; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; init; } = 20;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Tenants.Contracts;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Tenants.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取租户审核领取信息查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record GetTenantReviewClaimQuery(long TenantId) : IRequest<TenantReviewClaimDto?>;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Tenants.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户审核日志。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TenantAuditLog : AuditableEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long TenantId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作类型。
|
||||||
|
/// </summary>
|
||||||
|
public TenantAuditAction Action { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作标题。
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作人 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long? OperatorId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作人名称。
|
||||||
|
/// </summary>
|
||||||
|
public string? OperatorName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作前状态。
|
||||||
|
/// </summary>
|
||||||
|
public TenantStatus? PreviousStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作后状态。
|
||||||
|
/// </summary>
|
||||||
|
public TenantStatus? CurrentStatus { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Tenants.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户审核领取记录。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TenantReviewClaim : AuditableEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long TenantId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取人 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long ClaimedBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取人名称。
|
||||||
|
/// </summary>
|
||||||
|
public string ClaimedByName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime ClaimedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放时间(为空表示未释放)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ReleasedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Tenants.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户审核操作类型。
|
||||||
|
/// </summary>
|
||||||
|
public enum TenantAuditAction
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建租户。
|
||||||
|
/// </summary>
|
||||||
|
Created = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取审核。
|
||||||
|
/// </summary>
|
||||||
|
ReviewClaimed = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放审核。
|
||||||
|
/// </summary>
|
||||||
|
ReviewReleased = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 强制接管审核。
|
||||||
|
/// </summary>
|
||||||
|
ReviewForceClaimed = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过。
|
||||||
|
/// </summary>
|
||||||
|
Approved = 4,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核驳回。
|
||||||
|
/// </summary>
|
||||||
|
Rejected = 5,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结租户。
|
||||||
|
/// </summary>
|
||||||
|
Frozen = 6,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解冻租户。
|
||||||
|
/// </summary>
|
||||||
|
Unfrozen = 7,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新信息。
|
||||||
|
/// </summary>
|
||||||
|
Updated = 8
|
||||||
|
}
|
||||||
@@ -137,6 +137,44 @@ public interface ITenantRepository
|
|||||||
/// <param name="cancellationToken">取消标记。</param>
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
/// <returns>异步操作任务。</returns>
|
/// <returns>异步操作任务。</returns>
|
||||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取租户当前有效的审核领取记录。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tenantId">租户 ID。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>审核领取记录,未找到返回 null。</returns>
|
||||||
|
Task<TenantReviewClaim?> GetActiveReviewClaimAsync(long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增审核领取记录。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="claim">审核领取记录。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>异步操作任务。</returns>
|
||||||
|
Task AddReviewClaimAsync(TenantReviewClaim claim, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增审核日志。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="auditLog">审核日志。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>异步操作任务。</returns>
|
||||||
|
Task AddAuditLogAsync(TenantAuditLog auditLog, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取租户审核日志列表(分页)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tenantId">租户 ID。</param>
|
||||||
|
/// <param name="page">页码(从 1 开始)。</param>
|
||||||
|
/// <param name="pageSize">每页条数。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>审核日志列表和总数。</returns>
|
||||||
|
Task<(IReadOnlyList<TenantAuditLog> Items, int TotalCount)> GetAuditLogsAsync(
|
||||||
|
long tenantId,
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -69,6 +69,14 @@ public class TakeoutAppDbContext(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<TenantPayment> TenantPayments => Set<TenantPayment>();
|
public DbSet<TenantPayment> TenantPayments => Set<TenantPayment>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// 租户审核领取记录。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<TenantReviewClaim> TenantReviewClaims => Set<TenantReviewClaim>();
|
||||||
|
/// <summary>
|
||||||
|
/// 租户审核日志。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<TenantAuditLog> TenantAuditLogs => Set<TenantAuditLog>();
|
||||||
|
/// <summary>
|
||||||
/// 商户实体。
|
/// 商户实体。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<Merchant> Merchants => Set<Merchant>();
|
public DbSet<Merchant> Merchants => Set<Merchant>();
|
||||||
|
|||||||
@@ -236,4 +236,54 @@ public sealed class EfTenantRepository(TakeoutAdminDbContext context) : ITenantR
|
|||||||
// 1. 保存变更
|
// 1. 保存变更
|
||||||
return context.SaveChangesAsync(cancellationToken);
|
return context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<TenantReviewClaim?> GetActiveReviewClaimAsync(long tenantId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 1. 查询当前有效的审核领取记录(未释放的)
|
||||||
|
return context.TenantReviewClaims
|
||||||
|
.Where(c => c.TenantId == tenantId && c.ReleasedAt == null && c.DeletedAt == null)
|
||||||
|
.OrderByDescending(c => c.ClaimedAt)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task AddReviewClaimAsync(TenantReviewClaim claim, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 1. 新增审核领取记录
|
||||||
|
await context.TenantReviewClaims.AddAsync(claim, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task AddAuditLogAsync(TenantAuditLog auditLog, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 1. 新增审核日志
|
||||||
|
await context.TenantAuditLogs.AddAsync(auditLog, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<(IReadOnlyList<TenantAuditLog> Items, int TotalCount)> GetAuditLogsAsync(
|
||||||
|
long tenantId,
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 1. 构建基础查询
|
||||||
|
var query = context.TenantAuditLogs
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(a => a.TenantId == tenantId && a.DeletedAt == null);
|
||||||
|
|
||||||
|
// 2. 获取总数
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 3. 分页查询(按创建时间倒序)
|
||||||
|
var items = await query
|
||||||
|
.OrderByDescending(a => a.CreatedAt)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 4. 返回结果
|
||||||
|
return (items, totalCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user