This commit is contained in:
2026-01-05 19:11:16 +08:00
73 changed files with 400 additions and 323 deletions

View File

@@ -23,10 +23,10 @@ public sealed class CancelBillingCommandHandler(
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
}
// 2. (空行后) 取消账单(领域规则校验在实体方法内)
// 2. 取消账单(领域规则校验在实体方法内)
billing.Cancel(request.Reason);
// 3. (空行后) 持久化
// 3. 持久化
await billingRepository.UpdateAsync(billing, cancellationToken);
await billingRepository.SaveChangesAsync(cancellationToken);

View File

@@ -30,14 +30,14 @@ public sealed class ConfirmPaymentCommandHandler(
throw new BusinessException(ErrorCodes.Unauthorized, "未登录或无效的操作者身份");
}
// 2. (空行后) 查询账单
// 2. 查询账单
var billing = await billingRepository.FindByIdAsync(request.BillingId, cancellationToken);
if (billing is null)
{
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
}
// 3. (空行后) 业务规则检查
// 3. 业务规则检查
if (billing.Status == TenantBillingStatus.Paid)
{
throw new BusinessException(ErrorCodes.BusinessError, "已支付账单不允许重复收款");
@@ -48,7 +48,7 @@ public sealed class ConfirmPaymentCommandHandler(
throw new BusinessException(ErrorCodes.BusinessError, "已取消账单不允许收款");
}
// 4. (空行后) 金额边界:不允许超过剩余应收(与前端校验保持一致)
// 4. 金额边界:不允许超过剩余应收(与前端校验保持一致)
var totalAmount = billing.CalculateTotalAmount();
var remainingAmount = totalAmount - billing.AmountPaid;
if (request.Amount > remainingAmount)
@@ -56,7 +56,7 @@ public sealed class ConfirmPaymentCommandHandler(
throw new BusinessException(ErrorCodes.BadRequest, "支付金额不能超过剩余应收");
}
// 5. (空行后) 幂等校验:交易号唯一
// 5. 幂等校验:交易号唯一
if (!string.IsNullOrWhiteSpace(request.TransactionNo))
{
var exists = await paymentRepository.GetByTransactionNoAsync(request.TransactionNo.Trim(), cancellationToken);
@@ -66,7 +66,7 @@ public sealed class ConfirmPaymentCommandHandler(
}
}
// 6. (空行后) 构建支付记录并立即审核通过
// 6. 构建支付记录并立即审核通过
var now = DateTime.UtcNow;
var payment = new TenantPayment
{
@@ -84,15 +84,15 @@ public sealed class ConfirmPaymentCommandHandler(
payment.Verify(currentUserAccessor.UserId);
// 7. (空行后) 同步更新账单已收金额/状态(支持分次收款)
// 7. 同步更新账单已收金额/状态(支持分次收款)
billing.MarkAsPaid(payment.Amount, payment.TransactionNo ?? string.Empty);
// 8. (空行后) 持久化变更(同一 DbContext 下单次 SaveChanges 可提交两张表)
// 8. 持久化变更(同一 DbContext 下单次 SaveChanges 可提交两张表)
await paymentRepository.AddAsync(payment, cancellationToken);
await billingRepository.UpdateAsync(billing, cancellationToken);
await paymentRepository.SaveChangesAsync(cancellationToken);
// 9. (空行后) 返回 DTO
// 9. 返回 DTO
return payment.ToPaymentRecordDto();
}
}

View File

@@ -30,7 +30,7 @@ public sealed class CreateBillingCommandHandler(
throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
}
// 2. (空行后) 构建账单实体
// 2. 构建账单实体
var now = DateTime.UtcNow;
var statementNo = $"BIL-{now:yyyyMMdd}-{idGenerator.NextId()}";
var lineItemsJson = JsonSerializer.Serialize(request.LineItems);
@@ -54,11 +54,11 @@ public sealed class CreateBillingCommandHandler(
Notes = request.Notes
};
// 3. (空行后) 持久化账单
// 3. 持久化账单
await billingRepository.AddAsync(billing, cancellationToken);
await billingRepository.SaveChangesAsync(cancellationToken);
// 4. (空行后) 返回详情 DTO
// 4. 返回详情 DTO
return billing.ToBillingDetailDto([], tenant.Name);
}
}

View File

@@ -24,14 +24,14 @@ public sealed class ExportBillingsQueryHandler(
throw new BusinessException(ErrorCodes.BadRequest, "账单 ID 列表不能为空");
}
// 2. (空行后) 查询账单数据
// 2. 查询账单数据
var billings = await billingRepository.GetByIdsAsync(request.BillingIds, cancellationToken);
if (billings.Count == 0)
{
throw new BusinessException(ErrorCodes.NotFound, "未找到任何匹配的账单");
}
// 3. (空行后) 根据格式导出
// 3. 根据格式导出
var format = (request.Format ?? string.Empty).Trim().ToLowerInvariant();
return format switch
{

View File

@@ -30,7 +30,7 @@ public sealed class GenerateSubscriptionBillingCommandHandler(
throw new BusinessException(ErrorCodes.NotFound, "订阅不存在");
}
// 2. (空行后) 校验套餐价格信息
// 2. 校验套餐价格信息
var subscription = detail.Subscription;
var package = detail.Package;
if (package is null)
@@ -38,7 +38,7 @@ public sealed class GenerateSubscriptionBillingCommandHandler(
throw new BusinessException(ErrorCodes.BusinessError, "订阅未关联有效套餐,无法生成账单");
}
// 3. (空行后) 按订阅周期选择价格(简化规则:优先按年/按月)
// 3. 按订阅周期选择价格(简化规则:优先按年/按月)
var billingPeriodDays = (subscription.EffectiveTo - subscription.EffectiveFrom).TotalDays;
var amountDue = billingPeriodDays >= 300
? package.YearlyPrice
@@ -49,14 +49,14 @@ public sealed class GenerateSubscriptionBillingCommandHandler(
throw new BusinessException(ErrorCodes.BusinessError, "套餐价格未配置,无法生成账单");
}
// 4. (空行后) 幂等校验:同一周期开始时间仅允许存在一张未取消账单
// 4. 幂等校验:同一周期开始时间仅允许存在一张未取消账单
var exists = await billingRepository.ExistsNotCancelledByPeriodStartAsync(subscription.TenantId, subscription.EffectiveFrom, cancellationToken);
if (exists)
{
throw new BusinessException(ErrorCodes.Conflict, "该订阅周期的账单已存在");
}
// 5. (空行后) 构建账单实体
// 5. 构建账单实体
var now = DateTime.UtcNow;
var statementNo = $"BIL-{now:yyyyMMdd}-{idGenerator.NextId()}";
var lineItems = new List<BillingLineItemDto>
@@ -91,11 +91,11 @@ public sealed class GenerateSubscriptionBillingCommandHandler(
Notes = subscription.Notes
};
// 6. (空行后) 持久化账单
// 6. 持久化账单
await billingRepository.AddAsync(billing, cancellationToken);
await billingRepository.SaveChangesAsync(cancellationToken);
// 7. (空行后) 返回详情 DTO
// 7. 返回详情 DTO
return billing.ToBillingDetailDto([], detail.TenantName);
}
}

