diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs index be3a541..9919579 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs @@ -261,4 +261,133 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController // 3. 返回成功 return ApiResponse.Ok(null, "审核驳回"); } + + /// + /// 获取租户审核领取信息。 + /// + /// 租户 ID(雪花算法)。 + /// 取消标记。 + /// 审核领取信息,未领取返回 null。 + [HttpGet("{id:long}/review/claim")] + [PermissionAuthorize("tenant:review")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> GetReviewClaim( + long id, + CancellationToken cancellationToken = default) + { + // 1. 构造查询 + var query = new GetTenantReviewClaimQuery(id); + + // 2. 执行查询 + var result = await mediator.Send(query, cancellationToken); + + // 3. 返回结果 + return ApiResponse.Ok(result); + } + + /// + /// 领取租户审核。 + /// + /// 租户 ID(雪花算法)。 + /// 取消标记。 + /// 审核领取信息。 + [HttpPost("{id:long}/review/claim")] + [PermissionAuthorize("tenant:review")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] + public async Task> ClaimReview( + long id, + CancellationToken cancellationToken = default) + { + // 1. 构造命令 + var command = new ClaimTenantReviewCommand(id); + + // 2. 执行命令 + var result = await mediator.Send(command, cancellationToken); + + // 3. 返回结果 + return ApiResponse.Ok(result); + } + + /// + /// 强制接管租户审核(仅超级管理员)。 + /// + /// 租户 ID(雪花算法)。 + /// 取消标记。 + /// 审核领取信息。 + [HttpPost("{id:long}/review/force-claim")] + [PermissionAuthorize("tenant:review:force-claim")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] + public async Task> ForceClaimReview( + long id, + CancellationToken cancellationToken = default) + { + // 1. 构造命令 + var command = new ForceClaimTenantReviewCommand(id); + + // 2. 执行命令 + var result = await mediator.Send(command, cancellationToken); + + // 3. 返回结果 + return ApiResponse.Ok(result); + } + + /// + /// 释放租户审核领取。 + /// + /// 租户 ID(雪花算法)。 + /// 取消标记。 + /// 无内容。 + [HttpPost("{id:long}/review/release")] + [PermissionAuthorize("tenant:review")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] + public async Task> ReleaseReviewClaim( + long id, + CancellationToken cancellationToken = default) + { + // 1. 构造命令 + var command = new ReleaseTenantReviewClaimCommand(id); + + // 2. 执行命令 + await mediator.Send(command, cancellationToken); + + // 3. 返回成功 + return ApiResponse.Ok(null, "已释放审核"); + } + + /// + /// 获取租户审核日志列表(分页)。 + /// + /// 租户 ID(雪花算法)。 + /// 页码(从 1 开始)。 + /// 每页条数。 + /// 取消标记。 + /// 审核日志分页列表。 + [HttpGet("{id:long}/audits")] + [PermissionAuthorize("tenant:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> 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>.Ok(result); + } } diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ClaimTenantReviewCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ClaimTenantReviewCommand.cs new file mode 100644 index 0000000..5da46b6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ClaimTenantReviewCommand.cs @@ -0,0 +1,9 @@ +using MediatR; +using TakeoutSaaS.Application.App.Tenants.Contracts; + +namespace TakeoutSaaS.Application.App.Tenants.Commands; + +/// +/// 领取租户审核命令。 +/// +public sealed record ClaimTenantReviewCommand(long TenantId) : IRequest; diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ForceClaimTenantReviewCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ForceClaimTenantReviewCommand.cs new file mode 100644 index 0000000..04c4077 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ForceClaimTenantReviewCommand.cs @@ -0,0 +1,9 @@ +using MediatR; +using TakeoutSaaS.Application.App.Tenants.Contracts; + +namespace TakeoutSaaS.Application.App.Tenants.Commands; + +/// +/// 强制接管租户审核命令。 +/// +public sealed record ForceClaimTenantReviewCommand(long TenantId) : IRequest; diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReleaseTenantReviewClaimCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReleaseTenantReviewClaimCommand.cs new file mode 100644 index 0000000..5aa27d2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReleaseTenantReviewClaimCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Tenants.Commands; + +/// +/// 释放租户审核领取命令。 +/// +public sealed record ReleaseTenantReviewClaimCommand(long TenantId) : IRequest; diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Contracts/TenantAuditLogDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Contracts/TenantAuditLogDto.cs new file mode 100644 index 0000000..65bacaa --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Contracts/TenantAuditLogDto.cs @@ -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; + +/// +/// 租户审核日志 DTO。 +/// +public sealed class TenantAuditLogDto +{ + /// + /// 日志 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 操作类型。 + /// + public TenantAuditAction Action { get; init; } + + /// + /// 操作标题。 + /// + public string Title { get; init; } = string.Empty; + + /// + /// 操作描述。 + /// + public string? Description { get; init; } + + /// + /// 操作人名称。 + /// + public string? OperatorName { get; init; } + + /// + /// 操作前状态。 + /// + public TenantStatus? PreviousStatus { get; init; } + + /// + /// 操作后状态。 + /// + public TenantStatus? CurrentStatus { get; init; } + + /// + /// 创建时间。 + /// + public DateTime CreatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Contracts/TenantReviewClaimDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Contracts/TenantReviewClaimDto.cs new file mode 100644 index 0000000..45b8f66 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Contracts/TenantReviewClaimDto.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.App.Tenants.Contracts; + +/// +/// 租户审核领取信息 DTO。 +/// +public sealed class TenantReviewClaimDto +{ + /// + /// 记录 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 领取人 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long ClaimedBy { get; init; } + + /// + /// 领取人名称。 + /// + public string ClaimedByName { get; init; } = string.Empty; + + /// + /// 领取时间。 + /// + public DateTime ClaimedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ClaimTenantReviewCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ClaimTenantReviewCommandHandler.cs new file mode 100644 index 0000000..0f10788 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ClaimTenantReviewCommandHandler.cs @@ -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; + +/// +/// 领取租户审核命令处理器。 +/// +public sealed class ClaimTenantReviewCommandHandler( + ITenantRepository tenantRepository, + ICurrentUserAccessor currentUserAccessor, + IIdGenerator idGenerator, + ILogger logger) + : IRequestHandler +{ + /// + public async Task 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 + }; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ForceClaimTenantReviewCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ForceClaimTenantReviewCommandHandler.cs new file mode 100644 index 0000000..8800ffa --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ForceClaimTenantReviewCommandHandler.cs @@ -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; + +/// +/// 强制接管租户审核命令处理器。 +/// +public sealed class ForceClaimTenantReviewCommandHandler( + ITenantRepository tenantRepository, + ICurrentUserAccessor currentUserAccessor, + IIdGenerator idGenerator, + ILogger logger) + : IRequestHandler +{ + /// + public async Task 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 + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantAuditLogsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantAuditLogsQueryHandler.cs new file mode 100644 index 0000000..ac0f629 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantAuditLogsQueryHandler.cs @@ -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; + +/// +/// 获取租户审核日志查询处理器。 +/// +public sealed class GetTenantAuditLogsQueryHandler(ITenantRepository tenantRepository) + : IRequestHandler> +{ + /// + public async Task> 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(dtos, totalCount, request.Page, request.PageSize); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantReviewClaimQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantReviewClaimQueryHandler.cs new file mode 100644 index 0000000..4e754fd --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantReviewClaimQueryHandler.cs @@ -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; + +/// +/// 获取租户审核领取信息查询处理器。 +/// +public sealed class GetTenantReviewClaimQueryHandler(ITenantRepository tenantRepository) + : IRequestHandler +{ + /// + public async Task 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 + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReleaseTenantReviewClaimCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReleaseTenantReviewClaimCommandHandler.cs new file mode 100644 index 0000000..d14b5b2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReleaseTenantReviewClaimCommandHandler.cs @@ -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; + +/// +/// 释放租户审核领取命令处理器。 +/// +public sealed class ReleaseTenantReviewClaimCommandHandler( + ITenantRepository tenantRepository, + ICurrentUserAccessor currentUserAccessor, + IIdGenerator idGenerator, + ILogger logger) + : IRequestHandler +{ + /// + 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); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantAuditLogsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantAuditLogsQuery.cs new file mode 100644 index 0000000..51bfd17 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantAuditLogsQuery.cs @@ -0,0 +1,26 @@ +using MediatR; +using TakeoutSaaS.Application.App.Tenants.Contracts; +using TakeoutSaaS.Shared.Abstractions.Results; + +namespace TakeoutSaaS.Application.App.Tenants.Queries; + +/// +/// 获取租户审核日志查询。 +/// +public sealed record GetTenantAuditLogsQuery : IRequest> +{ + /// + /// 租户 ID。 + /// + public long TenantId { get; init; } + + /// + /// 页码(从 1 开始)。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 20; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantReviewClaimQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantReviewClaimQuery.cs new file mode 100644 index 0000000..20e4a26 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantReviewClaimQuery.cs @@ -0,0 +1,9 @@ +using MediatR; +using TakeoutSaaS.Application.App.Tenants.Contracts; + +namespace TakeoutSaaS.Application.App.Tenants.Queries; + +/// +/// 获取租户审核领取信息查询。 +/// +public sealed record GetTenantReviewClaimQuery(long TenantId) : IRequest; diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantAuditLog.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantAuditLog.cs new file mode 100644 index 0000000..d20d108 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantAuditLog.cs @@ -0,0 +1,50 @@ +using TakeoutSaaS.Domain.Tenants.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Tenants.Entities; + +/// +/// 租户审核日志。 +/// +public sealed class TenantAuditLog : AuditableEntityBase +{ + /// + /// 租户 ID。 + /// + public long TenantId { get; set; } + + /// + /// 操作类型。 + /// + public TenantAuditAction Action { get; set; } + + /// + /// 操作标题。 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 操作描述。 + /// + public string? Description { get; set; } + + /// + /// 操作人 ID。 + /// + public long? OperatorId { get; set; } + + /// + /// 操作人名称。 + /// + public string? OperatorName { get; set; } + + /// + /// 操作前状态。 + /// + public TenantStatus? PreviousStatus { get; set; } + + /// + /// 操作后状态。 + /// + public TenantStatus? CurrentStatus { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantReviewClaim.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantReviewClaim.cs new file mode 100644 index 0000000..46886d8 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantReviewClaim.cs @@ -0,0 +1,34 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Tenants.Entities; + +/// +/// 租户审核领取记录。 +/// +public sealed class TenantReviewClaim : AuditableEntityBase +{ + /// + /// 租户 ID。 + /// + public long TenantId { get; set; } + + /// + /// 领取人 ID。 + /// + public long ClaimedBy { get; set; } + + /// + /// 领取人名称。 + /// + public string ClaimedByName { get; set; } = string.Empty; + + /// + /// 领取时间。 + /// + public DateTime ClaimedAt { get; set; } + + /// + /// 释放时间(为空表示未释放)。 + /// + public DateTime? ReleasedAt { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantAuditAction.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantAuditAction.cs new file mode 100644 index 0000000..aa20c99 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Enums/TenantAuditAction.cs @@ -0,0 +1,52 @@ +namespace TakeoutSaaS.Domain.Tenants.Enums; + +/// +/// 租户审核操作类型。 +/// +public enum TenantAuditAction +{ + /// + /// 创建租户。 + /// + Created = 0, + + /// + /// 领取审核。 + /// + ReviewClaimed = 1, + + /// + /// 释放审核。 + /// + ReviewReleased = 2, + + /// + /// 强制接管审核。 + /// + ReviewForceClaimed = 3, + + /// + /// 审核通过。 + /// + Approved = 4, + + /// + /// 审核驳回。 + /// + Rejected = 5, + + /// + /// 冻结租户。 + /// + Frozen = 6, + + /// + /// 解冻租户。 + /// + Unfrozen = 7, + + /// + /// 更新信息。 + /// + Updated = 8 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs index 72c84b1..217555a 100644 --- a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantRepository.cs @@ -137,6 +137,44 @@ public interface ITenantRepository /// 取消标记。 /// 异步操作任务。 Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// 获取租户当前有效的审核领取记录。 + /// + /// 租户 ID。 + /// 取消标记。 + /// 审核领取记录,未找到返回 null。 + Task GetActiveReviewClaimAsync(long tenantId, CancellationToken cancellationToken = default); + + /// + /// 新增审核领取记录。 + /// + /// 审核领取记录。 + /// 取消标记。 + /// 异步操作任务。 + Task AddReviewClaimAsync(TenantReviewClaim claim, CancellationToken cancellationToken = default); + + /// + /// 新增审核日志。 + /// + /// 审核日志。 + /// 取消标记。 + /// 异步操作任务。 + Task AddAuditLogAsync(TenantAuditLog auditLog, CancellationToken cancellationToken = default); + + /// + /// 获取租户审核日志列表(分页)。 + /// + /// 租户 ID。 + /// 页码(从 1 开始)。 + /// 每页条数。 + /// 取消标记。 + /// 审核日志列表和总数。 + Task<(IReadOnlyList Items, int TotalCount)> GetAuditLogsAsync( + long tenantId, + int page, + int pageSize, + CancellationToken cancellationToken = default); } /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index b24e59f..96e904c 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -69,6 +69,14 @@ public class TakeoutAppDbContext( /// public DbSet TenantPayments => Set(); /// + /// 租户审核领取记录。 + /// + public DbSet TenantReviewClaims => Set(); + /// + /// 租户审核日志。 + /// + public DbSet TenantAuditLogs => Set(); + /// /// 商户实体。 /// public DbSet Merchants => Set(); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs index ddc0d86..6f33571 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantRepository.cs @@ -236,4 +236,54 @@ public sealed class EfTenantRepository(TakeoutAdminDbContext context) : ITenantR // 1. 保存变更 return context.SaveChangesAsync(cancellationToken); } + + /// + public Task 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); + } + + /// + public async Task AddReviewClaimAsync(TenantReviewClaim claim, CancellationToken cancellationToken = default) + { + // 1. 新增审核领取记录 + await context.TenantReviewClaims.AddAsync(claim, cancellationToken); + } + + /// + public async Task AddAuditLogAsync(TenantAuditLog auditLog, CancellationToken cancellationToken = default) + { + // 1. 新增审核日志 + await context.TenantAuditLogs.AddAsync(auditLog, cancellationToken); + } + + /// + public async Task<(IReadOnlyList 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); + } }