diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ClaimMerchantReviewCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ClaimMerchantReviewCommand.cs
deleted file mode 100644
index 3002b49..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ClaimMerchantReviewCommand.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Merchants.Dto;
-
-namespace TakeoutSaaS.Application.App.Merchants.Commands;
-
-///
-/// 领取商户审核命令。
-///
-public sealed class ClaimMerchantReviewCommand : IRequest
-{
- ///
- /// 商户 ID。
- ///
- public long MerchantId { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReleaseClaimCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReleaseClaimCommand.cs
deleted file mode 100644
index 5a06141..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReleaseClaimCommand.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Merchants.Dto;
-
-namespace TakeoutSaaS.Application.App.Merchants.Commands;
-
-///
-/// 释放商户审核领取命令。
-///
-public sealed class ReleaseClaimCommand : IRequest
-{
- ///
- /// 商户 ID。
- ///
- public long MerchantId { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantCommand.cs
deleted file mode 100644
index 00236ba..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantCommand.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Merchants.Dto;
-
-namespace TakeoutSaaS.Application.App.Merchants.Commands;
-
-///
-/// 审核商户入驻。
-///
-public sealed record ReviewMerchantCommand(
- [param: Required] long MerchantId,
- bool Approve,
- string? Remarks) : IRequest;
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantDocumentCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantDocumentCommand.cs
deleted file mode 100644
index 0e01c26..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/ReviewMerchantDocumentCommand.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Merchants.Dto;
-
-namespace TakeoutSaaS.Application.App.Merchants.Commands;
-
-///
-/// 审核商户证照。
-///
-public sealed record ReviewMerchantDocumentCommand(
- [property: Required] long MerchantId,
- [property: Required] long DocumentId,
- bool Approve,
- string? Remarks) : IRequest;
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/RevokeMerchantReviewCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/RevokeMerchantReviewCommand.cs
deleted file mode 100644
index 30580f4..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/RevokeMerchantReviewCommand.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using MediatR;
-
-namespace TakeoutSaaS.Application.App.Merchants.Commands;
-
-///
-/// 撤销商户审核命令。
-///
-public sealed class RevokeMerchantReviewCommand : IRequest
-{
- ///
- /// 商户 ID。
- ///
- public long MerchantId { get; init; }
-
- ///
- /// 撤销原因。
- ///
- public string Reason { get; init; } = string.Empty;
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/ClaimInfoDto.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/ClaimInfoDto.cs
deleted file mode 100644
index 999aead..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/ClaimInfoDto.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System.Text.Json.Serialization;
-using TakeoutSaaS.Shared.Abstractions.Serialization;
-
-namespace TakeoutSaaS.Application.App.Merchants.Dto;
-
-///
-/// 审核领取信息 DTO。
-///
-public sealed class ClaimInfoDto
-{
- ///
- /// 商户 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long MerchantId { get; init; }
-
- ///
- /// 领取人 ID。
- ///
- [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
- public long? ClaimedBy { get; init; }
-
- ///
- /// 领取人名称。
- ///
- public string? ClaimedByName { get; init; }
-
- ///
- /// 领取时间。
- ///
- public DateTime? ClaimedAt { get; init; }
-
- ///
- /// 领取过期时间。
- ///
- public DateTime? ClaimExpiresAt { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantReviewListItemDto.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantReviewListItemDto.cs
deleted file mode 100644
index 2775ff8..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/MerchantReviewListItemDto.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using System.Text.Json.Serialization;
-using TakeoutSaaS.Domain.Common.Enums;
-using TakeoutSaaS.Domain.Merchants.Enums;
-using TakeoutSaaS.Shared.Abstractions.Serialization;
-
-namespace TakeoutSaaS.Application.App.Merchants.Dto;
-
-///
-/// 待审核商户列表项 DTO。
-///
-public sealed class MerchantReviewListItemDto
-{
- ///
- /// 商户 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long Id { get; init; }
-
- ///
- /// 租户 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long TenantId { get; init; }
-
- ///
- /// 租户名称。
- ///
- public string? TenantName { get; init; }
-
- ///
- /// 商户名称。
- ///
- public string Name { get; init; } = string.Empty;
-
- ///
- /// 经营模式。
- ///
- public OperatingMode? OperatingMode { get; init; }
-
- ///
- /// 营业执照号。
- ///
- public string? LicenseNumber { get; init; }
-
- ///
- /// 审核状态。
- ///
- public MerchantStatus Status { get; init; }
-
- ///
- /// 领取人名称。
- ///
- public string? ClaimedByName { get; init; }
-
- ///
- /// 领取时间。
- ///
- public DateTime? ClaimedAt { get; init; }
-
- ///
- /// 领取过期时间。
- ///
- public DateTime? ClaimExpiresAt { get; init; }
-
- ///
- /// 创建时间。
- ///
- public DateTime CreatedAt { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ClaimMerchantReviewHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ClaimMerchantReviewHandler.cs
deleted file mode 100644
index fdd10a0..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ClaimMerchantReviewHandler.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Merchants.Commands;
-using TakeoutSaaS.Application.App.Merchants.Dto;
-using TakeoutSaaS.Domain.Merchants.Entities;
-using TakeoutSaaS.Domain.Merchants.Enums;
-using TakeoutSaaS.Domain.Merchants.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Exceptions;
-using TakeoutSaaS.Shared.Abstractions.Security;
-
-namespace TakeoutSaaS.Application.App.Merchants.Handlers;
-
-///
-/// 领取商户审核处理器。
-///
-public sealed class ClaimMerchantReviewHandler(
- IMerchantRepository merchantRepository,
- ICurrentUserAccessor currentUserAccessor)
- : IRequestHandler
-{
- ///
- public async Task Handle(ClaimMerchantReviewCommand request, CancellationToken cancellationToken)
- {
- var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
-
- if (merchant.Status != MerchantStatus.Pending)
- {
- throw new BusinessException(ErrorCodes.Conflict, "商户不在待审核状态");
- }
-
- var now = DateTime.UtcNow;
- if (merchant.ClaimedBy.HasValue && merchant.ClaimExpiresAt.HasValue && merchant.ClaimExpiresAt > now)
- {
- if (merchant.ClaimedBy != currentUserAccessor.UserId)
- {
- throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {merchant.ClaimedByName} 领取");
- }
-
- return ToDto(merchant);
- }
-
- var actorName = currentUserAccessor.IsAuthenticated
- ? $"user:{currentUserAccessor.UserId}"
- : "system";
-
- merchant.ClaimedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId;
- merchant.ClaimedByName = actorName;
- merchant.ClaimedAt = now;
- merchant.ClaimExpiresAt = now.AddMinutes(30);
-
- await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
- await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
- {
- TenantId = merchant.TenantId,
- MerchantId = merchant.Id,
- Action = MerchantAuditAction.ReviewClaimed,
- Title = "领取审核",
- Description = $"领取人:{actorName}",
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName
- }, cancellationToken);
- await merchantRepository.SaveChangesAsync(cancellationToken);
-
- return ToDto(merchant);
- }
-
- private static ClaimInfoDto ToDto(Domain.Merchants.Entities.Merchant merchant)
- => new()
- {
- MerchantId = merchant.Id,
- ClaimedBy = merchant.ClaimedBy,
- ClaimedByName = merchant.ClaimedByName,
- ClaimedAt = merchant.ClaimedAt,
- ClaimExpiresAt = merchant.ClaimExpiresAt
- };
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantReviewClaimQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantReviewClaimQueryHandler.cs
deleted file mode 100644
index 5578635..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantReviewClaimQueryHandler.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Merchants.Dto;
-using TakeoutSaaS.Application.App.Merchants.Queries;
-using TakeoutSaaS.Domain.Merchants.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Exceptions;
-
-namespace TakeoutSaaS.Application.App.Merchants.Handlers;
-
-///
-/// 商户审核领取信息查询处理器。
-///
-public sealed class GetMerchantReviewClaimQueryHandler(IMerchantRepository merchantRepository)
- : IRequestHandler
-{
- ///
- public async Task Handle(GetMerchantReviewClaimQuery request, CancellationToken cancellationToken)
- {
- var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
-
- if (!merchant.ClaimedBy.HasValue)
- {
- return null;
- }
-
- if (merchant.ClaimExpiresAt.HasValue && merchant.ClaimExpiresAt <= DateTime.UtcNow)
- {
- return null;
- }
-
- return new ClaimInfoDto
- {
- MerchantId = merchant.Id,
- ClaimedBy = merchant.ClaimedBy,
- ClaimedByName = merchant.ClaimedByName,
- ClaimedAt = merchant.ClaimedAt,
- ClaimExpiresAt = merchant.ClaimExpiresAt
- };
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetPendingReviewListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetPendingReviewListQueryHandler.cs
deleted file mode 100644
index d570d71..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetPendingReviewListQueryHandler.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Merchants.Dto;
-using TakeoutSaaS.Application.App.Merchants.Queries;
-using TakeoutSaaS.Domain.Merchants.Enums;
-using TakeoutSaaS.Domain.Merchants.Repositories;
-using TakeoutSaaS.Domain.Tenants.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Results;
-
-namespace TakeoutSaaS.Application.App.Merchants.Handlers;
-
-///
-/// 待审核商户列表处理器。
-///
-public sealed class GetPendingReviewListQueryHandler(
- IMerchantRepository merchantRepository,
- ITenantRepository tenantRepository)
- : IRequestHandler>
-{
- ///
- public async Task> Handle(
- GetPendingReviewListQuery request,
- CancellationToken cancellationToken)
- {
- var merchants = await merchantRepository.SearchAsync(
- request.TenantId,
- MerchantStatus.Pending,
- request.OperatingMode,
- request.Keyword,
- cancellationToken);
-
- var total = merchants.Count;
- var paged = merchants
- .OrderByDescending(x => x.CreatedAt)
- .Skip((request.Page - 1) * request.PageSize)
- .Take(request.PageSize)
- .ToList();
-
- var tenantIds = paged.Select(x => x.TenantId).Distinct().ToArray();
- var tenants = await tenantRepository.FindByIdsAsync(tenantIds, cancellationToken);
- var tenantLookup = tenants.ToDictionary(x => x.Id, x => x.Name);
-
- var items = paged.Select(merchant => new MerchantReviewListItemDto
- {
- Id = merchant.Id,
- TenantId = merchant.TenantId,
- TenantName = tenantLookup.TryGetValue(merchant.TenantId, out var name) ? name : null,
- Name = merchant.BrandName,
- OperatingMode = merchant.OperatingMode,
- LicenseNumber = merchant.BusinessLicenseNumber,
- Status = merchant.Status,
- ClaimedByName = merchant.ClaimedByName,
- ClaimedAt = merchant.ClaimedAt,
- ClaimExpiresAt = merchant.ClaimExpiresAt,
- CreatedAt = merchant.CreatedAt
- }).ToList();
-
- return new PagedResult(items, request.Page, request.PageSize, total);
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReleaseClaimHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReleaseClaimHandler.cs
deleted file mode 100644
index a9df182..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReleaseClaimHandler.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Merchants.Commands;
-using TakeoutSaaS.Application.App.Merchants.Dto;
-using TakeoutSaaS.Domain.Merchants.Entities;
-using TakeoutSaaS.Domain.Merchants.Enums;
-using TakeoutSaaS.Domain.Merchants.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Exceptions;
-using TakeoutSaaS.Shared.Abstractions.Security;
-
-namespace TakeoutSaaS.Application.App.Merchants.Handlers;
-
-///
-/// 释放审核领取处理器。
-///
-public sealed class ReleaseClaimHandler(
- IMerchantRepository merchantRepository,
- ICurrentUserAccessor currentUserAccessor)
- : IRequestHandler
-{
- ///
- public async Task Handle(ReleaseClaimCommand request, CancellationToken cancellationToken)
- {
- var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
-
- if (!merchant.ClaimedBy.HasValue)
- {
- return null;
- }
-
- var now = DateTime.UtcNow;
- var claimExpired = merchant.ClaimExpiresAt.HasValue && merchant.ClaimExpiresAt <= now;
-
- if (!claimExpired && merchant.ClaimedBy != currentUserAccessor.UserId)
- {
- throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {merchant.ClaimedByName} 领取");
- }
-
- var actorName = currentUserAccessor.IsAuthenticated
- ? $"user:{currentUserAccessor.UserId}"
- : "system";
-
- merchant.ClaimedBy = null;
- merchant.ClaimedByName = null;
- merchant.ClaimedAt = null;
- merchant.ClaimExpiresAt = null;
-
- await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
- await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
- {
- TenantId = merchant.TenantId,
- MerchantId = merchant.Id,
- Action = MerchantAuditAction.ReviewReleased,
- Title = "释放审核",
- Description = $"释放人:{actorName}",
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName
- }, cancellationToken);
- await merchantRepository.SaveChangesAsync(cancellationToken);
-
- return null;
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantCommandHandler.cs
deleted file mode 100644
index 20ddefa..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantCommandHandler.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Merchants.Commands;
-using TakeoutSaaS.Application.App.Merchants.Dto;
-using TakeoutSaaS.Domain.Merchants.Entities;
-using TakeoutSaaS.Domain.Merchants.Enums;
-using TakeoutSaaS.Domain.Merchants.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Exceptions;
-using TakeoutSaaS.Shared.Abstractions.Security;
-
-namespace TakeoutSaaS.Application.App.Merchants.Handlers;
-
-///
-/// 商户审核处理器。
-///
-public sealed class ReviewMerchantCommandHandler(
- IMerchantRepository merchantRepository,
- ICurrentUserAccessor currentUserAccessor)
- : IRequestHandler
-{
- ///
- /// 审核商户。
- ///
- /// 审核命令。
- /// 取消标记。
- /// 商户 DTO。
- public async Task Handle(ReviewMerchantCommand request, CancellationToken cancellationToken)
- {
- // 1. 读取商户
- var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
-
- var now = DateTime.UtcNow;
- if (!merchant.ClaimedBy.HasValue || !merchant.ClaimExpiresAt.HasValue || merchant.ClaimExpiresAt <= now)
- {
- throw new BusinessException(ErrorCodes.Conflict, "请先领取审核");
- }
-
- if (merchant.ClaimedBy != currentUserAccessor.UserId)
- {
- throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {merchant.ClaimedByName} 领取");
- }
-
- if (merchant.Status != MerchantStatus.Pending)
- {
- throw new BusinessException(ErrorCodes.Conflict, "商户不在待审核状态");
- }
-
- // 2. 更新审核状态
- merchant.Status = request.Approve ? MerchantStatus.Approved : MerchantStatus.Rejected;
- merchant.ReviewRemarks = request.Remarks;
- merchant.LastReviewedAt = now;
- merchant.LastReviewedBy = ResolveOperatorId();
- if (request.Approve && merchant.JoinedAt == null)
- {
- merchant.JoinedAt = now;
- }
-
- if (request.Approve)
- {
- merchant.IsFrozen = false;
- merchant.FrozenReason = null;
- merchant.FrozenAt = null;
- merchant.ApprovedAt = now;
- merchant.ApprovedBy = ResolveOperatorId();
- }
- else
- {
- merchant.IsFrozen = false;
- merchant.FrozenReason = null;
- merchant.FrozenAt = null;
- }
-
- merchant.ClaimedBy = null;
- merchant.ClaimedByName = null;
- merchant.ClaimedAt = null;
- merchant.ClaimExpiresAt = null;
-
- // 3. 持久化与审计
- await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
- await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
- {
- TenantId = merchant.TenantId,
- MerchantId = merchant.Id,
- Action = request.Approve ? MerchantAuditAction.ReviewApproved : MerchantAuditAction.ReviewRejected,
- Title = request.Approve ? "商户审核通过" : "商户审核驳回",
- Description = request.Remarks,
- OperatorId = ResolveOperatorId(),
- OperatorName = ResolveOperatorName()
- }, cancellationToken);
- await merchantRepository.SaveChangesAsync(cancellationToken);
-
- // 4. 返回 DTO
- return MerchantMapping.ToDto(merchant);
- }
-
- private long? ResolveOperatorId()
- {
- var id = currentUserAccessor.UserId;
- return id == 0 ? null : id;
- }
-
- private string ResolveOperatorName()
- {
- var id = currentUserAccessor.UserId;
- return id == 0 ? "system" : $"user:{id}";
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantDocumentCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantDocumentCommandHandler.cs
deleted file mode 100644
index a8a037f..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ReviewMerchantDocumentCommandHandler.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Merchants.Commands;
-using TakeoutSaaS.Application.App.Merchants.Dto;
-using TakeoutSaaS.Domain.Merchants.Entities;
-using TakeoutSaaS.Domain.Merchants.Enums;
-using TakeoutSaaS.Domain.Merchants.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Exceptions;
-using TakeoutSaaS.Shared.Abstractions.Security;
-using TakeoutSaaS.Shared.Abstractions.Tenancy;
-
-namespace TakeoutSaaS.Application.App.Merchants.Handlers;
-
-///
-/// 审核证照处理器。
-///
-public sealed class ReviewMerchantDocumentCommandHandler(
- IMerchantRepository merchantRepository,
- ITenantProvider tenantProvider,
- ICurrentUserAccessor currentUserAccessor)
- : IRequestHandler
-{
- ///
- /// 审核商户证照。
- ///
- /// 审核命令。
- /// 取消标记。
- /// 证照 DTO。
- public async Task Handle(ReviewMerchantDocumentCommand request, CancellationToken cancellationToken)
- {
- // 1. 读取证照
- var tenantId = tenantProvider.GetCurrentTenantId();
- var document = await merchantRepository.FindDocumentByIdAsync(request.MerchantId, tenantId, request.DocumentId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "证照不存在");
-
- // 2. 若状态无变化且备注相同,直接返回
- var targetStatus = request.Approve ? MerchantDocumentStatus.Approved : MerchantDocumentStatus.Rejected;
- if (document.Status == targetStatus && document.Remarks == request.Remarks)
- {
- return MerchantMapping.ToDto(document);
- }
-
- // 3. 更新状态
- document.Status = targetStatus;
- document.Remarks = request.Remarks;
-
- // 4. 持久化与审计
- await merchantRepository.UpdateDocumentAsync(document, cancellationToken);
- await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
- {
- TenantId = tenantId,
- MerchantId = document.MerchantId,
- Action = MerchantAuditAction.DocumentReviewed,
- Title = request.Approve ? "证照审核通过" : "证照审核驳回",
- Description = request.Remarks,
- OperatorId = ResolveOperatorId(),
- OperatorName = ResolveOperatorName()
- }, cancellationToken);
- await merchantRepository.SaveChangesAsync(cancellationToken);
-
- // 5. 返回 DTO
- return MerchantMapping.ToDto(document);
- }
-
- private long? ResolveOperatorId()
- {
- var id = currentUserAccessor.UserId;
- return id == 0 ? null : id;
- }
-
- private string ResolveOperatorName()
- {
- var id = currentUserAccessor.UserId;
- return id == 0 ? "system" : $"user:{id}";
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/RevokeMerchantReviewHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/RevokeMerchantReviewHandler.cs
deleted file mode 100644
index deb76cf..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/RevokeMerchantReviewHandler.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Merchants.Commands;
-using TakeoutSaaS.Domain.Merchants.Entities;
-using TakeoutSaaS.Domain.Merchants.Enums;
-using TakeoutSaaS.Domain.Merchants.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Exceptions;
-using TakeoutSaaS.Shared.Abstractions.Security;
-
-namespace TakeoutSaaS.Application.App.Merchants.Handlers;
-
-///
-/// 撤销商户审核处理器。
-///
-public sealed class RevokeMerchantReviewHandler(
- IMerchantRepository merchantRepository,
- ICurrentUserAccessor currentUserAccessor)
- : IRequestHandler
-{
- ///
- public async Task Handle(RevokeMerchantReviewCommand request, CancellationToken cancellationToken)
- {
- var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
-
- if (merchant.Status != MerchantStatus.Approved)
- {
- throw new BusinessException(ErrorCodes.Conflict, "商户不在已审核状态");
- }
-
- var now = DateTime.UtcNow;
- var actorName = currentUserAccessor.IsAuthenticated
- ? $"user:{currentUserAccessor.UserId}"
- : "system";
-
- merchant.Status = MerchantStatus.Pending;
- merchant.IsFrozen = true;
- merchant.FrozenReason = request.Reason;
- merchant.FrozenAt = now;
- merchant.ReviewRemarks = request.Reason;
- merchant.LastReviewedAt = now;
- merchant.LastReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId;
- merchant.ClaimedBy = null;
- merchant.ClaimedByName = null;
- merchant.ClaimedAt = null;
- merchant.ClaimExpiresAt = null;
-
- await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
- await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
- {
- TenantId = merchant.TenantId,
- MerchantId = merchant.Id,
- Action = MerchantAuditAction.ReviewRevoked,
- Title = "撤销审核",
- Description = request.Reason,
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName
- }, cancellationToken);
- await merchantRepository.SaveChangesAsync(cancellationToken);
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantReviewClaimQuery.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantReviewClaimQuery.cs
deleted file mode 100644
index e829e3e..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetMerchantReviewClaimQuery.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Merchants.Dto;
-
-namespace TakeoutSaaS.Application.App.Merchants.Queries;
-
-///
-/// 获取商户审核领取信息查询。
-///
-public sealed record GetMerchantReviewClaimQuery(long MerchantId) : IRequest;
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetPendingReviewListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetPendingReviewListQuery.cs
deleted file mode 100644
index 3300be0..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetPendingReviewListQuery.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Merchants.Dto;
-using TakeoutSaaS.Domain.Common.Enums;
-using TakeoutSaaS.Shared.Abstractions.Results;
-
-namespace TakeoutSaaS.Application.App.Merchants.Queries;
-
-///
-/// 待审核商户列表查询。
-///
-public sealed class GetPendingReviewListQuery : IRequest>
-{
- ///
- /// 关键词(商户名称/营业执照号)。
- ///
- public string? Keyword { get; init; }
-
- ///
- /// 经营模式筛选。
- ///
- public OperatingMode? OperatingMode { get; init; }
-
- ///
- /// 租户筛选。
- ///
- public long? TenantId { get; init; }
-
- ///
- /// 页码。
- ///
- public int Page { get; init; } = 1;
-
- ///
- /// 每页条数。
- ///
- public int PageSize { get; init; } = 20;
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/ReviewMerchantValidator.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/ReviewMerchantValidator.cs
deleted file mode 100644
index 69ab1c7..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/ReviewMerchantValidator.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using FluentValidation;
-using TakeoutSaaS.Application.App.Merchants.Commands;
-
-namespace TakeoutSaaS.Application.App.Merchants.Validators;
-
-///
-/// 商户审核命令验证器。
-///
-public sealed class ReviewMerchantValidator : AbstractValidator
-{
- ///
- /// 初始化验证规则。
- ///
- public ReviewMerchantValidator()
- {
- RuleFor(x => x.MerchantId).GreaterThan(0);
- RuleFor(x => x.Remarks)
- .NotEmpty()
- .When(x => !x.Approve);
- RuleFor(x => x.Remarks)
- .MaximumLength(500)
- .When(x => !string.IsNullOrWhiteSpace(x.Remarks));
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ApproveStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ApproveStoreCommand.cs
deleted file mode 100644
index 882d9e0..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ApproveStoreCommand.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Commands;
-
-///
-/// 审核通过命令。
-///
-public sealed record ApproveStoreCommand : IRequest
-{
- ///
- /// 门店 ID。
- ///
- public long StoreId { get; init; }
-
- ///
- /// 审核备注。
- ///
- public string? Remark { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ForceCloseStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ForceCloseStoreCommand.cs
deleted file mode 100644
index 14cf24a..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ForceCloseStoreCommand.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Commands;
-
-///
-/// 强制关闭门店命令。
-///
-public sealed record ForceCloseStoreCommand : IRequest
-{
- ///
- /// 门店 ID。
- ///
- public long StoreId { get; init; }
-
- ///
- /// 关闭原因。
- ///
- public string Reason { get; init; } = string.Empty;
-
- ///
- /// 备注。
- ///
- public string? Remark { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/RejectStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/RejectStoreCommand.cs
deleted file mode 100644
index 1f0f238..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/RejectStoreCommand.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Commands;
-
-///
-/// 审核驳回命令。
-///
-public sealed record RejectStoreCommand : IRequest
-{
- ///
- /// 门店 ID。
- ///
- public long StoreId { get; init; }
-
- ///
- /// 驳回原因 ID。
- ///
- public long RejectionReasonId { get; init; }
-
- ///
- /// 驳回原因补充说明。
- ///
- public string? RejectionReasonText { get; init; }
-
- ///
- /// 审核备注。
- ///
- public string? Remark { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ReopenStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ReopenStoreCommand.cs
deleted file mode 100644
index 74755ff..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Commands/ReopenStoreCommand.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Commands;
-
-///
-/// 解除强制关闭命令。
-///
-public sealed record ReopenStoreCommand : IRequest
-{
- ///
- /// 门店 ID。
- ///
- public long StoreId { get; init; }
-
- ///
- /// 备注。
- ///
- public string? Remark { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/PendingStoreAuditDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/PendingStoreAuditDto.cs
deleted file mode 100644
index 3087fd5..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/PendingStoreAuditDto.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-using System.Text.Json.Serialization;
-using TakeoutSaaS.Domain.Stores.Enums;
-using TakeoutSaaS.Shared.Abstractions.Serialization;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-///
-/// 待审核门店 DTO。
-///
-public sealed record PendingStoreAuditDto
-{
- ///
- /// 门店 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long StoreId { get; init; }
-
- ///
- /// 门店名称。
- ///
- public string StoreName { get; init; } = string.Empty;
-
- ///
- /// 门店编码。
- ///
- public string StoreCode { get; init; } = string.Empty;
-
- ///
- /// 租户 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long TenantId { get; init; }
-
- ///
- /// 租户名称。
- ///
- public string TenantName { get; init; } = string.Empty;
-
- ///
- /// 商户 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long MerchantId { get; init; }
-
- ///
- /// 商户名称。
- ///
- public string MerchantName { get; init; } = string.Empty;
-
- ///
- /// 门头招牌图。
- ///
- public string? SignboardImageUrl { get; init; }
-
- ///
- /// 完整地址。
- ///
- public string FullAddress { get; init; } = string.Empty;
-
- ///
- /// 主体类型。
- ///
- public StoreOwnershipType OwnershipType { get; init; }
-
- ///
- /// 提交时间。
- ///
- public DateTime? SubmittedAt { get; init; }
-
- ///
- /// 等待天数。
- ///
- public int WaitingDays { get; init; }
-
- ///
- /// 是否超时。
- ///
- public bool IsOverdue { get; init; }
-
- ///
- /// 资质数量。
- ///
- public int QualificationCount { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditActionResultDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditActionResultDto.cs
deleted file mode 100644
index d99b10b..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditActionResultDto.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System.Text.Json.Serialization;
-using TakeoutSaaS.Domain.Stores.Enums;
-using TakeoutSaaS.Shared.Abstractions.Serialization;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-///
-/// 审核/风控操作结果 DTO。
-///
-public sealed record StoreAuditActionResultDto
-{
- ///
- /// 门店 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long StoreId { get; init; }
-
- ///
- /// 审核状态。
- ///
- public StoreAuditStatus AuditStatus { get; init; }
-
- ///
- /// 经营状态。
- ///
- public StoreBusinessStatus BusinessStatus { get; init; }
-
- ///
- /// 驳回原因。
- ///
- public string? RejectionReason { get; init; }
-
- ///
- /// 提示信息。
- ///
- public string? Message { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDailyTrendDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDailyTrendDto.cs
deleted file mode 100644
index a4d84f3..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDailyTrendDto.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-///
-/// 审核统计趋势项。
-///
-public sealed record StoreAuditDailyTrendDto
-{
- ///
- /// 日期。
- ///
- public DateOnly Date { get; init; }
-
- ///
- /// 提交数量。
- ///
- public int Submitted { get; init; }
-
- ///
- /// 通过数量。
- ///
- public int Approved { get; init; }
-
- ///
- /// 驳回数量。
- ///
- public int Rejected { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDetailDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDetailDto.cs
deleted file mode 100644
index 3530167..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditDetailDto.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using TakeoutSaaS.Application.App.Stores.Dto;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-///
-/// 门店审核详情 DTO。
-///
-public sealed record StoreAuditDetailDto
-{
- ///
- /// 门店信息。
- ///
- public StoreAuditStoreDto Store { get; init; } = new();
-
- ///
- /// 租户信息。
- ///
- public StoreAuditTenantDto Tenant { get; init; } = new();
-
- ///
- /// 商户信息。
- ///
- public StoreAuditMerchantDto Merchant { get; init; } = new();
-
- ///
- /// 资质列表。
- ///
- public IReadOnlyList Qualifications { get; init; } = [];
-
- ///
- /// 审核记录。
- ///
- public IReadOnlyList AuditHistory { get; init; } = [];
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditMerchantDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditMerchantDto.cs
deleted file mode 100644
index 5dcbc6b..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditMerchantDto.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System.Text.Json.Serialization;
-using TakeoutSaaS.Shared.Abstractions.Serialization;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-///
-/// 门店审核详情 - 商户信息。
-///
-public sealed record StoreAuditMerchantDto
-{
- ///
- /// 商户 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long Id { get; init; }
-
- ///
- /// 商户名称。
- ///
- public string Name { get; init; } = string.Empty;
-
- ///
- /// 法人或主体名称。
- ///
- public string? LegalName { get; init; }
-
- ///
- /// 统一社会信用代码。
- ///
- public string? CreditCode { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditRecordDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditRecordDto.cs
deleted file mode 100644
index 15b8993..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditRecordDto.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using System.Text.Json.Serialization;
-using TakeoutSaaS.Domain.Stores.Enums;
-using TakeoutSaaS.Shared.Abstractions.Serialization;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-///
-/// 门店审核记录 DTO。
-///
-public sealed record StoreAuditRecordDto
-{
- ///
- /// 记录 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long Id { get; init; }
-
- ///
- /// 审核动作。
- ///
- public StoreAuditAction Action { get; init; }
-
- ///
- /// 动作名称。
- ///
- public string ActionName { get; init; } = string.Empty;
-
- ///
- /// 操作人 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long? OperatorId { get; init; }
-
- ///
- /// 操作人名称。
- ///
- public string OperatorName { get; init; } = string.Empty;
-
- ///
- /// 操作前状态。
- ///
- public StoreAuditStatus? PreviousStatus { get; init; }
-
- ///
- /// 操作后状态。
- ///
- public StoreAuditStatus NewStatus { get; init; }
-
- ///
- /// 驳回理由 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long? RejectionReasonId { get; init; }
-
- ///
- /// 驳回理由。
- ///
- public string? RejectionReasonText { get; init; }
-
- ///
- /// 备注。
- ///
- public string? Remark { get; init; }
-
- ///
- /// 创建时间。
- ///
- public DateTime CreatedAt { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStatisticsDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStatisticsDto.cs
deleted file mode 100644
index 495b10f..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStatisticsDto.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-///
-/// 审核统计 DTO。
-///
-public sealed record StoreAuditStatisticsDto
-{
- ///
- /// 待审核数量。
- ///
- public int PendingCount { get; init; }
-
- ///
- /// 超时数量。
- ///
- public int OverdueCount { get; init; }
-
- ///
- /// 审核通过数量。
- ///
- public int ApprovedCount { get; init; }
-
- ///
- /// 审核驳回数量。
- ///
- public int RejectedCount { get; init; }
-
- ///
- /// 平均处理时长(小时)。
- ///
- public double AvgProcessingHours { get; init; }
-
- ///
- /// 每日趋势。
- ///
- public IReadOnlyList DailyTrend { get; init; } = [];
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStoreDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStoreDto.cs
deleted file mode 100644
index 110638e..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditStoreDto.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-using System.Text.Json.Serialization;
-using TakeoutSaaS.Domain.Stores.Enums;
-using TakeoutSaaS.Shared.Abstractions.Serialization;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-///
-/// 门店审核详情 - 门店信息。
-///
-public sealed record StoreAuditStoreDto
-{
- ///
- /// 门店 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long Id { get; init; }
-
- ///
- /// 门店名称。
- ///
- public string Name { get; init; } = string.Empty;
-
- ///
- /// 门店编码。
- ///
- public string Code { get; init; } = string.Empty;
-
- ///
- /// 联系电话。
- ///
- public string? Phone { get; init; }
-
- ///
- /// 门头招牌图。
- ///
- public string? SignboardImageUrl { get; init; }
-
- ///
- /// 省份。
- ///
- public string? Province { get; init; }
-
- ///
- /// 城市。
- ///
- public string? City { get; init; }
-
- ///
- /// 区县。
- ///
- public string? District { get; init; }
-
- ///
- /// 详细地址。
- ///
- public string? Address { get; init; }
-
- ///
- /// 经度。
- ///
- public double? Longitude { get; init; }
-
- ///
- /// 纬度。
- ///
- public double? Latitude { get; init; }
-
- ///
- /// 主体类型。
- ///
- public StoreOwnershipType OwnershipType { get; init; }
-
- ///
- /// 审核状态。
- ///
- public StoreAuditStatus AuditStatus { get; init; }
-
- ///
- /// 提交时间。
- ///
- public DateTime? SubmittedAt { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditTenantDto.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditTenantDto.cs
deleted file mode 100644
index 50d3cfd..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Dto/StoreAuditTenantDto.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System.Text.Json.Serialization;
-using TakeoutSaaS.Shared.Abstractions.Serialization;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-///
-/// 门店审核详情 - 租户信息。
-///
-public sealed record StoreAuditTenantDto
-{
- ///
- /// 租户 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long Id { get; init; }
-
- ///
- /// 租户名称。
- ///
- public string Name { get; init; } = string.Empty;
-
- ///
- /// 联系人。
- ///
- public string? ContactName { get; init; }
-
- ///
- /// 联系电话。
- ///
- public string? ContactPhone { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ApproveStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ApproveStoreCommandHandler.cs
deleted file mode 100644
index a08d565..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ApproveStoreCommandHandler.cs
+++ /dev/null
@@ -1,158 +0,0 @@
-using System.Data;
-using System.Data.Common;
-using MediatR;
-using Microsoft.Extensions.Logging;
-using TakeoutSaaS.Application.App.StoreAudits.Commands;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-using TakeoutSaaS.Domain.Stores.Entities;
-using TakeoutSaaS.Domain.Stores.Enums;
-using TakeoutSaaS.Domain.Stores.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Data;
-using TakeoutSaaS.Shared.Abstractions.Exceptions;
-using TakeoutSaaS.Shared.Abstractions.Security;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
-
-///
-/// 审核通过处理器。
-///
-public sealed class ApproveStoreCommandHandler(
- IStoreRepository storeRepository,
- IDapperExecutor dapperExecutor,
- ICurrentUserAccessor currentUserAccessor,
- ILogger logger)
- : IRequestHandler
-{
- ///
- public async Task Handle(ApproveStoreCommand request, CancellationToken cancellationToken)
- {
- // 1. 获取门店快照
- var snapshot = await QueryStoreSnapshotAsync(request.StoreId, cancellationToken);
- if (!snapshot.HasValue)
- {
- throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
- }
-
- // 1.1 (空行后) 校验审核状态
- if (snapshot.Value.AuditStatus != StoreAuditStatus.Pending)
- {
- throw new BusinessException(ErrorCodes.Conflict, "门店不处于待审核状态");
- }
-
- // 2. (空行后) 获取门店实体
- var store = await storeRepository.FindByIdAsync(request.StoreId, snapshot.Value.TenantId, cancellationToken);
- if (store is null)
- {
- throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
- }
-
- // 3. (空行后) 更新状态并记录审核
- var previousStatus = store.AuditStatus;
- var now = DateTime.UtcNow;
- store.AuditStatus = StoreAuditStatus.Activated;
- store.BusinessStatus = StoreBusinessStatus.Resting;
- store.ActivatedAt ??= now;
- store.RejectionReason = null;
- store.ClosureReason = null;
- store.ClosureReasonText = null;
-
- await storeRepository.UpdateStoreAsync(store, cancellationToken);
- await storeRepository.AddAuditRecordAsync(new StoreAuditRecord
- {
- StoreId = store.Id,
- Action = StoreAuditAction.Approve,
- PreviousStatus = previousStatus,
- NewStatus = store.AuditStatus,
- OperatorId = ResolveOperatorId(),
- OperatorName = ResolveOperatorName(),
- Remarks = request.Remark
- }, cancellationToken);
- await storeRepository.SaveChangesAsync(cancellationToken);
- logger.LogInformation("门店 {StoreId} 审核通过", store.Id);
-
- // 4. (空行后) 返回结果
- return new StoreAuditActionResultDto
- {
- StoreId = store.Id,
- AuditStatus = store.AuditStatus,
- BusinessStatus = store.BusinessStatus,
- Message = "门店已激活"
- };
- }
-
- private async Task<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?> QueryStoreSnapshotAsync(
- long storeId,
- CancellationToken cancellationToken)
- {
- // 1. 查询门店基础字段
- return await dapperExecutor.QueryAsync<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?>(
- DatabaseConstants.AppDataSource,
- DatabaseConnectionRole.Read,
- async (connection, token) =>
- {
- await using var command = CreateCommand(
- connection,
- BuildStoreSnapshotSql(),
- [
- ("storeId", storeId)
- ]);
-
- // 1.1 (空行后) 执行查询
- await using var reader = await command.ExecuteReaderAsync(token);
- if (!await reader.ReadAsync(token))
- {
- return null;
- }
-
- // 1.2 (空行后) 返回快照
- return (
- reader.GetInt64(reader.GetOrdinal("TenantId")),
- (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")),
- (StoreBusinessStatus)reader.GetInt32(reader.GetOrdinal("BusinessStatus")));
- },
- cancellationToken);
- }
-
- private long? ResolveOperatorId()
- {
- var id = currentUserAccessor.UserId;
- return id == 0 ? null : id;
- }
-
- private string ResolveOperatorName()
- {
- var id = currentUserAccessor.UserId;
- return id == 0 ? "system" : $"user:{id}";
- }
-
- private static string BuildStoreSnapshotSql()
- {
- return """
- select
- s."TenantId",
- s."AuditStatus",
- s."BusinessStatus"
- from public.stores s
- where s."DeletedAt" is null
- and s."Id" = @storeId;
- """;
- }
-
- private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
- {
- var command = connection.CreateCommand();
- command.CommandText = sql;
-
- // 1. (空行后) 绑定参数
- foreach (var (name, value) in parameters)
- {
- var p = command.CreateParameter();
- p.ParameterName = name;
- p.Value = value ?? DBNull.Value;
- command.Parameters.Add(p);
- }
-
- return (DbCommand)command;
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ForceCloseStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ForceCloseStoreCommandHandler.cs
deleted file mode 100644
index 8adbdac..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ForceCloseStoreCommandHandler.cs
+++ /dev/null
@@ -1,161 +0,0 @@
-using System.Data;
-using System.Data.Common;
-using MediatR;
-using Microsoft.Extensions.Logging;
-using TakeoutSaaS.Application.App.StoreAudits.Commands;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-using TakeoutSaaS.Domain.Stores.Entities;
-using TakeoutSaaS.Domain.Stores.Enums;
-using TakeoutSaaS.Domain.Stores.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Data;
-using TakeoutSaaS.Shared.Abstractions.Exceptions;
-using TakeoutSaaS.Shared.Abstractions.Security;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
-
-///
-/// 强制关闭门店处理器。
-///
-public sealed class ForceCloseStoreCommandHandler(
- IStoreRepository storeRepository,
- IDapperExecutor dapperExecutor,
- ICurrentUserAccessor currentUserAccessor,
- ILogger logger)
- : IRequestHandler
-{
- ///
- public async Task Handle(ForceCloseStoreCommand request, CancellationToken cancellationToken)
- {
- // 1. 获取门店快照
- var snapshot = await QueryStoreSnapshotAsync(request.StoreId, cancellationToken);
- if (!snapshot.HasValue)
- {
- throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
- }
-
- // 1.1 (空行后) 校验审核与经营状态
- if (snapshot.Value.AuditStatus != StoreAuditStatus.Activated)
- {
- throw new BusinessException(ErrorCodes.Conflict, "门店未激活,无法强制关闭");
- }
-
- if (snapshot.Value.BusinessStatus == StoreBusinessStatus.ForceClosed)
- {
- throw new BusinessException(ErrorCodes.Conflict, "门店已处于强制关闭状态");
- }
-
- // 2. (空行后) 获取门店实体
- var store = await storeRepository.FindByIdAsync(request.StoreId, snapshot.Value.TenantId, cancellationToken);
- if (store is null)
- {
- throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
- }
-
- // 3. (空行后) 更新状态并记录风控
- var now = DateTime.UtcNow;
- store.BusinessStatus = StoreBusinessStatus.ForceClosed;
- store.ClosureReason = StoreClosureReason.PlatformSuspended;
- store.ClosureReasonText = request.Reason;
- store.ForceCloseReason = request.Reason;
- store.ForceClosedAt = now;
-
- await storeRepository.UpdateStoreAsync(store, cancellationToken);
- await storeRepository.AddAuditRecordAsync(new StoreAuditRecord
- {
- StoreId = store.Id,
- Action = StoreAuditAction.ForceClose,
- PreviousStatus = store.AuditStatus,
- NewStatus = store.AuditStatus,
- OperatorId = ResolveOperatorId(),
- OperatorName = ResolveOperatorName(),
- Remarks = request.Remark
- }, cancellationToken);
- await storeRepository.SaveChangesAsync(cancellationToken);
- logger.LogInformation("门店 {StoreId} 强制关闭", store.Id);
-
- // 4. (空行后) 返回结果
- return new StoreAuditActionResultDto
- {
- StoreId = store.Id,
- AuditStatus = store.AuditStatus,
- BusinessStatus = store.BusinessStatus,
- Message = "门店已强制关闭"
- };
- }
-
- private async Task<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?> QueryStoreSnapshotAsync(
- long storeId,
- CancellationToken cancellationToken)
- {
- // 1. 查询门店基础字段
- return await dapperExecutor.QueryAsync<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?>(
- DatabaseConstants.AppDataSource,
- DatabaseConnectionRole.Read,
- async (connection, token) =>
- {
- await using var command = CreateCommand(
- connection,
- BuildStoreSnapshotSql(),
- [
- ("storeId", storeId)
- ]);
-
- // 1.1 (空行后) 执行查询
- await using var reader = await command.ExecuteReaderAsync(token);
- if (!await reader.ReadAsync(token))
- {
- return null;
- }
-
- // 1.2 (空行后) 返回快照
- return (
- reader.GetInt64(reader.GetOrdinal("TenantId")),
- (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")),
- (StoreBusinessStatus)reader.GetInt32(reader.GetOrdinal("BusinessStatus")));
- },
- cancellationToken);
- }
-
- private long? ResolveOperatorId()
- {
- var id = currentUserAccessor.UserId;
- return id == 0 ? null : id;
- }
-
- private string ResolveOperatorName()
- {
- var id = currentUserAccessor.UserId;
- return id == 0 ? "system" : $"user:{id}";
- }
-
- private static string BuildStoreSnapshotSql()
- {
- return """
- select
- s."TenantId",
- s."AuditStatus",
- s."BusinessStatus"
- from public.stores s
- where s."DeletedAt" is null
- and s."Id" = @storeId;
- """;
- }
-
- private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
- {
- var command = connection.CreateCommand();
- command.CommandText = sql;
-
- // 1. (空行后) 绑定参数
- foreach (var (name, value) in parameters)
- {
- var p = command.CreateParameter();
- p.ParameterName = name;
- p.Value = value ?? DBNull.Value;
- command.Parameters.Add(p);
- }
-
- return (DbCommand)command;
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditDetailQueryHandler.cs
deleted file mode 100644
index 6d2a1ca..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditDetailQueryHandler.cs
+++ /dev/null
@@ -1,322 +0,0 @@
-using System.Data;
-using System.Data.Common;
-using MediatR;
-using TakeoutSaaS.Application.App.StoreAudits;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-using TakeoutSaaS.Application.App.StoreAudits.Queries;
-using TakeoutSaaS.Application.App.Stores.Dto;
-using TakeoutSaaS.Domain.Stores.Enums;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Data;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
-
-///
-/// 门店审核详情查询处理器。
-///
-public sealed class GetStoreAuditDetailQueryHandler(
- IDapperExecutor dapperExecutor)
- : IRequestHandler
-{
- ///
- public async Task Handle(GetStoreAuditDetailQuery request, CancellationToken cancellationToken)
- {
- // 1. 查询门店与主体信息
- return await dapperExecutor.QueryAsync(
- DatabaseConstants.AppDataSource,
- DatabaseConnectionRole.Read,
- async (connection, token) =>
- {
- // 1.1 查询门店基础信息
- await using var storeCommand = CreateCommand(
- connection,
- BuildStoreSql(),
- [
- ("storeId", request.StoreId)
- ]);
-
- await using var reader = await storeCommand.ExecuteReaderAsync(token);
- if (!await reader.ReadAsync(token))
- {
- return null;
- }
-
- // 1.2 (空行后) 映射门店信息
- var store = new StoreAuditStoreDto
- {
- Id = reader.GetInt64(reader.GetOrdinal("StoreId")),
- Name = reader.GetString(reader.GetOrdinal("StoreName")),
- Code = reader.GetString(reader.GetOrdinal("StoreCode")),
- Phone = reader.IsDBNull(reader.GetOrdinal("Phone")) ? null : reader.GetString(reader.GetOrdinal("Phone")),
- SignboardImageUrl = reader.IsDBNull(reader.GetOrdinal("SignboardImageUrl"))
- ? null
- : reader.GetString(reader.GetOrdinal("SignboardImageUrl")),
- Province = reader.IsDBNull(reader.GetOrdinal("Province")) ? null : reader.GetString(reader.GetOrdinal("Province")),
- City = reader.IsDBNull(reader.GetOrdinal("City")) ? null : reader.GetString(reader.GetOrdinal("City")),
- District = reader.IsDBNull(reader.GetOrdinal("District")) ? null : reader.GetString(reader.GetOrdinal("District")),
- Address = reader.IsDBNull(reader.GetOrdinal("Address")) ? null : reader.GetString(reader.GetOrdinal("Address")),
- Longitude = reader.IsDBNull(reader.GetOrdinal("Longitude")) ? null : reader.GetDouble(reader.GetOrdinal("Longitude")),
- Latitude = reader.IsDBNull(reader.GetOrdinal("Latitude")) ? null : reader.GetDouble(reader.GetOrdinal("Latitude")),
- OwnershipType = (StoreOwnershipType)reader.GetInt32(reader.GetOrdinal("OwnershipType")),
- AuditStatus = (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")),
- SubmittedAt = reader.IsDBNull(reader.GetOrdinal("SubmittedAt"))
- ? null
- : reader.GetDateTime(reader.GetOrdinal("SubmittedAt"))
- };
-
- // 1.3 (空行后) 映射租户信息
- var tenant = new StoreAuditTenantDto
- {
- Id = reader.GetInt64(reader.GetOrdinal("TenantId")),
- Name = reader.GetString(reader.GetOrdinal("TenantName")),
- ContactName = reader.IsDBNull(reader.GetOrdinal("TenantContactName"))
- ? null
- : reader.GetString(reader.GetOrdinal("TenantContactName")),
- ContactPhone = reader.IsDBNull(reader.GetOrdinal("TenantContactPhone"))
- ? null
- : reader.GetString(reader.GetOrdinal("TenantContactPhone"))
- };
-
- // 1.4 (空行后) 映射商户信息
- var merchant = new StoreAuditMerchantDto
- {
- Id = reader.GetInt64(reader.GetOrdinal("MerchantId")),
- Name = reader.GetString(reader.GetOrdinal("MerchantName")),
- LegalName = reader.IsDBNull(reader.GetOrdinal("MerchantLegalName"))
- ? null
- : reader.GetString(reader.GetOrdinal("MerchantLegalName")),
- CreditCode = reader.IsDBNull(reader.GetOrdinal("MerchantCreditCode"))
- ? null
- : reader.GetString(reader.GetOrdinal("MerchantCreditCode"))
- };
-
- // 2. (空行后) 查询资质列表
- var qualifications = await QueryQualificationsAsync(connection, request.StoreId, token);
-
- // 3. (空行后) 查询审核记录
- var auditHistory = await QueryAuditRecordsAsync(connection, request.StoreId, token);
-
- // 4. (空行后) 组装结果
- return new StoreAuditDetailDto
- {
- Store = store,
- Tenant = tenant,
- Merchant = merchant,
- Qualifications = qualifications,
- AuditHistory = auditHistory
- };
- },
- cancellationToken);
- }
-
- private static async Task> QueryQualificationsAsync(
- IDbConnection connection,
- long storeId,
- CancellationToken cancellationToken)
- {
- // 1. 查询门店资质
- await using var command = CreateCommand(
- connection,
- BuildQualificationSql(),
- [
- ("storeId", storeId)
- ]);
-
- await using var reader = await command.ExecuteReaderAsync(cancellationToken);
- var items = new List();
- if (!reader.HasRows)
- {
- return items;
- }
-
- // 2. (空行后) 映射资质 DTO
- var today = DateOnly.FromDateTime(DateTime.UtcNow);
- while (await reader.ReadAsync(cancellationToken))
- {
- DateOnly? expiresAt = reader.IsDBNull(reader.GetOrdinal("ExpiresAt"))
- ? null
- : DateOnly.FromDateTime(reader.GetDateTime(reader.GetOrdinal("ExpiresAt")));
- int? daysUntilExpiry = expiresAt.HasValue
- ? expiresAt.Value.DayNumber - today.DayNumber
- : null;
- var isExpired = expiresAt.HasValue && expiresAt.Value < today;
- var isExpiringSoon = expiresAt.HasValue
- && expiresAt.Value >= today
- && expiresAt.Value <= today.AddDays(30);
-
- // 2.1 (空行后) 写入列表
- items.Add(new StoreQualificationDto
- {
- Id = reader.GetInt64(reader.GetOrdinal("Id")),
- StoreId = reader.GetInt64(reader.GetOrdinal("StoreId")),
- QualificationType = (StoreQualificationType)reader.GetInt32(reader.GetOrdinal("QualificationType")),
- FileUrl = reader.GetString(reader.GetOrdinal("FileUrl")),
- DocumentNumber = reader.IsDBNull(reader.GetOrdinal("DocumentNumber"))
- ? null
- : reader.GetString(reader.GetOrdinal("DocumentNumber")),
- IssuedAt = reader.IsDBNull(reader.GetOrdinal("IssuedAt"))
- ? null
- : DateOnly.FromDateTime(reader.GetDateTime(reader.GetOrdinal("IssuedAt"))),
- ExpiresAt = expiresAt,
- IsExpired = isExpired,
- IsExpiringSoon = isExpiringSoon,
- DaysUntilExpiry = daysUntilExpiry,
- SortOrder = reader.GetInt32(reader.GetOrdinal("SortOrder")),
- CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")),
- UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
- ? null
- : reader.GetDateTime(reader.GetOrdinal("UpdatedAt"))
- });
- }
-
- // 3. (空行后) 返回结果
- return items;
- }
-
- private static async Task> QueryAuditRecordsAsync(
- IDbConnection connection,
- long storeId,
- CancellationToken cancellationToken)
- {
- // 1. 查询审核记录
- await using var command = CreateCommand(
- connection,
- BuildAuditRecordSql(),
- [
- ("storeId", storeId)
- ]);
-
- await using var reader = await command.ExecuteReaderAsync(cancellationToken);
- var items = new List();
- if (!reader.HasRows)
- {
- return items;
- }
-
- // 2. (空行后) 映射审核记录 DTO
- while (await reader.ReadAsync(cancellationToken))
- {
- var action = (StoreAuditAction)reader.GetInt32(reader.GetOrdinal("Action"));
- items.Add(new StoreAuditRecordDto
- {
- Id = reader.GetInt64(reader.GetOrdinal("Id")),
- Action = action,
- ActionName = StoreAuditActionNameResolver.Resolve(action),
- OperatorId = reader.IsDBNull(reader.GetOrdinal("OperatorId"))
- ? null
- : reader.GetInt64(reader.GetOrdinal("OperatorId")),
- OperatorName = reader.GetString(reader.GetOrdinal("OperatorName")),
- PreviousStatus = reader.IsDBNull(reader.GetOrdinal("PreviousStatus"))
- ? null
- : (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("PreviousStatus")),
- NewStatus = (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("NewStatus")),
- RejectionReasonId = reader.IsDBNull(reader.GetOrdinal("RejectionReasonId"))
- ? null
- : reader.GetInt64(reader.GetOrdinal("RejectionReasonId")),
- RejectionReasonText = reader.IsDBNull(reader.GetOrdinal("RejectionReason"))
- ? null
- : reader.GetString(reader.GetOrdinal("RejectionReason")),
- Remark = reader.IsDBNull(reader.GetOrdinal("Remarks"))
- ? null
- : reader.GetString(reader.GetOrdinal("Remarks")),
- CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt"))
- });
- }
-
- // 3. (空行后) 返回结果
- return items;
- }
-
- private static string BuildStoreSql()
- {
- return """
- select
- s."Id" as "StoreId",
- s."Name" as "StoreName",
- s."Code" as "StoreCode",
- s."Phone",
- s."SignboardImageUrl",
- s."Province",
- s."City",
- s."District",
- s."Address",
- s."Longitude",
- s."Latitude",
- s."OwnershipType",
- s."AuditStatus",
- s."SubmittedAt",
- s."TenantId",
- t."Name" as "TenantName",
- t."ContactName" as "TenantContactName",
- t."ContactPhone" as "TenantContactPhone",
- s."MerchantId",
- m."BrandName" as "MerchantName",
- m."LegalPerson" as "MerchantLegalName",
- m."TaxNumber" as "MerchantCreditCode"
- from public.stores s
- join public.tenants t on t."Id" = s."TenantId" and t."DeletedAt" is null
- join public.merchants m on m."Id" = s."MerchantId" and m."DeletedAt" is null
- where s."DeletedAt" is null
- and s."Id" = @storeId;
- """;
- }
-
- private static string BuildQualificationSql()
- {
- return """
- select
- q."Id",
- q."StoreId",
- q."QualificationType",
- q."FileUrl",
- q."DocumentNumber",
- q."IssuedAt",
- q."ExpiresAt",
- q."SortOrder",
- q."CreatedAt",
- q."UpdatedAt"
- from public.store_qualifications q
- where q."DeletedAt" is null
- and q."StoreId" = @storeId
- order by q."SortOrder", q."QualificationType";
- """;
- }
-
- private static string BuildAuditRecordSql()
- {
- return """
- select
- r."Id",
- r."Action",
- r."OperatorId",
- r."OperatorName",
- r."PreviousStatus",
- r."NewStatus",
- r."RejectionReasonId",
- r."RejectionReason",
- r."Remarks",
- r."CreatedAt"
- from public.store_audit_records r
- where r."DeletedAt" is null
- and r."StoreId" = @storeId
- order by r."CreatedAt" desc;
- """;
- }
-
- private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
- {
- var command = connection.CreateCommand();
- command.CommandText = sql;
-
- // 1. (空行后) 绑定参数
- foreach (var (name, value) in parameters)
- {
- var p = command.CreateParameter();
- p.ParameterName = name;
- p.Value = value ?? DBNull.Value;
- command.Parameters.Add(p);
- }
-
- return (DbCommand)command;
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditStatisticsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditStatisticsQueryHandler.cs
deleted file mode 100644
index f710a79..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/GetStoreAuditStatisticsQueryHandler.cs
+++ /dev/null
@@ -1,279 +0,0 @@
-using System.Data;
-using System.Data.Common;
-using MediatR;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-using TakeoutSaaS.Application.App.StoreAudits.Queries;
-using TakeoutSaaS.Domain.Stores.Enums;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Data;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
-
-///
-/// 审核统计查询处理器。
-///
-public sealed class GetStoreAuditStatisticsQueryHandler(
- IDapperExecutor dapperExecutor)
- : IRequestHandler
-{
- ///
- public async Task Handle(GetStoreAuditStatisticsQuery request, CancellationToken cancellationToken)
- {
- // 1. 规范化日期范围
- var today = DateTime.UtcNow.Date;
- var dateFrom = request.DateFrom?.Date ?? today.AddDays(-30);
- var dateTo = request.DateTo?.Date ?? today;
- if (dateFrom > dateTo)
- {
- (dateFrom, dateTo) = (dateTo, dateFrom);
- }
-
- // 1.1 (空行后) 计算统计边界
- var dateToExclusive = dateTo.AddDays(1);
- var overdueDeadline = DateTime.UtcNow.AddDays(-7);
-
- // 2. (空行后) 查询统计
- return await dapperExecutor.QueryAsync(
- DatabaseConstants.AppDataSource,
- DatabaseConnectionRole.Read,
- async (connection, token) =>
- {
- // 2.1 读取待审核与超时数量
- var pendingCount = await ExecuteScalarIntAsync(
- connection,
- BuildPendingCountSql(),
- [
- ("pendingStatus", (int)StoreAuditStatus.Pending)
- ],
- token);
- var overdueCount = await ExecuteScalarIntAsync(
- connection,
- BuildOverdueCountSql(),
- [
- ("pendingStatus", (int)StoreAuditStatus.Pending),
- ("overdueDeadline", overdueDeadline)
- ],
- token);
-
- // 2.2 (空行后) 读取通过/驳回数量
- var (approvedCount, rejectedCount) = await QueryApproveRejectCountsAsync(
- connection,
- dateFrom,
- dateToExclusive,
- token);
-
- // 2.3 (空行后) 读取平均处理时长
- var avgProcessingHours = await ExecuteScalarDoubleAsync(
- connection,
- BuildAvgProcessingSql(),
- [
- ("dateFrom", dateFrom),
- ("dateTo", dateToExclusive),
- ("approveAction", (int)StoreAuditAction.Approve),
- ("rejectAction", (int)StoreAuditAction.Reject)
- ],
- token);
-
- // 2.4 (空行后) 读取每日趋势
- var dailyTrend = await QueryDailyTrendAsync(
- connection,
- dateFrom,
- dateToExclusive,
- token);
-
- // 2.5 (空行后) 组装结果
- return new StoreAuditStatisticsDto
- {
- PendingCount = pendingCount,
- OverdueCount = overdueCount,
- ApprovedCount = approvedCount,
- RejectedCount = rejectedCount,
- AvgProcessingHours = avgProcessingHours,
- DailyTrend = dailyTrend
- };
- },
- cancellationToken);
- }
-
- private static async Task<(int ApprovedCount, int RejectedCount)> QueryApproveRejectCountsAsync(
- IDbConnection connection,
- DateTime dateFrom,
- DateTime dateTo,
- CancellationToken cancellationToken)
- {
- // 1. 查询通过/驳回统计
- await using var command = CreateCommand(
- connection,
- BuildApproveRejectCountSql(),
- [
- ("dateFrom", dateFrom),
- ("dateTo", dateTo),
- ("approveAction", (int)StoreAuditAction.Approve),
- ("rejectAction", (int)StoreAuditAction.Reject)
- ]);
-
- await using var reader = await command.ExecuteReaderAsync(cancellationToken);
- if (!await reader.ReadAsync(cancellationToken))
- {
- return (0, 0);
- }
-
- // 2. (空行后) 返回统计
- var approved = reader.IsDBNull(reader.GetOrdinal("ApprovedCount")) ? 0 : reader.GetInt32(reader.GetOrdinal("ApprovedCount"));
- var rejected = reader.IsDBNull(reader.GetOrdinal("RejectedCount")) ? 0 : reader.GetInt32(reader.GetOrdinal("RejectedCount"));
- return (approved, rejected);
- }
-
- private static async Task> QueryDailyTrendAsync(
- IDbConnection connection,
- DateTime dateFrom,
- DateTime dateTo,
- CancellationToken cancellationToken)
- {
- // 1. 查询每日趋势
- await using var command = CreateCommand(
- connection,
- BuildDailyTrendSql(),
- [
- ("dateFrom", dateFrom),
- ("dateTo", dateTo),
- ("submitAction", (int)StoreAuditAction.Submit),
- ("resubmitAction", (int)StoreAuditAction.Resubmit),
- ("approveAction", (int)StoreAuditAction.Approve),
- ("rejectAction", (int)StoreAuditAction.Reject)
- ]);
-
- await using var reader = await command.ExecuteReaderAsync(cancellationToken);
- var items = new List();
- if (!reader.HasRows)
- {
- return items;
- }
-
- // 2. (空行后) 映射趋势项
- var dateOrdinal = reader.GetOrdinal("Date");
- var submittedOrdinal = reader.GetOrdinal("SubmittedCount");
- var approvedOrdinal = reader.GetOrdinal("ApprovedCount");
- var rejectedOrdinal = reader.GetOrdinal("RejectedCount");
- while (await reader.ReadAsync(cancellationToken))
- {
- var date = reader.GetDateTime(dateOrdinal);
- items.Add(new StoreAuditDailyTrendDto
- {
- Date = DateOnly.FromDateTime(date),
- Submitted = reader.GetInt32(submittedOrdinal),
- Approved = reader.GetInt32(approvedOrdinal),
- Rejected = reader.GetInt32(rejectedOrdinal)
- });
- }
-
- // 3. (空行后) 返回趋势列表
- return items;
- }
-
- private static string BuildPendingCountSql()
- {
- return """
- select count(*)
- from public.stores s
- where s."DeletedAt" is null
- and s."AuditStatus" = @pendingStatus;
- """;
- }
-
- private static string BuildOverdueCountSql()
- {
- return """
- select count(*)
- from public.stores s
- where s."DeletedAt" is null
- and s."AuditStatus" = @pendingStatus
- and s."SubmittedAt" <= @overdueDeadline;
- """;
- }
-
- private static string BuildApproveRejectCountSql()
- {
- return """
- select
- sum(case when r."Action" = @approveAction then 1 else 0 end) as "ApprovedCount",
- sum(case when r."Action" = @rejectAction then 1 else 0 end) as "RejectedCount"
- from public.store_audit_records r
- where r."DeletedAt" is null
- and r."CreatedAt" >= @dateFrom
- and r."CreatedAt" < @dateTo
- and r."Action" in (@approveAction, @rejectAction);
- """;
- }
-
- private static string BuildAvgProcessingSql()
- {
- return """
- select
- avg(extract(epoch from (r."CreatedAt" - s."SubmittedAt")) / 3600.0) as "AvgHours"
- from public.store_audit_records r
- join public.stores s on s."Id" = r."StoreId" and s."DeletedAt" is null
- where r."DeletedAt" is null
- and r."Action" in (@approveAction, @rejectAction)
- and r."CreatedAt" >= @dateFrom
- and r."CreatedAt" < @dateTo
- and s."SubmittedAt" is not null;
- """;
- }
-
- private static string BuildDailyTrendSql()
- {
- return """
- select
- date_trunc('day', r."CreatedAt") as "Date",
- sum(case when r."Action" in (@submitAction, @resubmitAction) then 1 else 0 end) as "SubmittedCount",
- sum(case when r."Action" = @approveAction then 1 else 0 end) as "ApprovedCount",
- sum(case when r."Action" = @rejectAction then 1 else 0 end) as "RejectedCount"
- from public.store_audit_records r
- where r."DeletedAt" is null
- and r."CreatedAt" >= @dateFrom
- and r."CreatedAt" < @dateTo
- group by date_trunc('day', r."CreatedAt")
- order by date_trunc('day', r."CreatedAt");
- """;
- }
-
- private static async Task ExecuteScalarIntAsync(
- IDbConnection connection,
- string sql,
- (string Name, object? Value)[] parameters,
- CancellationToken cancellationToken)
- {
- await using var command = CreateCommand(connection, sql, parameters);
- var result = await command.ExecuteScalarAsync(cancellationToken);
- return result is null or DBNull ? 0 : Convert.ToInt32(result);
- }
-
- private static async Task ExecuteScalarDoubleAsync(
- IDbConnection connection,
- string sql,
- (string Name, object? Value)[] parameters,
- CancellationToken cancellationToken)
- {
- await using var command = CreateCommand(connection, sql, parameters);
- var result = await command.ExecuteScalarAsync(cancellationToken);
- return result is null or DBNull ? 0d : Convert.ToDouble(result);
- }
-
- private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
- {
- var command = connection.CreateCommand();
- command.CommandText = sql;
-
- // 1. (空行后) 绑定参数
- foreach (var (name, value) in parameters)
- {
- var p = command.CreateParameter();
- p.ParameterName = name;
- p.Value = value ?? DBNull.Value;
- command.Parameters.Add(p);
- }
-
- return (DbCommand)command;
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListPendingStoreAuditsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListPendingStoreAuditsQueryHandler.cs
deleted file mode 100644
index 1973112..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListPendingStoreAuditsQueryHandler.cs
+++ /dev/null
@@ -1,250 +0,0 @@
-using System.Data;
-using System.Data.Common;
-using MediatR;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-using TakeoutSaaS.Application.App.StoreAudits.Queries;
-using TakeoutSaaS.Domain.Stores.Enums;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Data;
-using TakeoutSaaS.Shared.Abstractions.Results;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
-
-///
-/// 待审核门店列表查询处理器。
-///
-public sealed class ListPendingStoreAuditsQueryHandler(
- IDapperExecutor dapperExecutor)
- : IRequestHandler>
-{
- ///
- public async Task> Handle(ListPendingStoreAuditsQuery request, CancellationToken cancellationToken)
- {
- // 1. 参数规范化
- var page = request.Page <= 0 ? 1 : request.Page;
- var pageSize = request.PageSize is <= 0 or > 200 ? 20 : request.PageSize;
- var keyword = string.IsNullOrWhiteSpace(request.Keyword) ? null : request.Keyword.Trim();
- var offset = (page - 1) * pageSize;
- var now = DateTime.UtcNow;
- var overdueDeadline = now.AddDays(-7);
-
- // 2. (空行后) 排序白名单
- var orderBy = request.SortBy?.Trim() switch
- {
- "StoreName" => "s.\"Name\"",
- "MerchantName" => "m.\"BrandName\"",
- "TenantName" => "t.\"Name\"",
- "SubmittedAt" => "s.\"SubmittedAt\"",
- _ => "s.\"SubmittedAt\""
- };
-
- // 3. (空行后) 执行查询
- return await dapperExecutor.QueryAsync(
- DatabaseConstants.AppDataSource,
- DatabaseConnectionRole.Read,
- async (connection, token) =>
- {
- // 3.1 统计总数
- var total = await ExecuteScalarIntAsync(
- connection,
- BuildCountSql(),
- [
- ("tenantId", request.TenantId),
- ("keyword", keyword),
- ("submittedFrom", request.SubmittedFrom),
- ("submittedTo", request.SubmittedTo),
- ("overdueOnly", request.OverdueOnly),
- ("overdueDeadline", overdueDeadline),
- ("pendingStatus", (int)StoreAuditStatus.Pending)
- ],
- token);
-
- // 3.2 (空行后) 查询列表
- var listSql = BuildListSql(orderBy, request.SortDesc);
- await using var listCommand = CreateCommand(
- connection,
- listSql,
- [
- ("tenantId", request.TenantId),
- ("keyword", keyword),
- ("submittedFrom", request.SubmittedFrom),
- ("submittedTo", request.SubmittedTo),
- ("overdueOnly", request.OverdueOnly),
- ("overdueDeadline", overdueDeadline),
- ("pendingStatus", (int)StoreAuditStatus.Pending),
- ("offset", offset),
- ("limit", pageSize)
- ]);
-
- await using var reader = await listCommand.ExecuteReaderAsync(token);
-
- // 3.3 (空行后) 读取并映射
- var items = new List();
- if (!reader.HasRows)
- {
- return new PagedResult(items, page, pageSize, total);
- }
-
- // 3.3.1 (空行后) 初始化字段序号
- var storeIdOrdinal = reader.GetOrdinal("StoreId");
- var storeNameOrdinal = reader.GetOrdinal("StoreName");
- var storeCodeOrdinal = reader.GetOrdinal("StoreCode");
- var tenantIdOrdinal = reader.GetOrdinal("TenantId");
- var tenantNameOrdinal = reader.GetOrdinal("TenantName");
- var merchantIdOrdinal = reader.GetOrdinal("MerchantId");
- var merchantNameOrdinal = reader.GetOrdinal("MerchantName");
- var signboardOrdinal = reader.GetOrdinal("SignboardImageUrl");
- var provinceOrdinal = reader.GetOrdinal("Province");
- var cityOrdinal = reader.GetOrdinal("City");
- var districtOrdinal = reader.GetOrdinal("District");
- var addressOrdinal = reader.GetOrdinal("Address");
- var ownershipOrdinal = reader.GetOrdinal("OwnershipType");
- var submittedAtOrdinal = reader.GetOrdinal("SubmittedAt");
- var qualificationCountOrdinal = reader.GetOrdinal("QualificationCount");
-
- while (await reader.ReadAsync(token))
- {
- DateTime? submittedAt = reader.IsDBNull(submittedAtOrdinal)
- ? null
- : reader.GetDateTime(submittedAtOrdinal);
- var waitingDays = submittedAt.HasValue
- ? (int)Math.Floor((now - submittedAt.Value).TotalDays)
- : 0;
- if (waitingDays < 0)
- {
- waitingDays = 0;
- }
-
- // 3.3.2 (空行后) 组装地址信息
- var province = reader.IsDBNull(provinceOrdinal) ? null : reader.GetString(provinceOrdinal);
- var city = reader.IsDBNull(cityOrdinal) ? null : reader.GetString(cityOrdinal);
- var district = reader.IsDBNull(districtOrdinal) ? null : reader.GetString(districtOrdinal);
- var address = reader.IsDBNull(addressOrdinal) ? null : reader.GetString(addressOrdinal);
- var fullAddress = string.Concat(
- province ?? string.Empty,
- city ?? string.Empty,
- district ?? string.Empty,
- address ?? string.Empty);
-
- items.Add(new PendingStoreAuditDto
- {
- StoreId = reader.GetInt64(storeIdOrdinal),
- StoreName = reader.GetString(storeNameOrdinal),
- StoreCode = reader.GetString(storeCodeOrdinal),
- TenantId = reader.GetInt64(tenantIdOrdinal),
- TenantName = reader.GetString(tenantNameOrdinal),
- MerchantId = reader.GetInt64(merchantIdOrdinal),
- MerchantName = reader.GetString(merchantNameOrdinal),
- SignboardImageUrl = reader.IsDBNull(signboardOrdinal) ? null : reader.GetString(signboardOrdinal),
- FullAddress = fullAddress,
- OwnershipType = (StoreOwnershipType)reader.GetInt32(ownershipOrdinal),
- SubmittedAt = submittedAt,
- WaitingDays = waitingDays,
- IsOverdue = submittedAt.HasValue && submittedAt.Value <= overdueDeadline,
- QualificationCount = reader.GetInt32(qualificationCountOrdinal)
- });
- }
-
- // 3.4 (空行后) 返回分页结果
- return new PagedResult(items, page, pageSize, total);
- },
- cancellationToken);
- }
-
- private static string BuildCountSql()
- {
- return """
- select count(*)
- from public.stores s
- join public.merchants m on m."Id" = s."MerchantId" and m."DeletedAt" is null
- join public.tenants t on t."Id" = s."TenantId" and t."DeletedAt" is null
- where s."DeletedAt" is null
- and s."AuditStatus" = @pendingStatus
- and (@tenantId::bigint is null or s."TenantId" = @tenantId)
- and (
- @keyword::text is null
- or s."Name" ilike ('%' || @keyword::text || '%')
- or s."Code" ilike ('%' || @keyword::text || '%')
- or m."BrandName" ilike ('%' || @keyword::text || '%')
- )
- and (@submittedFrom::timestamp with time zone is null or s."SubmittedAt" >= @submittedFrom)
- and (@submittedTo::timestamp with time zone is null or s."SubmittedAt" <= @submittedTo)
- and (@overdueOnly::boolean = false or s."SubmittedAt" <= @overdueDeadline);
- """;
- }
-
- private static string BuildListSql(string orderBy, bool sortDesc)
- {
- var direction = sortDesc ? "desc" : "asc";
-
- // 1. (空行后) 构造列表 SQL
- return $"""
- select
- s."Id" as "StoreId",
- s."Name" as "StoreName",
- s."Code" as "StoreCode",
- s."TenantId",
- t."Name" as "TenantName",
- s."MerchantId",
- m."BrandName" as "MerchantName",
- s."SignboardImageUrl",
- s."Province",
- s."City",
- s."District",
- s."Address",
- s."OwnershipType",
- s."SubmittedAt",
- (
- select count(1)
- from public.store_qualifications q
- where q."StoreId" = s."Id" and q."DeletedAt" is null
- ) as "QualificationCount"
- from public.stores s
- join public.merchants m on m."Id" = s."MerchantId" and m."DeletedAt" is null
- join public.tenants t on t."Id" = s."TenantId" and t."DeletedAt" is null
- where s."DeletedAt" is null
- and s."AuditStatus" = @pendingStatus
- and (@tenantId::bigint is null or s."TenantId" = @tenantId)
- and (
- @keyword::text is null
- or s."Name" ilike ('%' || @keyword::text || '%')
- or s."Code" ilike ('%' || @keyword::text || '%')
- or m."BrandName" ilike ('%' || @keyword::text || '%')
- )
- and (@submittedFrom::timestamp with time zone is null or s."SubmittedAt" >= @submittedFrom)
- and (@submittedTo::timestamp with time zone is null or s."SubmittedAt" <= @submittedTo)
- and (@overdueOnly::boolean = false or s."SubmittedAt" <= @overdueDeadline)
- order by {orderBy} {direction}
- offset @offset
- limit @limit;
- """;
- }
-
- private static async Task ExecuteScalarIntAsync(
- IDbConnection connection,
- string sql,
- (string Name, object? Value)[] parameters,
- CancellationToken cancellationToken)
- {
- await using var command = CreateCommand(connection, sql, parameters);
- var result = await command.ExecuteScalarAsync(cancellationToken);
- return result is null or DBNull ? 0 : Convert.ToInt32(result);
- }
-
- private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
- {
- var command = connection.CreateCommand();
- command.CommandText = sql;
-
- // 1. (空行后) 绑定参数
- foreach (var (name, value) in parameters)
- {
- var p = command.CreateParameter();
- p.ParameterName = name;
- p.Value = value ?? DBNull.Value;
- command.Parameters.Add(p);
- }
-
- return (DbCommand)command;
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListStoreAuditRecordsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListStoreAuditRecordsQueryHandler.cs
deleted file mode 100644
index 8731d0b..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ListStoreAuditRecordsQueryHandler.cs
+++ /dev/null
@@ -1,166 +0,0 @@
-using System.Data;
-using System.Data.Common;
-using MediatR;
-using TakeoutSaaS.Application.App.StoreAudits;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-using TakeoutSaaS.Application.App.StoreAudits.Queries;
-using TakeoutSaaS.Domain.Stores.Enums;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Data;
-using TakeoutSaaS.Shared.Abstractions.Results;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
-
-///
-/// 门店审核记录查询处理器。
-///
-public sealed class ListStoreAuditRecordsQueryHandler(
- IDapperExecutor dapperExecutor)
- : IRequestHandler>
-{
- ///
- public async Task> Handle(ListStoreAuditRecordsQuery request, CancellationToken cancellationToken)
- {
- // 1. 参数规范化
- var page = request.Page <= 0 ? 1 : request.Page;
- var pageSize = request.PageSize is <= 0 or > 200 ? 20 : request.PageSize;
- var offset = (page - 1) * pageSize;
-
- // 2. (空行后) 查询审核记录
- return await dapperExecutor.QueryAsync(
- DatabaseConstants.AppDataSource,
- DatabaseConnectionRole.Read,
- async (connection, token) =>
- {
- // 2.1 统计总数
- var total = await ExecuteScalarIntAsync(
- connection,
- BuildCountSql(),
- [
- ("storeId", request.StoreId)
- ],
- token);
-
- // 2.2 (空行后) 查询列表
- await using var listCommand = CreateCommand(
- connection,
- BuildListSql(),
- [
- ("storeId", request.StoreId),
- ("offset", offset),
- ("limit", pageSize)
- ]);
-
- await using var reader = await listCommand.ExecuteReaderAsync(token);
-
- // 2.3 (空行后) 映射列表
- var items = new List();
- if (!reader.HasRows)
- {
- return new PagedResult(items, page, pageSize, total);
- }
-
- // 2.3.1 (空行后) 初始化字段序号
- var idOrdinal = reader.GetOrdinal("Id");
- var actionOrdinal = reader.GetOrdinal("Action");
- var operatorIdOrdinal = reader.GetOrdinal("OperatorId");
- var operatorNameOrdinal = reader.GetOrdinal("OperatorName");
- var previousStatusOrdinal = reader.GetOrdinal("PreviousStatus");
- var newStatusOrdinal = reader.GetOrdinal("NewStatus");
- var rejectionReasonIdOrdinal = reader.GetOrdinal("RejectionReasonId");
- var rejectionReasonOrdinal = reader.GetOrdinal("RejectionReason");
- var remarksOrdinal = reader.GetOrdinal("Remarks");
- var createdAtOrdinal = reader.GetOrdinal("CreatedAt");
-
- while (await reader.ReadAsync(token))
- {
- var action = (StoreAuditAction)reader.GetInt32(actionOrdinal);
- items.Add(new StoreAuditRecordDto
- {
- Id = reader.GetInt64(idOrdinal),
- Action = action,
- ActionName = StoreAuditActionNameResolver.Resolve(action),
- OperatorId = reader.IsDBNull(operatorIdOrdinal) ? null : reader.GetInt64(operatorIdOrdinal),
- OperatorName = reader.GetString(operatorNameOrdinal),
- PreviousStatus = reader.IsDBNull(previousStatusOrdinal)
- ? null
- : (StoreAuditStatus)reader.GetInt32(previousStatusOrdinal),
- NewStatus = (StoreAuditStatus)reader.GetInt32(newStatusOrdinal),
- RejectionReasonId = reader.IsDBNull(rejectionReasonIdOrdinal)
- ? null
- : reader.GetInt64(rejectionReasonIdOrdinal),
- RejectionReasonText = reader.IsDBNull(rejectionReasonOrdinal)
- ? null
- : reader.GetString(rejectionReasonOrdinal),
- Remark = reader.IsDBNull(remarksOrdinal) ? null : reader.GetString(remarksOrdinal),
- CreatedAt = reader.GetDateTime(createdAtOrdinal)
- });
- }
-
- // 2.4 (空行后) 返回分页结果
- return new PagedResult(items, page, pageSize, total);
- },
- cancellationToken);
- }
-
- private static string BuildCountSql()
- {
- return """
- select count(*)
- from public.store_audit_records r
- where r."DeletedAt" is null
- and r."StoreId" = @storeId;
- """;
- }
-
- private static string BuildListSql()
- {
- return """
- select
- r."Id",
- r."Action",
- r."OperatorId",
- r."OperatorName",
- r."PreviousStatus",
- r."NewStatus",
- r."RejectionReasonId",
- r."RejectionReason",
- r."Remarks",
- r."CreatedAt"
- from public.store_audit_records r
- where r."DeletedAt" is null
- and r."StoreId" = @storeId
- order by r."CreatedAt" desc
- offset @offset
- limit @limit;
- """;
- }
-
- private static async Task ExecuteScalarIntAsync(
- IDbConnection connection,
- string sql,
- (string Name, object? Value)[] parameters,
- CancellationToken cancellationToken)
- {
- await using var command = CreateCommand(connection, sql, parameters);
- var result = await command.ExecuteScalarAsync(cancellationToken);
- return result is null or DBNull ? 0 : Convert.ToInt32(result);
- }
-
- private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
- {
- var command = connection.CreateCommand();
- command.CommandText = sql;
-
- // 1. (空行后) 绑定参数
- foreach (var (name, value) in parameters)
- {
- var p = command.CreateParameter();
- p.ParameterName = name;
- p.Value = value ?? DBNull.Value;
- command.Parameters.Add(p);
- }
-
- return (DbCommand)command;
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/RejectStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/RejectStoreCommandHandler.cs
deleted file mode 100644
index fdc6ec5..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/RejectStoreCommandHandler.cs
+++ /dev/null
@@ -1,157 +0,0 @@
-using System.Data;
-using System.Data.Common;
-using MediatR;
-using Microsoft.Extensions.Logging;
-using TakeoutSaaS.Application.App.StoreAudits.Commands;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-using TakeoutSaaS.Domain.Stores.Entities;
-using TakeoutSaaS.Domain.Stores.Enums;
-using TakeoutSaaS.Domain.Stores.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Data;
-using TakeoutSaaS.Shared.Abstractions.Exceptions;
-using TakeoutSaaS.Shared.Abstractions.Security;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
-
-///
-/// 审核驳回处理器。
-///
-public sealed class RejectStoreCommandHandler(
- IStoreRepository storeRepository,
- IDapperExecutor dapperExecutor,
- ICurrentUserAccessor currentUserAccessor,
- ILogger logger)
- : IRequestHandler
-{
- ///
- public async Task Handle(RejectStoreCommand request, CancellationToken cancellationToken)
- {
- // 1. 获取门店快照
- var snapshot = await QueryStoreSnapshotAsync(request.StoreId, cancellationToken);
- if (!snapshot.HasValue)
- {
- throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
- }
-
- // 1.1 (空行后) 校验审核状态
- if (snapshot.Value.AuditStatus != StoreAuditStatus.Pending)
- {
- throw new BusinessException(ErrorCodes.Conflict, "门店不处于待审核状态");
- }
-
- // 2. (空行后) 获取门店实体
- var store = await storeRepository.FindByIdAsync(request.StoreId, snapshot.Value.TenantId, cancellationToken);
- if (store is null)
- {
- throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
- }
-
- // 3. (空行后) 更新状态并记录审核
- var previousStatus = store.AuditStatus;
- store.AuditStatus = StoreAuditStatus.Rejected;
- store.RejectionReason = request.RejectionReasonText;
- store.BusinessStatus = StoreBusinessStatus.Resting;
-
- await storeRepository.UpdateStoreAsync(store, cancellationToken);
- await storeRepository.AddAuditRecordAsync(new StoreAuditRecord
- {
- StoreId = store.Id,
- Action = StoreAuditAction.Reject,
- PreviousStatus = previousStatus,
- NewStatus = store.AuditStatus,
- OperatorId = ResolveOperatorId(),
- OperatorName = ResolveOperatorName(),
- RejectionReasonId = request.RejectionReasonId,
- RejectionReason = request.RejectionReasonText,
- Remarks = request.Remark
- }, cancellationToken);
- await storeRepository.SaveChangesAsync(cancellationToken);
- logger.LogInformation("门店 {StoreId} 审核驳回", store.Id);
-
- // 4. (空行后) 返回结果
- return new StoreAuditActionResultDto
- {
- StoreId = store.Id,
- AuditStatus = store.AuditStatus,
- BusinessStatus = store.BusinessStatus,
- RejectionReason = store.RejectionReason,
- Message = "已驳回,租户可修改后重新提交"
- };
- }
-
- private async Task<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?> QueryStoreSnapshotAsync(
- long storeId,
- CancellationToken cancellationToken)
- {
- // 1. 查询门店基础字段
- return await dapperExecutor.QueryAsync<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?>(
- DatabaseConstants.AppDataSource,
- DatabaseConnectionRole.Read,
- async (connection, token) =>
- {
- await using var command = CreateCommand(
- connection,
- BuildStoreSnapshotSql(),
- [
- ("storeId", storeId)
- ]);
-
- // 1.1 (空行后) 执行查询
- await using var reader = await command.ExecuteReaderAsync(token);
- if (!await reader.ReadAsync(token))
- {
- return null;
- }
-
- // 1.2 (空行后) 返回快照
- return (
- reader.GetInt64(reader.GetOrdinal("TenantId")),
- (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")),
- (StoreBusinessStatus)reader.GetInt32(reader.GetOrdinal("BusinessStatus")));
- },
- cancellationToken);
- }
-
- private long? ResolveOperatorId()
- {
- var id = currentUserAccessor.UserId;
- return id == 0 ? null : id;
- }
-
- private string ResolveOperatorName()
- {
- var id = currentUserAccessor.UserId;
- return id == 0 ? "system" : $"user:{id}";
- }
-
- private static string BuildStoreSnapshotSql()
- {
- return """
- select
- s."TenantId",
- s."AuditStatus",
- s."BusinessStatus"
- from public.stores s
- where s."DeletedAt" is null
- and s."Id" = @storeId;
- """;
- }
-
- private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
- {
- var command = connection.CreateCommand();
- command.CommandText = sql;
-
- // 1. (空行后) 绑定参数
- foreach (var (name, value) in parameters)
- {
- var p = command.CreateParameter();
- p.ParameterName = name;
- p.Value = value ?? DBNull.Value;
- command.Parameters.Add(p);
- }
-
- return (DbCommand)command;
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ReopenStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ReopenStoreCommandHandler.cs
deleted file mode 100644
index a02d535..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Handlers/ReopenStoreCommandHandler.cs
+++ /dev/null
@@ -1,160 +0,0 @@
-using System.Data;
-using System.Data.Common;
-using MediatR;
-using Microsoft.Extensions.Logging;
-using TakeoutSaaS.Application.App.StoreAudits.Commands;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-using TakeoutSaaS.Domain.Stores.Entities;
-using TakeoutSaaS.Domain.Stores.Enums;
-using TakeoutSaaS.Domain.Stores.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Data;
-using TakeoutSaaS.Shared.Abstractions.Exceptions;
-using TakeoutSaaS.Shared.Abstractions.Security;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
-
-///
-/// 解除强制关闭处理器。
-///
-public sealed class ReopenStoreCommandHandler(
- IStoreRepository storeRepository,
- IDapperExecutor dapperExecutor,
- ICurrentUserAccessor currentUserAccessor,
- ILogger logger)
- : IRequestHandler
-{
- ///
- public async Task Handle(ReopenStoreCommand request, CancellationToken cancellationToken)
- {
- // 1. 获取门店快照
- var snapshot = await QueryStoreSnapshotAsync(request.StoreId, cancellationToken);
- if (!snapshot.HasValue)
- {
- throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
- }
-
- // 1.1 (空行后) 校验审核与经营状态
- if (snapshot.Value.AuditStatus != StoreAuditStatus.Activated)
- {
- throw new BusinessException(ErrorCodes.Conflict, "门店未激活,无法解除关闭");
- }
-
- if (snapshot.Value.BusinessStatus != StoreBusinessStatus.ForceClosed)
- {
- throw new BusinessException(ErrorCodes.Conflict, "门店未处于强制关闭状态");
- }
-
- // 2. (空行后) 获取门店实体
- var store = await storeRepository.FindByIdAsync(request.StoreId, snapshot.Value.TenantId, cancellationToken);
- if (store is null)
- {
- throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
- }
-
- // 3. (空行后) 更新状态并记录风控
- store.BusinessStatus = StoreBusinessStatus.Resting;
- store.ClosureReason = null;
- store.ClosureReasonText = null;
- store.ForceCloseReason = null;
- store.ForceClosedAt = null;
-
- await storeRepository.UpdateStoreAsync(store, cancellationToken);
- await storeRepository.AddAuditRecordAsync(new StoreAuditRecord
- {
- StoreId = store.Id,
- Action = StoreAuditAction.Reopen,
- PreviousStatus = store.AuditStatus,
- NewStatus = store.AuditStatus,
- OperatorId = ResolveOperatorId(),
- OperatorName = ResolveOperatorName(),
- Remarks = request.Remark
- }, cancellationToken);
- await storeRepository.SaveChangesAsync(cancellationToken);
- logger.LogInformation("门店 {StoreId} 解除强制关闭", store.Id);
-
- // 4. (空行后) 返回结果
- return new StoreAuditActionResultDto
- {
- StoreId = store.Id,
- AuditStatus = store.AuditStatus,
- BusinessStatus = store.BusinessStatus,
- Message = "强制关闭已解除,门店恢复为休息中状态"
- };
- }
-
- private async Task<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?> QueryStoreSnapshotAsync(
- long storeId,
- CancellationToken cancellationToken)
- {
- // 1. 查询门店基础字段
- return await dapperExecutor.QueryAsync<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?>(
- DatabaseConstants.AppDataSource,
- DatabaseConnectionRole.Read,
- async (connection, token) =>
- {
- await using var command = CreateCommand(
- connection,
- BuildStoreSnapshotSql(),
- [
- ("storeId", storeId)
- ]);
-
- // 1.1 (空行后) 执行查询
- await using var reader = await command.ExecuteReaderAsync(token);
- if (!await reader.ReadAsync(token))
- {
- return null;
- }
-
- // 1.2 (空行后) 返回快照
- return (
- reader.GetInt64(reader.GetOrdinal("TenantId")),
- (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")),
- (StoreBusinessStatus)reader.GetInt32(reader.GetOrdinal("BusinessStatus")));
- },
- cancellationToken);
- }
-
- private long? ResolveOperatorId()
- {
- var id = currentUserAccessor.UserId;
- return id == 0 ? null : id;
- }
-
- private string ResolveOperatorName()
- {
- var id = currentUserAccessor.UserId;
- return id == 0 ? "system" : $"user:{id}";
- }
-
- private static string BuildStoreSnapshotSql()
- {
- return """
- select
- s."TenantId",
- s."AuditStatus",
- s."BusinessStatus"
- from public.stores s
- where s."DeletedAt" is null
- and s."Id" = @storeId;
- """;
- }
-
- private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
- {
- var command = connection.CreateCommand();
- command.CommandText = sql;
-
- // 1. (空行后) 绑定参数
- foreach (var (name, value) in parameters)
- {
- var p = command.CreateParameter();
- p.ParameterName = name;
- p.Value = value ?? DBNull.Value;
- command.Parameters.Add(p);
- }
-
- return (DbCommand)command;
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditDetailQuery.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditDetailQuery.cs
deleted file mode 100644
index 17e528c..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditDetailQuery.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Queries;
-
-///
-/// 获取门店审核详情查询。
-///
-public sealed record GetStoreAuditDetailQuery : IRequest
-{
- ///
- /// 门店 ID。
- ///
- public long StoreId { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditStatisticsQuery.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditStatisticsQuery.cs
deleted file mode 100644
index c756851..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/GetStoreAuditStatisticsQuery.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Queries;
-
-///
-/// 获取审核统计查询。
-///
-public sealed record GetStoreAuditStatisticsQuery : IRequest
-{
- ///
- /// 起始日期。
- ///
- public DateTime? DateFrom { get; init; }
-
- ///
- /// 截止日期。
- ///
- public DateTime? DateTo { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListPendingStoreAuditsQuery.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListPendingStoreAuditsQuery.cs
deleted file mode 100644
index a7f2748..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListPendingStoreAuditsQuery.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-using TakeoutSaaS.Shared.Abstractions.Results;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Queries;
-
-///
-/// 查询待审核门店列表。
-///
-public sealed record ListPendingStoreAuditsQuery : IRequest>
-{
- ///
- /// 租户 ID。
- ///
- public long? TenantId { get; init; }
-
- ///
- /// 关键词。
- ///
- public string? Keyword { get; init; }
-
- ///
- /// 提交起始时间。
- ///
- public DateTime? SubmittedFrom { get; init; }
-
- ///
- /// 提交截止时间。
- ///
- public DateTime? SubmittedTo { get; init; }
-
- ///
- /// 是否只显示超时。
- ///
- public bool OverdueOnly { get; init; }
-
- ///
- /// 页码。
- ///
- public int Page { get; init; } = 1;
-
- ///
- /// 每页数量。
- ///
- public int PageSize { get; init; } = 20;
-
- ///
- /// 排序字段。
- ///
- public string? SortBy { get; init; }
-
- ///
- /// 是否降序。
- ///
- public bool SortDesc { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListStoreAuditRecordsQuery.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListStoreAuditRecordsQuery.cs
deleted file mode 100644
index ac05c3a..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Queries/ListStoreAuditRecordsQuery.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.StoreAudits.Dto;
-using TakeoutSaaS.Shared.Abstractions.Results;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Queries;
-
-///
-/// 查询门店审核记录。
-///
-public sealed record ListStoreAuditRecordsQuery : IRequest>
-{
- ///
- /// 门店 ID。
- ///
- public long StoreId { get; init; }
-
- ///
- /// 页码。
- ///
- public int Page { get; init; } = 1;
-
- ///
- /// 每页数量。
- ///
- public int PageSize { get; init; } = 20;
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/StoreAuditActionNameResolver.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/StoreAuditActionNameResolver.cs
deleted file mode 100644
index c4f35f4..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/StoreAuditActionNameResolver.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using TakeoutSaaS.Domain.Stores.Enums;
-
-namespace TakeoutSaaS.Application.App.StoreAudits;
-
-///
-/// 门店审核动作名称解析器。
-///
-public static class StoreAuditActionNameResolver
-{
- ///
- /// 获取动作名称。
- ///
- /// 审核动作。
- /// 动作名称。
- public static string Resolve(StoreAuditAction action) => action switch
- {
- StoreAuditAction.Submit => "提交审核",
- StoreAuditAction.Resubmit => "重新提交",
- StoreAuditAction.Approve => "审核通过",
- StoreAuditAction.Reject => "审核驳回",
- StoreAuditAction.ForceClose => "强制关闭",
- StoreAuditAction.Reopen => "解除关闭",
- StoreAuditAction.AutoActivate => "自动激活",
- _ => "未知操作"
- };
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ApproveStoreCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ApproveStoreCommandValidator.cs
deleted file mode 100644
index fddba8b..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ApproveStoreCommandValidator.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using FluentValidation;
-using TakeoutSaaS.Application.App.StoreAudits.Commands;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Validators;
-
-///
-/// 审核通过命令验证器。
-///
-public sealed class ApproveStoreCommandValidator : AbstractValidator
-{
- ///
- /// 初始化验证规则。
- ///
- public ApproveStoreCommandValidator()
- {
- RuleFor(x => x.StoreId).GreaterThan(0);
- RuleFor(x => x.Remark).MaximumLength(500);
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ForceCloseStoreCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ForceCloseStoreCommandValidator.cs
deleted file mode 100644
index 31e3db9..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ForceCloseStoreCommandValidator.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using FluentValidation;
-using TakeoutSaaS.Application.App.StoreAudits.Commands;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Validators;
-
-///
-/// 强制关闭命令验证器。
-///
-public sealed class ForceCloseStoreCommandValidator : AbstractValidator
-{
- ///
- /// 初始化验证规则。
- ///
- public ForceCloseStoreCommandValidator()
- {
- RuleFor(x => x.StoreId).GreaterThan(0);
- RuleFor(x => x.Reason).NotEmpty().MaximumLength(500);
- RuleFor(x => x.Remark).MaximumLength(500);
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/RejectStoreCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/RejectStoreCommandValidator.cs
deleted file mode 100644
index 8cf278e..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/RejectStoreCommandValidator.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using FluentValidation;
-using TakeoutSaaS.Application.App.StoreAudits.Commands;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Validators;
-
-///
-/// 审核驳回命令验证器。
-///
-public sealed class RejectStoreCommandValidator : AbstractValidator
-{
- ///
- /// 初始化验证规则。
- ///
- public RejectStoreCommandValidator()
- {
- RuleFor(x => x.StoreId).GreaterThan(0);
- RuleFor(x => x.RejectionReasonId).GreaterThan(0);
- RuleFor(x => x.RejectionReasonText).MaximumLength(500);
- RuleFor(x => x.Remark).MaximumLength(500);
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ReopenStoreCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ReopenStoreCommandValidator.cs
deleted file mode 100644
index e44fc46..0000000
--- a/src/Application/TakeoutSaaS.Application/App/StoreAudits/Validators/ReopenStoreCommandValidator.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using FluentValidation;
-using TakeoutSaaS.Application.App.StoreAudits.Commands;
-
-namespace TakeoutSaaS.Application.App.StoreAudits.Validators;
-
-///
-/// 解除强制关闭命令验证器。
-///
-public sealed class ReopenStoreCommandValidator : AbstractValidator
-{
- ///
- /// 初始化验证规则。
- ///
- public ReopenStoreCommandValidator()
- {
- RuleFor(x => x.StoreId).GreaterThan(0);
- RuleFor(x => x.Remark).MaximumLength(500);
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/BindInitialTenantSubscriptionCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/BindInitialTenantSubscriptionCommand.cs
deleted file mode 100644
index 56192f5..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/BindInitialTenantSubscriptionCommand.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 租户初次绑定订阅命令(默认 0 个月)。
-///
-public sealed record BindInitialTenantSubscriptionCommand : IRequest
-{
- ///
- /// 租户 ID(雪花算法)。
- ///
- [Required]
- public long TenantId { get; init; }
-
- ///
- /// 套餐 ID。
- ///
- [Required]
- public long TenantPackageId { get; init; }
-
- ///
- /// 是否自动续费。
- ///
- public bool AutoRenew { get; init; }
-}
-
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ChangeTenantSubscriptionPlanCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ChangeTenantSubscriptionPlanCommand.cs
deleted file mode 100644
index 5c052be..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ChangeTenantSubscriptionPlanCommand.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 套餐升降配命令。
-///
-public sealed record ChangeTenantSubscriptionPlanCommand : IRequest
-{
- ///
- /// 租户 ID(雪花算法)。
- ///
- [Required]
- public long TenantId { get; init; }
-
- ///
- /// 现有订阅 ID。
- ///
- [Required]
- public long TenantSubscriptionId { get; init; }
-
- ///
- /// 目标套餐 ID。
- ///
- [Required]
- public long TargetPackageId { get; init; }
-
- ///
- /// 是否立即生效,否则在下一结算周期生效。
- ///
- public bool Immediate { get; init; }
-
- ///
- /// 调整备注。
- ///
- public string? Notes { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ClaimTenantReviewCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ClaimTenantReviewCommand.cs
deleted file mode 100644
index 7d95631..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ClaimTenantReviewCommand.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 领取租户入驻审核命令。
-///
-public sealed record ClaimTenantReviewCommand : IRequest
-{
- ///
- /// 租户 ID(雪花算法)。
- ///
- [Required]
- public long TenantId { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAdminResetLinkTokenCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAdminResetLinkTokenCommand.cs
deleted file mode 100644
index 0005a64..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAdminResetLinkTokenCommand.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using MediatR;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 生成租户主管理员重置链接令牌命令(平台超级管理员使用)。
-///
-public sealed record CreateTenantAdminResetLinkTokenCommand : IRequest
-{
- ///
- /// 目标租户 ID。
- ///
- public required long TenantId { get; init; }
-}
-
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantBillingCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantBillingCommand.cs
deleted file mode 100644
index 1bc64a7..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantBillingCommand.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Domain.Tenants.Enums;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 创建租户账单命令。
-///
-public sealed record CreateTenantBillingCommand : IRequest
-{
- ///
- /// 租户 ID(雪花算法)。
- ///
- public long TenantId { get; init; }
-
- ///
- /// 账单编号。
- ///
- public string StatementNo { get; init; } = string.Empty;
-
- ///
- /// 计费周期开始时间(UTC)。
- ///
- public DateTime PeriodStart { get; init; }
-
- ///
- /// 计费周期结束时间(UTC)。
- ///
- public DateTime PeriodEnd { get; init; }
-
- ///
- /// 应付金额。
- ///
- public decimal AmountDue { get; init; }
-
- ///
- /// 已付金额。
- ///
- public decimal AmountPaid { get; init; }
-
- ///
- /// 账单状态。
- ///
- public TenantBillingStatus Status { get; init; } = TenantBillingStatus.Pending;
-
- ///
- /// 到期日(UTC)。
- ///
- public DateTime DueDate { get; init; }
-
- ///
- /// 账单明细 JSON。
- ///
- public string? LineItemsJson { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantManuallyCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantManuallyCommand.cs
deleted file mode 100644
index f7af18d..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantManuallyCommand.cs
+++ /dev/null
@@ -1,264 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Domain.Tenants.Enums;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 后台手动新增租户并直接入驻(创建租户 + 认证 + 订阅 + 管理员账号)。
-///
-public sealed record CreateTenantManuallyCommand : IRequest
-{
- ///
- /// 租户短编码,作为跨系统引用的唯一标识。
- ///
- [Required]
- [StringLength(64)]
- public string Code { get; init; } = string.Empty;
-
- ///
- /// 租户全称或品牌名称。
- ///
- [Required]
- [StringLength(128)]
- public string Name { get; init; } = string.Empty;
-
- ///
- /// 对外展示的简称。
- ///
- public string? ShortName { get; init; }
-
- ///
- /// 法人或公司主体名称。
- ///
- public string? LegalEntityName { get; init; }
-
- ///
- /// 所属行业,如餐饮、零售等。
- ///
- public string? Industry { get; init; }
-
- ///
- /// LOGO 图片地址。
- ///
- public string? LogoUrl { get; init; }
-
- ///
- /// 品牌海报或封面图。
- ///
- public string? CoverImageUrl { get; init; }
-
- ///
- /// 官网或主要宣传链接。
- ///
- public string? Website { get; init; }
-
- ///
- /// 所在国家/地区。
- ///
- public string? Country { get; init; }
-
- ///
- /// 所在省份或州。
- ///
- public string? Province { get; init; }
-
- ///
- /// 所在城市。
- ///
- public string? City { get; init; }
-
- ///
- /// 详细地址信息。
- ///
- public string? Address { get; init; }
-
- ///
- /// 主联系人姓名。
- ///
- public string? ContactName { get; init; }
-
- ///
- /// 主联系人电话(唯一)。
- ///
- public string? ContactPhone { get; init; }
-
- ///
- /// 主联系人邮箱。
- ///
- public string? ContactEmail { get; init; }
-
- ///
- /// 业务标签集合(逗号分隔)。
- ///
- public string? Tags { get; init; }
-
- ///
- /// 备注信息,用于运营记录特殊说明。
- ///
- public string? Remarks { get; init; }
-
- ///
- /// 最近一次暂停服务时间。
- ///
- public DateTime? SuspendedAt { get; init; }
-
- ///
- /// 暂停或终止的原因说明。
- ///
- public string? SuspensionReason { get; init; }
-
- ///
- /// 租户当前状态,默认 Active(直接入驻)。
- ///
- public TenantStatus TenantStatus { get; init; } = TenantStatus.Active;
-
- ///
- /// 购买套餐 ID。
- ///
- [Required]
- public long TenantPackageId { get; init; }
-
- ///
- /// 订阅时长(月)。
- ///
- [Range(1, int.MaxValue)]
- public int DurationMonths { get; init; } = 12;
-
- ///
- /// 是否自动续费。
- ///
- public bool AutoRenew { get; init; } = false;
-
- ///
- /// 订阅生效时间(UTC),为空则立即生效。
- ///
- public DateTime? SubscriptionEffectiveFrom { get; init; }
-
- ///
- /// 下次计费时间(UTC),为空则默认等于到期时间。
- ///
- public DateTime? NextBillingDate { get; init; }
-
- ///
- /// 订阅状态,默认 Active。
- ///
- public SubscriptionStatus SubscriptionStatus { get; init; } = SubscriptionStatus.Active;
-
- ///
- /// 预定下次切换的套餐 ID。
- ///
- public long? ScheduledPackageId { get; init; }
-
- ///
- /// 订阅备注。
- ///
- public string? SubscriptionNotes { get; init; }
-
- ///
- /// 实名状态,默认 Approved(直接通过)。
- ///
- public TenantVerificationStatus VerificationStatus { get; init; } = TenantVerificationStatus.Approved;
-
- ///
- /// 营业执照编号。
- ///
- public string? BusinessLicenseNumber { get; init; }
-
- ///
- /// 营业执照扫描件地址。
- ///
- public string? BusinessLicenseUrl { get; init; }
-
- ///
- /// 法人姓名。
- ///
- public string? LegalPersonName { get; init; }
-
- ///
- /// 法人身份证号。
- ///
- public string? LegalPersonIdNumber { get; init; }
-
- ///
- /// 法人身份证人像面图片地址。
- ///
- public string? LegalPersonIdFrontUrl { get; init; }
-
- ///
- /// 法人身份证国徽面图片地址。
- ///
- public string? LegalPersonIdBackUrl { get; init; }
-
- ///
- /// 对公账户户名。
- ///
- public string? BankAccountName { get; init; }
-
- ///
- /// 对公银行账号。
- ///
- public string? BankAccountNumber { get; init; }
-
- ///
- /// 开户行名称。
- ///
- public string? BankName { get; init; }
-
- ///
- /// 其他补充资料 JSON。
- ///
- public string? AdditionalDataJson { get; init; }
-
- ///
- /// 提交时间(UTC),为空则默认当前时间。
- ///
- public DateTime? SubmittedAt { get; init; }
-
- ///
- /// 审核时间(UTC),为空则默认当前时间。
- ///
- public DateTime? ReviewedAt { get; init; }
-
- ///
- /// 审核人姓名(展示用),为空则默认当前用户。
- ///
- public string? ReviewedByName { get; init; }
-
- ///
- /// 审核备注。
- ///
- public string? ReviewRemarks { get; init; }
-
- ///
- /// 租户管理员账号。
- ///
- [Required]
- [StringLength(128)]
- public string AdminAccount { get; init; } = string.Empty;
-
- ///
- /// 租户管理员显示名。
- ///
- [Required]
- [StringLength(128)]
- public string AdminDisplayName { get; init; } = string.Empty;
-
- ///
- /// 管理员初始密码(明文,仅用于创建时生成哈希,不会被持久化回传)。
- ///
- [Required]
- [StringLength(128, MinimumLength = 6)]
- public string AdminPassword { get; init; } = string.Empty;
-
- ///
- /// 管理员头像。
- ///
- public string? AdminAvatar { get; init; }
-
- ///
- /// 关联商户 ID(若有)。
- ///
- public long? AdminMerchantId { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantPackageCommand.cs
deleted file mode 100644
index 6f709ba..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantPackageCommand.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Domain.Tenants.Enums;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 创建租户套餐命令。
-///
-public sealed record CreateTenantPackageCommand : IRequest
-{
- ///
- /// 套餐名称。
- ///
- public string Name { get; init; } = string.Empty;
-
- ///
- /// 套餐描述。
- ///
- public string? Description { get; init; }
-
- ///
- /// 套餐类型。
- ///
- public TenantPackageType PackageType { get; init; } = TenantPackageType.Standard;
-
- ///
- /// 月付价格。
- ///
- public decimal? MonthlyPrice { get; init; }
-
- ///
- /// 年付价格。
- ///
- public decimal? YearlyPrice { get; init; }
-
- ///
- /// 最大门店数。
- ///
- public int? MaxStoreCount { get; init; }
-
- ///
- /// 最大账号数。
- ///
- public int? MaxAccountCount { get; init; }
-
- ///
- /// 存储上限(GB)。
- ///
- public int? MaxStorageGb { get; init; }
-
- ///
- /// 短信额度。
- ///
- public int? MaxSmsCredits { get; init; }
-
- ///
- /// 配送单上限。
- ///
- public int? MaxDeliveryOrders { get; init; }
-
- ///
- /// 权益明细 JSON。
- ///
- public string? FeaturePoliciesJson { get; init; }
-
- ///
- /// 是否仍启用(平台控制)。
- ///
- public bool IsActive { get; init; } = true;
-
- ///
- /// 是否对外可见(展示页/套餐列表可见性)。
- ///
- public bool IsPublicVisible { get; init; } = true;
-
- ///
- /// 是否允许新租户购买/选择(仅影响新购)。
- ///
- public bool IsAllowNewTenantPurchase { get; init; } = true;
-
- ///
- /// 发布状态(草稿/已发布)。
- ///
- public TenantPackagePublishStatus? PublishStatus { get; init; }
-
- ///
- /// 是否推荐展示(运营推荐标识)。
- ///
- public bool IsRecommended { get; init; }
-
- ///
- /// 套餐标签(用于展示与对比页)。
- ///
- public string[] Tags { get; init; } = [];
-
- ///
- /// 展示排序,数值越小越靠前。
- ///
- public int SortOrder { get; init; } = 0;
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantSubscriptionCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantSubscriptionCommand.cs
deleted file mode 100644
index b31a92d..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantSubscriptionCommand.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 新建或续费订阅。
-///
-public sealed record CreateTenantSubscriptionCommand : IRequest
-{
- ///
- /// 租户 ID(雪花算法)。
- ///
- [Required]
- public long TenantId { get; init; }
-
- ///
- /// 套餐 ID。
- ///
- [Required]
- public long TenantPackageId { get; init; }
-
- ///
- /// 订阅时长(月)。
- ///
- public int DurationMonths { get; init; }
-
- ///
- /// 是否自动续费。
- ///
- public bool AutoRenew { get; init; }
-
- ///
- /// 备注信息。
- ///
- public string? Notes { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/DeleteTenantPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/DeleteTenantPackageCommand.cs
deleted file mode 100644
index 9f46a76..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/DeleteTenantPackageCommand.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using MediatR;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 删除租户套餐命令。
-///
-public sealed record DeleteTenantPackageCommand : IRequest
-{
- ///
- /// 套餐 ID。
- ///
- public long TenantPackageId { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ExtendTenantSubscriptionCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ExtendTenantSubscriptionCommand.cs
deleted file mode 100644
index cd5c378..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ExtendTenantSubscriptionCommand.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 延期/赠送租户订阅时长(按当前订阅套餐续费)。
-///
-public sealed record ExtendTenantSubscriptionCommand : IRequest
-{
- ///
- /// 租户 ID(雪花算法)。
- ///
- [Required]
- public long TenantId { get; init; }
-
- ///
- /// 赠送/延期时长(月)。
- ///
- [Range(1, 120)]
- public int DurationMonths { get; init; }
-
- ///
- /// 备注信息。
- ///
- [MaxLength(256)]
- public string? Notes { get; init; }
-}
-
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ForceClaimTenantReviewCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ForceClaimTenantReviewCommand.cs
deleted file mode 100644
index c782b98..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ForceClaimTenantReviewCommand.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 强制接管租户入驻审核命令(仅超级管理员可用)。
-///
-public sealed record ForceClaimTenantReviewCommand : IRequest
-{
- ///
- /// 租户 ID(雪花算法)。
- ///
- [Required]
- public long TenantId { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/FreezeTenantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/FreezeTenantCommand.cs
deleted file mode 100644
index c8db6f6..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/FreezeTenantCommand.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 冻结租户(将租户状态置为暂停)。
-///
-public sealed record FreezeTenantCommand : IRequest
-{
- ///
- /// 租户 ID(雪花算法)。
- ///
- [Required]
- public long TenantId { get; init; }
-
- ///
- /// 冻结原因。
- ///
- [Required]
- [MaxLength(256)]
- public string Reason { get; init; } = string.Empty;
-}
-
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ImpersonateTenantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ImpersonateTenantCommand.cs
deleted file mode 100644
index 1ea701a..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ImpersonateTenantCommand.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.Identity.Contracts;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 伪装登录租户命令(平台超级管理员使用)。
-///
-public sealed record ImpersonateTenantCommand : IRequest
-{
- ///
- /// 目标租户 ID。
- ///
- public required long TenantId { get; init; }
-}
-
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkTenantBillingPaidCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkTenantBillingPaidCommand.cs
deleted file mode 100644
index e2c9f18..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkTenantBillingPaidCommand.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 标记租户账单已支付命令。
-///
-public sealed record MarkTenantBillingPaidCommand : IRequest
-{
- ///
- /// 租户 ID(雪花算法)。
- ///
- public long TenantId { get; init; }
-
- ///
- /// 账单 ID。
- ///
- public long BillingId { get; init; }
-
- ///
- /// 本次支付金额。
- ///
- public decimal AmountPaid { get; init; }
-
- ///
- /// 支付时间(UTC)。
- ///
- public DateTime PaidAt { get; init; } = DateTime.UtcNow;
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/RegisterTenantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/RegisterTenantCommand.cs
deleted file mode 100644
index d26a96b..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/RegisterTenantCommand.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 注册租户命令。
-///
-public sealed record RegisterTenantCommand : IRequest
-{
- ///
- /// 唯一租户编码。
- ///
- [Required]
- [StringLength(64)]
- public string Code { get; init; } = string.Empty;
-
- ///
- /// 租户名称。
- ///
- [Required]
- [StringLength(128)]
- public string Name { get; init; } = string.Empty;
-
- ///
- /// 租户简称。
- ///
- public string? ShortName { get; init; }
-
- ///
- /// 行业类型。
- ///
- public string? Industry { get; init; }
-
- ///
- /// 联系人姓名。
- ///
- public string? ContactName { get; init; }
-
- ///
- /// 联系人电话。
- ///
- public string? ContactPhone { get; init; }
-
- ///
- /// 联系人邮箱。
- ///
- public string? ContactEmail { get; init; }
-
- ///
- /// 购买套餐 ID。
- ///
- [Required]
- public long TenantPackageId { get; init; }
-
- ///
- /// 订阅时长(月),默认 12 个月。
- ///
- public int DurationMonths { get; init; } = 12;
-
- ///
- /// 是否自动续费。
- ///
- public bool AutoRenew { get; init; } = true;
-
- ///
- /// 生效时间(UTC),为空则立即生效。
- ///
- public DateTime? EffectiveFrom { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReleaseTenantReviewClaimCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReleaseTenantReviewClaimCommand.cs
deleted file mode 100644
index e6cb24d..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReleaseTenantReviewClaimCommand.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 释放租户入驻审核领取命令。
-///
-public sealed record ReleaseTenantReviewClaimCommand : IRequest
-{
- ///
- /// 租户 ID(雪花算法)。
- ///
- [Required]
- public long TenantId { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReviewTenantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReviewTenantCommand.cs
deleted file mode 100644
index 7e7a0ae..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ReviewTenantCommand.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Domain.Common.Enums;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 审核租户命令。
-///
-public sealed record ReviewTenantCommand : IRequest
-{
- ///
- /// 租户 ID(雪花算法)。
- ///
- [Required]
- public long TenantId { get; init; }
-
- ///
- /// 是否通过审核。
- ///
- public bool Approve { get; init; }
-
- ///
- /// 审核备注或拒绝原因。
- ///
- public string? Reason { get; init; }
-
- ///
- /// 审核通过后续费时长(月)。
- ///
- public int? RenewMonths { get; init; }
-
- ///
- /// 经营模式(审核通过时必填)。
- ///
- public OperatingMode? OperatingMode { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UnfreezeTenantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UnfreezeTenantCommand.cs
deleted file mode 100644
index 6d99553..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UnfreezeTenantCommand.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using MediatR;
-using System.ComponentModel.DataAnnotations;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 解冻租户(恢复租户状态)。
-///
-public sealed record UnfreezeTenantCommand : IRequest
-{
- ///
- /// 租户 ID(雪花算法)。
- ///
- [Required]
- public long TenantId { get; init; }
-
- ///
- /// 解冻备注(可选)。
- ///
- [MaxLength(256)]
- public string? Reason { get; init; }
-}
-
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantPackageCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantPackageCommand.cs
deleted file mode 100644
index 285a032..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/UpdateTenantPackageCommand.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Domain.Tenants.Enums;
-
-namespace TakeoutSaaS.Application.App.Tenants.Commands;
-
-///
-/// 更新租户套餐命令。
-///
-public sealed record UpdateTenantPackageCommand : IRequest
-{
- ///
- /// 套餐 ID。
- ///
- public long TenantPackageId { get; init; }
-
- ///
- /// 套餐名称。
- ///
- public string Name { get; init; } = string.Empty;
-
- ///
- /// 套餐描述。
- ///
- public string? Description { get; init; }
-
- ///
- /// 套餐类型。
- ///
- public TenantPackageType PackageType { get; init; } = TenantPackageType.Standard;
-
- ///
- /// 月付价格。
- ///
- public decimal? MonthlyPrice { get; init; }
-
- ///
- /// 年付价格。
- ///
- public decimal? YearlyPrice { get; init; }
-
- ///
- /// 最大门店数。
- ///
- public int? MaxStoreCount { get; init; }
-
- ///
- /// 最大账号数。
- ///
- public int? MaxAccountCount { get; init; }
-
- ///
- /// 存储上限(GB)。
- ///
- public int? MaxStorageGb { get; init; }
-
- ///
- /// 短信额度。
- ///
- public int? MaxSmsCredits { get; init; }
-
- ///
- /// 配送单上限。
- ///
- public int? MaxDeliveryOrders { get; init; }
-
- ///
- /// 权益明细 JSON。
- ///
- public string? FeaturePoliciesJson { get; init; }
-
- ///
- /// 是否仍启用(平台控制)。
- ///
- public bool IsActive { get; init; } = true;
-
- ///
- /// 是否对外可见(展示页/套餐列表可见性)。
- ///
- public bool IsPublicVisible { get; init; } = true;
-
- ///
- /// 是否允许新租户购买/选择(仅影响新购)。
- ///
- public bool IsAllowNewTenantPurchase { get; init; } = true;
-
- ///
- /// 发布状态(草稿/已发布)。
- ///
- public TenantPackagePublishStatus? PublishStatus { get; init; }
-
- ///
- /// 是否推荐展示(运营推荐标识)。
- ///
- public bool IsRecommended { get; init; }
-
- ///
- /// 套餐标签(用于展示与对比页)。
- ///
- public string[] Tags { get; init; } = [];
-
- ///
- /// 展示排序,数值越小越靠前。
- ///
- public int SortOrder { get; init; } = 0;
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantAuditLogDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantAuditLogDto.cs
deleted file mode 100644
index ff3f9dc..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantAuditLogDto.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System.Text.Json.Serialization;
-using TakeoutSaaS.Domain.Tenants.Enums;
-using TakeoutSaaS.Shared.Abstractions.Serialization;
-
-namespace TakeoutSaaS.Application.App.Tenants.Dto;
-
-///
-/// 租户审核日志 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/Dto/TenantDetailDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantDetailDto.cs
deleted file mode 100644
index 4b642ff..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantDetailDto.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-namespace TakeoutSaaS.Application.App.Tenants.Dto;
-
-///
-/// 租户详情 DTO。
-///
-public sealed class TenantDetailDto
-{
- ///
- /// 基础信息。
- ///
- public TenantDto Tenant { get; init; } = new();
-
- ///
- /// 实名信息。
- ///
- public TenantVerificationDto? Verification { get; init; }
-
- ///
- /// 当前订阅。
- ///
- public TenantSubscriptionDto? Subscription { get; init; }
-
- ///
- /// 当前套餐详情。
- ///
- public TenantPackageDto? Package { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantDto.cs
deleted file mode 100644
index a565e29..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantDto.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-using System.Text.Json.Serialization;
-using TakeoutSaaS.Domain.Common.Enums;
-using TakeoutSaaS.Domain.Tenants.Enums;
-using TakeoutSaaS.Shared.Abstractions.Serialization;
-
-namespace TakeoutSaaS.Application.App.Tenants.Dto;
-
-///
-/// 租户基础信息 DTO。
-///
-public sealed class TenantDto
-{
- ///
- /// 租户 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long Id { get; init; }
-
- ///
- /// 租户编码。
- ///
- public string Code { get; init; } = string.Empty;
-
- ///
- /// 名称。
- ///
- public string Name { get; init; } = string.Empty;
-
- ///
- /// 简称。
- ///
- public string? ShortName { get; init; }
-
- ///
- /// 联系人。
- ///
- public string? ContactName { get; init; }
-
- ///
- /// 联系电话。
- ///
- public string? ContactPhone { get; init; }
-
- ///
- /// 邮箱。
- ///
- public string? ContactEmail { get; init; }
-
- ///
- /// 当前状态。
- ///
- public TenantStatus Status { get; init; }
-
- ///
- /// 实名状态。
- ///
- public TenantVerificationStatus VerificationStatus { get; init; }
-
- ///
- /// 经营模式。
- ///
- public OperatingMode? OperatingMode { get; init; }
-
- ///
- /// 当前套餐 ID。
- ///
- [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
- public long? CurrentPackageId { get; init; }
-
- ///
- /// 当前订阅有效期开始。
- ///
- public DateTime? EffectiveFrom { get; init; }
-
- ///
- /// 当前订阅有效期结束。
- ///
- public DateTime? EffectiveTo { get; init; }
-
- ///
- /// 是否自动续费。
- ///
- public bool AutoRenew { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageTenantDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageTenantDto.cs
deleted file mode 100644
index 9c49c80..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageTenantDto.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using TakeoutSaaS.Domain.Tenants.Enums;
-
-namespace TakeoutSaaS.Application.App.Tenants.Dto;
-
-///
-/// 套餐使用租户 DTO(用于平台查看套餐关联租户列表)。
-///
-public sealed class TenantPackageTenantDto
-{
- ///
- /// 租户 ID。
- ///
- public long TenantId { get; init; }
-
- ///
- /// 租户编码。
- ///
- public string Code { get; init; } = string.Empty;
-
- ///
- /// 租户名称。
- ///
- public string Name { get; init; } = string.Empty;
-
- ///
- /// 租户状态。
- ///
- public TenantStatus Status { get; init; }
-
- ///
- /// 联系人。
- ///
- public string? ContactName { get; init; }
-
- ///
- /// 联系电话。
- ///
- public string? ContactPhone { get; init; }
-
- ///
- /// 当前订阅生效时间(UTC)。
- ///
- public DateTime SubscriptionEffectiveFrom { get; init; }
-
- ///
- /// 当前订阅到期时间(UTC)。
- ///
- public DateTime SubscriptionEffectiveTo { get; init; }
-}
-
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageUsageDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageUsageDto.cs
deleted file mode 100644
index cc3f951..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageUsageDto.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-namespace TakeoutSaaS.Application.App.Tenants.Dto;
-
-///
-/// 套餐使用统计 DTO(订阅关联数量、使用租户数量)。
-///
-public sealed class TenantPackageUsageDto
-{
- ///
- /// 套餐 ID。
- ///
- public long TenantPackageId { get; init; }
-
- ///
- /// 当前有效订阅数量(以当前时间为准)。
- ///
- public int ActiveSubscriptionCount { get; init; }
-
- ///
- /// 当前使用租户数量(以当前时间为准,按租户去重)。
- ///
- public int ActiveTenantCount { get; init; }
-
- ///
- /// 历史总订阅记录数量(不含软删)。
- ///
- public int TotalSubscriptionCount { get; init; }
-
- ///
- /// MRR(Monthly Recurring Revenue)粗看:按“当前有效订阅数 × 套餐月付等效价”估算。
- ///
- public decimal Mrr { get; init; }
-
- ///
- /// ARR(Annual Recurring Revenue)粗看:按“当前有效订阅数 × 套餐年付等效价”估算。
- ///
- public decimal Arr { get; init; }
-
- ///
- /// 未来 7 天内到期的使用租户数量(按租户去重,以当前时间为准,口径:有效订阅)。
- ///
- public int ExpiringTenantCount7Days { get; init; }
-
- ///
- /// 未来 15 天内到期的使用租户数量(按租户去重,以当前时间为准,口径:有效订阅)。
- ///
- public int ExpiringTenantCount15Days { get; init; }
-
- ///
- /// 未来 30 天内到期的使用租户数量(按租户去重,以当前时间为准,口径:有效订阅)。
- ///
- public int ExpiringTenantCount30Days { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantReviewClaimDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantReviewClaimDto.cs
deleted file mode 100644
index 565a543..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantReviewClaimDto.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System.Text.Json.Serialization;
-using TakeoutSaaS.Shared.Abstractions.Serialization;
-
-namespace TakeoutSaaS.Application.App.Tenants.Dto;
-
-///
-/// 租户审核领取信息 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/Dto/TenantSubscriptionDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantSubscriptionDto.cs
deleted file mode 100644
index 3699896..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantSubscriptionDto.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System.Text.Json.Serialization;
-using TakeoutSaaS.Domain.Tenants.Enums;
-using TakeoutSaaS.Shared.Abstractions.Serialization;
-
-namespace TakeoutSaaS.Application.App.Tenants.Dto;
-
-///
-/// 租户订阅 DTO。
-///
-public sealed class TenantSubscriptionDto
-{
- ///
- /// 订阅 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long Id { get; init; }
-
- ///
- /// 租户 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long TenantId { get; init; }
-
- ///
- /// 套餐 ID。
- ///
- [JsonConverter(typeof(SnowflakeIdJsonConverter))]
- public long TenantPackageId { get; init; }
-
- ///
- /// 状态。
- ///
- public SubscriptionStatus Status { get; init; }
-
- ///
- /// 生效时间。
- ///
- public DateTime EffectiveFrom { get; init; }
-
- ///
- /// 到期时间。
- ///
- public DateTime EffectiveTo { get; init; }
-
- ///
- /// 下次扣费时间。
- ///
- public DateTime? NextBillingDate { get; init; }
-
- ///
- /// 是否自动续费。
- ///
- public bool AutoRenew { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/BindInitialTenantSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/BindInitialTenantSubscriptionCommandHandler.cs
deleted file mode 100644
index 4da9d2c..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/BindInitialTenantSubscriptionCommandHandler.cs
+++ /dev/null
@@ -1,112 +0,0 @@
-using MediatR;
-using System.Collections.Concurrent;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Application.App.Tenants;
-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.Tenancy;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 租户初次绑定订阅处理器。
-///
-public sealed class BindInitialTenantSubscriptionCommandHandler(
- ITenantRepository tenantRepository,
- IIdGenerator idGenerator,
- ITenantProvider tenantProvider)
- : IRequestHandler
-{
- private static readonly ConcurrentDictionary TenantLocks = new();
-
- ///
- public async Task Handle(BindInitialTenantSubscriptionCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验租户上下文
- var currentTenantId = tenantProvider.GetCurrentTenantId();
- if (currentTenantId == 0 || currentTenantId != request.TenantId)
- {
- throw new BusinessException(ErrorCodes.Forbidden, "租户上下文不匹配,请在请求头 X-Tenant-Id 指定目标租户");
- }
-
- // 1.2 获取租户级幂等锁,避免并发重复创建
- var tenantLock = GetTenantLock(request.TenantId);
- await tenantLock.WaitAsync(cancellationToken);
- try
- {
- // 2. 获取租户
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
-
- // 3. 幂等校验:若已存在订阅则直接返回
- var existing = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
- if (existing is not null)
- {
- return existing.ToSubscriptionDto()
- ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅读取失败");
- }
-
- // 4. 创建 0 个月订阅(待支付/待生效)
- var now = DateTime.UtcNow;
- var subscription = new TenantSubscription
- {
- Id = idGenerator.NextId(),
- TenantId = tenant.Id,
- TenantPackageId = request.TenantPackageId,
- EffectiveFrom = now,
- EffectiveTo = now,
- NextBillingDate = now,
- Status = SubscriptionStatus.Pending,
- AutoRenew = request.AutoRenew,
- Notes = "初次绑定订阅"
- };
-
- // 5. 记录订阅与历史
- await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
- await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory
- {
- Id = idGenerator.NextId(),
- TenantId = tenant.Id,
- TenantSubscriptionId = subscription.Id,
- FromPackageId = request.TenantPackageId,
- ToPackageId = request.TenantPackageId,
- ChangeType = SubscriptionChangeType.New,
- EffectiveFrom = now,
- EffectiveTo = now,
- Amount = null,
- Currency = null,
- Notes = "初次绑定订阅(0 个月)"
- }, cancellationToken);
-
- // 6. 记录审计日志
- await tenantRepository.AddAuditLogAsync(new TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.SubscriptionUpdated,
- Title = "初次绑定订阅",
- Description = $"套餐 {request.TenantPackageId},时长 0 月"
- }, cancellationToken);
-
- // 7. 保存变更
- await tenantRepository.SaveChangesAsync(cancellationToken);
-
- // 8. 返回 DTO
- return subscription.ToSubscriptionDto()
- ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅绑定失败");
- }
- finally
- {
- // 9. 释放幂等锁
- tenantLock.Release();
- }
- }
-
- // 获取或创建租户级幂等锁实例。
- private static SemaphoreSlim GetTenantLock(long tenantId)
- => TenantLocks.GetOrAdd(tenantId, _ => new SemaphoreSlim(1, 1));
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ChangeTenantSubscriptionPlanCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ChangeTenantSubscriptionPlanCommandHandler.cs
deleted file mode 100644
index de73ccf..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ChangeTenantSubscriptionPlanCommandHandler.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-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;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 套餐升降配处理器。
-///
-public sealed class ChangeTenantSubscriptionPlanCommandHandler(
- ITenantRepository tenantRepository,
- IIdGenerator idGenerator)
- : IRequestHandler
-{
- ///
- public async Task Handle(ChangeTenantSubscriptionPlanCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验租户与订阅存在性
- _ = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
-
- var subscription = await tenantRepository.FindSubscriptionByIdAsync(request.TenantId, request.TenantSubscriptionId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "订阅不存在");
-
- var previousPackage = subscription.TenantPackageId;
-
- // 2. 根据立即生效或排期设置目标套餐
- if (request.Immediate)
- {
- subscription.TenantPackageId = request.TargetPackageId;
- subscription.ScheduledPackageId = null;
- }
- else
- {
- subscription.ScheduledPackageId = request.TargetPackageId;
- }
-
- // 3. 更新订阅并记录变更历史
- await tenantRepository.UpdateSubscriptionAsync(subscription, cancellationToken);
- await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory
- {
- Id = idGenerator.NextId(),
- TenantId = subscription.TenantId,
- TenantSubscriptionId = subscription.Id,
- FromPackageId = previousPackage,
- ToPackageId = request.TargetPackageId,
- ChangeType = SubscriptionChangeType.Upgrade,
- EffectiveFrom = subscription.EffectiveFrom,
- EffectiveTo = subscription.EffectiveTo,
- Notes = request.Notes
- }, cancellationToken);
-
- // 4. 记录审计日志
- await tenantRepository.AddAuditLogAsync(new TenantAuditLog
- {
- TenantId = subscription.TenantId,
- Action = TenantAuditAction.SubscriptionPlanChanged,
- Title = request.Immediate ? "套餐立即变更" : "套餐排期变更",
- Description = request.Notes,
- PreviousStatus = null,
- CurrentStatus = null
- }, cancellationToken);
-
- // 5. 保存并返回 DTO
- await tenantRepository.SaveChangesAsync(cancellationToken);
-
- return subscription.ToSubscriptionDto()
- ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅更新失败");
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ClaimTenantReviewCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ClaimTenantReviewCommandHandler.cs
deleted file mode 100644
index 705a32f..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ClaimTenantReviewCommandHandler.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Application.Identity.Abstractions;
-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.Security;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 领取租户入驻审核处理器。
-///
-public sealed class ClaimTenantReviewCommandHandler(
- ITenantRepository tenantRepository,
- ICurrentUserAccessor currentUserAccessor,
- IAdminAuthService adminAuthService)
- : IRequestHandler
-{
- ///
- public async Task Handle(ClaimTenantReviewCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验租户存在
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
-
- // 2. 查询是否已领取
- var existingClaim = await tenantRepository.GetActiveReviewClaimAsync(request.TenantId, cancellationToken);
- if (existingClaim != null)
- {
- if (existingClaim.ClaimedBy == currentUserAccessor.UserId)
- {
- return existingClaim.ToDto();
- }
-
- throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {existingClaim.ClaimedByName} 领取");
- }
-
- // 3. 获取当前用户显示名(用于展示快照)
- var profile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
- var displayName = string.IsNullOrWhiteSpace(profile.DisplayName)
- ? $"user:{currentUserAccessor.UserId}"
- : profile.DisplayName;
-
- // 4. 构造领取记录与审计日志
- var now = DateTime.UtcNow;
- var claim = new TenantReviewClaim
- {
- TenantId = request.TenantId,
- ClaimedBy = currentUserAccessor.UserId,
- ClaimedByName = displayName,
- ClaimedAt = now,
- ReleasedAt = null
- };
-
- var auditLog = new TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.ReviewClaimed,
- Title = "领取审核",
- Description = $"领取人:{displayName}",
- OperatorId = currentUserAccessor.UserId,
- OperatorName = displayName,
- PreviousStatus = tenant.Status,
- CurrentStatus = tenant.Status
- };
-
- // 5. 写入领取记录(处理并发领取冲突)
- var success = await tenantRepository.TryAddReviewClaimAsync(claim, auditLog, cancellationToken);
- if (!success)
- {
- var current = await tenantRepository.GetActiveReviewClaimAsync(request.TenantId, cancellationToken);
- if (current == null)
- {
- throw new BusinessException(ErrorCodes.Conflict, "审核领取失败,请刷新后重试");
- }
-
- if (current.ClaimedBy == currentUserAccessor.UserId)
- {
- return current.ToDto();
- }
-
- throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {current.ClaimedByName} 领取");
- }
-
- // 6. 返回领取结果
- return claim.ToDto();
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAdminResetLinkTokenCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAdminResetLinkTokenCommandHandler.cs
deleted file mode 100644
index 788dfa3..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAdminResetLinkTokenCommandHandler.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.Identity.Abstractions;
-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.Security;
-using TakeoutSaaS.Shared.Abstractions.Tenancy;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 生成租户主管理员重置链接令牌处理器(平台超级管理员使用)。
-///
-public sealed class CreateTenantAdminResetLinkTokenCommandHandler(
- ITenantRepository tenantRepository,
- ITenantProvider tenantProvider,
- ITenantContextAccessor tenantContextAccessor,
- IIdentityUserRepository identityUserRepository,
- ICurrentUserAccessor currentUserAccessor,
- IAdminAuthService adminAuthService,
- IAdminPasswordResetTokenStore tokenStore)
- : IRequestHandler
-{
- private const long PlatformRootTenantId = 1000000000001;
-
- ///
- public async Task Handle(CreateTenantAdminResetLinkTokenCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验仅允许平台超级管理员执行
- var currentTenantId = tenantProvider.GetCurrentTenantId();
- if (currentTenantId != PlatformRootTenantId)
- {
- throw new BusinessException(ErrorCodes.Forbidden, "仅平台超级管理员可生成重置链接");
- }
-
- // 2. 校验租户存在且存在主管理员
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
-
- // 2.1 若缺少主管理员则自动回填(兼容历史数据)
- if (!tenant.PrimaryOwnerUserId.HasValue || tenant.PrimaryOwnerUserId.Value == 0)
- {
- var originalContextForFix = tenantContextAccessor.Current;
- tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "admin:reset-link:fix-owner");
- try
- {
- var users = await identityUserRepository.SearchAsync(tenant.Id, keyword: null, cancellationToken);
- var ownerCandidate = users.OrderBy(x => x.CreatedAt).FirstOrDefault();
- if (ownerCandidate == null)
- {
- throw new BusinessException(ErrorCodes.BadRequest, "该租户未配置主管理员账号,且未找到可用管理员账号");
- }
-
- tenant.PrimaryOwnerUserId = ownerCandidate.Id;
- await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
- await tenantRepository.SaveChangesAsync(cancellationToken);
- }
- finally
- {
- tenantContextAccessor.Current = originalContextForFix;
- }
- }
-
- // 3. 签发一次性重置令牌(默认 24 小时有效)
- var token = await tokenStore.IssueAsync(tenant.PrimaryOwnerUserId.Value, DateTime.UtcNow.AddHours(24), cancellationToken);
-
- // 4. 写入审计日志
- var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
- var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
- ? $"user:{currentUserAccessor.UserId}"
- : operatorProfile.DisplayName;
-
- var auditLog = new TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.AdminResetLinkIssued,
- Title = "生成重置链接",
- Description = $"操作者:{operatorName},目标用户ID:{tenant.PrimaryOwnerUserId.Value}",
- OperatorId = currentUserAccessor.UserId,
- OperatorName = operatorName,
- PreviousStatus = tenant.Status,
- CurrentStatus = tenant.Status
- };
- await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken);
- await tenantRepository.SaveChangesAsync(cancellationToken);
-
- // 5. 返回令牌
- return token;
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs
index 83ae3a1..3521ffc 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs
@@ -7,6 +7,7 @@ using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
@@ -15,6 +16,7 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers;
///
public sealed class CreateTenantAnnouncementCommandHandler(
ITenantAnnouncementRepository announcementRepository,
+ ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor)
: IRequestHandler
{
@@ -26,12 +28,31 @@ public sealed class CreateTenantAnnouncementCommandHandler(
/// 公告 DTO。
public async Task Handle(CreateTenantAnnouncementCommand request, CancellationToken cancellationToken)
{
- // 1. 校验标题与内容
+ // 1. 校验租户上下文(租户端禁止跨租户/平台公告)
+ var currentTenantId = tenantProvider.GetCurrentTenantId();
+ if (currentTenantId <= 0)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
+ }
+
+ // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致
+ if (request.TenantId > 0 && request.TenantId != currentTenantId)
+ {
+ throw new BusinessException(ErrorCodes.Forbidden, "无权创建其他租户公告");
+ }
+
+ // 3. (空行后) 校验标题与内容
if (string.IsNullOrWhiteSpace(request.Title) || string.IsNullOrWhiteSpace(request.Content))
{
throw new BusinessException(ErrorCodes.ValidationFailed, "公告标题和内容不能为空");
}
+ // 4. (空行后) 校验公告发布范围:租户端仅允许租户公告
+ if (request.PublisherScope != PublisherScope.Tenant)
+ {
+ throw new BusinessException(ErrorCodes.ValidationFailed, "租户端不允许创建平台公告");
+ }
+
if (string.IsNullOrWhiteSpace(request.TargetType))
{
throw new BusinessException(ErrorCodes.ValidationFailed, "目标受众类型不能为空");
@@ -42,13 +63,8 @@ public sealed class CreateTenantAnnouncementCommandHandler(
throw new BusinessException(ErrorCodes.ValidationFailed, "生效开始时间必须早于结束时间");
}
- if (request.TenantId == 0 && request.PublisherScope != PublisherScope.Platform)
- {
- throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId=0 仅允许平台公告");
- }
-
- // 2. 构建公告实体
- var tenantId = request.PublisherScope == PublisherScope.Platform ? 0 : request.TenantId;
+ // 5. (空行后) 构建公告实体
+ var tenantId = currentTenantId;
var publisherUserId = currentUserAccessor.UserId == 0 ? (long?)null : currentUserAccessor.UserId;
var announcement = new TenantAnnouncement
{
@@ -66,7 +82,7 @@ public sealed class CreateTenantAnnouncementCommandHandler(
TargetParameters = request.TargetParameters
};
- // 3. 持久化并返回 DTO
+ // 6. (空行后) 持久化并返回 DTO
await announcementRepository.AddAsync(announcement, cancellationToken);
await announcementRepository.SaveChangesAsync(cancellationToken);
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantBillingCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantBillingCommandHandler.cs
deleted file mode 100644
index 3819e63..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantBillingCommandHandler.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Domain.Tenants.Entities;
-using TakeoutSaaS.Domain.Tenants.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Exceptions;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 创建租户账单处理器。
-///
-public sealed class CreateTenantBillingCommandHandler(ITenantBillingRepository billingRepository)
- : IRequestHandler
-{
- ///
- /// 处理创建租户账单请求。
- ///
- /// 创建命令。
- /// 取消标记。
- /// 账单 DTO。
- public async Task Handle(CreateTenantBillingCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验账单编号
- if (string.IsNullOrWhiteSpace(request.StatementNo))
- {
- throw new BusinessException(ErrorCodes.BadRequest, "账单编号不能为空");
- }
-
- // 2. 构建账单实体
- var bill = new TenantBillingStatement
- {
- TenantId = request.TenantId,
- StatementNo = request.StatementNo.Trim(),
- PeriodStart = request.PeriodStart,
- PeriodEnd = request.PeriodEnd,
- AmountDue = request.AmountDue,
- AmountPaid = request.AmountPaid,
- Status = request.Status,
- DueDate = request.DueDate,
- LineItemsJson = request.LineItemsJson
- };
-
- // 3. 持久化账单
- await billingRepository.AddAsync(bill, cancellationToken);
- await billingRepository.SaveChangesAsync(cancellationToken);
-
- // 4. 返回 DTO
- return bill.ToDto();
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs
deleted file mode 100644
index 8297b18..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs
+++ /dev/null
@@ -1,269 +0,0 @@
-using MediatR;
-using Microsoft.AspNetCore.Identity;
-using Microsoft.Extensions.Logging;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Application.Identity.Commands;
-using TakeoutSaaS.Domain.Identity.Entities;
-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;
-using TakeoutSaaS.Shared.Abstractions.Tenancy;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 后台手动新增租户处理器。
-///
-public sealed class CreateTenantManuallyCommandHandler(
- ITenantRepository tenantRepository,
- ITenantPackageRepository tenantPackageRepository,
- IIdentityUserRepository identityUserRepository,
- IRoleRepository roleRepository,
- IPasswordHasher passwordHasher,
- IIdGenerator idGenerator,
- IMediator mediator,
- ITenantContextAccessor tenantContextAccessor,
- ICurrentUserAccessor currentUserAccessor,
- ILogger logger)
- : IRequestHandler
-{
- ///
- public async Task Handle(CreateTenantManuallyCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验订阅时长
- if (request.DurationMonths <= 0)
- {
- throw new BusinessException(ErrorCodes.ValidationFailed, "订阅时长必须大于 0");
- }
-
- // 2. 校验租户编码唯一性
- var normalizedCode = request.Code.Trim();
- if (await tenantRepository.ExistsByCodeAsync(normalizedCode, cancellationToken))
- {
- throw new BusinessException(ErrorCodes.Conflict, $"租户编码 {normalizedCode} 已存在");
- }
-
- // 3. 校验联系人手机号唯一性(仅当填写时)
- if (!string.IsNullOrWhiteSpace(request.ContactPhone))
- {
- var normalizedPhone = request.ContactPhone.Trim();
- if (await tenantRepository.ExistsByContactPhoneAsync(normalizedPhone, cancellationToken))
- {
- throw new BusinessException(ErrorCodes.Conflict, $"手机号 {normalizedPhone} 已注册");
- }
- }
-
- // 4. 校验管理员账号唯一性
- var normalizedAccount = request.AdminAccount.Trim();
- if (await identityUserRepository.ExistsByAccountAsync(normalizedAccount, cancellationToken))
- {
- throw new BusinessException(ErrorCodes.Conflict, $"账号 {normalizedAccount} 已存在");
- }
-
- // 5. 校验套餐存在且可用
- var package = await tenantPackageRepository.FindByIdAsync(request.TenantPackageId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "套餐不存在");
- if (!package.IsActive)
- {
- throw new BusinessException(ErrorCodes.BadRequest, "套餐未启用,无法绑定订阅");
- }
-
- // 6. 计算订阅生效与到期时间(UTC)
- var now = DateTime.UtcNow;
- var subscriptionEffectiveFrom = request.SubscriptionEffectiveFrom ?? now;
- var subscriptionEffectiveTo = subscriptionEffectiveFrom.AddMonths(request.DurationMonths);
-
- // 7. 构建租户与订阅
- var tenantId = idGenerator.NextId();
- var tenant = new Tenant
- {
- Id = tenantId,
- Code = normalizedCode,
- Name = request.Name.Trim(),
- ShortName = request.ShortName,
- LegalEntityName = request.LegalEntityName,
- Industry = request.Industry,
- LogoUrl = request.LogoUrl,
- CoverImageUrl = request.CoverImageUrl,
- Website = request.Website,
- Country = request.Country,
- Province = request.Province,
- City = request.City,
- Address = request.Address,
- ContactName = request.ContactName,
- ContactPhone = string.IsNullOrWhiteSpace(request.ContactPhone) ? null : request.ContactPhone.Trim(),
- ContactEmail = request.ContactEmail,
- Status = request.TenantStatus,
- EffectiveFrom = subscriptionEffectiveFrom,
- EffectiveTo = subscriptionEffectiveTo,
- SuspendedAt = request.SuspendedAt,
- SuspensionReason = request.SuspensionReason,
- Tags = request.Tags,
- Remarks = request.Remarks
- };
-
- // 8. 构建订阅实体
- var subscription = new TenantSubscription
- {
- Id = idGenerator.NextId(),
- TenantId = tenantId,
- TenantPackageId = request.TenantPackageId,
- EffectiveFrom = subscriptionEffectiveFrom,
- EffectiveTo = subscriptionEffectiveTo,
- NextBillingDate = request.NextBillingDate ?? subscriptionEffectiveTo,
- Status = request.SubscriptionStatus,
- AutoRenew = request.AutoRenew,
- ScheduledPackageId = request.ScheduledPackageId,
- Notes = request.SubscriptionNotes
- };
-
- // 9. 构建认证资料(默认直接通过)
- var actorName = currentUserAccessor.IsAuthenticated
- ? $"user:{currentUserAccessor.UserId}"
- : "system";
-
- var verification = new TenantVerificationProfile
- {
- Id = idGenerator.NextId(),
- TenantId = tenantId,
- Status = request.VerificationStatus,
- BusinessLicenseNumber = request.BusinessLicenseNumber,
- BusinessLicenseUrl = request.BusinessLicenseUrl,
- LegalPersonName = request.LegalPersonName,
- LegalPersonIdNumber = request.LegalPersonIdNumber,
- LegalPersonIdFrontUrl = request.LegalPersonIdFrontUrl,
- LegalPersonIdBackUrl = request.LegalPersonIdBackUrl,
- BankAccountName = request.BankAccountName,
- BankAccountNumber = request.BankAccountNumber,
- BankName = request.BankName,
- AdditionalDataJson = request.AdditionalDataJson,
- SubmittedAt = request.SubmittedAt ?? now,
- ReviewedAt = request.ReviewedAt ?? now,
- ReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- ReviewedByName = string.IsNullOrWhiteSpace(request.ReviewedByName) ? actorName : request.ReviewedByName,
- ReviewRemarks = request.ReviewRemarks
- };
-
- // 10. 写入审计日志与订阅历史
- await tenantRepository.AddAuditLogAsync(new TenantAuditLog
- {
- TenantId = tenantId,
- Action = TenantAuditAction.RegistrationSubmitted,
- Title = "后台手动创建",
- Description = $"绑定套餐 {request.TenantPackageId},订阅 {request.DurationMonths} 月",
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName,
- PreviousStatus = null,
- CurrentStatus = tenant.Status
- }, cancellationToken);
-
- await tenantRepository.AddAuditLogAsync(new TenantAuditLog
- {
- TenantId = tenantId,
- Action = TenantAuditAction.SubscriptionUpdated,
- Title = "订阅初始化",
- Description = $"生效:{subscription.EffectiveFrom:yyyy-MM-dd HH:mm:ss},到期:{subscription.EffectiveTo:yyyy-MM-dd HH:mm:ss}",
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName,
- PreviousStatus = null,
- CurrentStatus = tenant.Status
- }, cancellationToken);
-
- await tenantRepository.AddAuditLogAsync(new TenantAuditLog
- {
- TenantId = tenantId,
- Action = TenantAuditAction.VerificationApproved,
- Title = "认证已通过",
- Description = request.ReviewRemarks,
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName,
- PreviousStatus = null,
- CurrentStatus = tenant.Status
- }, cancellationToken);
-
- await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory
- {
- TenantId = tenantId,
- TenantSubscriptionId = subscription.Id,
- FromPackageId = request.TenantPackageId,
- ToPackageId = request.TenantPackageId,
- ChangeType = SubscriptionChangeType.New,
- EffectiveFrom = subscription.EffectiveFrom,
- EffectiveTo = subscription.EffectiveTo,
- Amount = null,
- Currency = null,
- Notes = request.SubscriptionNotes
- }, cancellationToken);
-
- // 11. 持久化租户、订阅与认证资料
- await tenantRepository.AddTenantAsync(tenant, cancellationToken);
- await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
- await tenantRepository.UpsertVerificationProfileAsync(verification, cancellationToken);
- await tenantRepository.SaveChangesAsync(cancellationToken);
-
- // 12. 临时切换租户上下文,保证身份与权限写入正确
- var previousContext = tenantContextAccessor.Current;
- tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "manual-create");
- try
- {
- // 13. 创建租户管理员账号
- var adminUser = new IdentityUser
- {
- TenantId = tenant.Id,
- Account = normalizedAccount,
- DisplayName = request.AdminDisplayName.Trim(),
- PasswordHash = string.Empty,
- Phone = string.IsNullOrWhiteSpace(request.ContactPhone) ? null : request.ContactPhone.Trim(),
- Email = string.IsNullOrWhiteSpace(request.ContactEmail) ? null : request.ContactEmail.Trim(),
- MerchantId = request.AdminMerchantId,
- Avatar = request.AdminAvatar
- };
- adminUser.PasswordHash = passwordHasher.HashPassword(adminUser, request.AdminPassword);
- await identityUserRepository.AddAsync(adminUser, cancellationToken);
- await identityUserRepository.SaveChangesAsync(cancellationToken);
-
- // 14. 初始化租户管理员角色模板并绑定角色
- await mediator.Send(new InitializeRoleTemplatesCommand
- {
- TemplateCodes = new[] { "tenant-admin" }
- }, cancellationToken);
-
- var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenant.Id, cancellationToken);
- if (tenantAdminRole != null)
- {
- await mediator.Send(new AssignUserRolesCommand
- {
- UserId = adminUser.Id,
- RoleIds = new[] { tenantAdminRole.Id }
- }, cancellationToken);
- }
-
- // 15. 回写租户所有者账号
- tenant.PrimaryOwnerUserId = adminUser.Id;
- await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
- await tenantRepository.SaveChangesAsync(cancellationToken);
- }
- finally
- {
- // 16. 恢复上下文
- tenantContextAccessor.Current = previousContext;
- }
-
- // 17. 返回创建结果
- logger.LogInformation("已后台手动创建租户 {TenantCode}", tenant.Code);
-
- return new TenantDetailDto
- {
- Tenant = TenantMapping.ToDto(tenant, subscription, verification),
- Verification = verification.ToVerificationDto(),
- Subscription = subscription.ToSubscriptionDto(),
- Package = package.ToDto()
- };
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantPackageCommandHandler.cs
deleted file mode 100644
index bc43211..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantPackageCommandHandler.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-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;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 创建租户套餐处理器。
-///
-public sealed class CreateTenantPackageCommandHandler(ITenantPackageRepository packageRepository)
- : IRequestHandler
-{
- ///
- public async Task Handle(CreateTenantPackageCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验套餐名称
- if (string.IsNullOrWhiteSpace(request.Name))
- {
- throw new BusinessException(ErrorCodes.BadRequest, "套餐名称不能为空");
- }
-
- // 2. 构建套餐实体
- var package = new TenantPackage
- {
- Name = request.Name.Trim(),
- Description = request.Description,
- PackageType = request.PackageType,
- MonthlyPrice = request.MonthlyPrice,
- YearlyPrice = request.YearlyPrice,
- MaxStoreCount = request.MaxStoreCount,
- MaxAccountCount = request.MaxAccountCount,
- MaxStorageGb = request.MaxStorageGb,
- MaxSmsCredits = request.MaxSmsCredits,
- MaxDeliveryOrders = request.MaxDeliveryOrders,
- FeaturePoliciesJson = request.FeaturePoliciesJson,
- IsActive = request.IsActive,
- IsPublicVisible = request.IsPublicVisible,
- IsAllowNewTenantPurchase = request.IsAllowNewTenantPurchase,
- PublishStatus = request.PublishStatus ?? TenantPackagePublishStatus.Draft,
- IsRecommended = request.IsRecommended,
- Tags = request.Tags ?? [],
- SortOrder = request.SortOrder
- };
-
- // 3. 持久化并返回
- await packageRepository.AddAsync(package, cancellationToken);
- await packageRepository.SaveChangesAsync(cancellationToken);
-
- return package.ToDto();
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantSubscriptionCommandHandler.cs
deleted file mode 100644
index 4d59ffe..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantSubscriptionCommandHandler.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-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;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 新建/续费订阅处理器。
-///
-public sealed class CreateTenantSubscriptionCommandHandler(
- ITenantRepository tenantRepository,
- IIdGenerator idGenerator)
- : IRequestHandler
-{
- ///
- public async Task Handle(CreateTenantSubscriptionCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验订阅时长
- if (request.DurationMonths <= 0)
- {
- throw new BusinessException(ErrorCodes.BadRequest, "订阅时长必须大于 0");
- }
-
- // 2. 获取租户与当前订阅
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
-
- var current = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
- var from = current?.EffectiveTo ?? tenant.EffectiveTo ?? DateTime.UtcNow;
- var effectiveFrom = from > DateTime.UtcNow ? from : DateTime.UtcNow;
- var effectiveTo = effectiveFrom.AddMonths(request.DurationMonths);
-
- // 3. 创建订阅实体
- var subscription = new TenantSubscription
- {
- Id = idGenerator.NextId(),
- TenantId = tenant.Id,
- TenantPackageId = request.TenantPackageId,
- EffectiveFrom = effectiveFrom,
- EffectiveTo = effectiveTo,
- NextBillingDate = effectiveTo,
- Status = SubscriptionStatus.Active,
- AutoRenew = request.AutoRenew,
- Notes = request.Notes
- };
-
- // 4. 记录订阅与历史
- await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
- await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory
- {
- Id = idGenerator.NextId(),
- TenantId = tenant.Id,
- TenantSubscriptionId = subscription.Id,
- FromPackageId = current?.TenantPackageId ?? request.TenantPackageId,
- ToPackageId = request.TenantPackageId,
- ChangeType = current == null ? SubscriptionChangeType.New : SubscriptionChangeType.Renew,
- EffectiveFrom = effectiveFrom,
- EffectiveTo = effectiveTo,
- Amount = null,
- Currency = null,
- Notes = request.Notes
- }, cancellationToken);
-
- // 5. 记录审计
- await tenantRepository.AddAuditLogAsync(new TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.SubscriptionUpdated,
- Title = current == null ? "创建订阅" : "续费订阅",
- Description = $"套餐 {request.TenantPackageId} 时长 {request.DurationMonths} 月"
- }, cancellationToken);
-
- // 6. 保存变更
- await tenantRepository.SaveChangesAsync(cancellationToken);
-
- // 7. 返回 DTO
- return subscription.ToSubscriptionDto()
- ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅生成失败");
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantAnnouncementCommandHandler.cs
index ca38d89..e175625 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantAnnouncementCommandHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantAnnouncementCommandHandler.cs
@@ -1,13 +1,18 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Commands;
using TakeoutSaaS.Domain.Tenants.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Constants;
+using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
///
/// 删除公告处理器。
///
-public sealed class DeleteTenantAnnouncementCommandHandler(ITenantAnnouncementRepository announcementRepository)
+public sealed class DeleteTenantAnnouncementCommandHandler(
+ ITenantAnnouncementRepository announcementRepository,
+ ITenantProvider tenantProvider)
: IRequestHandler
{
///
@@ -18,11 +23,24 @@ public sealed class DeleteTenantAnnouncementCommandHandler(ITenantAnnouncementRe
/// 执行结果。
public async Task Handle(DeleteTenantAnnouncementCommand request, CancellationToken cancellationToken)
{
- // 1. 删除公告
- await announcementRepository.DeleteAsync(request.TenantId, request.AnnouncementId, cancellationToken);
+ // 1. 校验租户上下文(租户端禁止跨租户)
+ var currentTenantId = tenantProvider.GetCurrentTenantId();
+ if (currentTenantId <= 0)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
+ }
+
+ // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致
+ if (request.TenantId > 0 && request.TenantId != currentTenantId)
+ {
+ throw new BusinessException(ErrorCodes.Forbidden, "无权删除其他租户公告");
+ }
+
+ // 3. (空行后) 删除公告
+ await announcementRepository.DeleteAsync(currentTenantId, request.AnnouncementId, cancellationToken);
await announcementRepository.SaveChangesAsync(cancellationToken);
- // 2. 返回执行结果
+ // 4. (空行后) 返回执行结果
return true;
}
}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantPackageCommandHandler.cs
deleted file mode 100644
index c0f6fb4..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/DeleteTenantPackageCommandHandler.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Domain.Tenants.Repositories;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 删除租户套餐处理器。
-///
-public sealed class DeleteTenantPackageCommandHandler(ITenantPackageRepository packageRepository)
- : IRequestHandler
-{
- ///
- public async Task Handle(DeleteTenantPackageCommand request, CancellationToken cancellationToken)
- {
- // 1. 删除套餐
- await packageRepository.DeleteAsync(request.TenantPackageId, cancellationToken);
- await packageRepository.SaveChangesAsync(cancellationToken);
-
- // 2. 返回执行结果
- return true;
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ExtendTenantSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ExtendTenantSubscriptionCommandHandler.cs
deleted file mode 100644
index 0bcb1d7..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ExtendTenantSubscriptionCommandHandler.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-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 ExtendTenantSubscriptionCommandHandler(
- ITenantRepository tenantRepository,
- IIdGenerator idGenerator,
- ICurrentUserAccessor currentUserAccessor)
- : IRequestHandler
-{
- ///
- public async Task Handle(ExtendTenantSubscriptionCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验时长
- if (request.DurationMonths <= 0)
- {
- throw new BusinessException(ErrorCodes.BadRequest, "延期/赠送时长必须大于 0");
- }
-
- // 2. 获取租户与当前订阅
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
-
- var current = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.BadRequest, "订阅不存在,无法延期/赠送");
-
- var now = DateTime.UtcNow;
- var effectiveFrom = current.EffectiveTo > now ? current.EffectiveTo : now;
- var effectiveTo = effectiveFrom.AddMonths(request.DurationMonths);
-
- var previousStatus = tenant.Status;
- var actorName = currentUserAccessor.IsAuthenticated
- ? $"user:{currentUserAccessor.UserId}"
- : "system";
-
- // 3. 创建续费订阅
- var subscription = new TenantSubscription
- {
- Id = idGenerator.NextId(),
- TenantId = tenant.Id,
- TenantPackageId = current.TenantPackageId,
- EffectiveFrom = effectiveFrom,
- EffectiveTo = effectiveTo,
- NextBillingDate = effectiveTo,
- Status = SubscriptionStatus.Active,
- AutoRenew = current.AutoRenew,
- Notes = request.Notes
- };
-
- await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
- await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory
- {
- Id = idGenerator.NextId(),
- TenantId = tenant.Id,
- TenantSubscriptionId = subscription.Id,
- FromPackageId = current.TenantPackageId,
- ToPackageId = current.TenantPackageId,
- ChangeType = SubscriptionChangeType.Renew,
- EffectiveFrom = effectiveFrom,
- EffectiveTo = effectiveTo,
- Amount = null,
- Currency = null,
- Notes = request.Notes
- }, cancellationToken);
-
- // 4. 若租户处于到期状态则恢复为正常(冻结状态需先解冻)
- if (tenant.Status == TenantStatus.Expired)
- {
- tenant.Status = TenantStatus.Active;
- }
-
- tenant.EffectiveFrom = subscription.EffectiveFrom;
- tenant.EffectiveTo = subscription.EffectiveTo;
-
- await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
-
- // 5. 记录审计
- await tenantRepository.AddAuditLogAsync(new TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.SubscriptionUpdated,
- Title = "延期/赠送时长",
- Description = $"续费 {request.DurationMonths} 月,到期时间:{current.EffectiveTo:yyyy-MM-dd HH:mm:ss} -> {subscription.EffectiveTo:yyyy-MM-dd HH:mm:ss}",
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName,
- PreviousStatus = previousStatus,
- CurrentStatus = tenant.Status
- }, cancellationToken);
-
- // 6. 保存并返回
- await tenantRepository.SaveChangesAsync(cancellationToken);
- return subscription.ToSubscriptionDto()
- ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅生成失败");
- }
-}
-
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ForceClaimTenantReviewCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ForceClaimTenantReviewCommandHandler.cs
deleted file mode 100644
index 90e9cfe..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ForceClaimTenantReviewCommandHandler.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Application.Identity.Abstractions;
-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.Security;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 强制接管租户入驻审核处理器。
-///
-public sealed class ForceClaimTenantReviewCommandHandler(
- ITenantRepository tenantRepository,
- ICurrentUserAccessor currentUserAccessor,
- IAdminAuthService adminAuthService)
- : IRequestHandler
-{
- ///
- public async Task Handle(ForceClaimTenantReviewCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验租户存在
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
-
- // 2. 获取当前用户显示名(用于展示快照)
- var profile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
- var displayName = string.IsNullOrWhiteSpace(profile.DisplayName)
- ? $"user:{currentUserAccessor.UserId}"
- : profile.DisplayName;
-
- // 3. 读取当前领取记录(可跟踪用于更新)
- var claim = await tenantRepository.FindActiveReviewClaimAsync(request.TenantId, cancellationToken);
- if (claim == null)
- {
- // 4. 未领取则直接创建(记录强制接管动作)
- var now = DateTime.UtcNow;
- var created = new TenantReviewClaim
- {
- TenantId = request.TenantId,
- ClaimedBy = currentUserAccessor.UserId,
- ClaimedByName = displayName,
- ClaimedAt = now,
- ReleasedAt = null
- };
-
- var auditLog = new TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.ReviewForceClaimed,
- Title = "强制接管审核",
- Description = $"接管人:{displayName}",
- OperatorId = currentUserAccessor.UserId,
- OperatorName = displayName,
- PreviousStatus = tenant.Status,
- CurrentStatus = tenant.Status
- };
-
- var success = await tenantRepository.TryAddReviewClaimAsync(created, auditLog, cancellationToken);
- if (!success)
- {
- var current = await tenantRepository.GetActiveReviewClaimAsync(request.TenantId, cancellationToken);
- if (current != null)
- {
- return current.ToDto();
- }
-
- throw new BusinessException(ErrorCodes.Conflict, "审核接管失败,请刷新后重试");
- }
-
- return created.ToDto();
- }
-
- // 5. 已由自己领取则直接返回
- if (claim.ClaimedBy == currentUserAccessor.UserId)
- {
- return claim.ToDto();
- }
-
- // 6. 更新领取人并记录审计
- var previousOwner = claim.ClaimedByName;
- claim.ClaimedBy = currentUserAccessor.UserId;
- claim.ClaimedByName = displayName;
- claim.ClaimedAt = DateTime.UtcNow;
-
- await tenantRepository.UpdateReviewClaimAsync(claim, cancellationToken);
- await tenantRepository.AddAuditLogAsync(new TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.ReviewForceClaimed,
- Title = "强制接管审核",
- Description = $"原领取人:{previousOwner},接管人:{displayName}",
- OperatorId = currentUserAccessor.UserId,
- OperatorName = displayName,
- PreviousStatus = tenant.Status,
- CurrentStatus = tenant.Status
- }, cancellationToken);
-
- await tenantRepository.SaveChangesAsync(cancellationToken);
- return claim.ToDto();
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/FreezeTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/FreezeTenantCommandHandler.cs
deleted file mode 100644
index b48bbae..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/FreezeTenantCommandHandler.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-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.Security;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 冻结租户处理器。
-///
-public sealed class FreezeTenantCommandHandler(
- ITenantRepository tenantRepository,
- ICurrentUserAccessor currentUserAccessor)
- : IRequestHandler
-{
- ///
- public async Task Handle(FreezeTenantCommand request, CancellationToken cancellationToken)
- {
- // 1. 获取租户与订阅
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
-
- var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
- var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken);
-
- var previousStatus = tenant.Status;
- if (tenant.Status == TenantStatus.Closed)
- {
- throw new BusinessException(ErrorCodes.BadRequest, "已注销租户不可冻结");
- }
-
- if (tenant.Status == TenantStatus.Suspended)
- {
- throw new BusinessException(ErrorCodes.Conflict, "租户已被冻结");
- }
-
- // 2. 更新租户状态
- tenant.Status = TenantStatus.Suspended;
- tenant.SuspendedAt = DateTime.UtcNow;
- tenant.SuspensionReason = request.Reason;
-
- // 3. 同步暂停订阅
- if (subscription != null)
- {
- subscription.Status = SubscriptionStatus.Suspended;
- await tenantRepository.UpdateSubscriptionAsync(subscription, cancellationToken);
- }
-
- await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
-
- // 4. 记录审计
- var actorName = currentUserAccessor.IsAuthenticated
- ? $"user:{currentUserAccessor.UserId}"
- : "system";
-
- await tenantRepository.AddAuditLogAsync(new TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.StatusChanged,
- Title = "冻结租户",
- Description = request.Reason,
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName,
- PreviousStatus = previousStatus,
- CurrentStatus = tenant.Status
- }, cancellationToken);
-
- // 5. 保存并返回
- await tenantRepository.SaveChangesAsync(cancellationToken);
- return TenantMapping.ToDto(tenant, subscription, verification);
- }
-}
-
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantAuditLogsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantAuditLogsQueryHandler.cs
deleted file mode 100644
index 76aafb6..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantAuditLogsQueryHandler.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-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 logs = await tenantRepository.GetAuditLogsAsync(request.TenantId, cancellationToken);
- var total = logs.Count;
-
- // 2. 分页映射
- var paged = logs
- .Skip((request.Page - 1) * request.PageSize)
- .Take(request.PageSize)
- .Select(TenantMapping.ToDto)
- .ToList();
-
- // 3. 返回分页结果
- return new PagedResult(paged, request.Page, request.PageSize, total);
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantBillQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantBillQueryHandler.cs
index 18a6aae..c6b7c50 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantBillQueryHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantBillQueryHandler.cs
@@ -2,13 +2,18 @@ using MediatR;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Constants;
+using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
///
/// 账单详情查询处理器。
///
-public sealed class GetTenantBillQueryHandler(ITenantBillingRepository billingRepository)
+public sealed class GetTenantBillQueryHandler(
+ ITenantBillingRepository billingRepository,
+ ITenantProvider tenantProvider)
: IRequestHandler
{
///
@@ -19,10 +24,23 @@ public sealed class GetTenantBillQueryHandler(ITenantBillingRepository billingRe
/// 账单 DTO 或 null。
public async Task Handle(GetTenantBillQuery request, CancellationToken cancellationToken)
{
- // 1. 查询账单
- var bill = await billingRepository.FindByIdAsync(request.TenantId, request.BillingId, cancellationToken);
+ // 1. 校验租户上下文(租户端禁止跨租户)
+ var currentTenantId = tenantProvider.GetCurrentTenantId();
+ if (currentTenantId <= 0)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
+ }
- // 2. 返回 DTO 或 null
+ // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致
+ if (request.TenantId > 0 && request.TenantId != currentTenantId)
+ {
+ throw new BusinessException(ErrorCodes.Forbidden, "无权查询其他租户账单");
+ }
+
+ // 3. (空行后) 查询账单
+ var bill = await billingRepository.FindByIdAsync(currentTenantId, request.BillingId, cancellationToken);
+
+ // 4. (空行后) 返回 DTO 或 null
return bill?.ToDto();
}
}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantByIdQueryHandler.cs
deleted file mode 100644
index 7ae6f83..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantByIdQueryHandler.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Application.App.Tenants.Queries;
-using TakeoutSaaS.Domain.Tenants.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Exceptions;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 租户详情查询处理器。
-///
-public sealed class GetTenantByIdQueryHandler(
- ITenantRepository tenantRepository,
- ITenantPackageRepository tenantPackageRepository)
- : IRequestHandler
-{
- ///
- public async Task Handle(GetTenantByIdQuery request, CancellationToken cancellationToken)
- {
- // 1. 查询租户
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
-
- // 2. 查询订阅与认证
- var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
- var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken);
-
- // 3. 查询当前套餐
- var package = subscription == null
- ? null
- : await tenantPackageRepository.FindByIdAsync(subscription.TenantPackageId, cancellationToken);
-
- // 4. 组装返回
- return new TenantDetailDto
- {
- Tenant = TenantMapping.ToDto(tenant, subscription, verification),
- Verification = verification.ToVerificationDto(),
- Subscription = subscription.ToSubscriptionDto(),
- Package = package?.ToDto()
- };
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageTenantsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageTenantsQueryHandler.cs
deleted file mode 100644
index f85a5b7..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageTenantsQueryHandler.cs
+++ /dev/null
@@ -1,230 +0,0 @@
-using MediatR;
-using System.Data;
-using System.Data.Common;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Application.App.Tenants.Queries;
-using TakeoutSaaS.Domain.Tenants.Enums;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Data;
-using TakeoutSaaS.Shared.Abstractions.Results;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 查询套餐当前使用租户列表处理器。
-///
-public sealed class GetTenantPackageTenantsQueryHandler(IDapperExecutor dapperExecutor)
- : IRequestHandler>
-{
- ///
- public async Task> Handle(GetTenantPackageTenantsQuery request, CancellationToken cancellationToken)
- {
- // 1. 参数规范化
- var page = request.Page <= 0 ? 1 : request.Page;
- var pageSize = request.PageSize <= 0 ? 20 : request.PageSize;
- var keyword = string.IsNullOrWhiteSpace(request.Keyword) ? null : request.Keyword.Trim();
-
- // 2. 以当前时间为准筛选“有效订阅”
- var now = DateTime.UtcNow;
- var expiringDays = request.ExpiringWithinDays is > 0 ? request.ExpiringWithinDays : null;
- var expiryEnd = expiringDays.HasValue ? now.AddDays(expiringDays.Value) : (DateTime?)null;
- var offset = (page - 1) * pageSize;
-
- // 3. 查询总数 + 列表
- return await dapperExecutor.QueryAsync(
- DatabaseConstants.AppDataSource,
- DatabaseConnectionRole.Read,
- async (connection, token) =>
- {
- // 3.1 统计总数
- var total = await ExecuteScalarIntAsync(
- connection,
- BuildCountSql(),
- [
- ("packageId", request.TenantPackageId),
- ("now", now),
- ("expiryEnd", expiryEnd),
- ("keyword", keyword)
- ],
- token);
-
- // 3.2 查询列表
- var listSql = BuildListSql(expiryEnd.HasValue);
- await using var listCommand = CreateCommand(
- connection,
- listSql,
- [
- ("packageId", request.TenantPackageId),
- ("now", now),
- ("expiryEnd", expiryEnd),
- ("keyword", keyword),
- ("offset", offset),
- ("limit", pageSize)
- ]);
-
- await using var reader = await listCommand.ExecuteReaderAsync(token);
- var items = new List();
- while (await reader.ReadAsync(token))
- {
- items.Add(new TenantPackageTenantDto
- {
- TenantId = reader.GetInt64(0),
- Code = reader.GetString(1),
- Name = reader.GetString(2),
- Status = (TenantStatus)reader.GetInt32(3),
- ContactName = reader.IsDBNull(4) ? null : reader.GetString(4),
- ContactPhone = reader.IsDBNull(5) ? null : reader.GetString(5),
- SubscriptionEffectiveFrom = reader.GetDateTime(6),
- SubscriptionEffectiveTo = reader.GetDateTime(7)
- });
- }
-
- // 3.3 返回分页
- return new PagedResult(items, page, pageSize, total);
- },
- cancellationToken);
- }
-
- private static string BuildCountSql()
- {
- return """
- select count(*)
- from public.tenants t
- where t."DeletedAt" is null
- and (
- @keyword::text is null
- or t."Name" ilike ('%' || @keyword::text || '%')
- or t."Code" ilike ('%' || @keyword::text || '%')
- or coalesce(t."ContactName", '') ilike ('%' || @keyword::text || '%')
- or coalesce(t."ContactPhone", '') ilike ('%' || @keyword::text || '%')
- )
- and exists (
- select 1
- from public.tenant_subscriptions s
- where s."DeletedAt" is null
- and s."TenantId" = t."Id"
- and s."TenantPackageId" = @packageId
- and s."Status" = 1
- and s."EffectiveFrom" <= @now
- and s."EffectiveTo" >= @now
- and (
- @expiryEnd::timestamp with time zone is null
- or s."EffectiveTo" <= @expiryEnd
- )
- );
- """;
- }
-
- private static string BuildListSql(bool orderByExpiryAsc)
- {
- if (orderByExpiryAsc)
- {
- return """
- select
- t."Id" as "TenantId",
- t."Code",
- t."Name",
- t."Status",
- t."ContactName",
- t."ContactPhone",
- s."EffectiveFrom",
- s."EffectiveTo"
- from public.tenants t
- join lateral (
- select s."EffectiveFrom", s."EffectiveTo"
- from public.tenant_subscriptions s
- where s."DeletedAt" is null
- and s."TenantId" = t."Id"
- and s."TenantPackageId" = @packageId
- and s."Status" = 1
- and s."EffectiveFrom" <= @now
- and s."EffectiveTo" >= @now
- and (
- @expiryEnd::timestamp with time zone is null
- or s."EffectiveTo" <= @expiryEnd
- )
- order by s."EffectiveTo" asc
- limit 1
- ) s on true
- where t."DeletedAt" is null
- and (
- @keyword::text is null
- or t."Name" ilike ('%' || @keyword::text || '%')
- or t."Code" ilike ('%' || @keyword::text || '%')
- or coalesce(t."ContactName", '') ilike ('%' || @keyword::text || '%')
- or coalesce(t."ContactPhone", '') ilike ('%' || @keyword::text || '%')
- )
- order by s."EffectiveTo" asc
- offset @offset
- limit @limit;
- """;
- }
-
- return """
- select
- t."Id" as "TenantId",
- t."Code",
- t."Name",
- t."Status",
- t."ContactName",
- t."ContactPhone",
- s."EffectiveFrom",
- s."EffectiveTo"
- from public.tenants t
- join lateral (
- select s."EffectiveFrom", s."EffectiveTo"
- from public.tenant_subscriptions s
- where s."DeletedAt" is null
- and s."TenantId" = t."Id"
- and s."TenantPackageId" = @packageId
- and s."Status" = 1
- and s."EffectiveFrom" <= @now
- and s."EffectiveTo" >= @now
- and (
- @expiryEnd::timestamp with time zone is null
- or s."EffectiveTo" <= @expiryEnd
- )
- order by s."EffectiveTo" desc
- limit 1
- ) s on true
- where t."DeletedAt" is null
- and (
- @keyword::text is null
- or t."Name" ilike ('%' || @keyword::text || '%')
- or t."Code" ilike ('%' || @keyword::text || '%')
- or coalesce(t."ContactName", '') ilike ('%' || @keyword::text || '%')
- or coalesce(t."ContactPhone", '') ilike ('%' || @keyword::text || '%')
- )
- order by t."CreatedAt" desc
- offset @offset
- limit @limit;
- """;
- }
-
- private static async Task ExecuteScalarIntAsync(
- IDbConnection connection,
- string sql,
- (string Name, object? Value)[] parameters,
- CancellationToken cancellationToken)
- {
- await using var command = CreateCommand(connection, sql, parameters);
- var result = await command.ExecuteScalarAsync(cancellationToken);
- return result is null or DBNull ? 0 : Convert.ToInt32(result);
- }
-
- private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
- {
- var command = connection.CreateCommand();
- command.CommandText = sql;
-
- foreach (var (name, value) in parameters)
- {
- var p = command.CreateParameter();
- p.ParameterName = name;
- p.Value = value ?? DBNull.Value;
- command.Parameters.Add(p);
- }
-
- return (DbCommand)command;
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageUsagesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageUsagesQueryHandler.cs
deleted file mode 100644
index b5e5f18..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageUsagesQueryHandler.cs
+++ /dev/null
@@ -1,153 +0,0 @@
-using MediatR;
-using System.Data;
-using System.Data.Common;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Application.App.Tenants.Queries;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Data;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 查询套餐使用统计处理器。
-///
-public sealed class GetTenantPackageUsagesQueryHandler(IDapperExecutor dapperExecutor)
- : IRequestHandler>
-{
- ///
- public async Task> Handle(GetTenantPackageUsagesQuery request, CancellationToken cancellationToken)
- {
- // 1. 规范化输入
- var ids = request.TenantPackageIds?
- .Where(x => x > 0)
- .Distinct()
- .ToArray();
-
- // 2. 构造 SQL(以当前时间为准统计“有效订阅/使用租户/到期分布”)
- var now = DateTime.UtcNow;
- var date7 = now.AddDays(7);
- var date15 = now.AddDays(15);
- var date30 = now.AddDays(30);
- var sql = BuildSql(ids, out var parameters, now, date7, date15, date30);
-
- // 3. 查询统计结果
- return await dapperExecutor.QueryAsync(
- DatabaseConstants.AppDataSource,
- DatabaseConnectionRole.Read,
- async (connection, token) =>
- {
- await using var command = CreateCommand(connection, sql, parameters);
- await using var reader = await command.ExecuteReaderAsync(token);
- var list = new List();
-
- // 4. 逐行读取
- while (await reader.ReadAsync(token))
- {
- list.Add(new TenantPackageUsageDto
- {
- TenantPackageId = reader.GetInt64(0),
- ActiveSubscriptionCount = reader.GetInt32(1),
- ActiveTenantCount = reader.GetInt32(2),
- TotalSubscriptionCount = reader.GetInt32(3),
- Mrr = reader.IsDBNull(4) ? 0m : reader.GetDecimal(4),
- Arr = reader.IsDBNull(5) ? 0m : reader.GetDecimal(5),
- ExpiringTenantCount7Days = reader.IsDBNull(6) ? 0 : reader.GetInt32(6),
- ExpiringTenantCount15Days = reader.IsDBNull(7) ? 0 : reader.GetInt32(7),
- ExpiringTenantCount30Days = reader.IsDBNull(8) ? 0 : reader.GetInt32(8)
- });
- }
-
- return (IReadOnlyList)list;
- },
- cancellationToken);
- }
-
- private static string BuildSql(
- long[]? ids,
- out (string Name, object? Value)[] parameters,
- DateTime now,
- DateTime date7,
- DateTime date15,
- DateTime date30)
- {
- // 1. 基础查询:先按订阅表聚合,再回连套餐表计算 MRR/ARR
- var builder = new System.Text.StringBuilder();
- builder.AppendLine("""
- with stats as (
- select
- "TenantPackageId" as "TenantPackageId",
- count(*) filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now) as "ActiveSubscriptionCount",
- count(distinct "TenantId") filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now) as "ActiveTenantCount",
- count(distinct "TenantId") filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now and "EffectiveTo" <= @date7) as "ExpiringTenantCount7Days",
- count(distinct "TenantId") filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now and "EffectiveTo" <= @date15) as "ExpiringTenantCount15Days",
- count(distinct "TenantId") filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now and "EffectiveTo" <= @date30) as "ExpiringTenantCount30Days",
- count(*) as "TotalSubscriptionCount"
- from public.tenant_subscriptions
- where "DeletedAt" is null
- """);
-
- var list = new List<(string Name, object? Value)>
- {
- ("now", now),
- ("date7", date7),
- ("date15", date15),
- ("date30", date30)
- };
-
- // 2. 可选按套餐 ID 过滤
- if (ids is { Length: > 0 })
- {
- builder.Append(" and \"TenantPackageId\" in (");
- for (var i = 0; i < ids.Length; i++)
- {
- if (i > 0)
- {
- builder.Append(',');
- }
-
- var name = $"p{i}";
- builder.Append($"@{name}");
- list.Add((name, ids[i]));
- }
-
- builder.AppendLine(")");
- }
-
- // 3. 分组与回连套餐表
- builder.AppendLine("""
- group by "TenantPackageId"
- )
- select
- s."TenantPackageId" as "TenantPackageId",
- s."ActiveSubscriptionCount" as "ActiveSubscriptionCount",
- s."ActiveTenantCount" as "ActiveTenantCount",
- s."TotalSubscriptionCount" as "TotalSubscriptionCount",
- (s."ActiveSubscriptionCount"::numeric * coalesce(p."MonthlyPrice", (p."YearlyPrice" / 12.0), 0))::numeric(18, 2) as "Mrr",
- (s."ActiveSubscriptionCount"::numeric * coalesce(p."YearlyPrice", (p."MonthlyPrice" * 12), 0))::numeric(18, 2) as "Arr",
- s."ExpiringTenantCount7Days" as "ExpiringTenantCount7Days",
- s."ExpiringTenantCount15Days" as "ExpiringTenantCount15Days",
- s."ExpiringTenantCount30Days" as "ExpiringTenantCount30Days"
- from stats s
- left join public.tenant_packages p on p."Id" = s."TenantPackageId" and p."DeletedAt" is null;
- """);
-
- parameters = list.ToArray();
- return builder.ToString();
- }
-
- private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
- {
- var command = connection.CreateCommand();
- command.CommandText = sql;
-
- foreach (var (name, value) in parameters)
- {
- var p = command.CreateParameter();
- p.ParameterName = name;
- p.Value = value ?? DBNull.Value;
- command.Parameters.Add(p);
- }
-
- return (DbCommand)command;
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantProgressQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantProgressQueryHandler.cs
index 25eaca6..efe1cbe 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantProgressQueryHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantProgressQueryHandler.cs
@@ -5,27 +5,43 @@ using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
///
/// 租户入住进度查询处理器。
///
-public sealed class GetTenantProgressQueryHandler(ITenantRepository tenantRepository)
+public sealed class GetTenantProgressQueryHandler(
+ ITenantRepository tenantRepository,
+ ITenantProvider tenantProvider)
: IRequestHandler
{
///
public async Task Handle(GetTenantProgressQuery request, CancellationToken cancellationToken)
{
- // 1. 查询租户
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
+ // 1. 校验租户上下文(租户端禁止跨租户)
+ var currentTenantId = tenantProvider.GetCurrentTenantId();
+ if (currentTenantId <= 0)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
+ }
+
+ // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致
+ if (request.TenantId > 0 && request.TenantId != currentTenantId)
+ {
+ throw new BusinessException(ErrorCodes.Forbidden, "无权查询其他租户进度");
+ }
+
+ // 3. (空行后) 查询租户
+ var tenant = await tenantRepository.FindByIdAsync(currentTenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
- // 2. 查询订阅与实名
- var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
- var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken);
+ // 4. (空行后) 查询订阅与实名
+ var subscription = await tenantRepository.GetActiveSubscriptionAsync(currentTenantId, cancellationToken);
+ var verification = await tenantRepository.GetVerificationProfileAsync(currentTenantId, cancellationToken);
- // 3. 组装进度信息
+ // 5. (空行后) 组装进度信息
return new TenantProgressDto
{
TenantId = tenant.Id,
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantQuotaUsageHistoryQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantQuotaUsageHistoryQueryHandler.cs
index 57ecd65..4d46550 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantQuotaUsageHistoryQueryHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantQuotaUsageHistoryQueryHandler.cs
@@ -9,6 +9,7 @@ using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Data;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Results;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
@@ -17,22 +18,36 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers;
///
public sealed class GetTenantQuotaUsageHistoryQueryHandler(
ITenantRepository tenantRepository,
+ ITenantProvider tenantProvider,
IDapperExecutor dapperExecutor)
: IRequestHandler>
{
///
public async Task> Handle(GetTenantQuotaUsageHistoryQuery request, CancellationToken cancellationToken)
{
- // 1. 校验租户存在
- _ = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
+ // 1. 校验租户上下文(租户端禁止跨租户)
+ var currentTenantId = tenantProvider.GetCurrentTenantId();
+ if (currentTenantId <= 0)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
+ }
+
+ // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致
+ if (request.TenantId > 0 && request.TenantId != currentTenantId)
+ {
+ throw new BusinessException(ErrorCodes.Forbidden, "无权查询其他租户配额历史");
+ }
+
+ // 3. (空行后) 校验租户存在
+ _ = await tenantRepository.FindByIdAsync(currentTenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
- // 2. 规范化分页
+ // 4. (空行后) 规范化分页
var page = request.Page <= 0 ? 1 : request.Page;
var pageSize = request.PageSize is <= 0 or > 100 ? 10 : request.PageSize;
var offset = (page - 1) * pageSize;
- // 3. 查询总数 + 列表
+ // 5. (空行后) 查询总数 + 列表
return await dapperExecutor.QueryAsync(
DatabaseConstants.AppDataSource,
DatabaseConnectionRole.Read,
@@ -43,7 +58,7 @@ public sealed class GetTenantQuotaUsageHistoryQueryHandler(
connection,
BuildCountSql(),
[
- ("tenantId", request.TenantId),
+ ("tenantId", currentTenantId),
("quotaType", request.QuotaType.HasValue ? (int)request.QuotaType.Value : null),
("startDate", request.StartDate),
("endDate", request.EndDate)
@@ -55,7 +70,7 @@ public sealed class GetTenantQuotaUsageHistoryQueryHandler(
connection,
BuildListSql(),
[
- ("tenantId", request.TenantId),
+ ("tenantId", currentTenantId),
("quotaType", request.QuotaType.HasValue ? (int)request.QuotaType.Value : null),
("startDate", request.StartDate),
("endDate", request.EndDate),
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantReviewClaimQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantReviewClaimQueryHandler.cs
deleted file mode 100644
index 4f90a4c..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantReviewClaimQueryHandler.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-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. 查询当前领取信息(未领取返回 null)
- var claim = await tenantRepository.GetActiveReviewClaimAsync(request.TenantId, cancellationToken);
- return claim?.ToDto();
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ImpersonateTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ImpersonateTenantCommandHandler.cs
deleted file mode 100644
index 55c60ca..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ImpersonateTenantCommandHandler.cs
+++ /dev/null
@@ -1,109 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.Identity.Abstractions;
-using TakeoutSaaS.Application.Identity.Contracts;
-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.Security;
-using TakeoutSaaS.Shared.Abstractions.Tenancy;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 伪装登录租户处理器(平台超级管理员使用)。
-///
-public sealed class ImpersonateTenantCommandHandler(
- ITenantRepository tenantRepository,
- ITenantProvider tenantProvider,
- ITenantContextAccessor tenantContextAccessor,
- IIdentityUserRepository identityUserRepository,
- ICurrentUserAccessor currentUserAccessor,
- IAdminAuthService adminAuthService,
- IJwtTokenService jwtTokenService)
- : IRequestHandler
-{
- private const long PlatformRootTenantId = 1000000000001;
-
- ///
- public async Task Handle(ImpersonateTenantCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验仅允许平台超级管理员执行
- var currentTenantId = tenantProvider.GetCurrentTenantId();
- if (currentTenantId != PlatformRootTenantId)
- {
- throw new BusinessException(ErrorCodes.Forbidden, "仅平台超级管理员可执行伪装登录");
- }
-
- // 2. 读取操作者信息(在平台租户上下文内)
- var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
- var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
- ? $"user:{currentUserAccessor.UserId}"
- : operatorProfile.DisplayName;
-
- // 2. 校验租户存在且存在主管理员
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
-
- // 2.1 若缺少主管理员则自动回填(兼容历史数据)
- if (!tenant.PrimaryOwnerUserId.HasValue || tenant.PrimaryOwnerUserId.Value == 0)
- {
- var originalContextForFix = tenantContextAccessor.Current;
- tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "admin:impersonate:fix-owner");
- try
- {
- var users = await identityUserRepository.SearchAsync(tenant.Id, keyword: null, cancellationToken);
- var ownerCandidate = users.OrderBy(x => x.CreatedAt).FirstOrDefault();
- if (ownerCandidate == null)
- {
- throw new BusinessException(ErrorCodes.BadRequest, "该租户未配置主管理员账号,且未找到可用管理员账号");
- }
-
- tenant.PrimaryOwnerUserId = ownerCandidate.Id;
- await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
- await tenantRepository.SaveChangesAsync(cancellationToken);
- }
- finally
- {
- tenantContextAccessor.Current = originalContextForFix;
- }
- }
-
- // 3. 进入目标租户上下文以读取租户内用户(避免多租户查询过滤导致找不到用户)
- var originalTenantContext = tenantContextAccessor.Current;
- tenantContextAccessor.Current = new TenantContext(tenant.Id, null, "admin:impersonate");
- try
- {
- // 4. 为租户主管理员签发令牌
- var targetProfile = await adminAuthService.GetProfileAsync(tenant.PrimaryOwnerUserId.Value, cancellationToken);
- var token = await jwtTokenService.CreateTokensAsync(targetProfile, false, cancellationToken);
-
- // 5. 恢复租户上下文后写入审计日志
- tenantContextAccessor.Current = originalTenantContext;
- var auditLog = new TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.ImpersonatedLogin,
- Title = "伪装登录",
- Description = $"操作者:{operatorName},目标账号:{targetProfile.Account}",
- OperatorId = currentUserAccessor.UserId,
- OperatorName = operatorName,
- PreviousStatus = tenant.Status,
- CurrentStatus = tenant.Status
- };
- await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken);
- await tenantRepository.SaveChangesAsync(cancellationToken);
-
- // 6. 返回令牌
- return token;
- }
- finally
- {
- // 7. 确保恢复租户上下文
- tenantContextAccessor.Current = originalTenantContext;
- }
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantBillingPaidCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantBillingPaidCommandHandler.cs
deleted file mode 100644
index 5ad5776..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantBillingPaidCommandHandler.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Domain.Tenants.Enums;
-using TakeoutSaaS.Domain.Tenants.Repositories;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 标记账单支付处理器。
-///
-public sealed class MarkTenantBillingPaidCommandHandler(ITenantBillingRepository billingRepository)
- : IRequestHandler
-{
- ///
- /// 标记账单支付。
- ///
- /// 标记命令。
- /// 取消标记。
- /// 账单 DTO 或 null。
- public async Task Handle(MarkTenantBillingPaidCommand request, CancellationToken cancellationToken)
- {
- // 1. 查询账单
- var bill = await billingRepository.FindByIdAsync(request.TenantId, request.BillingId, cancellationToken);
- if (bill == null)
- {
- return null;
- }
-
- // 2. 更新支付状态
- bill.AmountPaid = request.AmountPaid;
- bill.Status = TenantBillingStatus.Paid;
- bill.DueDate = bill.DueDate;
-
- // 3. 持久化变更
- await billingRepository.UpdateAsync(bill, cancellationToken);
- await billingRepository.SaveChangesAsync(cancellationToken);
-
- // 4. 返回 DTO
- return bill.ToDto();
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantNotificationReadCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantNotificationReadCommandHandler.cs
index 6fd9a8f..7d75258 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantNotificationReadCommandHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkTenantNotificationReadCommandHandler.cs
@@ -2,13 +2,18 @@ using MediatR;
using TakeoutSaaS.Application.App.Tenants.Commands;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Domain.Tenants.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Constants;
+using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
///
/// 标记通知已读处理器。
///
-public sealed class MarkTenantNotificationReadCommandHandler(ITenantNotificationRepository notificationRepository)
+public sealed class MarkTenantNotificationReadCommandHandler(
+ ITenantNotificationRepository notificationRepository,
+ ITenantProvider tenantProvider)
: IRequestHandler
{
///
@@ -19,14 +24,27 @@ public sealed class MarkTenantNotificationReadCommandHandler(ITenantNotification
/// 通知 DTO 或 null。
public async Task Handle(MarkTenantNotificationReadCommand request, CancellationToken cancellationToken)
{
- // 1. 查询通知
- var notification = await notificationRepository.FindByIdAsync(request.TenantId, request.NotificationId, cancellationToken);
+ // 1. 校验租户上下文(租户端禁止跨租户)
+ var currentTenantId = tenantProvider.GetCurrentTenantId();
+ if (currentTenantId <= 0)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
+ }
+
+ // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致
+ if (request.TenantId > 0 && request.TenantId != currentTenantId)
+ {
+ throw new BusinessException(ErrorCodes.Forbidden, "无权标记其他租户通知");
+ }
+
+ // 3. (空行后) 查询通知
+ var notification = await notificationRepository.FindByIdAsync(currentTenantId, request.NotificationId, cancellationToken);
if (notification == null)
{
return null;
}
- // 2. 若未读则标记已读
+ // 4. (空行后) 若未读则标记已读
if (notification.ReadAt == null)
{
notification.ReadAt = DateTime.UtcNow;
@@ -34,7 +52,7 @@ public sealed class MarkTenantNotificationReadCommandHandler(ITenantNotification
await notificationRepository.SaveChangesAsync(cancellationToken);
}
- // 3. 返回 DTO
+ // 5. (空行后) 返回 DTO
return notification.ToDto();
}
}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RegisterTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RegisterTenantCommandHandler.cs
deleted file mode 100644
index e236ab1..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RegisterTenantCommandHandler.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-using MediatR;
-using Microsoft.Extensions.Logging;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-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;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 租户注册处理器。
-///
-public sealed class RegisterTenantCommandHandler(
- ITenantRepository tenantRepository,
- IIdGenerator idGenerator,
- ILogger logger)
- : IRequestHandler
-{
- ///
- public async Task Handle(RegisterTenantCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验订阅时长
- if (request.DurationMonths <= 0)
- {
- throw new BusinessException(ErrorCodes.BadRequest, "订阅时长必须大于 0");
- }
-
- // 2. 检查租户编码唯一性
- if (await tenantRepository.ExistsByCodeAsync(request.Code, cancellationToken))
- {
- throw new BusinessException(ErrorCodes.Conflict, $"租户编码 {request.Code} 已存在");
- }
-
- // 3. 计算生效时间
- var now = DateTime.UtcNow;
- var effectiveFrom = request.EffectiveFrom ?? now;
- var effectiveTo = effectiveFrom.AddMonths(request.DurationMonths);
-
- // 4. 构建租户实体
- var tenant = new Tenant
- {
- Id = idGenerator.NextId(),
- Code = request.Code.Trim(),
- Name = request.Name,
- ShortName = request.ShortName,
- Industry = request.Industry,
- ContactName = request.ContactName,
- ContactPhone = request.ContactPhone,
- ContactEmail = request.ContactEmail,
- Status = TenantStatus.PendingReview,
- EffectiveFrom = effectiveFrom,
- EffectiveTo = effectiveTo
- };
-
- // 5. 构建订阅实体
- var subscription = new TenantSubscription
- {
- Id = idGenerator.NextId(),
- TenantId = tenant.Id,
- TenantPackageId = request.TenantPackageId,
- EffectiveFrom = effectiveFrom,
- EffectiveTo = effectiveTo,
- NextBillingDate = effectiveTo,
- Status = SubscriptionStatus.Pending,
- AutoRenew = request.AutoRenew,
- Notes = "Init subscription"
- };
-
- // 6. 持久化租户、订阅和审计日志
- await tenantRepository.AddTenantAsync(tenant, cancellationToken);
- await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
- await tenantRepository.AddAuditLogAsync(new TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.RegistrationSubmitted,
- Title = "租户注册",
- Description = $"提交套餐 {request.TenantPackageId},时长 {request.DurationMonths} 月"
- }, cancellationToken);
-
- await tenantRepository.SaveChangesAsync(cancellationToken);
-
- // 7. 记录日志
- logger.LogInformation("已注册租户 {TenantCode}", tenant.Code);
-
- // 8. 返回 DTO
- return TenantMapping.ToDto(tenant, subscription, null);
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReleaseTenantReviewClaimCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReleaseTenantReviewClaimCommandHandler.cs
deleted file mode 100644
index 7124f01..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReleaseTenantReviewClaimCommandHandler.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Application.Identity.Abstractions;
-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.Security;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 释放租户入驻审核领取处理器。
-///
-public sealed class ReleaseTenantReviewClaimCommandHandler(
- ITenantRepository tenantRepository,
- ICurrentUserAccessor currentUserAccessor,
- IAdminAuthService adminAuthService)
- : IRequestHandler
-{
- ///
- public async Task Handle(ReleaseTenantReviewClaimCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验租户存在
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
-
- // 2. 查询当前领取记录
- var claim = await tenantRepository.FindActiveReviewClaimAsync(request.TenantId, cancellationToken);
- if (claim == null)
- {
- return null;
- }
-
- // 3. 非领取人不允许释放(如需接管请使用强制接管)
- if (claim.ClaimedBy != currentUserAccessor.UserId)
- {
- throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {claim.ClaimedByName} 领取");
- }
-
- // 4. 释放领取并记录审计
- var profile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
- var displayName = string.IsNullOrWhiteSpace(profile.DisplayName)
- ? $"user:{currentUserAccessor.UserId}"
- : profile.DisplayName;
-
- claim.ReleasedAt = DateTime.UtcNow;
- await tenantRepository.UpdateReviewClaimAsync(claim, cancellationToken);
- await tenantRepository.AddAuditLogAsync(new TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.ReviewClaimReleased,
- Title = "释放审核",
- Description = $"释放人:{displayName}",
- OperatorId = currentUserAccessor.UserId,
- OperatorName = displayName,
- PreviousStatus = tenant.Status,
- CurrentStatus = tenant.Status
- }, cancellationToken);
-
- await tenantRepository.SaveChangesAsync(cancellationToken);
- return claim.ToDto();
- }
-}
-
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReviewTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReviewTenantCommandHandler.cs
deleted file mode 100644
index 7909b76..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReviewTenantCommandHandler.cs
+++ /dev/null
@@ -1,226 +0,0 @@
-using MediatR;
-using System.Security.Cryptography;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Domain.Merchants.Entities;
-using TakeoutSaaS.Domain.Merchants.Enums;
-using TakeoutSaaS.Domain.Merchants.Repositories;
-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 ReviewTenantCommandHandler(
- ITenantRepository tenantRepository,
- IMerchantRepository merchantRepository,
- ICurrentUserAccessor currentUserAccessor,
- IIdGenerator idGenerator)
- : IRequestHandler
-{
- ///
- public async Task Handle(ReviewTenantCommand request, CancellationToken cancellationToken)
- {
- // 1. 获取租户与认证资料
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
-
- var reviewClaim = await tenantRepository.FindActiveReviewClaimAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.Conflict, "请先领取审核");
-
- if (reviewClaim.ClaimedBy != currentUserAccessor.UserId)
- {
- throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {reviewClaim.ClaimedByName} 领取");
- }
-
- var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.BadRequest, "请先提交实名认证资料");
-
- var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
-
- // 2. 记录审核人
- var actorName = currentUserAccessor.IsAuthenticated
- ? $"user:{currentUserAccessor.UserId}"
- : "system";
-
- // 3. 写入审核信息
- verification.ReviewedAt = DateTime.UtcNow;
- verification.ReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId;
- verification.ReviewedByName = actorName;
- verification.ReviewRemarks = request.Reason;
-
- var previousStatus = tenant.Status;
-
- // 4. 更新租户与订阅状态
- if (request.Approve)
- {
- if (!request.OperatingMode.HasValue)
- {
- throw new BusinessException(ErrorCodes.ValidationFailed, "审核通过时必须选择经营模式");
- }
-
- var renewMonths = request.RenewMonths ?? 0;
- if (renewMonths <= 0)
- {
- throw new BusinessException(ErrorCodes.ValidationFailed, "续费时长必须为正整数(月)");
- }
-
- var now = DateTime.UtcNow;
- verification.Status = TenantVerificationStatus.Approved;
- tenant.Status = TenantStatus.Active;
- tenant.OperatingMode = request.OperatingMode;
- if (subscription != null)
- {
- subscription.Status = SubscriptionStatus.Active;
-
- if (subscription.EffectiveFrom == default || subscription.EffectiveFrom > now)
- {
- subscription.EffectiveFrom = now;
- }
-
- var previousEffectiveTo = subscription.EffectiveTo;
- var baseEffectiveTo = subscription.EffectiveTo > now ? subscription.EffectiveTo : now;
- subscription.EffectiveTo = baseEffectiveTo.AddMonths(renewMonths);
- subscription.NextBillingDate = subscription.EffectiveTo;
-
- await tenantRepository.AddAuditLogAsync(new Domain.Tenants.Entities.TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.SubscriptionUpdated,
- Title = "订阅续费",
- Description = $"续费 {renewMonths} 月,到期时间:{previousEffectiveTo:yyyy-MM-dd HH:mm:ss} -> {subscription.EffectiveTo:yyyy-MM-dd HH:mm:ss}",
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName,
- PreviousStatus = previousStatus,
- CurrentStatus = tenant.Status
- }, cancellationToken);
- }
- else
- {
- throw new BusinessException(ErrorCodes.BadRequest, "订阅不存在,无法续费");
- }
-
- var existingMerchant = await merchantRepository.FindByTenantIdAsync(tenant.Id, cancellationToken);
- if (existingMerchant == null)
- {
- var merchant = new Merchant
- {
- Id = idGenerator.NextId(),
- TenantId = tenant.Id,
- BrandName = tenant.Name,
- BrandAlias = tenant.ShortName,
- Category = tenant.Industry,
- ContactPhone = tenant.ContactPhone ?? string.Empty,
- ContactEmail = tenant.ContactEmail,
- BusinessLicenseNumber = verification.BusinessLicenseNumber,
- BusinessLicenseImageUrl = verification.BusinessLicenseUrl,
- LegalPerson = verification.LegalPersonName,
- Province = tenant.Province,
- City = tenant.City,
- Address = tenant.Address,
- Status = MerchantStatus.Approved,
- OperatingMode = request.OperatingMode,
- ApprovedAt = now,
- ApprovedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- JoinedAt = now,
- LastReviewedAt = now,
- LastReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- IsFrozen = false,
- RowVersion = RandomNumberGenerator.GetBytes(16)
- };
-
- await merchantRepository.AddMerchantAsync(merchant, cancellationToken);
- await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
- {
- TenantId = tenant.Id,
- MerchantId = merchant.Id,
- Action = MerchantAuditAction.ReviewApproved,
- Title = "商户审核通过",
- Description = request.Reason,
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName
- }, cancellationToken);
- }
- else
- {
- existingMerchant.Status = MerchantStatus.Approved;
- existingMerchant.OperatingMode = request.OperatingMode;
- existingMerchant.ApprovedAt = now;
- existingMerchant.ApprovedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId;
- existingMerchant.LastReviewedAt = now;
- existingMerchant.LastReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId;
- existingMerchant.IsFrozen = false;
- existingMerchant.FrozenReason = null;
- existingMerchant.FrozenAt = null;
- await merchantRepository.UpdateMerchantAsync(existingMerchant, cancellationToken);
- await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
- {
- TenantId = tenant.Id,
- MerchantId = existingMerchant.Id,
- Action = MerchantAuditAction.ReviewApproved,
- Title = "商户审核通过",
- Description = request.Reason,
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName
- }, cancellationToken);
- }
- }
- else
- {
- verification.Status = TenantVerificationStatus.Rejected;
- tenant.Status = TenantStatus.PendingReview;
- if (subscription != null)
- {
- subscription.Status = SubscriptionStatus.Suspended;
- }
- }
-
- // 5. 持久化租户与认证资料
- await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
- await tenantRepository.UpsertVerificationProfileAsync(verification, cancellationToken);
- if (subscription != null)
- {
- await tenantRepository.UpdateSubscriptionAsync(subscription, cancellationToken);
- }
-
- // 6. 记录审核日志
- await tenantRepository.AddAuditLogAsync(new Domain.Tenants.Entities.TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = request.Approve ? TenantAuditAction.VerificationApproved : TenantAuditAction.VerificationRejected,
- Title = request.Approve ? "审核通过" : "审核驳回",
- Description = request.Reason,
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName,
- PreviousStatus = previousStatus,
- CurrentStatus = tenant.Status
- }, cancellationToken);
-
- // 7. 审核完成自动释放领取
- reviewClaim.ReleasedAt = DateTime.UtcNow;
- await tenantRepository.UpdateReviewClaimAsync(reviewClaim, cancellationToken);
- await tenantRepository.AddAuditLogAsync(new Domain.Tenants.Entities.TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.ReviewClaimReleased,
- Title = "审核完成释放",
- Description = $"释放人:{actorName}",
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName,
- PreviousStatus = tenant.Status,
- CurrentStatus = tenant.Status
- }, cancellationToken);
-
- // 8. 保存并返回 DTO
- await tenantRepository.SaveChangesAsync(cancellationToken);
- await merchantRepository.SaveChangesAsync(cancellationToken);
-
- return TenantMapping.ToDto(tenant, subscription, verification);
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantBillsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantBillsQueryHandler.cs
index d20ebf6..3286117 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantBillsQueryHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantBillsQueryHandler.cs
@@ -3,13 +3,18 @@ using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Results;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+using TakeoutSaaS.Shared.Abstractions.Constants;
+using TakeoutSaaS.Shared.Abstractions.Exceptions;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
///
/// 账单分页查询处理器。
///
-public sealed class SearchTenantBillsQueryHandler(ITenantBillingRepository billingRepository)
+public sealed class SearchTenantBillsQueryHandler(
+ ITenantBillingRepository billingRepository,
+ ITenantProvider tenantProvider)
: IRequestHandler>
{
///
@@ -20,16 +25,29 @@ public sealed class SearchTenantBillsQueryHandler(ITenantBillingRepository billi
/// 分页结果。
public async Task> Handle(SearchTenantBillsQuery request, CancellationToken cancellationToken)
{
- // 1. 查询账单
- var bills = await billingRepository.SearchAsync(request.TenantId, request.Status, request.From, request.To, cancellationToken);
+ // 1. 校验租户上下文(租户端禁止跨租户)
+ var currentTenantId = tenantProvider.GetCurrentTenantId();
+ if (currentTenantId <= 0)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
+ }
- // 2. 排序与分页
+ // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致
+ if (request.TenantId > 0 && request.TenantId != currentTenantId)
+ {
+ throw new BusinessException(ErrorCodes.Forbidden, "无权查询其他租户账单");
+ }
+
+ // 3. (空行后) 查询账单
+ var bills = await billingRepository.SearchAsync(currentTenantId, request.Status, request.From, request.To, cancellationToken);
+
+ // 4. (空行后) 排序与分页
var ordered = bills.OrderByDescending(x => x.PeriodEnd).ToList();
var page = request.Page <= 0 ? 1 : request.Page;
var size = request.PageSize <= 0 ? 20 : request.PageSize;
var items = ordered.Skip((page - 1) * size).Take(size).Select(x => x.ToDto()).ToList();
- // 3. 返回分页结果
+ // 5. (空行后) 返回分页结果
return new PagedResult(items, page, size, ordered.Count);
}
}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantNotificationsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantNotificationsQueryHandler.cs
index b74ada5..c37a1df 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantNotificationsQueryHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantNotificationsQueryHandler.cs
@@ -3,13 +3,18 @@ using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Results;
+using TakeoutSaaS.Shared.Abstractions.Constants;
+using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
///
/// 通知分页查询处理器。
///
-public sealed class SearchTenantNotificationsQueryHandler(ITenantNotificationRepository notificationRepository)
+public sealed class SearchTenantNotificationsQueryHandler(
+ ITenantNotificationRepository notificationRepository,
+ ITenantProvider tenantProvider)
: IRequestHandler>
{
///
@@ -20,22 +25,35 @@ public sealed class SearchTenantNotificationsQueryHandler(ITenantNotificationRep
/// 分页结果。
public async Task> Handle(SearchTenantNotificationsQuery request, CancellationToken cancellationToken)
{
- // 1. 查询通知
+ // 1. 校验租户上下文(租户端禁止跨租户)
+ var currentTenantId = tenantProvider.GetCurrentTenantId();
+ if (currentTenantId <= 0)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
+ }
+
+ // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致
+ if (request.TenantId > 0 && request.TenantId != currentTenantId)
+ {
+ throw new BusinessException(ErrorCodes.Forbidden, "无权查询其他租户通知");
+ }
+
+ // 3. (空行后) 查询通知
var notifications = await notificationRepository.SearchAsync(
- request.TenantId,
+ currentTenantId,
request.Severity,
request.UnreadOnly,
null,
null,
cancellationToken);
- // 2. 排序与分页
+ // 4. (空行后) 排序与分页
var ordered = notifications.OrderByDescending(x => x.SentAt).ToList();
var page = request.Page <= 0 ? 1 : request.Page;
var size = request.PageSize <= 0 ? 20 : request.PageSize;
var items = ordered.Skip((page - 1) * size).Take(size).Select(x => x.ToDto()).ToList();
- // 3. 返回分页结果
+ // 5. (空行后) 返回分页结果
return new PagedResult(items, page, size, ordered.Count);
}
}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantPackagesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantPackagesQueryHandler.cs
deleted file mode 100644
index 4081c91..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantPackagesQueryHandler.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-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 SearchTenantPackagesQueryHandler(ITenantPackageRepository packageRepository)
- : IRequestHandler>
-{
- ///
- public async Task> Handle(SearchTenantPackagesQuery request, CancellationToken cancellationToken)
- {
- // 1. 查询套餐
- var packages = await packageRepository.SearchAsync(request.Keyword, request.IsActive, cancellationToken);
-
- // 2. 排序与分页
- var ordered = packages
- .OrderBy(x => x.SortOrder)
- .ThenByDescending(x => x.CreatedAt)
- .ToList();
- var pageIndex = request.Page <= 0 ? 1 : request.Page;
- var size = request.PageSize <= 0 ? 20 : request.PageSize;
- var pagedItems = ordered
- .Skip((pageIndex - 1) * size)
- .Take(size)
- .Select(x => x.ToDto())
- .ToList();
-
- // 3. 返回分页结果
- return new PagedResult(pagedItems, pageIndex, size, ordered.Count);
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantsQueryHandler.cs
deleted file mode 100644
index cba1dfe..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SearchTenantsQueryHandler.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-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 SearchTenantsQueryHandler(ITenantRepository tenantRepository)
- : IRequestHandler>
-{
- ///
- public async Task> Handle(SearchTenantsQuery request, CancellationToken cancellationToken)
- {
- // 1. 按条件分页查询租户
- var (tenants, total) = await tenantRepository.SearchPagedAsync(
- request.Status,
- request.VerificationStatus,
- request.Name,
- request.ContactName,
- request.ContactPhone,
- request.Keyword,
- request.Page,
- request.PageSize,
- cancellationToken);
-
- // 2. 无数据直接返回
- if (tenants.Count == 0)
- {
- return new PagedResult([], request.Page, request.PageSize, total);
- }
-
- // 3. 批量查询订阅与实名资料(避免 N+1)
- var tenantIds = tenants.Select(x => x.Id).ToArray();
- var subscriptions = await tenantRepository.GetSubscriptionsAsync(tenantIds, cancellationToken);
- var verifications = await tenantRepository.GetVerificationProfilesAsync(tenantIds, cancellationToken);
-
- // 4. 构建订阅与实名资料映射
- var subscriptionByTenantId = subscriptions
- .GroupBy(x => x.TenantId)
- .ToDictionary(x => x.Key, x => x.FirstOrDefault());
- var verificationByTenantId = verifications.ToDictionary(x => x.TenantId);
-
- // 5. 映射 DTO(带订阅与认证)
- var result = new List(tenants.Count);
- foreach (var tenant in tenants)
- {
- subscriptionByTenantId.TryGetValue(tenant.Id, out var subscription);
- verificationByTenantId.TryGetValue(tenant.Id, out var verification);
- result.Add(TenantMapping.ToDto(tenant, subscription, verification));
- }
-
- // 6. 返回分页结果
- return new PagedResult(result, request.Page, request.PageSize, total);
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SubmitTenantVerificationCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SubmitTenantVerificationCommandHandler.cs
index 8771c72..93802db 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SubmitTenantVerificationCommandHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SubmitTenantVerificationCommandHandler.cs
@@ -7,6 +7,7 @@ using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Ids;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
@@ -15,21 +16,35 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers;
///
public sealed class SubmitTenantVerificationCommandHandler(
ITenantRepository tenantRepository,
+ ITenantProvider tenantProvider,
IIdGenerator idGenerator)
: IRequestHandler
{
///
public async Task Handle(SubmitTenantVerificationCommand request, CancellationToken cancellationToken)
{
- // 1. 获取租户
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
+ // 1. 校验租户上下文(租户端禁止跨租户)
+ var currentTenantId = tenantProvider.GetCurrentTenantId();
+ if (currentTenantId <= 0)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
+ }
+
+ // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致
+ if (request.TenantId > 0 && request.TenantId != currentTenantId)
+ {
+ throw new BusinessException(ErrorCodes.Forbidden, "无权提交其他租户实名认证资料");
+ }
+
+ // 3. (空行后) 获取租户
+ var tenant = await tenantRepository.FindByIdAsync(currentTenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
- // 2. 读取或初始化实名资料
- var profile = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken)
+ // 4. (空行后) 读取或初始化实名资料
+ var profile = await tenantRepository.GetVerificationProfileAsync(currentTenantId, cancellationToken)
?? new TenantVerificationProfile { Id = idGenerator.NextId(), TenantId = tenant.Id };
- // 3. 填充资料
+ // 5. (空行后) 填充资料
profile.BusinessLicenseNumber = request.BusinessLicenseNumber;
profile.BusinessLicenseUrl = request.BusinessLicenseUrl;
profile.LegalPersonName = request.LegalPersonName;
@@ -47,7 +62,7 @@ public sealed class SubmitTenantVerificationCommandHandler(
profile.ReviewedBy = null;
profile.ReviewedByName = null;
- // 4. 保存资料并记录审计
+ // 6. (空行后) 保存资料并记录审计
await tenantRepository.UpsertVerificationProfileAsync(profile, cancellationToken);
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
{
@@ -58,7 +73,7 @@ public sealed class SubmitTenantVerificationCommandHandler(
}, cancellationToken);
await tenantRepository.SaveChangesAsync(cancellationToken);
- // 5. 返回 DTO
+ // 7. (空行后) 返回 DTO
return profile.ToVerificationDto()
?? throw new BusinessException(ErrorCodes.InternalServerError, "实名资料保存失败");
}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UnfreezeTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UnfreezeTenantCommandHandler.cs
deleted file mode 100644
index b51275e..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UnfreezeTenantCommandHandler.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-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.Security;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 解冻租户处理器。
-///
-public sealed class UnfreezeTenantCommandHandler(
- ITenantRepository tenantRepository,
- ICurrentUserAccessor currentUserAccessor)
- : IRequestHandler
-{
- ///
- public async Task Handle(UnfreezeTenantCommand request, CancellationToken cancellationToken)
- {
- // 1. 获取租户与订阅
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
- ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
-
- var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
- var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken);
-
- var previousStatus = tenant.Status;
- if (tenant.Status != TenantStatus.Suspended)
- {
- throw new BusinessException(ErrorCodes.BadRequest, "当前租户未处于冻结状态");
- }
-
- // 2. 计算恢复状态(到期则回到到期状态)
- var now = DateTime.UtcNow;
- var isExpired = subscription != null && subscription.EffectiveTo <= now;
-
- tenant.Status = isExpired ? TenantStatus.Expired : TenantStatus.Active;
- tenant.SuspendedAt = null;
- tenant.SuspensionReason = null;
-
- // 3. 同步订阅状态
- if (subscription != null)
- {
- subscription.Status = isExpired ? SubscriptionStatus.GracePeriod : SubscriptionStatus.Active;
- await tenantRepository.UpdateSubscriptionAsync(subscription, cancellationToken);
- }
-
- await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
-
- // 4. 记录审计
- var actorName = currentUserAccessor.IsAuthenticated
- ? $"user:{currentUserAccessor.UserId}"
- : "system";
-
- await tenantRepository.AddAuditLogAsync(new TenantAuditLog
- {
- TenantId = tenant.Id,
- Action = TenantAuditAction.StatusChanged,
- Title = "解冻租户",
- Description = request.Reason,
- OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
- OperatorName = actorName,
- PreviousStatus = previousStatus,
- CurrentStatus = tenant.Status
- }, cancellationToken);
-
- // 5. 保存并返回
- await tenantRepository.SaveChangesAsync(cancellationToken);
- return TenantMapping.ToDto(tenant, subscription, verification);
- }
-}
-
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs
index ed19111..6f34e4e 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs
@@ -5,18 +5,34 @@ using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
///
/// 更新公告处理器。
///
-public sealed class UpdateTenantAnnouncementCommandHandler(ITenantAnnouncementRepository announcementRepository)
+public sealed class UpdateTenantAnnouncementCommandHandler(
+ ITenantAnnouncementRepository announcementRepository,
+ ITenantProvider tenantProvider)
: IRequestHandler
{
public async Task Handle(UpdateTenantAnnouncementCommand request, CancellationToken cancellationToken)
{
- // 1. 校验输入
+ // 1. 校验租户上下文(租户端禁止跨租户)
+ var currentTenantId = tenantProvider.GetCurrentTenantId();
+ if (currentTenantId <= 0)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
+ }
+
+ // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致
+ if (request.TenantId > 0 && request.TenantId != currentTenantId)
+ {
+ throw new BusinessException(ErrorCodes.Forbidden, "无权编辑其他租户公告");
+ }
+
+ // 3. (空行后) 校验输入
if (string.IsNullOrWhiteSpace(request.Title) || string.IsNullOrWhiteSpace(request.Content))
{
throw new BusinessException(ErrorCodes.ValidationFailed, "公告标题和内容不能为空");
@@ -27,8 +43,8 @@ public sealed class UpdateTenantAnnouncementCommandHandler(ITenantAnnouncementRe
throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空");
}
- // 2. 查询公告
- var announcement = await announcementRepository.FindByIdAsync(request.TenantId, request.AnnouncementId, cancellationToken);
+ // 4. (空行后) 查询公告
+ var announcement = await announcementRepository.FindByIdAsync(currentTenantId, request.AnnouncementId, cancellationToken);
if (announcement == null)
{
return null;
@@ -44,14 +60,14 @@ public sealed class UpdateTenantAnnouncementCommandHandler(ITenantAnnouncementRe
throw new BusinessException(ErrorCodes.Conflict, "仅草稿公告允许编辑");
}
- // 3. 更新字段
+ // 5. (空行后) 更新字段
announcement.Title = request.Title.Trim();
announcement.Content = request.Content;
announcement.TargetType = string.IsNullOrWhiteSpace(request.TargetType) ? announcement.TargetType : request.TargetType.Trim();
announcement.TargetParameters = request.TargetParameters;
announcement.RowVersion = request.RowVersion;
- // 4. 持久化
+ // 6. (空行后) 持久化
try
{
await announcementRepository.UpdateAsync(announcement, cancellationToken);
@@ -62,7 +78,7 @@ public sealed class UpdateTenantAnnouncementCommandHandler(ITenantAnnouncementRe
throw new BusinessException(ErrorCodes.Conflict, "公告已被修改,请刷新后重试");
}
- // 5. 返回 DTO
+ // 7. (空行后) 返回 DTO
return announcement.ToDto(false, null);
}
}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantCommandHandler.cs
index a8c4d94..b4ac9ff 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantCommandHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantCommandHandler.cs
@@ -4,6 +4,7 @@ using TakeoutSaaS.Application.App.Tenants.Commands;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
@@ -12,46 +13,55 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers;
///
public sealed class UpdateTenantCommandHandler(
ITenantRepository tenantRepository,
+ ITenantProvider tenantProvider,
ILogger logger)
: IRequestHandler
{
///
public async Task Handle(UpdateTenantCommand request, CancellationToken cancellationToken)
{
- // 1. 参数校验
- if (request.TenantId <= 0)
+ // 1. 校验租户上下文(租户端禁止跨租户)
+ var currentTenantId = tenantProvider.GetCurrentTenantId();
+ if (currentTenantId <= 0)
{
- throw new BusinessException(ErrorCodes.BadRequest, "tenantId 不能为空");
+ throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
}
+ // 2. (空行后) 兼容旧调用:若传入 TenantId,则必须与当前租户一致
+ if (request.TenantId > 0 && request.TenantId != currentTenantId)
+ {
+ throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他租户信息");
+ }
+
+ // 3. (空行后) 参数校验:租户名称
if (string.IsNullOrWhiteSpace(request.Name))
{
throw new BusinessException(ErrorCodes.BadRequest, "租户名称不能为空");
}
- // 2. 查询租户
- var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
+ // 4. (空行后) 查询租户
+ var tenant = await tenantRepository.FindByIdAsync(currentTenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
- // 3. 校验租户名称唯一性(排除自身)
+ // 5. (空行后) 校验租户名称唯一性(排除自身)
var normalizedName = request.Name.Trim();
- if (await tenantRepository.ExistsByNameAsync(normalizedName, excludeTenantId: request.TenantId, cancellationToken))
+ if (await tenantRepository.ExistsByNameAsync(normalizedName, excludeTenantId: currentTenantId, cancellationToken))
{
throw new BusinessException(ErrorCodes.Conflict, $"租户名称 {normalizedName} 已存在");
}
- // 4. 校验联系人手机号唯一性(仅当填写时)
+ // 6. (空行后) 校验联系人手机号唯一性(仅当填写时)
if (!string.IsNullOrWhiteSpace(request.ContactPhone))
{
var normalizedPhone = request.ContactPhone.Trim();
var existingTenantId = await tenantRepository.FindTenantIdByContactPhoneAsync(normalizedPhone, cancellationToken);
- if (existingTenantId.HasValue && existingTenantId.Value != request.TenantId)
+ if (existingTenantId.HasValue && existingTenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Conflict, $"手机号 {normalizedPhone} 已注册");
}
}
- // 5. 更新基础信息(禁止修改 Code)
+ // 7. (空行后) 更新基础信息(禁止修改 Code)
tenant.Name = normalizedName;
tenant.ShortName = string.IsNullOrWhiteSpace(request.ShortName) ? null : request.ShortName.Trim();
tenant.Industry = string.IsNullOrWhiteSpace(request.Industry) ? null : request.Industry.Trim();
@@ -59,14 +69,13 @@ public sealed class UpdateTenantCommandHandler(
tenant.ContactPhone = string.IsNullOrWhiteSpace(request.ContactPhone) ? null : request.ContactPhone.Trim();
tenant.ContactEmail = string.IsNullOrWhiteSpace(request.ContactEmail) ? null : request.ContactEmail.Trim();
- // 6. 持久化更新
+ // 8. (空行后) 持久化更新
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
await tenantRepository.SaveChangesAsync(cancellationToken);
- // 7. 记录日志
+ // 9. (空行后) 记录日志
logger.LogInformation("已更新租户基础信息 {TenantId}", tenant.Id);
return Unit.Value;
}
}
-
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantPackageCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantPackageCommandHandler.cs
deleted file mode 100644
index fd167de..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantPackageCommandHandler.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Domain.Tenants.Repositories;
-using TakeoutSaaS.Shared.Abstractions.Constants;
-using TakeoutSaaS.Shared.Abstractions.Exceptions;
-
-namespace TakeoutSaaS.Application.App.Tenants.Handlers;
-
-///
-/// 更新租户套餐处理器。
-///
-public sealed class UpdateTenantPackageCommandHandler(ITenantPackageRepository packageRepository)
- : IRequestHandler
-{
- ///
- public async Task Handle(UpdateTenantPackageCommand request, CancellationToken cancellationToken)
- {
- // 1. 校验必填项
- if (string.IsNullOrWhiteSpace(request.Name))
- {
- throw new BusinessException(ErrorCodes.BadRequest, "套餐名称不能为空");
- }
-
- // 2. 查询套餐
- var package = await packageRepository.FindByIdAsync(request.TenantPackageId, cancellationToken);
- if (package == null)
- {
- return null;
- }
-
- // 3. 更新字段
- package.Name = request.Name.Trim();
- package.Description = request.Description;
- package.PackageType = request.PackageType;
- package.MonthlyPrice = request.MonthlyPrice;
- package.YearlyPrice = request.YearlyPrice;
- package.MaxStoreCount = request.MaxStoreCount;
- package.MaxAccountCount = request.MaxAccountCount;
- package.MaxStorageGb = request.MaxStorageGb;
- package.MaxSmsCredits = request.MaxSmsCredits;
- package.MaxDeliveryOrders = request.MaxDeliveryOrders;
- package.FeaturePoliciesJson = request.FeaturePoliciesJson;
- package.IsActive = request.IsActive;
- package.IsPublicVisible = request.IsPublicVisible;
- package.IsAllowNewTenantPurchase = request.IsAllowNewTenantPurchase;
-
- // 3. 更新发布状态(若未传则保持不变,避免默认值覆盖)
- if (request.PublishStatus.HasValue)
- {
- package.PublishStatus = request.PublishStatus.Value;
- }
-
- // 4. 更新展示配置(推荐与标签)
- package.IsRecommended = request.IsRecommended;
- package.Tags = request.Tags ?? [];
- package.SortOrder = request.SortOrder;
-
- // 5. 持久化并返回
- await packageRepository.UpdateAsync(package, cancellationToken);
- await packageRepository.SaveChangesAsync(cancellationToken);
-
- return package.ToDto();
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantAuditLogsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantAuditLogsQuery.cs
deleted file mode 100644
index 10aa010..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantAuditLogsQuery.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Shared.Abstractions.Results;
-
-namespace TakeoutSaaS.Application.App.Tenants.Queries;
-
-///
-/// 租户审核日志查询。
-///
-public sealed record GetTenantAuditLogsQuery(
- long TenantId,
- int Page = 1,
- int PageSize = 20) : IRequest>;
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantByIdQuery.cs
deleted file mode 100644
index 43e226d..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantByIdQuery.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Queries;
-
-///
-/// 单个租户查询。
-///
-public sealed record GetTenantByIdQuery(long TenantId) : IRequest;
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageTenantsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageTenantsQuery.cs
deleted file mode 100644
index 3e05511..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageTenantsQuery.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Shared.Abstractions.Results;
-
-namespace TakeoutSaaS.Application.App.Tenants.Queries;
-
-///
-/// 查询指定套餐当前使用租户列表(按“当前有效订阅”口径)。
-///
-public sealed record GetTenantPackageTenantsQuery : IRequest>
-{
- ///
- /// 套餐 ID。
- ///
- public required long TenantPackageId { get; init; }
-
- ///
- /// 关键词(租户名称/编码/联系人/电话)。
- ///
- public string? Keyword { get; init; }
-
- ///
- /// 页码(从 1 开始)。
- ///
- public int Page { get; init; } = 1;
-
- ///
- /// 每页大小。
- ///
- public int PageSize { get; init; } = 20;
-
- ///
- /// 可选:未来 N 天内到期筛选(按“当前有效订阅”口径,且到期时间在 now ~ now+N 天内)。
- ///
- public int? ExpiringWithinDays { get; init; }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageUsagesQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageUsagesQuery.cs
deleted file mode 100644
index 729b149..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantPackageUsagesQuery.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Queries;
-
-///
-/// 查询套餐使用统计(订阅关联数量、使用租户数量)。
-///
-public sealed record GetTenantPackageUsagesQuery : IRequest>
-{
- ///
- /// 需要统计的套餐 ID 列表(为空表示统计全部)。
- ///
- public long[]? TenantPackageIds { get; init; }
-}
-
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantReviewClaimQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantReviewClaimQuery.cs
deleted file mode 100644
index 9fa6748..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantReviewClaimQuery.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-
-namespace TakeoutSaaS.Application.App.Tenants.Queries;
-
-///
-/// 获取租户审核领取信息查询。
-///
-public sealed record GetTenantReviewClaimQuery(long TenantId) : IRequest;
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantPackagesQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantPackagesQuery.cs
deleted file mode 100644
index c81a068..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantPackagesQuery.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Shared.Abstractions.Results;
-
-namespace TakeoutSaaS.Application.App.Tenants.Queries;
-
-///
-/// 分页查询租户套餐。
-///
-public sealed record SearchTenantPackagesQuery : IRequest>
-{
- ///
- /// 搜索关键词(名称/描述)。
- ///
- public string? Keyword { get; init; }
-
- ///
- /// 是否筛选可售套餐。
- ///
- public bool? IsActive { get; init; }
-
- ///
- /// 页码。
- ///
- public int Page { get; init; } = 1;
-
- ///
- /// 每页条数。
- ///
- public int PageSize { get; init; } = 20;
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantsQuery.cs
deleted file mode 100644
index 05f34d9..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/SearchTenantsQuery.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using MediatR;
-using TakeoutSaaS.Application.App.Tenants.Dto;
-using TakeoutSaaS.Domain.Tenants.Enums;
-using TakeoutSaaS.Shared.Abstractions.Results;
-
-namespace TakeoutSaaS.Application.App.Tenants.Queries;
-
-///
-/// 租户分页查询。
-///
-public sealed record SearchTenantsQuery : IRequest>
-{
- ///
- /// 租户状态(精确匹配)。
- ///
- public TenantStatus? Status { get; init; }
-
- ///
- /// 实名认证状态(精确匹配)。
- ///
- public TenantVerificationStatus? VerificationStatus { get; init; }
-
- ///
- /// 租户名称(模糊匹配)。
- ///
- public string? Name { get; init; }
-
- ///
- /// 联系人姓名(模糊匹配)。
- ///
- public string? ContactName { get; init; }
-
- ///
- /// 联系电话(模糊匹配)。
- ///
- public string? ContactPhone { get; init; }
-
- ///
- /// 兼容关键词:按“名称/编码/联系人/电话”做模糊匹配。
- ///
- public string? Keyword { get; init; }
-
- ///
- /// 页码(从 1 开始)。
- ///
- public int Page { get; init; } = 1;
-
- ///
- /// 每页大小。
- ///
- public int PageSize { get; init; } = 20;
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs
index 0616ea9..32d0d5f 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs
@@ -9,32 +9,6 @@ namespace TakeoutSaaS.Application.App.Tenants;
///
internal static class TenantMapping
{
- ///
- /// 将租户实体与订阅、认证信息映射为租户 DTO。
- ///
- /// 租户实体。
- /// 订阅信息。
- /// 认证信息。
- /// 租户 DTO。
- public static TenantDto ToDto(Tenant tenant, TenantSubscription? subscription, TenantVerificationProfile? verification)
- => new()
- {
- Id = tenant.Id,
- Code = tenant.Code,
- Name = tenant.Name,
- ShortName = tenant.ShortName,
- ContactName = tenant.ContactName,
- ContactPhone = tenant.ContactPhone,
- ContactEmail = tenant.ContactEmail,
- Status = tenant.Status,
- VerificationStatus = verification?.Status ?? Domain.Tenants.Enums.TenantVerificationStatus.Draft,
- OperatingMode = tenant.OperatingMode,
- CurrentPackageId = subscription?.TenantPackageId,
- EffectiveFrom = subscription?.EffectiveFrom ?? tenant.EffectiveFrom,
- EffectiveTo = subscription?.EffectiveTo ?? tenant.EffectiveTo,
- AutoRenew = subscription?.AutoRenew ?? false
- };
-
///
/// 将租户认证实体映射为 DTO。
///
@@ -65,60 +39,6 @@ internal static class TenantMapping
ReviewedAt = profile.ReviewedAt
};
- ///
- /// 将订阅实体映射为 DTO。
- ///
- /// 订阅实体。
- /// 订阅 DTO 或 null。
- public static TenantSubscriptionDto? ToSubscriptionDto(this TenantSubscription? subscription)
- => subscription == null
- ? null
- : new TenantSubscriptionDto
- {
- Id = subscription.Id,
- TenantId = subscription.TenantId,
- TenantPackageId = subscription.TenantPackageId,
- Status = subscription.Status,
- EffectiveFrom = subscription.EffectiveFrom,
- EffectiveTo = subscription.EffectiveTo,
- NextBillingDate = subscription.NextBillingDate,
- AutoRenew = subscription.AutoRenew
- };
-
- ///
- /// 将审计日志实体映射为 DTO。
- ///
- /// 审计日志实体。
- /// 审计日志 DTO。
- public static TenantAuditLogDto ToDto(this TenantAuditLog log)
- => new()
- {
- 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
- };
-
- ///
- /// 将审核领取实体映射为 DTO。
- ///
- /// 领取实体。
- /// 领取 DTO。
- public static TenantReviewClaimDto ToDto(this TenantReviewClaim claim)
- => new()
- {
- Id = claim.Id,
- TenantId = claim.TenantId,
- ClaimedBy = claim.ClaimedBy,
- ClaimedByName = claim.ClaimedByName,
- ClaimedAt = claim.ClaimedAt
- };
-
///
/// 将套餐实体映射为 DTO。
///
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/CreateAnnouncementCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/CreateAnnouncementCommandValidator.cs
index 69e04c4..036244c 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/CreateAnnouncementCommandValidator.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/CreateAnnouncementCommandValidator.cs
@@ -24,9 +24,13 @@ public sealed class CreateAnnouncementCommandValidator : AbstractValidator x.TargetType)
.NotEmpty();
- RuleFor(x => x)
- .Must(x => x.TenantId != 0 || x.PublisherScope == PublisherScope.Platform)
- .WithMessage("TenantId=0 仅允许平台公告");
+ RuleFor(x => x.TenantId)
+ .GreaterThan(0)
+ .WithMessage("TenantId 必须大于 0");
+
+ RuleFor(x => x.PublisherScope)
+ .Equal(PublisherScope.Tenant)
+ .WithMessage("租户端不允许创建平台公告");
RuleFor(x => x.EffectiveFrom)
.LessThan(x => x.EffectiveTo!.Value)
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/ReviewTenantValidator.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/ReviewTenantValidator.cs
deleted file mode 100644
index 84bd0a2..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/ReviewTenantValidator.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using FluentValidation;
-using TakeoutSaaS.Application.App.Tenants.Commands;
-
-namespace TakeoutSaaS.Application.App.Tenants.Validators;
-
-///
-/// 租户审核命令验证器。
-///
-public sealed class ReviewTenantValidator : AbstractValidator
-{
- ///
- /// 初始化验证规则。
- ///
- public ReviewTenantValidator()
- {
- RuleFor(x => x.TenantId).GreaterThan(0);
- RuleFor(x => x.Reason)
- .NotEmpty()
- .When(x => !x.Approve);
- RuleFor(x => x.OperatingMode)
- .NotNull()
- .When(x => x.Approve);
- RuleFor(x => x.RenewMonths)
- .NotNull()
- .GreaterThan(0)
- .When(x => x.Approve);
- }
-}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/SearchTenantsQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/SearchTenantsQueryValidator.cs
deleted file mode 100644
index 95ee503..0000000
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/SearchTenantsQueryValidator.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using FluentValidation;
-using TakeoutSaaS.Application.App.Tenants.Queries;
-
-namespace TakeoutSaaS.Application.App.Tenants.Validators;
-
-///
-/// 租户列表查询验证器。
-///
-public sealed class SearchTenantsQueryValidator : AbstractValidator
-{
- ///
- /// 初始化验证规则。
- ///
- public SearchTenantsQueryValidator()
- {
- RuleFor(x => x.Page).GreaterThan(0);
- RuleFor(x => x.PageSize).InclusiveBetween(1, 200);
- RuleFor(x => x.Keyword).MaximumLength(128);
- RuleFor(x => x.Name).MaximumLength(128);
- RuleFor(x => x.ContactName).MaximumLength(64);
- RuleFor(x => x.ContactPhone).MaximumLength(32);
- }
-}
diff --git a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/CreateAnnouncementCommandValidatorTests.cs b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/CreateAnnouncementCommandValidatorTests.cs
index 9abd854..8bbae52 100644
--- a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/CreateAnnouncementCommandValidatorTests.cs
+++ b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/CreateAnnouncementCommandValidatorTests.cs
@@ -62,7 +62,7 @@ public sealed class CreateAnnouncementCommandValidatorTests
}
[Fact]
- public void GivenTenantIdZeroAndNotPlatform_WhenValidate_ThenShouldHaveError()
+ public void GivenTenantIdZero_WhenValidate_ThenShouldHaveError()
{
// Arrange
var command = AnnouncementTestData.CreateValidCreateCommand() with
@@ -75,7 +75,23 @@ public sealed class CreateAnnouncementCommandValidatorTests
var result = _validator.TestValidate(command);
// Assert
- result.ShouldHaveValidationErrorFor(x => x);
+ result.ShouldHaveValidationErrorFor(x => x.TenantId);
+ }
+
+ [Fact]
+ public void GivenPlatformPublisherScope_WhenValidate_ThenShouldHaveError()
+ {
+ // Arrange
+ var command = AnnouncementTestData.CreateValidCreateCommand() with
+ {
+ PublisherScope = PublisherScope.Platform
+ };
+
+ // Act
+ var result = _validator.TestValidate(command);
+
+ // Assert
+ result.ShouldHaveValidationErrorFor(x => x.PublisherScope);
}
[Fact]
diff --git a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs
index f370a4b..4275fec 100644
--- a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs
+++ b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs
@@ -56,7 +56,8 @@ public sealed class AnnouncementRegressionTests
context.ChangeTracker.Clear();
var repository = new EfTenantAnnouncementRepository(context);
- var handler = new UpdateTenantAnnouncementCommandHandler(repository);
+ var tenantProvider = new TestTenantProvider(700);
+ var handler = new UpdateTenantAnnouncementCommandHandler(repository, tenantProvider);
var command = new UpdateTenantAnnouncementCommand
{
diff --git a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs
index 29c793a..b46cc7e 100644
--- a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs
+++ b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs
@@ -137,7 +137,8 @@ public sealed class AnnouncementWorkflowTests
context.ChangeTracker.Clear();
var repository = new EfTenantAnnouncementRepository(context);
- var handler = new UpdateTenantAnnouncementCommandHandler(repository);
+ var tenantProvider = new TestTenantProvider(400);
+ var handler = new UpdateTenantAnnouncementCommandHandler(repository, tenantProvider);
var command = new UpdateTenantAnnouncementCommand
{
@@ -175,7 +176,8 @@ public sealed class AnnouncementWorkflowTests
context.ChangeTracker.Clear();
var repository = new EfTenantAnnouncementRepository(context);
- var handler = new UpdateTenantAnnouncementCommandHandler(repository);
+ var tenantProvider = new TestTenantProvider(500);
+ var handler = new UpdateTenantAnnouncementCommandHandler(repository, tenantProvider);
var command = new UpdateTenantAnnouncementCommand
{