View File

@@ -46,7 +46,7 @@ public sealed class GetBillingDetailQueryHandler(
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
}
// 1.2 (空行后) 读取账单行数据到内存(释放 Reader避免同连接并发执行命令
// 1.2 读取账单行数据到内存(释放 Reader避免同连接并发执行命令
var billingId = billReader.GetInt64(0);
var tenantId = billReader.GetInt64(1);
var tenantName = billReader.IsDBNull(2) ? string.Empty : billReader.GetString(2);
@@ -71,10 +71,10 @@ public sealed class GetBillingDetailQueryHandler(
DateTime? updatedAt = billReader.IsDBNull(21) ? null : billReader.GetDateTime(21);
long? updatedBy = billReader.IsDBNull(22) ? null : billReader.GetInt64(22);
// 1.3 (空行后) 主动释放账单 Reader确保后续查询不会触发 Npgsql 并发命令异常
// 1.3 主动释放账单 Reader确保后续查询不会触发 Npgsql 并发命令异常
await billReader.DisposeAsync();
// 1.4 (空行后) 反序列化账单明细
// 1.4 反序列化账单明细
var lineItems = new List<BillingLineItemDto>();
if (!string.IsNullOrWhiteSpace(lineItemsJson))
{
@@ -88,7 +88,7 @@ public sealed class GetBillingDetailQueryHandler(
}
}
// 1.5 (空行后) 查询支付记录
// 1.5 查询支付记录
var payments = new List<PaymentRecordDto>();
await using var paymentCommand = CreateCommand(
connection,
@@ -121,7 +121,7 @@ public sealed class GetBillingDetailQueryHandler(
});
}
// 1.6 (空行后) 组装详情 DTO
// 1.6 组装详情 DTO
var totalAmount = amountDue - discountAmount + taxAmount;
return new BillingDetailDto

View File

@@ -33,13 +33,13 @@ public sealed class GetBillingListQueryHandler(
var maxAmount = request.MaxAmount;
var offset = (page - 1) * pageSize;
// 1.1 (空行后) 金额区间规范化(避免 min > max 导致结果为空)
// 1.1 金额区间规范化(避免 min > max 导致结果为空)
if (minAmount.HasValue && maxAmount.HasValue && minAmount.Value > maxAmount.Value)
{
(minAmount, maxAmount) = (maxAmount, minAmount);
}
// 2. (空行后) 排序白名单(防 SQL 注入)
// 2. 排序白名单(防 SQL 注入)
var orderBy = request.SortBy?.Trim() switch
{
"DueDate" => "b.\"DueDate\"",
@@ -50,7 +50,7 @@ public sealed class GetBillingListQueryHandler(
_ => "b.\"CreatedAt\""
};
// 3. (空行后) 查询总数 + 列表
// 3. 查询总数 + 列表
return await dapperExecutor.QueryAsync(
DatabaseConstants.AppDataSource,
DatabaseConnectionRole.Read,
@@ -72,7 +72,7 @@ public sealed class GetBillingListQueryHandler(
],
token);
// 3.2 (空行后) 查询列表
// 3.2 查询列表
var listSql = BuildListSql(orderBy, request.SortDesc);
await using var listCommand = CreateCommand(
connection,
@@ -102,7 +102,7 @@ public sealed class GetBillingListQueryHandler(
var taxAmount = reader.GetDecimal(10);
var totalAmount = amountDue - discountAmount + taxAmount;
// 3.2.1 (空行后) 逾期辅助字段
// 3.2.1 逾期辅助字段
var isOverdue = status is TenantBillingStatus.Overdue
|| (status is TenantBillingStatus.Pending && dueDate < now);
var overdueDays = dueDate < now ? (int)(now - dueDate).TotalDays : 0;
@@ -132,7 +132,7 @@ public sealed class GetBillingListQueryHandler(
});
}
// 3.3 (空行后) 返回分页
// 3.3 返回分页
return new PagedResult<BillingListDto>(items, page, pageSize, total);
},
cancellationToken);

View File

