refactor: 精简手动创建租户命令字段
- 移除冗余字段:CoverImageUrl、Website、SuspendedAt、SuspensionReason - 移除冗余字段:ScheduledPackageId、AdditionalDataJson、ReviewedByName、ReviewRemarks、AdminMerchantId - 更新Handler移除对应字段的处理逻辑 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -42,16 +42,6 @@ public sealed record CreateTenantManuallyCommand : IRequest<TenantDetailDto>
|
||||
/// </summary>
|
||||
public string? LogoUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 封面图地址。
|
||||
/// </summary>
|
||||
public string? CoverImageUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 官网地址。
|
||||
/// </summary>
|
||||
public string? Website { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 国家/地区。
|
||||
/// </summary>
|
||||
@@ -97,16 +87,6 @@ public sealed record CreateTenantManuallyCommand : IRequest<TenantDetailDto>
|
||||
/// </summary>
|
||||
public string? Remarks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 暂停时间。
|
||||
/// </summary>
|
||||
public DateTime? SuspendedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 暂停原因。
|
||||
/// </summary>
|
||||
public string? SuspensionReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户状态。
|
||||
/// </summary>
|
||||
@@ -153,11 +133,6 @@ public sealed record CreateTenantManuallyCommand : IRequest<TenantDetailDto>
|
||||
/// </summary>
|
||||
public SubscriptionStatus SubscriptionStatus { get; init; } = SubscriptionStatus.Active;
|
||||
|
||||
/// <summary>
|
||||
/// 预约变更的套餐 ID。
|
||||
/// </summary>
|
||||
public string? ScheduledPackageId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 订阅备注。
|
||||
/// </summary>
|
||||
@@ -215,21 +190,6 @@ public sealed record CreateTenantManuallyCommand : IRequest<TenantDetailDto>
|
||||
/// </summary>
|
||||
public string? BankName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 附加数据 JSON。
|
||||
/// </summary>
|
||||
public string? AdditionalDataJson { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 审核人姓名。
|
||||
/// </summary>
|
||||
public string? ReviewedByName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 审核备注。
|
||||
/// </summary>
|
||||
public string? ReviewRemarks { get; init; }
|
||||
|
||||
// 4. 管理员账号(identity.identity_users)
|
||||
|
||||
/// <summary>
|
||||
@@ -252,11 +212,6 @@ public sealed record CreateTenantManuallyCommand : IRequest<TenantDetailDto>
|
||||
/// </summary>
|
||||
public string? AdminAvatar { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 管理员所属商户 ID。
|
||||
/// </summary>
|
||||
public string? AdminMerchantId { get; init; }
|
||||
|
||||
// 5. 账单信息(public.tenant_billing_statements)
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -54,58 +54,34 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "套餐 ID 无效");
|
||||
}
|
||||
|
||||
// 3. 解析可选的预约套餐 ID
|
||||
long? scheduledPackageId = null;
|
||||
if (!string.IsNullOrWhiteSpace(request.ScheduledPackageId))
|
||||
{
|
||||
if (!long.TryParse(request.ScheduledPackageId, out var parsedScheduledId) || parsedScheduledId <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "预约套餐 ID 无效");
|
||||
}
|
||||
|
||||
scheduledPackageId = parsedScheduledId;
|
||||
}
|
||||
|
||||
// 4. 解析可选的管理员商户 ID
|
||||
long? adminMerchantId = null;
|
||||
if (!string.IsNullOrWhiteSpace(request.AdminMerchantId))
|
||||
{
|
||||
if (!long.TryParse(request.AdminMerchantId, out var parsedMerchantId) || parsedMerchantId <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "管理员商户 ID 无效");
|
||||
}
|
||||
|
||||
adminMerchantId = parsedMerchantId;
|
||||
}
|
||||
|
||||
// 5. 校验租户编码唯一性
|
||||
// 3. 校验租户编码唯一性
|
||||
if (await tenantRepository.ExistsByCodeAsync(code, null, cancellationToken))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "租户编码已存在");
|
||||
}
|
||||
|
||||
// 6. 校验套餐存在性
|
||||
// 4. 校验套餐存在性
|
||||
var package = await tenantPackageRepository.GetByIdAsync(tenantPackageId, cancellationToken);
|
||||
if (package is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "套餐不存在");
|
||||
}
|
||||
|
||||
// 7. 生成租户 ID
|
||||
// 5. 生成租户 ID
|
||||
var tenantId = idGenerator.NextId();
|
||||
|
||||
// 8. 校验管理员账号唯一性(租户内)
|
||||
// 6. 校验管理员账号唯一性(租户内)
|
||||
if (await identityUserRepository.ExistsByAccountAsync(PortalType.Tenant, tenantId, adminAccount, null, cancellationToken))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "管理员账号已存在");
|
||||
}
|
||||
|
||||
// 9. 计算订阅时间
|
||||
// 7. 计算订阅时间
|
||||
var effectiveFrom = request.SubscriptionEffectiveFrom ?? DateTime.UtcNow;
|
||||
var effectiveTo = effectiveFrom.AddMonths(request.DurationMonths);
|
||||
var nextBillingDate = request.NextBillingDate ?? effectiveTo;
|
||||
|
||||
// 10. 创建管理员账号实体
|
||||
// 8. 创建管理员账号实体
|
||||
var adminUser = new IdentityUser
|
||||
{
|
||||
Id = idGenerator.NextId(),
|
||||
@@ -121,23 +97,23 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
LockedUntil = null,
|
||||
LastLoginAt = null,
|
||||
MustChangePassword = false,
|
||||
MerchantId = adminMerchantId,
|
||||
MerchantId = null,
|
||||
Avatar = request.AdminAvatar?.Trim()
|
||||
};
|
||||
adminUser.PasswordHash = passwordHasher.HashPassword(adminUser, request.AdminPassword);
|
||||
|
||||
// 11. 【Saga 步骤 1】先在 Identity 库创建管理员账号
|
||||
// 9. 【Saga 步骤 1】先在 Identity 库创建管理员账号
|
||||
await identityUserRepository.AddAsync(adminUser, cancellationToken);
|
||||
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("租户 {TenantId} 管理员账号 {AdminUserId} 创建成功", tenantId, adminUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// 12. 根据是否跳过审核确定租户状态和认证状态
|
||||
// 10. 根据是否跳过审核确定租户状态和认证状态
|
||||
var tenantStatus = request.IsSkipApproval ? TenantStatus.Active : TenantStatus.PendingReview;
|
||||
var verificationStatus = request.IsSkipApproval ? TenantVerificationStatus.Approved : TenantVerificationStatus.Pending;
|
||||
|
||||
// 13. 创建租户实体
|
||||
// 11. 创建租户实体
|
||||
var tenant = new Tenant
|
||||
{
|
||||
Id = tenantId,
|
||||
@@ -147,8 +123,6 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
LegalEntityName = request.LegalEntityName?.Trim(),
|
||||
Industry = request.Industry?.Trim(),
|
||||
LogoUrl = request.LogoUrl?.Trim(),
|
||||
CoverImageUrl = request.CoverImageUrl?.Trim(),
|
||||
Website = request.Website?.Trim(),
|
||||
Country = request.Country?.Trim(),
|
||||
Province = request.Province?.Trim(),
|
||||
City = request.City?.Trim(),
|
||||
@@ -159,14 +133,12 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
Tags = request.Tags?.Trim(),
|
||||
Remarks = request.Remarks?.Trim(),
|
||||
Status = tenantStatus,
|
||||
SuspendedAt = request.SuspendedAt,
|
||||
SuspensionReason = request.SuspensionReason?.Trim(),
|
||||
EffectiveFrom = effectiveFrom,
|
||||
EffectiveTo = effectiveTo,
|
||||
PrimaryOwnerUserId = adminUser.Id
|
||||
};
|
||||
|
||||
// 14. 创建订阅实体
|
||||
// 12. 创建订阅实体
|
||||
var subscription = new TenantSubscription
|
||||
{
|
||||
Id = idGenerator.NextId(),
|
||||
@@ -177,11 +149,10 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
EffectiveTo = effectiveTo,
|
||||
NextBillingDate = nextBillingDate,
|
||||
AutoRenew = request.AutoRenew,
|
||||
ScheduledPackageId = scheduledPackageId,
|
||||
Notes = request.SubscriptionNotes?.Trim()
|
||||
};
|
||||
|
||||
// 15. 创建认证资料实体
|
||||
// 13. 创建认证资料实体
|
||||
var verification = new TenantVerificationProfile
|
||||
{
|
||||
Id = idGenerator.NextId(),
|
||||
@@ -196,18 +167,15 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
BankAccountName = request.BankAccountName?.Trim(),
|
||||
BankAccountNumber = request.BankAccountNumber?.Trim(),
|
||||
BankName = request.BankName?.Trim(),
|
||||
AdditionalDataJson = request.AdditionalDataJson?.Trim(),
|
||||
SubmittedAt = DateTime.UtcNow,
|
||||
ReviewedAt = request.IsSkipApproval ? DateTime.UtcNow : null,
|
||||
ReviewedBy = request.IsSkipApproval ? currentUserAccessor.UserId : null,
|
||||
ReviewedByName = request.IsSkipApproval ? request.ReviewedByName?.Trim() : null,
|
||||
ReviewRemarks = request.IsSkipApproval ? request.ReviewRemarks?.Trim() : null
|
||||
ReviewedBy = request.IsSkipApproval ? currentUserAccessor.UserId : null
|
||||
};
|
||||
|
||||
// 16. 根据套餐配额创建配额使用记录
|
||||
// 14. 根据套餐配额创建配额使用记录
|
||||
var quotaUsages = CreateQuotaUsagesFromPackage(tenantId, package);
|
||||
|
||||
// 17. 创建账单记录和支付记录(可选)
|
||||
// 15. 创建账单记录和支付记录(可选)
|
||||
TenantBillingStatement? billing = null;
|
||||
TenantPayment? payment = null;
|
||||
if (request.CreateBilling)
|
||||
@@ -225,20 +193,20 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
request.BillingStatus,
|
||||
request.BillingNotes);
|
||||
|
||||
// 17. 如果账单状态为已支付,创建支付记录
|
||||
// 16. 如果账单状态为已支付,创建支付记录
|
||||
if (request.BillingStatus == TenantBillingStatus.Paid && billing.AmountDue > 0)
|
||||
{
|
||||
payment = CreatePaymentFromBilling(tenantId, billing);
|
||||
}
|
||||
}
|
||||
|
||||
// 18. 【Saga 步骤 2】在 App 库创建租户、订阅、认证资料、配额使用记录、账单、支付记录(使用事务)
|
||||
// 17. 【Saga 步骤 2】在 App 库创建租户、订阅、认证资料、配额使用记录、账单、支付记录(使用事务)
|
||||
await tenantRepository.CreateTenantWithRelatedDataAsync(tenant, subscription, verification, quotaUsages, billing, payment, cancellationToken);
|
||||
logger.LogInformation("租户 {TenantId} 及相关数据创建成功,跳过审核:{IsSkipApproval}", tenantId, request.IsSkipApproval);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 16. 【Saga 补偿】App 库操作失败,回滚 Identity 库的管理员账号
|
||||
// 18. 【Saga 补偿】App 库操作失败,回滚 Identity 库的管理员账号
|
||||
// 记录完整异常信息(包括内部异常)
|
||||
var fullErrorMessage = ex.InnerException?.Message ?? ex.Message;
|
||||
logger.LogError(ex, "租户 {TenantId} 创建失败,错误详情:{ErrorDetail},开始回滚管理员账号 {AdminUserId}",
|
||||
@@ -252,7 +220,7 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
}
|
||||
catch (Exception rollbackEx)
|
||||
{
|
||||
// 17. 补偿失败,记录严重错误(需要人工介入)
|
||||
// 19. 补偿失败,记录严重错误(需要人工介入)
|
||||
logger.LogCritical(
|
||||
rollbackEx,
|
||||
"严重:租户 {TenantId} 创建失败且管理员账号 {AdminUserId} 回滚失败,需要人工介入清理",
|
||||
@@ -260,11 +228,11 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
adminUser.Id);
|
||||
}
|
||||
|
||||
// 18. 重新抛出业务异常(包含详细错误信息)
|
||||
// 20. 重新抛出业务异常(包含详细错误信息)
|
||||
throw new BusinessException(ErrorCodes.InternalServerError, $"创建租户失败:{fullErrorMessage}");
|
||||
}
|
||||
|
||||
// 19. 查询并返回租户详情
|
||||
// 21. 查询并返回租户详情
|
||||
var detail = await mediator.Send(new GetTenantDetailQuery { TenantId = tenantId }, cancellationToken);
|
||||
return detail ?? throw new BusinessException(ErrorCodes.InternalServerError, "创建租户成功但查询详情失败");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user