feat:商户管理
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using MediatR;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Domain.Common.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
|
||||
@@ -29,4 +30,9 @@ public sealed record ReviewTenantCommand : IRequest<TenantDto>
|
||||
/// 审核通过后续费时长(月)。
|
||||
/// </summary>
|
||||
public int? RenewMonths { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 经营模式(审核通过时必填)。
|
||||
/// </summary>
|
||||
public OperatingMode? OperatingMode { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Common.Enums;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
@@ -55,6 +56,11 @@ public sealed class TenantDto
|
||||
/// </summary>
|
||||
public TenantVerificationStatus VerificationStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 经营模式。
|
||||
/// </summary>
|
||||
public OperatingMode? OperatingMode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前套餐 ID。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using MediatR;
|
||||
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;
|
||||
@@ -14,6 +17,7 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers;
|
||||
/// </summary>
|
||||
public sealed class ReviewTenantCommandHandler(
|
||||
ITenantRepository tenantRepository,
|
||||
IMerchantRepository merchantRepository,
|
||||
ICurrentUserAccessor currentUserAccessor)
|
||||
: IRequestHandler<ReviewTenantCommand, TenantDto>
|
||||
{
|
||||
@@ -53,19 +57,25 @@ public sealed class ReviewTenantCommandHandler(
|
||||
// 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;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (subscription.EffectiveFrom == default || subscription.EffectiveFrom > now)
|
||||
{
|
||||
subscription.EffectiveFrom = now;
|
||||
@@ -92,6 +102,69 @@ public sealed class ReviewTenantCommandHandler(
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "订阅不存在,无法续费");
|
||||
}
|
||||
|
||||
var existingMerchant = await merchantRepository.FindByTenantIdAsync(tenant.Id, cancellationToken);
|
||||
if (existingMerchant == null)
|
||||
{
|
||||
var merchant = new Merchant
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
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
|
||||
{
|
||||
@@ -141,6 +214,7 @@ public sealed class ReviewTenantCommandHandler(
|
||||
|
||||
// 8. 保存并返回 DTO
|
||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||
await merchantRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return TenantMapping.ToDto(tenant, subscription, verification);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ internal static class TenantMapping
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 租户审核命令验证器。
|
||||
/// </summary>
|
||||
public sealed class ReviewTenantValidator : AbstractValidator<ReviewTenantCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user