@@ -51,7 +51,7 @@ public sealed class GetBillingPaymentsQueryHandler(
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
}
// 1.2 (空行后) 查询支付记录
// 1.2 查询支付记录
await using var command = CreateCommand(
connection,
"""

View File

@@ -29,7 +29,7 @@ public sealed class GetBillingStatisticsQueryHandler(
var endDate = request.EndDate ?? DateTime.UtcNow;
var groupBy = NormalizeGroupBy(request.GroupBy);
// 2. (空行后) 查询统计数据(总览 + 趋势)
// 2. 查询统计数据(总览 + 趋势)
return await dapperExecutor.QueryAsync(
DatabaseConstants.AppDataSource,
DatabaseConnectionRole.Read,
@@ -59,7 +59,7 @@ public sealed class GetBillingStatisticsQueryHandler(
var totalAmountUnpaid = summaryReader.IsDBNull(7) ? 0m : summaryReader.GetDecimal(7);
var totalOverdueAmount = summaryReader.IsDBNull(8) ? 0m : summaryReader.GetDecimal(8);
// 2.2 (空行后) 趋势数据
// 2.2 趋势数据
await using var trendCommand = CreateCommand(
connection,
BuildTrendSql(groupBy),
@@ -83,7 +83,7 @@ public sealed class GetBillingStatisticsQueryHandler(
countTrend[key] = trendReader.IsDBNull(3) ? 0 : trendReader.GetInt32(3);
}
// 2.3 (空行后) 组装 DTO
// 2.3 组装 DTO
return new BillingStatisticsDto
{
TenantId = request.TenantId,

View File

@@ -31,7 +31,7 @@ public sealed class GetOverdueBillingsQueryHandler(
var offset = (page - 1) * pageSize;
var now = DateTime.UtcNow;
// 2. (空行后) 查询总数 + 列表
// 2. 查询总数 + 列表
return await dapperExecutor.QueryAsync(
DatabaseConstants.AppDataSource,
DatabaseConnectionRole.Read,
@@ -46,7 +46,7 @@ public sealed class GetOverdueBillingsQueryHandler(
],
token);
// 2.2 (空行后) 查询列表
// 2.2 查询列表
await using var listCommand = CreateCommand(
connection,
BuildListSql(),
@@ -93,7 +93,7 @@ public sealed class GetOverdueBillingsQueryHandler(
});
}
// 2.3 (空行后) 返回分页
// 2.3 返回分页
return new PagedResult<BillingListDto>(items, page, pageSize, total);
},
cancellationToken);

View File

@@ -34,7 +34,7 @@ public sealed class RecordPaymentCommandHandler(
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
}
// 2. (空行后) 业务规则检查
// 2. 业务规则检查
if (billing.Status == TenantBillingStatus.Paid)
{
throw new BusinessException(ErrorCodes.BusinessError, "已支付账单不允许重复收款");
@@ -45,7 +45,7 @@ public sealed class RecordPaymentCommandHandler(
throw new BusinessException(ErrorCodes.BusinessError, "已取消账单不允许收款");
}
// 3. (空行后) 幂等校验:交易号唯一
// 3. 幂等校验:交易号唯一
if (!string.IsNullOrWhiteSpace(request.TransactionNo))
{
var exists = await paymentRepository.GetByTransactionNoAsync(request.TransactionNo.Trim(), cancellationToken);
@@ -55,7 +55,7 @@ public sealed class RecordPaymentCommandHandler(
}
}
// 4. (空行后) 构建支付记录(默认待审核)
// 4. 构建支付记录(默认待审核)
var now = DateTime.UtcNow;
var payment = new TenantPayment
{
@@ -71,11 +71,11 @@ public sealed class RecordPaymentCommandHandler(
Notes = request.Notes
};
// 5. (空行后) 持久化变更
// 5. 持久化变更
await paymentRepository.AddAsync(payment, cancellationToken);
await paymentRepository.SaveChangesAsync(cancellationToken);
// 6. (空行后) 返回 DTO
// 6. 返回 DTO
return payment.ToPaymentRecordDto();
}
}

View File

@@ -24,7 +24,7 @@ public sealed class UpdateBillingStatusCommandHandler(
throw new BusinessException(ErrorCodes.NotFound, "账单不存在");
}
// 2. (空行后) 状态转换规则校验
// 2. 状态转换规则校验
if (billing.Status == TenantBillingStatus.Paid && request.NewStatus != TenantBillingStatus.Paid)
{
throw new BusinessException(ErrorCodes.BusinessError, "已支付账单不允许改为其他状态");
@@ -35,7 +35,7 @@ public sealed class UpdateBillingStatusCommandHandler(
throw new BusinessException(ErrorCodes.BusinessError, "已取消账单不允许变更状态");
}
// 3. (空行后) 更新状态与备注
// 3. 更新状态与备注
billing.Status = request.NewStatus;
if (!string.IsNullOrWhiteSpace(request.Notes))
{
@@ -44,7 +44,7 @@ public sealed class UpdateBillingStatusCommandHandler(
: $"{billing.Notes}\n[状态变更] {request.Notes}";
}
// 4. (空行后) 持久化
// 4. 持久化
await billingRepository.UpdateAsync(billing, cancellationToken);
await billingRepository.SaveChangesAsync(cancellationToken);

View File

@@ -26,24 +26,24 @@ public sealed class VerifyPaymentCommandHandler(
throw new BusinessException(ErrorCodes.Unauthorized, "未登录或无效的操作者身份");
}
// 2. (空行后) 查询支付记录
// 2. 查询支付记录
var payment = await paymentRepository.FindByIdAsync(request.PaymentId, cancellationToken);
if (payment is null)
{
throw new BusinessException(ErrorCodes.NotFound, "支付记录不存在");
}
// 3. (空行后) 查询关联账单
// 3. 查询关联账单
var billing = await billingRepository.FindByIdAsync(payment.BillingStatementId, cancellationToken);
if (billing is null)
{
throw new BusinessException(ErrorCodes.NotFound, "关联账单不存在");
}
// 4. (空行后) 归一化审核备注
// 4. 归一化审核备注
var normalizedNotes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim();
// 5. (空行后) 根据审核结果更新支付与账单状态
// 5. 根据审核结果更新支付与账单状态
if (request.Approved)
{
payment.Verify(currentUserAccessor.UserId);
@@ -57,17 +57,17 @@ public sealed class VerifyPaymentCommandHandler(
payment.Notes = normalizedNotes;
}
// 6. (空行后) 持久化更新状态
// 6. 持久化更新状态
await paymentRepository.UpdateAsync(payment, cancellationToken);
if (request.Approved)
{
await billingRepository.UpdateAsync(billing, cancellationToken);
}
// 7. (空行后) 保存数据库更改
// 7. 保存数据库更改
await paymentRepository.SaveChangesAsync(cancellationToken);
// 8. (空行后) 返回 DTO
// 8. 返回 DTO
return payment.ToPaymentRecordDto();
}
}

View File

@@ -15,32 +15,32 @@ public sealed class ConfirmPaymentCommandValidator : AbstractValidator<ConfirmPa
.GreaterThan(0)
.WithMessage("账单 ID 必须大于 0");
// 2. (空行后) 支付金额必须大于 0
// 2. 支付金额必须大于 0
RuleFor(x => x.Amount)
.GreaterThan(0)
.WithMessage("支付金额必须大于 0")
.LessThanOrEqualTo(1_000_000_000)
.WithMessage("支付金额不能超过 10 亿");
// 3. (空行后) 支付方式必填
// 3. 支付方式必填
RuleFor(x => x.Method)
.IsInEnum()
.WithMessage("支付方式无效");
// 4. (空行后) 交易号必填
// 4. 交易号必填
RuleFor(x => x.TransactionNo)
.NotEmpty()
.WithMessage("交易号不能为空")
.MaximumLength(64)
.WithMessage("交易号不能超过 64 个字符");
// 5. (空行后) 支付凭证 URL可选
// 5. 支付凭证 URL可选
RuleFor(x => x.ProofUrl)
.MaximumLength(500)
.WithMessage("支付凭证 URL 不能超过 500 个字符")
.When(x => !string.IsNullOrWhiteSpace(x.ProofUrl));
// 6. (空行后) 备注(可选)
// 6. 备注(可选)
RuleFor(x => x.Notes)
.MaximumLength(500)
.WithMessage("备注不能超过 500 个字符")

View File

@@ -15,27 +15,27 @@ public sealed class CreateBillingCommandValidator : AbstractValidator<CreateBill
.GreaterThan(0)
.WithMessage("租户 ID 必须大于 0");
// 2. (空行后) 账单类型必填
// 2. 账单类型必填
RuleFor(x => x.BillingType)
.IsInEnum()
.WithMessage("账单类型无效");
// 3. (空行后) 应付金额必须大于 0
// 3. 应付金额必须大于 0
RuleFor(x => x.AmountDue)
.GreaterThan(0)
.WithMessage("应付金额必须大于 0");
// 4. (空行后) 到期日必须是未来时间
// 4. 到期日必须是未来时间
RuleFor(x => x.DueDate)
.GreaterThan(DateTime.UtcNow)
.WithMessage("到期日必须是未来时间");
// 5. (空行后) 账单明细至少包含一项
// 5. 账单明细至少包含一项
RuleFor(x => x.LineItems)
.NotEmpty()
.WithMessage("账单明细不能为空");
// 6. (空行后) 账单明细项验证
// 6. 账单明细项验证
RuleForEach(x => x.LineItems)
.ChildRules(lineItem =>
{
@@ -64,7 +64,7 @@ public sealed class CreateBillingCommandValidator : AbstractValidator<CreateBill
.WithMessage("账单明细金额不能为负数");
});
// 7. (空行后) 备注长度限制(可选)
// 7. 备注长度限制(可选)
RuleFor(x => x.Notes)
.MaximumLength(500)
.WithMessage("备注不能超过 500 个字符")

View File

@@ -15,32 +15,32 @@ public sealed class RecordPaymentCommandValidator : AbstractValidator<RecordPaym
.GreaterThan(0)
.WithMessage("账单 ID 必须大于 0");
// 2. (空行后) 支付金额必须大于 0
// 2. 支付金额必须大于 0
RuleFor(x => x.Amount)
.GreaterThan(0)
.WithMessage("支付金额必须大于 0")
.LessThanOrEqualTo(1_000_000_000)
.WithMessage("支付金额不能超过 10 亿");
// 3. (空行后) 支付方式必填
// 3. 支付方式必填
RuleFor(x => x.Method)
.IsInEnum()
.WithMessage("支付方式无效");
// 4. (空行后) 交易号必填
// 4. 交易号必填
RuleFor(x => x.TransactionNo)
.NotEmpty()
.WithMessage("交易号不能为空")
.MaximumLength(64)
.WithMessage("交易号不能超过 64 个字符");
// 5. (空行后) 支付凭证 URL可选
// 5. 支付凭证 URL可选
RuleFor(x => x.ProofUrl)
.MaximumLength(500)
.WithMessage("支付凭证 URL 不能超过 500 个字符")
.When(x => !string.IsNullOrWhiteSpace(x.ProofUrl));
// 6. (空行后) 备注(可选)
// 6. 备注(可选)
RuleFor(x => x.Notes)
.MaximumLength(500)
.WithMessage("备注不能超过 500 个字符")

View File

@@ -15,12 +15,12 @@ public sealed class UpdateBillingStatusCommandValidator : AbstractValidator<Upda
.GreaterThan(0)
.WithMessage("账单 ID 必须大于 0");
// 2. (空行后) 状态枚举校验
// 2. 状态枚举校验
RuleFor(x => x.NewStatus)
.IsInEnum()
.WithMessage("新状态无效");
// 3. (空行后) 备注长度限制(可选)
// 3. 备注长度限制(可选)
RuleFor(x => x.Notes)
.MaximumLength(500)
.WithMessage("备注不能超过 500 个字符")

View File

@@ -96,7 +96,7 @@ public sealed class MerchantDetailDto
/// <summary>
/// 门店列表。
/// </summary>
public IReadOnlyList<StoreDto> Stores { get; init; } = [];
public IReadOnlyList<MerchantStoreDto> Stores { get; init; } = [];
/// <summary>
/// 并发控制版本。

View File

@@ -7,7 +7,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Dto;
/// <summary>
/// 商户详情门店 DTO。
/// </summary>
public sealed class StoreDto
public sealed class MerchantStoreDto
{
/// <summary>
/// 门店 ID。

View File

@@ -50,7 +50,7 @@ internal static class MerchantMapping
/// <summary>
/// 将商户实体映射为详情 DTO。
/// </summary>
public static MerchantDetailDto ToDetailDto(Merchant merchant, string? tenantName, IReadOnlyList<StoreDto> stores) => new()
public static MerchantDetailDto ToDetailDto(Merchant merchant, string? tenantName, IReadOnlyList<MerchantStoreDto> stores) => new()
{
Id = merchant.Id,
TenantId = merchant.TenantId,
@@ -150,7 +150,7 @@ internal static class MerchantMapping
/// <summary>
/// 将门店实体映射为 DTO。
/// </summary>
public static StoreDto ToStoreDto(Store store) => new()
public static MerchantStoreDto ToStoreDto(Store store) => new()
{
Id = store.Id,
Name = store.Name,
@@ -201,6 +201,6 @@ internal static class MerchantMapping
/// <summary>
/// 将门店集合映射为 DTO 集合。
/// </summary>
public static IReadOnlyList<StoreDto> ToStoreDtos(IEnumerable<Store> stores)
public static IReadOnlyList<MerchantStoreDto> ToStoreDtos(IEnumerable<Store> stores)
=> stores.Select(ToStoreDto).ToList();
}

View File

@@ -54,7 +54,7 @@ public sealed class PurchaseQuotaPackageCommandHandler(
quotaUsage.LimitValue += quotaPackage.QuotaValue;
await quotaPackageRepository.UpdateUsageAsync(quotaUsage, cancellationToken);
// 4.1 (空行后) 记录配额变更历史(购买配额包视为“剩余增加”)
// 4.1 记录配额变更历史(购买配额包视为“剩余增加”)
await quotaUsageHistoryRepository.AddAsync(new TenantQuotaUsageHistory
{
TenantId = request.TenantId,

View File

@@ -120,7 +120,7 @@ public sealed class ProcessAutoRenewalCommandHandler(
// 1. 以年月差作为周期(月),兼容“按月续费”模型
var months = (effectiveTo.Year - effectiveFrom.Year) * 12 + effectiveTo.Month - effectiveFrom.Month;
// 2. (空行后) 对不足 1 个月的情况兜底为 1
// 2. 对不足 1 个月的情况兜底为 1
return months <= 0 ? 1 : months;
}
@@ -134,7 +134,7 @@ public sealed class ProcessAutoRenewalCommandHandler(
return monthlyPrice * durationMonths;
}
// 2. (空行后) 按年 + 月组合计算金额
// 2. 按年 + 月组合计算金额
var years = durationMonths / 12;
var remainingMonths = durationMonths % 12;
return yearlyPrice.Value * years + monthlyPrice * remainingMonths;

View File

@@ -40,14 +40,14 @@ public sealed class ProcessSubscriptionExpiryCommandHandler(
await subscriptionRepository.UpdateAsync(subscription, cancellationToken);
}
// 3. (空行后) 宽限期到期自动暂停
// 3. 宽限期到期自动暂停
foreach (var subscription in gracePeriodExpired)
{
subscription.Status = SubscriptionStatus.Suspended;
await subscriptionRepository.UpdateAsync(subscription, cancellationToken);
}
// 4. (空行后) 保存变更
// 4. 保存变更
var totalChanged = expiredActive.Count + gracePeriodExpired.Count;
if (totalChanged > 0)
{

View File

@@ -63,7 +63,7 @@ public sealed class CheckTenantQuotaCommandHandler(
ResetCycle = ResolveResetCycle(request.QuotaType)
};
// 4.1 (空行后) 记录是否为首次初始化(用于落库历史)
// 4.1 记录是否为首次初始化(用于落库历史)
var isNewUsage = usage.Id == 0;
var usedAfter = usage.UsedValue + request.Delta;
@@ -79,7 +79,7 @@ public sealed class CheckTenantQuotaCommandHandler(
usage.UsedValue = usedAfter;
usage.ResetCycle ??= ResolveResetCycle(request.QuotaType);
// 5.1 (空行后) 落库历史(初始化 + 本次消耗)
// 5.1 落库历史(初始化 + 本次消耗)
var now = DateTime.UtcNow;
if (isNewUsage)
{

View File

@@ -39,13 +39,13 @@ public sealed class ClaimTenantReviewCommandHandler(
throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {existingClaim.ClaimedByName} 领取");
}
// 3. (空行后) 获取当前用户显示名(用于展示快照)
// 3. 获取当前用户显示名(用于展示快照)
var profile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var displayName = string.IsNullOrWhiteSpace(profile.DisplayName)
? $"user:{currentUserAccessor.UserId}"
: profile.DisplayName;
// 4. (空行后) 构造领取记录与审计日志
// 4. 构造领取记录与审计日志
var now = DateTime.UtcNow;
var claim = new TenantReviewClaim
{
@@ -68,7 +68,7 @@ public sealed class ClaimTenantReviewCommandHandler(
CurrentStatus = tenant.Status
};
// 5. (空行后) 写入领取记录(处理并发领取冲突)
// 5. 写入领取记录(处理并发领取冲突)
var success = await tenantRepository.TryAddReviewClaimAsync(claim, auditLog, cancellationToken);
if (!success)
{
@@ -86,7 +86,7 @@ public sealed class ClaimTenantReviewCommandHandler(
throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {current.ClaimedByName} 领取");
}
// 6. (空行后) 返回领取结果
// 6. 返回领取结果
return claim.ToDto();
}
}

View File

@@ -37,11 +37,11 @@ public sealed class CreateTenantAdminResetLinkTokenCommandHandler(
throw new BusinessException(ErrorCodes.Forbidden, "仅平台超级管理员可生成重置链接");
}
// 2. (空行后) 校验租户存在且存在主管理员
// 2. 校验租户存在且存在主管理员
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
// 2.1 (空行后) 若缺少主管理员则自动回填(兼容历史数据)
// 2.1 若缺少主管理员则自动回填(兼容历史数据)
if (!tenant.PrimaryOwnerUserId.HasValue || tenant.PrimaryOwnerUserId.Value == 0)
{
var originalContextForFix = tenantContextAccessor.Current;
@@ -65,10 +65,10 @@ public sealed class CreateTenantAdminResetLinkTokenCommandHandler(
}
}
// 3. (空行后) 签发一次性重置令牌(默认 24 小时有效)
// 3. 签发一次性重置令牌(默认 24 小时有效)
var token = await tokenStore.IssueAsync(tenant.PrimaryOwnerUserId.Value, DateTime.UtcNow.AddHours(24), cancellationToken);
// 4. (空行后) 写入审计日志
// 4. 写入审计日志
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
? $"user:{currentUserAccessor.UserId}"
@@ -88,7 +88,7 @@ public sealed class CreateTenantAdminResetLinkTokenCommandHandler(
await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken);
await tenantRepository.SaveChangesAsync(cancellationToken);
// 5. (空行后) 返回令牌
// 5. 返回令牌
return token;
}
}

View File

@@ -49,7 +49,7 @@ public sealed class CreateTenantManuallyCommandHandler(
throw new BusinessException(ErrorCodes.Conflict, $"租户编码 {normalizedCode} 已存在");
}
// 3. (空行后) 校验联系人手机号唯一性(仅当填写时)
// 3. 校验联系人手机号唯一性(仅当填写时)
if (!string.IsNullOrWhiteSpace(request.ContactPhone))
{
var normalizedPhone = request.ContactPhone.Trim();
@@ -59,14 +59,14 @@ public sealed class CreateTenantManuallyCommandHandler(
}
}
// 4. (空行后) 校验管理员账号唯一性
// 4. 校验管理员账号唯一性
var normalizedAccount = request.AdminAccount.Trim();
if (await identityUserRepository.ExistsByAccountAsync(normalizedAccount, cancellationToken))
{
throw new BusinessException(ErrorCodes.Conflict, $"账号 {normalizedAccount} 已存在");
}
// 5. (空行后) 校验套餐存在且可用
// 5. 校验套餐存在且可用
var package = await tenantPackageRepository.FindByIdAsync(request.TenantPackageId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "套餐不存在");
if (!package.IsActive)
@@ -74,12 +74,12 @@ public sealed class CreateTenantManuallyCommandHandler(
throw new BusinessException(ErrorCodes.BadRequest, "套餐未启用,无法绑定订阅");
}
// 6. (空行后) 计算订阅生效与到期时间UTC
// 6. 计算订阅生效与到期时间UTC
var now = DateTime.UtcNow;
var subscriptionEffectiveFrom = request.SubscriptionEffectiveFrom ?? now;
var subscriptionEffectiveTo = subscriptionEffectiveFrom.AddMonths(request.DurationMonths);
// 7. (空行后) 构建租户与订阅
// 7. 构建租户与订阅
var tenantId = idGenerator.NextId();
var tenant = new Tenant
{
@@ -108,7 +108,7 @@ public sealed class CreateTenantManuallyCommandHandler(
Remarks = request.Remarks
};
// 8. (空行后) 构建订阅实体
// 8. 构建订阅实体
var subscription = new TenantSubscription
{
Id = idGenerator.NextId(),
@@ -123,7 +123,7 @@ public sealed class CreateTenantManuallyCommandHandler(
Notes = request.SubscriptionNotes
};
// 9. (空行后) 构建认证资料(默认直接通过)
// 9. 构建认证资料(默认直接通过)
var actorName = currentUserAccessor.IsAuthenticated
? $"user:{currentUserAccessor.UserId}"
: "system";
@@ -150,7 +150,7 @@ public sealed class CreateTenantManuallyCommandHandler(
ReviewRemarks = request.ReviewRemarks
};
// 10. (空行后) 写入审计日志与订阅历史
// 10. 写入审计日志与订阅历史
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
{
TenantId = tenantId,
@@ -201,13 +201,13 @@ public sealed class CreateTenantManuallyCommandHandler(
Notes = request.SubscriptionNotes
}, cancellationToken);
// 11. (空行后) 持久化租户、订阅与认证资料
// 11. 持久化租户、订阅与认证资料
await tenantRepository.AddTenantAsync(tenant, cancellationToken);
await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
await tenantRepository.UpsertVerificationProfileAsync(verification, cancellationToken);
await tenantRepository.SaveChangesAsync(cancellationToken);
// 12. (空行后) 临时切换租户上下文,保证身份与权限写入正确
// 12. 临时切换租户上下文,保证身份与权限写入正确
var previousContext = tenantContextAccessor.Current;
tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "manual-create");
try
@@ -228,7 +228,7 @@ public sealed class CreateTenantManuallyCommandHandler(
await identityUserRepository.AddAsync(adminUser, cancellationToken);
await identityUserRepository.SaveChangesAsync(cancellationToken);
// 14. (空行后) 初始化租户管理员角色模板并绑定角色
// 14. 初始化租户管理员角色模板并绑定角色
await mediator.Send(new InitializeRoleTemplatesCommand
{
TemplateCodes = new[] { "tenant-admin" }
@@ -244,7 +244,7 @@ public sealed class CreateTenantManuallyCommandHandler(
}, cancellationToken);
}
// 15. (空行后) 回写租户所有者账号
// 15. 回写租户所有者账号
tenant.PrimaryOwnerUserId = adminUser.Id;
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
await tenantRepository.SaveChangesAsync(cancellationToken);
@@ -255,7 +255,7 @@ public sealed class CreateTenantManuallyCommandHandler(
tenantContextAccessor.Current = previousContext;
}
// 17. (空行后) 返回创建结果
// 17. 返回创建结果
logger.LogInformation("已后台手动创建租户 {TenantCode}", tenant.Code);
return new TenantDetailDto

View File

@@ -45,7 +45,7 @@ public sealed class ExtendTenantSubscriptionCommandHandler(
? $"user:{currentUserAccessor.UserId}"
: "system";
// 3. (空行后) 创建续费订阅
// 3. 创建续费订阅
var subscription = new TenantSubscription
{
Id = idGenerator.NextId(),
@@ -75,7 +75,7 @@ public sealed class ExtendTenantSubscriptionCommandHandler(
Notes = request.Notes
}, cancellationToken);
// 4. (空行后) 若租户处于到期状态则恢复为正常(冻结状态需先解冻)
// 4. 若租户处于到期状态则恢复为正常(冻结状态需先解冻)
if (tenant.Status == TenantStatus.Expired)
{
tenant.Status = TenantStatus.Active;
@@ -86,7 +86,7 @@ public sealed class ExtendTenantSubscriptionCommandHandler(
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
// 5. (空行后) 记录审计
// 5. 记录审计
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
{
TenantId = tenant.Id,

View File

@@ -33,7 +33,7 @@ public sealed class ForceClaimTenantReviewCommandHandler(
? $"user:{currentUserAccessor.UserId}"
: profile.DisplayName;
// 3. (空行后) 读取当前领取记录(可跟踪用于更新)
// 3. 读取当前领取记录(可跟踪用于更新)
var claim = await tenantRepository.FindActiveReviewClaimAsync(request.TenantId, cancellationToken);
if (claim == null)
{
@@ -75,13 +75,13 @@ public sealed class ForceClaimTenantReviewCommandHandler(
return created.ToDto();
}
// 5. (空行后) 已由自己领取则直接返回
// 5. 已由自己领取则直接返回
if (claim.ClaimedBy == currentUserAccessor.UserId)
{
return claim.ToDto();
}
// 6. (空行后) 更新领取人并记录审计
// 6. 更新领取人并记录审计
var previousOwner = claim.ClaimedByName;
claim.ClaimedBy = currentUserAccessor.UserId;
claim.ClaimedByName = displayName;

View File

@@ -44,7 +44,7 @@ public sealed class FreezeTenantCommandHandler(
tenant.SuspendedAt = DateTime.UtcNow;
tenant.SuspensionReason = request.Reason;
// 3. (空行后) 同步暂停订阅
// 3. 同步暂停订阅
if (subscription != null)
{
subscription.Status = SubscriptionStatus.Suspended;
@@ -53,7 +53,7 @@ public sealed class FreezeTenantCommandHandler(
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
// 4. (空行后) 记录审计
// 4. 记录审计
var actorName = currentUserAccessor.IsAuthenticated
? $"user:{currentUserAccessor.UserId}"
: "system";

View File

@@ -26,12 +26,12 @@ public sealed class GetTenantByIdQueryHandler(
var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken);
// 3. (空行后) 查询当前套餐
// 3. 查询当前套餐
var package = subscription == null
? null
: await tenantPackageRepository.FindByIdAsync(subscription.TenantPackageId, cancellationToken);
// 4. (空行后) 组装返回
// 4. 组装返回
return new TenantDetailDto
{
Tenant = TenantMapping.ToDto(tenant, subscription, verification),

View File

@@ -24,13 +24,13 @@ public sealed class GetTenantPackageTenantsQueryHandler(IDapperExecutor dapperEx
var pageSize = request.PageSize <= 0 ? 20 : request.PageSize;
var keyword = string.IsNullOrWhiteSpace(request.Keyword) ? null : request.Keyword.Trim();
// 2. (空行后) 以当前时间为准筛选“有效订阅”
// 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. (空行后) 查询总数 + 列表
// 3. 查询总数 + 列表
return await dapperExecutor.QueryAsync(
DatabaseConstants.AppDataSource,
DatabaseConnectionRole.Read,
@@ -48,7 +48,7 @@ public sealed class GetTenantPackageTenantsQueryHandler(IDapperExecutor dapperEx
],
token);
// 3.2 (空行后) 查询列表
// 3.2 查询列表
var listSql = BuildListSql(expiryEnd.HasValue);
await using var listCommand = CreateCommand(
connection,
@@ -79,7 +79,7 @@ public sealed class GetTenantPackageTenantsQueryHandler(IDapperExecutor dapperEx
});
}
// 3.3 (空行后) 返回分页
// 3.3 返回分页
return new PagedResult<TenantPackageTenantDto>(items, page, pageSize, total);
},
cancellationToken);

View File

@@ -23,14 +23,14 @@ public sealed class GetTenantPackageUsagesQueryHandler(IDapperExecutor dapperExe
.Distinct()
.ToArray();
// 2. (空行后) 构造 SQL以当前时间为准统计“有效订阅/使用租户/到期分布”)
// 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. (空行后) 查询统计结果
// 3. 查询统计结果
return await dapperExecutor.QueryAsync(
DatabaseConstants.AppDataSource,
DatabaseConnectionRole.Read,
@@ -40,7 +40,7 @@ public sealed class GetTenantPackageUsagesQueryHandler(IDapperExecutor dapperExe
await using var reader = await command.ExecuteReaderAsync(token);
var list = new List<TenantPackageUsageDto>();
// 4. (空行后) 逐行读取
// 4. 逐行读取
while (await reader.ReadAsync(token))
{
list.Add(new TenantPackageUsageDto
@@ -94,7 +94,7 @@ public sealed class GetTenantPackageUsagesQueryHandler(IDapperExecutor dapperExe
("date30", date30)
};
// 2. (空行后) 可选按套餐 ID 过滤
// 2. 可选按套餐 ID 过滤
if (ids is { Length: > 0 })
{
builder.Append(" and \"TenantPackageId\" in (");
@@ -113,7 +113,7 @@ public sealed class GetTenantPackageUsagesQueryHandler(IDapperExecutor dapperExe
builder.AppendLine(")");
}
// 3. (空行后) 分组与回连套餐表
// 3. 分组与回连套餐表
builder.AppendLine("""
group by "TenantPackageId"
)

