227 lines
10 KiB
C#
227 lines
10 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 租户审核处理器。
|
|
/// </summary>
|
|
public sealed class ReviewTenantCommandHandler(
|
|
ITenantRepository tenantRepository,
|
|
IMerchantRepository merchantRepository,
|
|
ICurrentUserAccessor currentUserAccessor,
|
|
IIdGenerator idGenerator)
|
|
: IRequestHandler<ReviewTenantCommand, TenantDto>
|
|
{
|
|
/// <inheritdoc />
|
|
public async Task<TenantDto> 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);
|
|
}
|
|
}
|