chore: add documentation comments and stylecop rules
This commit is contained in:
@@ -18,20 +18,19 @@ public sealed class ChangeTenantSubscriptionPlanCommandHandler(
|
||||
IIdGenerator idGenerator)
|
||||
: IRequestHandler<ChangeTenantSubscriptionPlanCommand, TenantSubscriptionDto>
|
||||
{
|
||||
private readonly ITenantRepository _tenantRepository = tenantRepository;
|
||||
private readonly IIdGenerator _idGenerator = idGenerator;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TenantSubscriptionDto> Handle(ChangeTenantSubscriptionPlanCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = await _tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
||||
// 1. 校验租户与订阅存在性
|
||||
_ = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||
|
||||
var subscription = await _tenantRepository.FindSubscriptionByIdAsync(request.TenantId, request.TenantSubscriptionId, cancellationToken)
|
||||
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;
|
||||
@@ -42,10 +41,11 @@ public sealed class ChangeTenantSubscriptionPlanCommandHandler(
|
||||
subscription.ScheduledPackageId = request.TargetPackageId;
|
||||
}
|
||||
|
||||
await _tenantRepository.UpdateSubscriptionAsync(subscription, cancellationToken);
|
||||
await _tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory
|
||||
// 3. 更新订阅并记录变更历史
|
||||
await tenantRepository.UpdateSubscriptionAsync(subscription, cancellationToken);
|
||||
await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory
|
||||
{
|
||||
Id = _idGenerator.NextId(),
|
||||
Id = idGenerator.NextId(),
|
||||
TenantId = subscription.TenantId,
|
||||
TenantSubscriptionId = subscription.Id,
|
||||
FromPackageId = previousPackage,
|
||||
@@ -56,7 +56,8 @@ public sealed class ChangeTenantSubscriptionPlanCommandHandler(
|
||||
Notes = request.Notes
|
||||
}, cancellationToken);
|
||||
|
||||
await _tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||
// 4. 记录审计日志
|
||||
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||
{
|
||||
TenantId = subscription.TenantId,
|
||||
Action = TenantAuditAction.SubscriptionPlanChanged,
|
||||
@@ -66,7 +67,8 @@ public sealed class ChangeTenantSubscriptionPlanCommandHandler(
|
||||
CurrentStatus = null
|
||||
}, cancellationToken);
|
||||
|
||||
await _tenantRepository.SaveChangesAsync(cancellationToken);
|
||||
// 5. 保存并返回 DTO
|
||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return subscription.ToSubscriptionDto()
|
||||
?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅更新失败");
|
||||
|
||||
@@ -24,18 +24,20 @@ public sealed class CheckTenantQuotaCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<QuotaCheckResultDto> Handle(CheckTenantQuotaCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验请求参数
|
||||
if (request.Delta <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "配额消耗量必须大于 0");
|
||||
}
|
||||
|
||||
// 2. 校验租户上下文
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (currentTenantId == 0 || currentTenantId != request.TenantId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "租户上下文不匹配,请在请求头 X-Tenant-Id 指定目标租户");
|
||||
}
|
||||
|
||||
// 1. 获取租户与当前订阅。
|
||||
// 3. 获取租户与当前订阅
|
||||
_ = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||
|
||||
@@ -50,7 +52,7 @@ public sealed class CheckTenantQuotaCommandHandler(
|
||||
|
||||
var limit = ResolveLimit(package, request.QuotaType);
|
||||
|
||||
// 2. 加载配额使用记录并计算。
|
||||
// 4. 加载配额使用记录并计算
|
||||
var usage = await quotaUsageRepository.FindAsync(request.TenantId, request.QuotaType, cancellationToken)
|
||||
?? new TenantQuotaUsage
|
||||
{
|
||||
@@ -69,12 +71,14 @@ public sealed class CheckTenantQuotaCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.Conflict, $"{request.QuotaType} 配额不足");
|
||||
}
|
||||
|
||||
// 5. 更新使用并保存
|
||||
usage.LimitValue = limit ?? usage.LimitValue;
|
||||
usage.UsedValue = usedAfter;
|
||||
usage.ResetCycle ??= ResolveResetCycle(request.QuotaType);
|
||||
|
||||
await PersistUsageAsync(usage, quotaUsageRepository, cancellationToken);
|
||||
|
||||
// 6. 返回结果
|
||||
return new QuotaCheckResultDto
|
||||
{
|
||||
QuotaType = request.QuotaType,
|
||||
|
||||
@@ -16,11 +16,13 @@ public sealed class CreateTenantAnnouncementCommandHandler(ITenantAnnouncementRe
|
||||
{
|
||||
public async Task<TenantAnnouncementDto> Handle(CreateTenantAnnouncementCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验标题与内容
|
||||
if (string.IsNullOrWhiteSpace(request.Title) || string.IsNullOrWhiteSpace(request.Content))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "公告标题和内容不能为空");
|
||||
}
|
||||
|
||||
// 2. 构建公告实体
|
||||
var announcement = new TenantAnnouncement
|
||||
{
|
||||
TenantId = request.TenantId,
|
||||
@@ -33,6 +35,7 @@ public sealed class CreateTenantAnnouncementCommandHandler(ITenantAnnouncementRe
|
||||
IsActive = request.IsActive
|
||||
};
|
||||
|
||||
// 3. 持久化并返回 DTO
|
||||
await announcementRepository.AddAsync(announcement, cancellationToken);
|
||||
await announcementRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@ public sealed class CreateTenantBillingCommandHandler(ITenantBillingRepository b
|
||||
{
|
||||
public async Task<TenantBillingDto> Handle(CreateTenantBillingCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验账单编号
|
||||
if (string.IsNullOrWhiteSpace(request.StatementNo))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "账单编号不能为空");
|
||||
}
|
||||
|
||||
// 2. 构建账单实体
|
||||
var bill = new TenantBillingStatement
|
||||
{
|
||||
TenantId = request.TenantId,
|
||||
@@ -34,9 +36,11 @@ public sealed class CreateTenantBillingCommandHandler(ITenantBillingRepository b
|
||||
LineItemsJson = request.LineItemsJson
|
||||
};
|
||||
|
||||
// 3. 持久化账单
|
||||
await billingRepository.AddAsync(bill, cancellationToken);
|
||||
await billingRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 4. 返回 DTO
|
||||
return bill.ToDto();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ public sealed class CreateTenantPackageCommandHandler(ITenantPackageRepository p
|
||||
/// <inheritdoc />
|
||||
public async Task<TenantPackageDto> 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(),
|
||||
@@ -38,6 +40,7 @@ public sealed class CreateTenantPackageCommandHandler(ITenantPackageRepository p
|
||||
IsActive = request.IsActive
|
||||
};
|
||||
|
||||
// 3. 持久化并返回
|
||||
await packageRepository.AddAsync(package, cancellationToken);
|
||||
await packageRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
|
||||
@@ -18,28 +18,28 @@ public sealed class CreateTenantSubscriptionCommandHandler(
|
||||
IIdGenerator idGenerator)
|
||||
: IRequestHandler<CreateTenantSubscriptionCommand, TenantSubscriptionDto>
|
||||
{
|
||||
private readonly ITenantRepository _tenantRepository = tenantRepository;
|
||||
private readonly IIdGenerator _idGenerator = idGenerator;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TenantSubscriptionDto> Handle(CreateTenantSubscriptionCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验订阅时长
|
||||
if (request.DurationMonths <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "订阅时长必须大于 0");
|
||||
}
|
||||
|
||||
var tenant = await _tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
||||
// 2. 获取租户与当前订阅
|
||||
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||
|
||||
var current = await _tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
|
||||
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(),
|
||||
Id = idGenerator.NextId(),
|
||||
TenantId = tenant.Id,
|
||||
TenantPackageId = request.TenantPackageId,
|
||||
EffectiveFrom = effectiveFrom,
|
||||
@@ -50,10 +50,11 @@ public sealed class CreateTenantSubscriptionCommandHandler(
|
||||
Notes = request.Notes
|
||||
};
|
||||
|
||||
await _tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
|
||||
await _tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory
|
||||
// 4. 记录订阅与历史
|
||||
await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
|
||||
await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory
|
||||
{
|
||||
Id = _idGenerator.NextId(),
|
||||
Id = idGenerator.NextId(),
|
||||
TenantId = tenant.Id,
|
||||
TenantSubscriptionId = subscription.Id,
|
||||
FromPackageId = current?.TenantPackageId ?? request.TenantPackageId,
|
||||
@@ -66,7 +67,8 @@ public sealed class CreateTenantSubscriptionCommandHandler(
|
||||
Notes = request.Notes
|
||||
}, cancellationToken);
|
||||
|
||||
await _tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||
// 5. 记录审计
|
||||
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||
{
|
||||
TenantId = tenant.Id,
|
||||
Action = TenantAuditAction.SubscriptionUpdated,
|
||||
@@ -74,8 +76,10 @@ public sealed class CreateTenantSubscriptionCommandHandler(
|
||||
Description = $"套餐 {request.TenantPackageId} 时长 {request.DurationMonths} 月"
|
||||
}, cancellationToken);
|
||||
|
||||
await _tenantRepository.SaveChangesAsync(cancellationToken);
|
||||
// 6. 保存变更
|
||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 7. 返回 DTO
|
||||
return subscription.ToSubscriptionDto()
|
||||
?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅生成失败");
|
||||
}
|
||||
|
||||
@@ -12,8 +12,11 @@ public sealed class DeleteTenantAnnouncementCommandHandler(ITenantAnnouncementRe
|
||||
{
|
||||
public async Task<bool> Handle(DeleteTenantAnnouncementCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 删除公告
|
||||
await announcementRepository.DeleteAsync(request.TenantId, request.AnnouncementId, cancellationToken);
|
||||
await announcementRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 2. 返回执行结果
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,11 @@ public sealed class DeleteTenantPackageCommandHandler(ITenantPackageRepository p
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Handle(DeleteTenantPackageCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 删除套餐
|
||||
await packageRepository.DeleteAsync(request.TenantPackageId, cancellationToken);
|
||||
await packageRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 2. 返回执行结果
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,14 @@ public sealed class GetTenantAnnouncementQueryHandler(
|
||||
{
|
||||
public async Task<TenantAnnouncementDto?> Handle(GetTenantAnnouncementQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询公告主体
|
||||
var announcement = await announcementRepository.FindByIdAsync(request.TenantId, request.AnnouncementId, cancellationToken);
|
||||
if (announcement == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 优先查用户级已读
|
||||
var userId = currentUserAccessor?.UserId ?? 0;
|
||||
var reads = await readRepository.GetByAnnouncementAsync(
|
||||
request.TenantId,
|
||||
@@ -37,6 +39,7 @@ public sealed class GetTenantAnnouncementQueryHandler(
|
||||
reads = tenantReads;
|
||||
}
|
||||
|
||||
// 3. 返回 DTO 并附带已读状态
|
||||
var readRecord = reads.FirstOrDefault();
|
||||
return announcement.ToDto(readRecord != null, readRecord?.ReadAt);
|
||||
}
|
||||
|
||||
@@ -13,20 +13,21 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers;
|
||||
public sealed class GetTenantAuditLogsQueryHandler(ITenantRepository tenantRepository)
|
||||
: IRequestHandler<GetTenantAuditLogsQuery, PagedResult<TenantAuditLogDto>>
|
||||
{
|
||||
private readonly ITenantRepository _tenantRepository = tenantRepository;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PagedResult<TenantAuditLogDto>> Handle(GetTenantAuditLogsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var logs = await _tenantRepository.GetAuditLogsAsync(request.TenantId, 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<TenantAuditLogDto>(paged, request.Page, request.PageSize, total);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ public sealed class GetTenantBillQueryHandler(ITenantBillingRepository billingRe
|
||||
{
|
||||
public async Task<TenantBillingDto?> Handle(GetTenantBillQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询账单
|
||||
var bill = await billingRepository.FindByIdAsync(request.TenantId, request.BillingId, cancellationToken);
|
||||
|
||||
// 2. 返回 DTO 或 null
|
||||
return bill?.ToDto();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,18 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers;
|
||||
public sealed class GetTenantByIdQueryHandler(ITenantRepository tenantRepository)
|
||||
: IRequestHandler<GetTenantByIdQuery, TenantDetailDto>
|
||||
{
|
||||
private readonly ITenantRepository _tenantRepository = tenantRepository;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TenantDetailDto> Handle(GetTenantByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenant = await _tenantRepository.FindByIdAsync(request.TenantId, 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);
|
||||
// 2. 查询订阅与认证
|
||||
var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
|
||||
var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken);
|
||||
|
||||
// 3. 组装返回
|
||||
return new TenantDetailDto
|
||||
{
|
||||
Tenant = TenantMapping.ToDto(tenant, subscription, verification),
|
||||
|
||||
@@ -14,7 +14,10 @@ public sealed class GetTenantPackageByIdQueryHandler(ITenantPackageRepository pa
|
||||
/// <inheritdoc />
|
||||
public async Task<TenantPackageDto?> Handle(GetTenantPackageByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询套餐
|
||||
var package = await packageRepository.FindByIdAsync(request.TenantPackageId, cancellationToken);
|
||||
|
||||
// 2. 返回 DTO 或 null
|
||||
return package?.ToDto();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,15 +20,18 @@ public sealed class MarkTenantAnnouncementReadCommandHandler(
|
||||
{
|
||||
public async Task<TenantAnnouncementDto?> Handle(MarkTenantAnnouncementReadCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询公告
|
||||
var announcement = await announcementRepository.FindByIdAsync(request.TenantId, request.AnnouncementId, cancellationToken);
|
||||
if (announcement == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 确定用户标识
|
||||
var userId = currentUserAccessor?.UserId ?? 0;
|
||||
var existing = await readRepository.FindAsync(request.TenantId, request.AnnouncementId, userId == 0 ? null : userId, cancellationToken);
|
||||
|
||||
// 3. 如未读则写入已读记录
|
||||
if (existing == null)
|
||||
{
|
||||
var record = new TenantAnnouncementRead
|
||||
@@ -44,6 +47,7 @@ public sealed class MarkTenantAnnouncementReadCommandHandler(
|
||||
existing = record;
|
||||
}
|
||||
|
||||
// 4. 返回带已读时间的公告 DTO
|
||||
return announcement.ToDto(true, existing.ReadAt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,23 @@ public sealed class MarkTenantBillingPaidCommandHandler(ITenantBillingRepository
|
||||
{
|
||||
public async Task<TenantBillingDto?> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ public sealed class MarkTenantNotificationReadCommandHandler(ITenantNotification
|
||||
{
|
||||
public async Task<TenantNotificationDto?> Handle(MarkTenantNotificationReadCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询通知
|
||||
var notification = await notificationRepository.FindByIdAsync(request.TenantId, request.NotificationId, cancellationToken);
|
||||
if (notification == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 若未读则标记已读
|
||||
if (notification.ReadAt == null)
|
||||
{
|
||||
notification.ReadAt = DateTime.UtcNow;
|
||||
@@ -26,6 +28,7 @@ public sealed class MarkTenantNotificationReadCommandHandler(ITenantNotification
|
||||
await notificationRepository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// 3. 返回 DTO
|
||||
return notification.ToDto();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,30 +20,30 @@ public sealed class RegisterTenantCommandHandler(
|
||||
ILogger<RegisterTenantCommandHandler> logger)
|
||||
: IRequestHandler<RegisterTenantCommand, TenantDto>
|
||||
{
|
||||
private readonly ITenantRepository _tenantRepository = tenantRepository;
|
||||
private readonly IIdGenerator _idGenerator = idGenerator;
|
||||
private readonly ILogger<RegisterTenantCommandHandler> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TenantDto> Handle(RegisterTenantCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验订阅时长
|
||||
if (request.DurationMonths <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "订阅时长必须大于 0");
|
||||
}
|
||||
|
||||
if (await _tenantRepository.ExistsByCodeAsync(request.Code, cancellationToken))
|
||||
// 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(),
|
||||
Id = idGenerator.NextId(),
|
||||
Code = request.Code.Trim(),
|
||||
Name = request.Name,
|
||||
ShortName = request.ShortName,
|
||||
@@ -56,9 +56,10 @@ public sealed class RegisterTenantCommandHandler(
|
||||
EffectiveTo = effectiveTo
|
||||
};
|
||||
|
||||
// 5. 构建订阅实体
|
||||
var subscription = new TenantSubscription
|
||||
{
|
||||
Id = _idGenerator.NextId(),
|
||||
Id = idGenerator.NextId(),
|
||||
TenantId = tenant.Id,
|
||||
TenantPackageId = request.TenantPackageId,
|
||||
EffectiveFrom = effectiveFrom,
|
||||
@@ -69,9 +70,10 @@ public sealed class RegisterTenantCommandHandler(
|
||||
Notes = "Init subscription"
|
||||
};
|
||||
|
||||
await _tenantRepository.AddTenantAsync(tenant, cancellationToken);
|
||||
await _tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
|
||||
await _tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||
// 6. 持久化租户、订阅和审计日志
|
||||
await tenantRepository.AddTenantAsync(tenant, cancellationToken);
|
||||
await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
|
||||
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||
{
|
||||
TenantId = tenant.Id,
|
||||
Action = TenantAuditAction.RegistrationSubmitted,
|
||||
@@ -79,10 +81,12 @@ public sealed class RegisterTenantCommandHandler(
|
||||
Description = $"提交套餐 {request.TenantPackageId},时长 {request.DurationMonths} 月"
|
||||
}, cancellationToken);
|
||||
|
||||
await _tenantRepository.SaveChangesAsync(cancellationToken);
|
||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("已注册租户 {TenantCode}", tenant.Code);
|
||||
// 7. 记录日志
|
||||
logger.LogInformation("已注册租户 {TenantCode}", tenant.Code);
|
||||
|
||||
// 8. 返回 DTO
|
||||
return TenantMapping.ToDto(tenant, subscription, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,31 +17,32 @@ public sealed class ReviewTenantCommandHandler(
|
||||
ICurrentUserAccessor currentUserAccessor)
|
||||
: IRequestHandler<ReviewTenantCommand, TenantDto>
|
||||
{
|
||||
private readonly ITenantRepository _tenantRepository = tenantRepository;
|
||||
private readonly ICurrentUserAccessor _currentUserAccessor = currentUserAccessor;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TenantDto> Handle(ReviewTenantCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenant = await _tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
||||
// 1. 获取租户与认证资料
|
||||
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||
|
||||
var verification = await _tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken)
|
||||
var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.BadRequest, "请先提交实名认证资料");
|
||||
|
||||
var subscription = await _tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
|
||||
var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
|
||||
|
||||
var actorName = _currentUserAccessor.IsAuthenticated
|
||||
? $"user:{_currentUserAccessor.UserId}"
|
||||
// 2. 记录审核人
|
||||
var actorName = currentUserAccessor.IsAuthenticated
|
||||
? $"user:{currentUserAccessor.UserId}"
|
||||
: "system";
|
||||
|
||||
// 3. 写入审核信息
|
||||
verification.ReviewedAt = DateTime.UtcNow;
|
||||
verification.ReviewedBy = _currentUserAccessor.UserId == 0 ? null : _currentUserAccessor.UserId;
|
||||
verification.ReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId;
|
||||
verification.ReviewedByName = actorName;
|
||||
verification.ReviewRemarks = request.Reason;
|
||||
|
||||
var previousStatus = tenant.Status;
|
||||
|
||||
// 4. 更新租户与订阅状态
|
||||
if (request.Approve)
|
||||
{
|
||||
verification.Status = TenantVerificationStatus.Approved;
|
||||
@@ -61,26 +62,29 @@ public sealed class ReviewTenantCommandHandler(
|
||||
}
|
||||
}
|
||||
|
||||
await _tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
||||
await _tenantRepository.UpsertVerificationProfileAsync(verification, cancellationToken);
|
||||
// 5. 持久化租户与认证资料
|
||||
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
|
||||
await tenantRepository.UpsertVerificationProfileAsync(verification, cancellationToken);
|
||||
if (subscription != null)
|
||||
{
|
||||
await _tenantRepository.UpdateSubscriptionAsync(subscription, cancellationToken);
|
||||
await tenantRepository.UpdateSubscriptionAsync(subscription, cancellationToken);
|
||||
}
|
||||
|
||||
await _tenantRepository.AddAuditLogAsync(new Domain.Tenants.Entities.TenantAuditLog
|
||||
// 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,
|
||||
OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
|
||||
OperatorName = actorName,
|
||||
PreviousStatus = previousStatus,
|
||||
CurrentStatus = tenant.Status
|
||||
}, cancellationToken);
|
||||
|
||||
await _tenantRepository.SaveChangesAsync(cancellationToken);
|
||||
// 7. 保存并返回 DTO
|
||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return TenantMapping.ToDto(tenant, subscription, verification);
|
||||
}
|
||||
|
||||
@@ -20,22 +20,27 @@ public sealed class SearchTenantAnnouncementsQueryHandler(
|
||||
{
|
||||
public async Task<PagedResult<TenantAnnouncementDto>> Handle(SearchTenantAnnouncementsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 过滤有效期条件
|
||||
var effectiveAt = request.OnlyEffective == true ? DateTime.UtcNow : (DateTime?)null;
|
||||
var announcements = await announcementRepository.SearchAsync(request.TenantId, request.AnnouncementType, request.IsActive, effectiveAt, cancellationToken);
|
||||
|
||||
// 2. 排序(优先级/时间)
|
||||
var ordered = announcements
|
||||
.OrderByDescending(x => x.Priority)
|
||||
.ThenByDescending(x => x.CreatedAt)
|
||||
.ToList();
|
||||
|
||||
// 3. 计算分页参数
|
||||
var page = request.Page <= 0 ? 1 : request.Page;
|
||||
var size = request.PageSize <= 0 ? 20 : request.PageSize;
|
||||
|
||||
// 4. 分页
|
||||
var pageItems = ordered
|
||||
.Skip((page - 1) * size)
|
||||
.Take(size)
|
||||
.ToList();
|
||||
|
||||
// 5. 构建已读映射
|
||||
var announcementIds = pageItems.Select(x => x.Id).ToArray();
|
||||
var userId = currentUserAccessor?.UserId ?? 0;
|
||||
|
||||
@@ -65,6 +70,7 @@ public sealed class SearchTenantAnnouncementsQueryHandler(
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 映射 DTO 并带上已读状态
|
||||
var items = pageItems
|
||||
.Select(a =>
|
||||
{
|
||||
@@ -73,6 +79,7 @@ public sealed class SearchTenantAnnouncementsQueryHandler(
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// 7. 返回分页结果
|
||||
return new PagedResult<TenantAnnouncementDto>(items, page, size, ordered.Count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,16 @@ public sealed class SearchTenantBillsQueryHandler(ITenantBillingRepository billi
|
||||
{
|
||||
public async Task<PagedResult<TenantBillingDto>> Handle(SearchTenantBillsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询账单
|
||||
var bills = await billingRepository.SearchAsync(request.TenantId, request.Status, request.From, request.To, cancellationToken);
|
||||
|
||||
// 2. 排序与分页
|
||||
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. 返回分页结果
|
||||
return new PagedResult<TenantBillingDto>(items, page, size, ordered.Count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ public sealed class SearchTenantNotificationsQueryHandler(ITenantNotificationRep
|
||||
{
|
||||
public async Task<PagedResult<TenantNotificationDto>> Handle(SearchTenantNotificationsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询通知
|
||||
var notifications = await notificationRepository.SearchAsync(
|
||||
request.TenantId,
|
||||
request.Severity,
|
||||
@@ -23,11 +24,13 @@ public sealed class SearchTenantNotificationsQueryHandler(ITenantNotificationRep
|
||||
null,
|
||||
cancellationToken);
|
||||
|
||||
// 2. 排序与分页
|
||||
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. 返回分页结果
|
||||
return new PagedResult<TenantNotificationDto>(items, page, size, ordered.Count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ public sealed class SearchTenantPackagesQueryHandler(ITenantPackageRepository pa
|
||||
/// <inheritdoc />
|
||||
public async Task<PagedResult<TenantPackageDto>> Handle(SearchTenantPackagesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询套餐
|
||||
var packages = await packageRepository.SearchAsync(request.Keyword, request.IsActive, cancellationToken);
|
||||
|
||||
// 2. 排序与分页
|
||||
var ordered = packages.OrderByDescending(x => x.CreatedAt).ToList();
|
||||
var pageIndex = request.Page <= 0 ? 1 : request.Page;
|
||||
var size = request.PageSize <= 0 ? 20 : request.PageSize;
|
||||
@@ -28,6 +30,7 @@ public sealed class SearchTenantPackagesQueryHandler(ITenantPackageRepository pa
|
||||
.Select(x => x.ToDto())
|
||||
.ToList();
|
||||
|
||||
// 3. 返回分页结果
|
||||
return new PagedResult<TenantPackageDto>(pagedItems, pageIndex, size, ordered.Count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,27 +13,29 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers;
|
||||
public sealed class SearchTenantsQueryHandler(ITenantRepository tenantRepository)
|
||||
: IRequestHandler<SearchTenantsQuery, PagedResult<TenantDto>>
|
||||
{
|
||||
private readonly ITenantRepository _tenantRepository = tenantRepository;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PagedResult<TenantDto>> Handle(SearchTenantsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenants = await _tenantRepository.SearchAsync(request.Status, request.Keyword, cancellationToken);
|
||||
// 1. 查询租户列表
|
||||
var tenants = await tenantRepository.SearchAsync(request.Status, request.Keyword, cancellationToken);
|
||||
var total = tenants.Count;
|
||||
|
||||
// 2. 分页
|
||||
var paged = tenants
|
||||
.Skip((request.Page - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.ToList();
|
||||
|
||||
// 3. 映射 DTO(带订阅与认证)
|
||||
var result = new List<TenantDto>(paged.Count);
|
||||
foreach (var tenant in paged)
|
||||
{
|
||||
var subscription = await _tenantRepository.GetActiveSubscriptionAsync(tenant.Id, cancellationToken);
|
||||
var verification = await _tenantRepository.GetVerificationProfileAsync(tenant.Id, cancellationToken);
|
||||
var subscription = await tenantRepository.GetActiveSubscriptionAsync(tenant.Id, cancellationToken);
|
||||
var verification = await tenantRepository.GetVerificationProfileAsync(tenant.Id, cancellationToken);
|
||||
result.Add(TenantMapping.ToDto(tenant, subscription, verification));
|
||||
}
|
||||
|
||||
// 4. 返回分页结果
|
||||
return new PagedResult<TenantDto>(result, request.Page, request.PageSize, total);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,18 +18,18 @@ public sealed class SubmitTenantVerificationCommandHandler(
|
||||
IIdGenerator idGenerator)
|
||||
: IRequestHandler<SubmitTenantVerificationCommand, TenantVerificationDto>
|
||||
{
|
||||
private readonly ITenantRepository _tenantRepository = tenantRepository;
|
||||
private readonly IIdGenerator _idGenerator = idGenerator;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TenantVerificationDto> Handle(SubmitTenantVerificationCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenant = await _tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
||||
// 1. 获取租户
|
||||
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
||||
|
||||
var profile = await _tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken)
|
||||
?? new TenantVerificationProfile { Id = _idGenerator.NextId(), TenantId = tenant.Id };
|
||||
// 2. 读取或初始化实名资料
|
||||
var profile = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken)
|
||||
?? new TenantVerificationProfile { Id = idGenerator.NextId(), TenantId = tenant.Id };
|
||||
|
||||
// 3. 填充资料
|
||||
profile.BusinessLicenseNumber = request.BusinessLicenseNumber;
|
||||
profile.BusinessLicenseUrl = request.BusinessLicenseUrl;
|
||||
profile.LegalPersonName = request.LegalPersonName;
|
||||
@@ -47,16 +47,18 @@ public sealed class SubmitTenantVerificationCommandHandler(
|
||||
profile.ReviewedBy = null;
|
||||
profile.ReviewedByName = null;
|
||||
|
||||
await _tenantRepository.UpsertVerificationProfileAsync(profile, cancellationToken);
|
||||
await _tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||
// 4. 保存资料并记录审计
|
||||
await tenantRepository.UpsertVerificationProfileAsync(profile, cancellationToken);
|
||||
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
|
||||
{
|
||||
TenantId = tenant.Id,
|
||||
Action = TenantAuditAction.VerificationSubmitted,
|
||||
Title = "提交实名认证资料",
|
||||
Description = request.BusinessLicenseNumber
|
||||
}, cancellationToken);
|
||||
await _tenantRepository.SaveChangesAsync(cancellationToken);
|
||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 5. 返回 DTO
|
||||
return profile.ToVerificationDto()
|
||||
?? throw new BusinessException(ErrorCodes.InternalServerError, "实名资料保存失败");
|
||||
}
|
||||
|
||||
@@ -15,17 +15,20 @@ public sealed class UpdateTenantAnnouncementCommandHandler(ITenantAnnouncementRe
|
||||
{
|
||||
public async Task<TenantAnnouncementDto?> Handle(UpdateTenantAnnouncementCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 校验输入
|
||||
if (string.IsNullOrWhiteSpace(request.Title) || string.IsNullOrWhiteSpace(request.Content))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "公告标题和内容不能为空");
|
||||
}
|
||||
|
||||
// 2. 查询公告
|
||||
var announcement = await announcementRepository.FindByIdAsync(request.TenantId, request.AnnouncementId, cancellationToken);
|
||||
if (announcement == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 更新字段
|
||||
announcement.Title = request.Title.Trim();
|
||||
announcement.Content = request.Content;
|
||||
announcement.AnnouncementType = request.AnnouncementType;
|
||||
@@ -34,9 +37,11 @@ public sealed class UpdateTenantAnnouncementCommandHandler(ITenantAnnouncementRe
|
||||
announcement.EffectiveTo = request.EffectiveTo;
|
||||
announcement.IsActive = request.IsActive;
|
||||
|
||||
// 4. 持久化
|
||||
await announcementRepository.UpdateAsync(announcement, cancellationToken);
|
||||
await announcementRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 5. 返回 DTO
|
||||
return announcement.ToDto(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,17 +16,20 @@ public sealed class UpdateTenantPackageCommandHandler(ITenantPackageRepository p
|
||||
/// <inheritdoc />
|
||||
public async Task<TenantPackageDto?> 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;
|
||||
@@ -40,6 +43,7 @@ public sealed class UpdateTenantPackageCommandHandler(ITenantPackageRepository p
|
||||
package.FeaturePoliciesJson = request.FeaturePoliciesJson;
|
||||
package.IsActive = request.IsActive;
|
||||
|
||||
// 4. 持久化并返回
|
||||
await packageRepository.UpdateAsync(package, cancellationToken);
|
||||
await packageRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user