View File

@@ -27,12 +27,12 @@ public sealed class GetTenantQuotaUsageHistoryQueryHandler(
_ = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
// 2. (空行后) 规范化分页
// 2. 规范化分页
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. (空行后) 查询总数 + 列表
// 3. 查询总数 + 列表
return await dapperExecutor.QueryAsync(
DatabaseConstants.AppDataSource,
DatabaseConnectionRole.Read,
@@ -50,7 +50,7 @@ public sealed class GetTenantQuotaUsageHistoryQueryHandler(
],
token);
// 3.2 (空行后) 查询列表
// 3.2 查询列表
await using var listCommand = CreateCommand(
connection,
BuildListSql(),
@@ -75,7 +75,7 @@ public sealed class GetTenantQuotaUsageHistoryQueryHandler(
decimal? changeAmount = reader.IsDBNull(5) ? null : reader.GetDecimal(5);
var changeReason = reader.IsDBNull(6) ? null : reader.GetString(6);
// 3.2.1 (空行后) 映射 DTO
// 3.2.1 映射 DTO
items.Add(new QuotaUsageHistoryDto
{
QuotaType = quotaType,
@@ -88,7 +88,7 @@ public sealed class GetTenantQuotaUsageHistoryQueryHandler(
});
}
// 3.3 (空行后) 返回分页
// 3.3 返回分页
return new PagedResult<QuotaUsageHistoryDto>(items, page, pageSize, total);
},
cancellationToken);

View File

@@ -38,17 +38,17 @@ public sealed class ImpersonateTenantCommandHandler(
throw new BusinessException(ErrorCodes.Forbidden, "仅平台超级管理员可执行伪装登录");
}
// 2. (空行后) 读取操作者信息(在平台租户上下文内)
// 2. 读取操作者信息(在平台租户上下文内)
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
? $"user:{currentUserAccessor.UserId}"
: operatorProfile.DisplayName;
// 2. (空行后) 校验租户存在且存在主管理员
// 2. 校验租户存在且存在主管理员
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
// 2.1 (空行后) 若缺少主管理员则自动回填(兼容历史数据)
// 2.1 若缺少主管理员则自动回填(兼容历史数据)
if (!tenant.PrimaryOwnerUserId.HasValue || tenant.PrimaryOwnerUserId.Value == 0)
{
var originalContextForFix = tenantContextAccessor.Current;
@@ -72,7 +72,7 @@ public sealed class ImpersonateTenantCommandHandler(
}
}
// 3. (空行后) 进入目标租户上下文以读取租户内用户(避免多租户查询过滤导致找不到用户)
// 3. 进入目标租户上下文以读取租户内用户(避免多租户查询过滤导致找不到用户)
var originalTenantContext = tenantContextAccessor.Current;
tenantContextAccessor.Current = new TenantContext(tenant.Id, null, "admin:impersonate");
try
@@ -81,7 +81,7 @@ public sealed class ImpersonateTenantCommandHandler(
var targetProfile = await adminAuthService.GetProfileAsync(tenant.PrimaryOwnerUserId.Value, cancellationToken);
var token = await jwtTokenService.CreateTokensAsync(targetProfile, false, cancellationToken);
// 5. (空行后) 恢复租户上下文后写入审计日志
// 5. 恢复租户上下文后写入审计日志
tenantContextAccessor.Current = originalTenantContext;
var auditLog = new TenantAuditLog
{
@@ -97,12 +97,12 @@ public sealed class ImpersonateTenantCommandHandler(
await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken);
await tenantRepository.SaveChangesAsync(cancellationToken);
// 6. (空行后) 返回令牌
// 6. 返回令牌
return token;
}
finally
{
// 7. (空行后) 确保恢复租户上下文
// 7. 确保恢复租户上下文
tenantContextAccessor.Current = originalTenantContext;
}
}

View File

@@ -34,13 +34,13 @@ public sealed class ReleaseTenantReviewClaimCommandHandler(
return null;
}
// 3. (空行后) 非领取人不允许释放(如需接管请使用强制接管)
// 3. 非领取人不允许释放(如需接管请使用强制接管)
if (claim.ClaimedBy != currentUserAccessor.UserId)
{
throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {claim.ClaimedByName} 领取");
}
// 4. (空行后) 释放领取并记录审计
// 4. 释放领取并记录审计
var profile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var displayName = string.IsNullOrWhiteSpace(profile.DisplayName)
? $"user:{currentUserAccessor.UserId}"

View File

@@ -202,7 +202,7 @@ public sealed class ReviewTenantCommandHandler(
CurrentStatus = tenant.Status
}, cancellationToken);
// 7. (空行后) 审核完成自动释放领取
// 7. 审核完成自动释放领取
reviewClaim.ReleasedAt = DateTime.UtcNow;
await tenantRepository.UpdateReviewClaimAsync(reviewClaim, cancellationToken);
await tenantRepository.AddAuditLogAsync(new Domain.Tenants.Entities.TenantAuditLog

View File

@@ -27,24 +27,24 @@ public sealed class SearchTenantsQueryHandler(ITenantRepository tenantRepository
request.PageSize,
cancellationToken);
// 2. (空行后) 无数据直接返回
// 2. 无数据直接返回
if (tenants.Count == 0)
{
return new PagedResult<TenantDto>([], request.Page, request.PageSize, total);
}
// 3. (空行后) 批量查询订阅与实名资料(避免 N+1
// 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. (空行后) 构建订阅与实名资料映射
// 4. 构建订阅与实名资料映射
var subscriptionByTenantId = subscriptions
.GroupBy(x => x.TenantId)
.ToDictionary(x => x.Key, x => x.FirstOrDefault());
var verificationByTenantId = verifications.ToDictionary(x => x.TenantId);
// 5. (空行后) 映射 DTO带订阅与认证
// 5. 映射 DTO带订阅与认证
var result = new List<TenantDto>(tenants.Count);
foreach (var tenant in tenants)
{
@@ -53,7 +53,7 @@ public sealed class SearchTenantsQueryHandler(ITenantRepository tenantRepository
result.Add(TenantMapping.ToDto(tenant, subscription, verification));
}
// 6. (空行后) 返回分页结果
// 6. 返回分页结果
return new PagedResult<TenantDto>(result, request.Page, request.PageSize, total);
}
}

View File

@@ -97,7 +97,7 @@ public sealed class SelfRegisterTenantCommandHandler(
await identityUserRepository.AddAsync(adminUser, cancellationToken);
await identityUserRepository.SaveChangesAsync(cancellationToken);
// 7.1 (空行后) 回填主管理员标识,确保后续伪装登录/重置管理员等能力可用
// 7.1 回填主管理员标识,确保后续伪装登录/重置管理员等能力可用
tenant.PrimaryOwnerUserId = adminUser.Id;
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
await tenantRepository.SaveChangesAsync(cancellationToken);

View File

@@ -42,7 +42,7 @@ public sealed class UnfreezeTenantCommandHandler(
tenant.SuspendedAt = null;
tenant.SuspensionReason = null;
// 3. (空行后) 同步订阅状态
// 3. 同步订阅状态
if (subscription != null)
{
subscription.Status = isExpired ? SubscriptionStatus.GracePeriod : SubscriptionStatus.Active;
@@ -51,7 +51,7 @@ public sealed class UnfreezeTenantCommandHandler(
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
// 4. (空行后) 记录审计
// 4. 记录审计
var actorName = currentUserAccessor.IsAuthenticated
? $"user:{currentUserAccessor.UserId}"
: "system";

View File

@@ -29,18 +29,18 @@ public sealed class UpdateTenantCommandHandler(
throw new BusinessException(ErrorCodes.BadRequest, "租户名称不能为空");
}
// 2. (空行后) 查询租户
// 2. 查询租户
var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
// 3. (空行后) 校验租户名称唯一性(排除自身)
// 3. 校验租户名称唯一性(排除自身)
var normalizedName = request.Name.Trim();
if (await tenantRepository.ExistsByNameAsync(normalizedName, excludeTenantId: request.TenantId, cancellationToken))
{
throw new BusinessException(ErrorCodes.Conflict, $"租户名称 {normalizedName} 已存在");
}
// 4. (空行后) 校验联系人手机号唯一性(仅当填写时)
// 4. 校验联系人手机号唯一性(仅当填写时)
if (!string.IsNullOrWhiteSpace(request.ContactPhone))
{
var normalizedPhone = request.ContactPhone.Trim();
@@ -51,7 +51,7 @@ public sealed class UpdateTenantCommandHandler(
}
}
// 5. (空行后) 更新基础信息(禁止修改 Code
// 5. 更新基础信息(禁止修改 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,11 +59,11 @@ 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. (空行后) 持久化更新
// 6. 持久化更新
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
await tenantRepository.SaveChangesAsync(cancellationToken);
// 7. (空行后) 记录日志
// 7. 记录日志
logger.LogInformation("已更新租户基础信息 {TenantId}", tenant.Id);
return Unit.Value;

View File

@@ -45,18 +45,18 @@ public sealed class UpdateTenantPackageCommandHandler(ITenantPackageRepository p
package.IsPublicVisible = request.IsPublicVisible;
package.IsAllowNewTenantPurchase = request.IsAllowNewTenantPurchase;
// 3. (空行后) 更新发布状态(若未传则保持不变,避免默认值覆盖)
// 3. 更新发布状态(若未传则保持不变,避免默认值覆盖)
if (request.PublishStatus.HasValue)
{
package.PublishStatus = request.PublishStatus.Value;
}
// 4. (空行后) 更新展示配置(推荐与标签)
// 4. 更新展示配置(推荐与标签)
package.IsRecommended = request.IsRecommended;
package.Tags = request.Tags ?? [];
package.SortOrder = request.SortOrder;
// 5. (空行后) 持久化并返回
// 5. 持久化并返回
await packageRepository.UpdateAsync(package, cancellationToken);
await packageRepository.SaveChangesAsync(cancellationToken);

View File

@@ -17,7 +17,7 @@ public sealed class GetTenantQuotaUsageHistoryQueryValidator : AbstractValidator
RuleFor(x => x.Page).GreaterThanOrEqualTo(1);
RuleFor(x => x.PageSize).InclusiveBetween(1, 100);
// (空行后) 时间范围校验
// 时间范围校验
When(x => x.StartDate.HasValue && x.EndDate.HasValue, () =>
{
RuleFor(x => x.EndDate!.Value).GreaterThanOrEqualTo(x => x.StartDate!.Value);