chore: add documentation comments and stylecop rules

This commit is contained in:
2025-12-04 11:25:01 +08:00
parent 17d143a351
commit 8e4c2b0e45
142 changed files with 1309 additions and 439 deletions

View File

@@ -13,12 +13,10 @@ namespace TakeoutSaaS.Application.App.Deliveries.Handlers;
public sealed class CreateDeliveryOrderCommandHandler(IDeliveryRepository deliveryRepository, ILogger<CreateDeliveryOrderCommandHandler> logger)
: IRequestHandler<CreateDeliveryOrderCommand, DeliveryOrderDto>
{
private readonly IDeliveryRepository _deliveryRepository = deliveryRepository;
private readonly ILogger<CreateDeliveryOrderCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<DeliveryOrderDto> Handle(CreateDeliveryOrderCommand request, CancellationToken cancellationToken)
{
// 1. 构建配送单实体
var deliveryOrder = new DeliveryOrder
{
OrderId = request.OrderId,
@@ -34,10 +32,14 @@ public sealed class CreateDeliveryOrderCommandHandler(IDeliveryRepository delive
FailureReason = request.FailureReason?.Trim()
};
await _deliveryRepository.AddDeliveryOrderAsync(deliveryOrder, cancellationToken);
await _deliveryRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("创建配送单 {DeliveryOrderId} 对应订单 {OrderId}", deliveryOrder.Id, deliveryOrder.OrderId);
// 2. 持久化配送单
await deliveryRepository.AddDeliveryOrderAsync(deliveryOrder, cancellationToken);
await deliveryRepository.SaveChangesAsync(cancellationToken);
// 3. 记录日志
logger.LogInformation("创建配送单 {DeliveryOrderId} 对应订单 {OrderId}", deliveryOrder.Id, deliveryOrder.OrderId);
// 4. 映射 DTO 返回
return MapToDto(deliveryOrder, []);
}

View File

@@ -15,23 +15,23 @@ public sealed class DeleteDeliveryOrderCommandHandler(
ILogger<DeleteDeliveryOrderCommandHandler> logger)
: IRequestHandler<DeleteDeliveryOrderCommand, bool>
{
private readonly IDeliveryRepository _deliveryRepository = deliveryRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly ILogger<DeleteDeliveryOrderCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<bool> Handle(DeleteDeliveryOrderCommand request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var existing = await _deliveryRepository.FindByIdAsync(request.DeliveryOrderId, tenantId, cancellationToken);
// 1. 获取租户并定位配送单
var tenantId = tenantProvider.GetCurrentTenantId();
var existing = await deliveryRepository.FindByIdAsync(request.DeliveryOrderId, tenantId, cancellationToken);
if (existing == null)
{
return false;
}
await _deliveryRepository.DeleteDeliveryOrderAsync(request.DeliveryOrderId, tenantId, cancellationToken);
await _deliveryRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("删除配送单 {DeliveryOrderId}", request.DeliveryOrderId);
// 2. 删除并保存
await deliveryRepository.DeleteDeliveryOrderAsync(request.DeliveryOrderId, tenantId, cancellationToken);
await deliveryRepository.SaveChangesAsync(cancellationToken);
// 3. 记录删除日志
logger.LogInformation("删除配送单 {DeliveryOrderId}", request.DeliveryOrderId);
return true;
}

View File

@@ -15,20 +15,23 @@ public sealed class GetDeliveryOrderByIdQueryHandler(
ITenantProvider tenantProvider)
: IRequestHandler<GetDeliveryOrderByIdQuery, DeliveryOrderDto?>
{
private readonly IDeliveryRepository _deliveryRepository = deliveryRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
public async Task<DeliveryOrderDto?> Handle(GetDeliveryOrderByIdQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var order = await _deliveryRepository.FindByIdAsync(request.DeliveryOrderId, tenantId, cancellationToken);
// 1. 读取当前租户标识
var tenantId = tenantProvider.GetCurrentTenantId();
// 2. 查询配送单主体
var order = await deliveryRepository.FindByIdAsync(request.DeliveryOrderId, tenantId, cancellationToken);
if (order == null)
{
return null;
}
var events = await _deliveryRepository.GetEventsAsync(order.Id, tenantId, cancellationToken);
// 3. 查询配送事件明细
var events = await deliveryRepository.GetEventsAsync(order.Id, tenantId, cancellationToken);
// 4. 映射为 DTO 返回
return MapToDto(order, events);
}

View File

@@ -15,21 +15,25 @@ public sealed class SearchDeliveryOrdersQueryHandler(
ITenantProvider tenantProvider)
: IRequestHandler<SearchDeliveryOrdersQuery, PagedResult<DeliveryOrderDto>>
{
private readonly IDeliveryRepository _deliveryRepository = deliveryRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
public async Task<PagedResult<DeliveryOrderDto>> Handle(SearchDeliveryOrdersQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var orders = await _deliveryRepository.SearchAsync(tenantId, request.Status, request.OrderId, cancellationToken);
// 1. 获取当前租户标识
var tenantId = tenantProvider.GetCurrentTenantId();
// 2. 查询配送单列表(租户隔离)
var orders = await deliveryRepository.SearchAsync(tenantId, request.Status, request.OrderId, cancellationToken);
// 3. 本地排序
var sorted = ApplySorting(orders, request.SortBy, request.SortDescending);
// 4. 本地分页
var paged = sorted
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToList();
// 5. 映射 DTO
var items = paged.Select(order => new DeliveryOrderDto
{
Id = order.Id,
@@ -48,6 +52,7 @@ public sealed class SearchDeliveryOrdersQueryHandler(
CreatedAt = order.CreatedAt
}).ToList();
// 6. 返回分页结果
return new PagedResult<DeliveryOrderDto>(items, request.Page, request.PageSize, orders.Count);
}

View File

@@ -17,20 +17,20 @@ public sealed class UpdateDeliveryOrderCommandHandler(
ILogger<UpdateDeliveryOrderCommandHandler> logger)
: IRequestHandler<UpdateDeliveryOrderCommand, DeliveryOrderDto?>
{
private readonly IDeliveryRepository _deliveryRepository = deliveryRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly ILogger<UpdateDeliveryOrderCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<DeliveryOrderDto?> Handle(UpdateDeliveryOrderCommand request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var existing = await _deliveryRepository.FindByIdAsync(request.DeliveryOrderId, tenantId, cancellationToken);
// 1. 获取当前租户标识
var tenantId = tenantProvider.GetCurrentTenantId();
// 2. 查询目标配送单
var existing = await deliveryRepository.FindByIdAsync(request.DeliveryOrderId, tenantId, cancellationToken);
if (existing == null)
{
return null;
}
// 3. 更新字段
existing.OrderId = request.OrderId;
existing.Provider = request.Provider;
existing.ProviderOrderId = request.ProviderOrderId?.Trim();
@@ -43,11 +43,15 @@ public sealed class UpdateDeliveryOrderCommandHandler(
existing.DeliveredAt = request.DeliveredAt;
existing.FailureReason = request.FailureReason?.Trim();
await _deliveryRepository.UpdateDeliveryOrderAsync(existing, cancellationToken);
await _deliveryRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("更新配送单 {DeliveryOrderId}", existing.Id);
// 4. 持久化变更
await deliveryRepository.UpdateDeliveryOrderAsync(existing, cancellationToken);
await deliveryRepository.SaveChangesAsync(cancellationToken);
var events = await _deliveryRepository.GetEventsAsync(existing.Id, tenantId, cancellationToken);
// 5. 记录更新日志
logger.LogInformation("更新配送单 {DeliveryOrderId}", existing.Id);
// 6. 查询事件并返回映射结果
var events = await deliveryRepository.GetEventsAsync(existing.Id, tenantId, cancellationToken);
return MapToDto(existing, events);
}

View File

@@ -22,20 +22,17 @@ public sealed class AddMerchantDocumentCommandHandler(
ICurrentUserAccessor currentUserAccessor)
: IRequestHandler<AddMerchantDocumentCommand, MerchantDocumentDto>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly IIdGenerator _idGenerator = idGenerator;
private readonly ICurrentUserAccessor _currentUserAccessor = currentUserAccessor;
public async Task<MerchantDocumentDto> Handle(AddMerchantDocumentCommand request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var merchant = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
// 1. 获取租户并查询商户
var tenantId = tenantProvider.GetCurrentTenantId();
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
// 2. 构建证照记录
var document = new MerchantDocument
{
Id = _idGenerator.NextId(),
Id = idGenerator.NextId(),
MerchantId = merchant.Id,
DocumentType = request.DocumentType,
Status = MerchantDocumentStatus.Pending,
@@ -45,8 +42,9 @@ public sealed class AddMerchantDocumentCommandHandler(
ExpiresAt = request.ExpiresAt
};
await _merchantRepository.AddDocumentAsync(document, cancellationToken);
await _merchantRepository.AddAuditLogAsync(new MerchantAuditLog
// 3. 持久化与审计
await merchantRepository.AddDocumentAsync(document, cancellationToken);
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
{
TenantId = tenantId,
MerchantId = merchant.Id,
@@ -57,20 +55,21 @@ public sealed class AddMerchantDocumentCommandHandler(
OperatorName = ResolveOperatorName()
}, cancellationToken);
await _merchantRepository.SaveChangesAsync(cancellationToken);
await merchantRepository.SaveChangesAsync(cancellationToken);
// 4. 返回 DTO
return MerchantMapping.ToDto(document);
}
private long? ResolveOperatorId()
{
var id = _currentUserAccessor.UserId;
var id = currentUserAccessor.UserId;
return id == 0 ? null : id;
}
private string ResolveOperatorName()
{
var id = _currentUserAccessor.UserId;
var id = currentUserAccessor.UserId;
return id == 0 ? "system" : $"user:{id}";
}
}

View File

@@ -18,22 +18,23 @@ public sealed class CreateMerchantCategoryCommandHandler(
ITenantProvider tenantProvider)
: IRequestHandler<CreateMerchantCategoryCommand, MerchantCategoryDto>
{
private readonly IMerchantCategoryRepository _categoryRepository = categoryRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
public async Task<MerchantCategoryDto> Handle(CreateMerchantCategoryCommand request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
var normalizedName = request.Name.Trim();
if (await _categoryRepository.ExistsAsync(normalizedName, tenantId, cancellationToken))
// 2. 检查重名
if (await categoryRepository.ExistsAsync(normalizedName, tenantId, cancellationToken))
{
throw new BusinessException(ErrorCodes.Conflict, $"类目“{normalizedName}”已存在");
}
var categories = await _categoryRepository.ListAsync(tenantId, cancellationToken);
// 3. 计算排序
var categories = await categoryRepository.ListAsync(tenantId, cancellationToken);
var targetOrder = request.DisplayOrder ?? (categories.Count == 0 ? 1 : categories.Max(x => x.DisplayOrder) + 1);
// 4. 构建实体
var entity = new MerchantCategory
{
Name = normalizedName,
@@ -41,8 +42,9 @@ public sealed class CreateMerchantCategoryCommandHandler(
IsActive = request.IsActive
};
await _categoryRepository.AddAsync(entity, cancellationToken);
await _categoryRepository.SaveChangesAsync(cancellationToken);
// 5. 持久化并返回
await categoryRepository.AddAsync(entity, cancellationToken);
await categoryRepository.SaveChangesAsync(cancellationToken);
return MerchantMapping.ToDto(entity);
}

View File

@@ -13,12 +13,10 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
public sealed class CreateMerchantCommandHandler(IMerchantRepository merchantRepository, ILogger<CreateMerchantCommandHandler> logger)
: IRequestHandler<CreateMerchantCommand, MerchantDto>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ILogger<CreateMerchantCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<MerchantDto> Handle(CreateMerchantCommand request, CancellationToken cancellationToken)
{
// 1. 构建商户实体
var merchant = new Merchant
{
BrandName = request.BrandName.Trim(),
@@ -31,10 +29,12 @@ public sealed class CreateMerchantCommandHandler(IMerchantRepository merchantRep
JoinedAt = DateTime.UtcNow
};
await _merchantRepository.AddMerchantAsync(merchant, cancellationToken);
await _merchantRepository.SaveChangesAsync(cancellationToken);
// 2. 持久化
await merchantRepository.AddMerchantAsync(merchant, cancellationToken);
await merchantRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("创建商户 {MerchantId} - {BrandName}", merchant.Id, merchant.BrandName);
// 3. 记录日志
logger.LogInformation("创建商户 {MerchantId} - {BrandName}", merchant.Id, merchant.BrandName);
return MapToDto(merchant);
}

View File

@@ -22,25 +22,23 @@ public sealed class CreateMerchantContractCommandHandler(
ICurrentUserAccessor currentUserAccessor)
: IRequestHandler<CreateMerchantContractCommand, MerchantContractDto>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly IIdGenerator _idGenerator = idGenerator;
private readonly ICurrentUserAccessor _currentUserAccessor = currentUserAccessor;
public async Task<MerchantContractDto> Handle(CreateMerchantContractCommand request, CancellationToken cancellationToken)
{
// 1. 校验时间
if (request.EndDate <= request.StartDate)
{
throw new BusinessException(ErrorCodes.BadRequest, "合同结束时间必须晚于开始时间");
}
var tenantId = _tenantProvider.GetCurrentTenantId();
var merchant = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
// 2. 查询商户
var tenantId = tenantProvider.GetCurrentTenantId();
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
// 3. 构建合同
var contract = new MerchantContract
{
Id = _idGenerator.NextId(),
Id = idGenerator.NextId(),
MerchantId = merchant.Id,
ContractNumber = request.ContractNumber.Trim(),
StartDate = request.StartDate,
@@ -48,8 +46,9 @@ public sealed class CreateMerchantContractCommandHandler(
FileUrl = request.FileUrl.Trim()
};
await _merchantRepository.AddContractAsync(contract, cancellationToken);
await _merchantRepository.AddAuditLogAsync(new MerchantAuditLog
// 4. 持久化与审计
await merchantRepository.AddContractAsync(contract, cancellationToken);
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
{
TenantId = tenantId,
MerchantId = merchant.Id,
@@ -60,19 +59,21 @@ public sealed class CreateMerchantContractCommandHandler(
OperatorName = ResolveOperatorName()
}, cancellationToken);
await _merchantRepository.SaveChangesAsync(cancellationToken);
await merchantRepository.SaveChangesAsync(cancellationToken);
// 5. 返回 DTO
return MerchantMapping.ToDto(contract);
}
private long? ResolveOperatorId()
{
var id = _currentUserAccessor.UserId;
var id = currentUserAccessor.UserId;
return id == 0 ? null : id;
}
private string ResolveOperatorName()
{
var id = _currentUserAccessor.UserId;
var id = currentUserAccessor.UserId;
return id == 0 ? "system" : $"user:{id}";
}
}

View File

@@ -13,21 +13,20 @@ public sealed class DeleteMerchantCategoryCommandHandler(
ITenantProvider tenantProvider)
: IRequestHandler<DeleteMerchantCategoryCommand, bool>
{
private readonly IMerchantCategoryRepository _categoryRepository = categoryRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
public async Task<bool> Handle(DeleteMerchantCategoryCommand request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var existing = await _categoryRepository.FindByIdAsync(request.CategoryId, tenantId, cancellationToken);
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
var existing = await categoryRepository.FindByIdAsync(request.CategoryId, tenantId, cancellationToken);
if (existing == null)
{
return false;
}
await _categoryRepository.RemoveAsync(existing, cancellationToken);
await _categoryRepository.SaveChangesAsync(cancellationToken);
// 2. 删除并保存
await categoryRepository.RemoveAsync(existing, cancellationToken);
await categoryRepository.SaveChangesAsync(cancellationToken);
return true;
}
}

View File

@@ -15,25 +15,21 @@ public sealed class DeleteMerchantCommandHandler(
ILogger<DeleteMerchantCommandHandler> logger)
: IRequestHandler<DeleteMerchantCommand, bool>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly ILogger<DeleteMerchantCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<bool> Handle(DeleteMerchantCommand request, CancellationToken cancellationToken)
{
// 1. 校验存在性
var tenantId = _tenantProvider.GetCurrentTenantId();
var existing = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken);
var tenantId = tenantProvider.GetCurrentTenantId();
var existing = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken);
if (existing == null)
{
return false;
}
// 2. 删除
await _merchantRepository.DeleteMerchantAsync(request.MerchantId, tenantId, cancellationToken);
await _merchantRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("删除商户 {MerchantId}", request.MerchantId);
await merchantRepository.DeleteMerchantAsync(request.MerchantId, tenantId, cancellationToken);
await merchantRepository.SaveChangesAsync(cancellationToken);
logger.LogInformation("删除商户 {MerchantId}", request.MerchantId);
return true;
}

View File

@@ -16,20 +16,21 @@ public sealed class GetMerchantAuditLogsQueryHandler(
ITenantProvider tenantProvider)
: IRequestHandler<GetMerchantAuditLogsQuery, PagedResult<MerchantAuditLogDto>>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
public async Task<PagedResult<MerchantAuditLogDto>> Handle(GetMerchantAuditLogsQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var logs = await _merchantRepository.GetAuditLogsAsync(request.MerchantId, tenantId, cancellationToken);
// 1. 获取租户上下文并查询日志
var tenantId = tenantProvider.GetCurrentTenantId();
var logs = await merchantRepository.GetAuditLogsAsync(request.MerchantId, tenantId, cancellationToken);
var total = logs.Count;
// 2. 分页映射
var paged = logs
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.Select(MerchantMapping.ToDto)
.ToList();
// 3. 返回结果
return new PagedResult<MerchantAuditLogDto>(paged, request.Page, request.PageSize, total);
}
}

View File

@@ -12,19 +12,18 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
public sealed class GetMerchantByIdQueryHandler(IMerchantRepository merchantRepository, ITenantProvider tenantProvider)
: IRequestHandler<GetMerchantByIdQuery, MerchantDto?>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
public async Task<MerchantDto?> Handle(GetMerchantByIdQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var merchant = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken);
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken);
if (merchant == null)
{
return null;
}
// 2. 返回 DTO
return new MerchantDto
{
Id = merchant.Id,

View File

@@ -15,14 +15,13 @@ public sealed class GetMerchantCategoriesQueryHandler(
ITenantProvider tenantProvider)
: IRequestHandler<GetMerchantCategoriesQuery, IReadOnlyList<string>>
{
private readonly IMerchantCategoryRepository _categoryRepository = categoryRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
public async Task<IReadOnlyList<string>> Handle(GetMerchantCategoriesQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var categories = await _categoryRepository.ListAsync(tenantId, cancellationToken);
// 1. 获取租户上下文并读取类目
var tenantId = tenantProvider.GetCurrentTenantId();
var categories = await categoryRepository.ListAsync(tenantId, cancellationToken);
// 2. 过滤启用类目并去重
return categories
.Where(x => x.IsActive)
.Select(x => x.Name.Trim())

View File

@@ -17,16 +17,15 @@ public sealed class GetMerchantContractsQueryHandler(
ITenantProvider tenantProvider)
: IRequestHandler<GetMerchantContractsQuery, IReadOnlyList<MerchantContractDto>>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
public async Task<IReadOnlyList<MerchantContractDto>> Handle(GetMerchantContractsQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
_ = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
// 1. 获取租户上下文并校验商户存在
var tenantId = tenantProvider.GetCurrentTenantId();
_ = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
var contracts = await _merchantRepository.GetContractsAsync(request.MerchantId, tenantId, cancellationToken);
// 2. 查询合同列表
var contracts = await merchantRepository.GetContractsAsync(request.MerchantId, tenantId, cancellationToken);
return MerchantMapping.ToContractDtos(contracts);
}
}

View File

@@ -16,18 +16,18 @@ public sealed class GetMerchantDetailQueryHandler(
ITenantProvider tenantProvider)
: IRequestHandler<GetMerchantDetailQuery, MerchantDetailDto>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
public async Task<MerchantDetailDto> Handle(GetMerchantDetailQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var merchant = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
// 1. 获取租户上下文并查询商户
var tenantId = tenantProvider.GetCurrentTenantId();
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
var documents = await _merchantRepository.GetDocumentsAsync(request.MerchantId, tenantId, cancellationToken);
var contracts = await _merchantRepository.GetContractsAsync(request.MerchantId, tenantId, cancellationToken);
// 2. 查询证照与合同
var documents = await merchantRepository.GetDocumentsAsync(request.MerchantId, tenantId, cancellationToken);
var contracts = await merchantRepository.GetContractsAsync(request.MerchantId, tenantId, cancellationToken);
// 3. 返回明细 DTO
return new MerchantDetailDto
{
Merchant = MerchantMapping.ToDto(merchant),

View File

@@ -17,16 +17,15 @@ public sealed class GetMerchantDocumentsQueryHandler(
ITenantProvider tenantProvider)
: IRequestHandler<GetMerchantDocumentsQuery, IReadOnlyList<MerchantDocumentDto>>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
public async Task<IReadOnlyList<MerchantDocumentDto>> Handle(GetMerchantDocumentsQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
_ = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
// 1. 获取租户上下文并校验商户存在
var tenantId = tenantProvider.GetCurrentTenantId();
_ = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
var documents = await _merchantRepository.GetDocumentsAsync(request.MerchantId, tenantId, cancellationToken);
// 2. 查询证照列表
var documents = await merchantRepository.GetDocumentsAsync(request.MerchantId, tenantId, cancellationToken);
return MerchantMapping.ToDocumentDtos(documents);
}
}

View File

@@ -15,13 +15,13 @@ public sealed class ListMerchantCategoriesQueryHandler(
ITenantProvider tenantProvider)
: IRequestHandler<ListMerchantCategoriesQuery, IReadOnlyList<MerchantCategoryDto>>
{
private readonly IMerchantCategoryRepository _categoryRepository = categoryRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
public async Task<IReadOnlyList<MerchantCategoryDto>> Handle(ListMerchantCategoriesQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var categories = await _categoryRepository.ListAsync(tenantId, cancellationToken);
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
var categories = await categoryRepository.ListAsync(tenantId, cancellationToken);
// 2. 映射 DTO
return MerchantMapping.ToCategoryDtos(categories);
}
}

View File

@@ -16,15 +16,14 @@ public sealed class ReorderMerchantCategoriesCommandHandler(
ITenantProvider tenantProvider)
: IRequestHandler<ReorderMerchantCategoriesCommand, bool>
{
private readonly IMerchantCategoryRepository _categoryRepository = categoryRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
public async Task<bool> Handle(ReorderMerchantCategoriesCommand request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var categories = await _categoryRepository.ListAsync(tenantId, cancellationToken);
// 1. 获取租户并查询类目
var tenantId = tenantProvider.GetCurrentTenantId();
var categories = await categoryRepository.ListAsync(tenantId, cancellationToken);
var map = categories.ToDictionary(x => x.Id);
// 2. 更新排序
foreach (var item in request.Items)
{
if (!map.TryGetValue(item.CategoryId, out var category))
@@ -35,8 +34,9 @@ public sealed class ReorderMerchantCategoriesCommandHandler(
category.DisplayOrder = item.DisplayOrder;
}
await _categoryRepository.UpdateRangeAsync(map.Values, cancellationToken);
await _categoryRepository.SaveChangesAsync(cancellationToken);
// 3. 持久化
await categoryRepository.UpdateRangeAsync(map.Values, cancellationToken);
await categoryRepository.SaveChangesAsync(cancellationToken);
return true;
}
}

View File

@@ -20,21 +20,20 @@ public sealed class ReviewMerchantCommandHandler(
ICurrentUserAccessor currentUserAccessor)
: IRequestHandler<ReviewMerchantCommand, MerchantDto>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly ICurrentUserAccessor _currentUserAccessor = currentUserAccessor;
public async Task<MerchantDto> Handle(ReviewMerchantCommand request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var merchant = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
// 1. 读取商户
var tenantId = tenantProvider.GetCurrentTenantId();
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
// 2. 已审核通过则直接返回
if (request.Approve && merchant.Status == MerchantStatus.Approved)
{
return MerchantMapping.ToDto(merchant);
}
// 3. 更新审核状态
var previousStatus = merchant.Status;
merchant.Status = request.Approve ? MerchantStatus.Approved : MerchantStatus.Rejected;
merchant.ReviewRemarks = request.Remarks;
@@ -44,8 +43,9 @@ public sealed class ReviewMerchantCommandHandler(
merchant.JoinedAt = DateTime.UtcNow;
}
await _merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
await _merchantRepository.AddAuditLogAsync(new MerchantAuditLog
// 4. 持久化与审计
await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
{
TenantId = tenantId,
MerchantId = merchant.Id,
@@ -55,20 +55,21 @@ public sealed class ReviewMerchantCommandHandler(
OperatorId = ResolveOperatorId(),
OperatorName = ResolveOperatorName()
}, cancellationToken);
await _merchantRepository.SaveChangesAsync(cancellationToken);
await merchantRepository.SaveChangesAsync(cancellationToken);
// 5. 返回 DTO
return MerchantMapping.ToDto(merchant);
}
private long? ResolveOperatorId()
{
var id = _currentUserAccessor.UserId;
var id = currentUserAccessor.UserId;
return id == 0 ? null : id;
}
private string ResolveOperatorName()
{
var id = _currentUserAccessor.UserId;
var id = currentUserAccessor.UserId;
return id == 0 ? "system" : $"user:{id}";
}
}

View File

@@ -20,27 +20,27 @@ public sealed class ReviewMerchantDocumentCommandHandler(
ICurrentUserAccessor currentUserAccessor)
: IRequestHandler<ReviewMerchantDocumentCommand, MerchantDocumentDto>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly ICurrentUserAccessor _currentUserAccessor = currentUserAccessor;
public async Task<MerchantDocumentDto> Handle(ReviewMerchantDocumentCommand request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var document = await _merchantRepository.FindDocumentByIdAsync(request.MerchantId, tenantId, request.DocumentId, cancellationToken)
// 1. 读取证照
var tenantId = tenantProvider.GetCurrentTenantId();
var document = await merchantRepository.FindDocumentByIdAsync(request.MerchantId, tenantId, request.DocumentId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "证照不存在");
// 2. 若状态无变化且备注相同,直接返回
var targetStatus = request.Approve ? MerchantDocumentStatus.Approved : MerchantDocumentStatus.Rejected;
if (document.Status == targetStatus && document.Remarks == request.Remarks)
{
return MerchantMapping.ToDto(document);
}
// 3. 更新状态
document.Status = targetStatus;
document.Remarks = request.Remarks;
await _merchantRepository.UpdateDocumentAsync(document, cancellationToken);
await _merchantRepository.AddAuditLogAsync(new MerchantAuditLog
// 4. 持久化与审计
await merchantRepository.UpdateDocumentAsync(document, cancellationToken);
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
{
TenantId = tenantId,
MerchantId = document.MerchantId,
@@ -50,20 +50,21 @@ public sealed class ReviewMerchantDocumentCommandHandler(
OperatorId = ResolveOperatorId(),
OperatorName = ResolveOperatorName()
}, cancellationToken);
await _merchantRepository.SaveChangesAsync(cancellationToken);
await merchantRepository.SaveChangesAsync(cancellationToken);
// 5. 返回 DTO
return MerchantMapping.ToDto(document);
}
private long? ResolveOperatorId()
{
var id = _currentUserAccessor.UserId;
var id = currentUserAccessor.UserId;
return id == 0 ? null : id;
}
private string ResolveOperatorName()
{
var id = _currentUserAccessor.UserId;
var id = currentUserAccessor.UserId;
return id == 0 ? "system" : $"user:{id}";
}
}

View File

@@ -15,21 +15,21 @@ public sealed class SearchMerchantsQueryHandler(
ITenantProvider tenantProvider)
: IRequestHandler<SearchMerchantsQuery, PagedResult<MerchantDto>>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
public async Task<PagedResult<MerchantDto>> Handle(SearchMerchantsQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var merchants = await _merchantRepository.SearchAsync(tenantId, request.Status, cancellationToken);
// 1. 获取租户并查询商户
var tenantId = tenantProvider.GetCurrentTenantId();
var merchants = await merchantRepository.SearchAsync(tenantId, request.Status, cancellationToken);
// 2. 排序与分页
var sorted = ApplySorting(merchants, request.SortBy, request.SortDescending);
var paged = sorted
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToList();
// 3. 映射 DTO
var items = paged.Select(merchant => new MerchantDto
{
Id = merchant.Id,
@@ -45,6 +45,7 @@ public sealed class SearchMerchantsQueryHandler(
CreatedAt = merchant.CreatedAt
}).ToList();
// 4. 返回分页结果
return new PagedResult<MerchantDto>(items, request.Page, request.PageSize, merchants.Count);
}

View File

@@ -16,16 +16,12 @@ public sealed class UpdateMerchantCommandHandler(
ILogger<UpdateMerchantCommandHandler> logger)
: IRequestHandler<UpdateMerchantCommand, MerchantDto?>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly ILogger<UpdateMerchantCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<MerchantDto?> Handle(UpdateMerchantCommand request, CancellationToken cancellationToken)
{
// 1. 读取现有商户
var tenantId = _tenantProvider.GetCurrentTenantId();
var existing = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken);
var tenantId = tenantProvider.GetCurrentTenantId();
var existing = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken);
if (existing == null)
{
return null;
@@ -41,9 +37,9 @@ public sealed class UpdateMerchantCommandHandler(
existing.Status = request.Status;
// 3. 持久化
await _merchantRepository.UpdateMerchantAsync(existing, cancellationToken);
await _merchantRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("更新商户 {MerchantId} - {BrandName}", existing.Id, existing.BrandName);
await merchantRepository.UpdateMerchantAsync(existing, cancellationToken);
await merchantRepository.SaveChangesAsync(cancellationToken);
logger.LogInformation("更新商户 {MerchantId} - {BrandName}", existing.Id, existing.BrandName);
// 4. 返回 DTO
return MapToDto(existing);

View File

@@ -20,16 +20,14 @@ public sealed class UpdateMerchantContractStatusCommandHandler(
ICurrentUserAccessor currentUserAccessor)
: IRequestHandler<UpdateMerchantContractStatusCommand, MerchantContractDto>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly ICurrentUserAccessor _currentUserAccessor = currentUserAccessor;
public async Task<MerchantContractDto> Handle(UpdateMerchantContractStatusCommand request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var contract = await _merchantRepository.FindContractByIdAsync(request.MerchantId, tenantId, request.ContractId, cancellationToken)
// 1. 查询合同
var tenantId = tenantProvider.GetCurrentTenantId();
var contract = await merchantRepository.FindContractByIdAsync(request.MerchantId, tenantId, request.ContractId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "合同不存在");
// 2. 更新状态
if (request.Status == ContractStatus.Active)
{
contract.Status = ContractStatus.Active;
@@ -46,8 +44,9 @@ public sealed class UpdateMerchantContractStatusCommandHandler(
contract.Status = request.Status;
}
await _merchantRepository.UpdateContractAsync(contract, cancellationToken);
await _merchantRepository.AddAuditLogAsync(new MerchantAuditLog
// 3. 持久化与审计
await merchantRepository.UpdateContractAsync(contract, cancellationToken);
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
{
TenantId = tenantId,
MerchantId = contract.MerchantId,
@@ -58,19 +57,21 @@ public sealed class UpdateMerchantContractStatusCommandHandler(
OperatorName = ResolveOperatorName()
}, cancellationToken);
await _merchantRepository.SaveChangesAsync(cancellationToken);
await merchantRepository.SaveChangesAsync(cancellationToken);
// 4. 返回 DTO
return MerchantMapping.ToDto(contract);
}
private long? ResolveOperatorId()
{
var id = _currentUserAccessor.UserId;
var id = currentUserAccessor.UserId;
return id == 0 ? null : id;
}
private string ResolveOperatorName()
{
var id = _currentUserAccessor.UserId;
var id = currentUserAccessor.UserId;
return id == 0 ? "system" : $"user:{id}";
}
}

View File

@@ -17,17 +17,13 @@ public sealed class CreateOrderCommandHandler(
ILogger<CreateOrderCommandHandler> logger)
: IRequestHandler<CreateOrderCommand, OrderDto>
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly IIdGenerator _idGenerator = idGenerator;
private readonly ILogger<CreateOrderCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<OrderDto> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
// 1. 构建订单
var order = new Order
{
Id = _idGenerator.NextId(),
Id = idGenerator.NextId(),
OrderNo = request.OrderNo.Trim(),
StoreId = request.StoreId,
Channel = request.Channel,
@@ -77,15 +73,17 @@ public sealed class CreateOrderCommandHandler(
}
// 4. 持久化
await _orderRepository.AddOrderAsync(order, cancellationToken);
await orderRepository.AddOrderAsync(order, cancellationToken);
if (items.Count > 0)
{
await _orderRepository.AddItemsAsync(items, cancellationToken);
await orderRepository.AddItemsAsync(items, cancellationToken);
}
await _orderRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("创建订单 {OrderNo} ({OrderId})", order.OrderNo, order.Id);
await orderRepository.SaveChangesAsync(cancellationToken);
// 5. 返回 DTO
// 5. 记录日志
logger.LogInformation("创建订单 {OrderNo} ({OrderId})", order.OrderNo, order.Id);
// 6. 返回 DTO
return MapToDto(order, items, [], []);
}

View File

@@ -15,26 +15,25 @@ public sealed class DeleteOrderCommandHandler(
ILogger<DeleteOrderCommandHandler> logger)
: IRequestHandler<DeleteOrderCommand, bool>
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly ILogger<DeleteOrderCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<bool> Handle(DeleteOrderCommand request, CancellationToken cancellationToken)
{
// 1. 校验存在性
var tenantId = _tenantProvider.GetCurrentTenantId();
var existing = await _orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken);
var tenantId = tenantProvider.GetCurrentTenantId();
var existing = await orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken);
if (existing == null)
{
return false;
}
// 2. 删除
await _orderRepository.DeleteOrderAsync(request.OrderId, tenantId, cancellationToken);
await _orderRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("删除订单 {OrderId}", request.OrderId);
await orderRepository.DeleteOrderAsync(request.OrderId, tenantId, cancellationToken);
await orderRepository.SaveChangesAsync(cancellationToken);
// 3. 记录日志
logger.LogInformation("删除订单 {OrderId}", request.OrderId);
// 4. 返回执行结果
return true;
}
}

View File

@@ -15,23 +15,25 @@ public sealed class GetOrderByIdQueryHandler(
ITenantProvider tenantProvider)
: IRequestHandler<GetOrderByIdQuery, OrderDto?>
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
public async Task<OrderDto?> Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var order = await _orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken);
// 1. 获取当前租户
var tenantId = tenantProvider.GetCurrentTenantId();
// 2. 查询订单主体
var order = await orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken);
if (order == null)
{
return null;
}
var items = await _orderRepository.GetItemsAsync(order.Id, tenantId, cancellationToken);
var histories = await _orderRepository.GetStatusHistoryAsync(order.Id, tenantId, cancellationToken);
var refunds = await _orderRepository.GetRefundsAsync(order.Id, tenantId, cancellationToken);
// 3. 查询关联明细
var items = await orderRepository.GetItemsAsync(order.Id, tenantId, cancellationToken);
var histories = await orderRepository.GetStatusHistoryAsync(order.Id, tenantId, cancellationToken);
var refunds = await orderRepository.GetRefundsAsync(order.Id, tenantId, cancellationToken);
// 4. 映射并返回
return MapToDto(order, items, histories, refunds);
}

View File

@@ -15,20 +15,20 @@ public sealed class SearchOrdersQueryHandler(
ITenantProvider tenantProvider)
: IRequestHandler<SearchOrdersQuery, PagedResult<OrderDto>>
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
public async Task<PagedResult<OrderDto>> Handle(SearchOrdersQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var orders = await _orderRepository.SearchAsync(tenantId, request.Status, request.PaymentStatus, cancellationToken);
// 1. 获取当前租户并查询订单
var tenantId = tenantProvider.GetCurrentTenantId();
var orders = await orderRepository.SearchAsync(tenantId, request.Status, request.PaymentStatus, cancellationToken);
// 2. 可选过滤:门店
if (request.StoreId.HasValue)
{
orders = orders.Where(x => x.StoreId == request.StoreId.Value).ToList();
}
// 3. 可选过滤:订单号模糊
if (!string.IsNullOrWhiteSpace(request.OrderNo))
{
var orderNo = request.OrderNo.Trim();
@@ -37,12 +37,14 @@ public sealed class SearchOrdersQueryHandler(
.ToList();
}
// 4. 排序与分页
var sorted = ApplySorting(orders, request.SortBy, request.SortDescending);
var paged = sorted
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToList();
// 5. 映射 DTO
var items = paged.Select(order => new OrderDto
{
Id = order.Id,
@@ -70,6 +72,7 @@ public sealed class SearchOrdersQueryHandler(
CreatedAt = order.CreatedAt
}).ToList();
// 6. 返回分页结果
return new PagedResult<OrderDto>(items, request.Page, request.PageSize, orders.Count);
}

View File

@@ -17,16 +17,12 @@ public sealed class UpdateOrderCommandHandler(
ILogger<UpdateOrderCommandHandler> logger)
: IRequestHandler<UpdateOrderCommand, OrderDto?>
{
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly ILogger<UpdateOrderCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<OrderDto?> Handle(UpdateOrderCommand request, CancellationToken cancellationToken)
{
// 1. 读取订单
var tenantId = _tenantProvider.GetCurrentTenantId();
var existing = await _orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken);
var tenantId = tenantProvider.GetCurrentTenantId();
var existing = await orderRepository.FindByIdAsync(request.OrderId, tenantId, cancellationToken);
if (existing == null)
{
return null;
@@ -55,14 +51,16 @@ public sealed class UpdateOrderCommandHandler(
existing.Remark = request.Remark?.Trim();
// 3. 持久化
await _orderRepository.UpdateOrderAsync(existing, cancellationToken);
await _orderRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("更新订单 {OrderNo} ({OrderId})", existing.OrderNo, existing.Id);
await orderRepository.UpdateOrderAsync(existing, cancellationToken);
await orderRepository.SaveChangesAsync(cancellationToken);
// 4. 读取关联数据并返回
var items = await _orderRepository.GetItemsAsync(existing.Id, tenantId, cancellationToken);
var histories = await _orderRepository.GetStatusHistoryAsync(existing.Id, tenantId, cancellationToken);
var refunds = await _orderRepository.GetRefundsAsync(existing.Id, tenantId, cancellationToken);
// 4. 记录更新日志
logger.LogInformation("更新订单 {OrderNo} ({OrderId})", existing.OrderNo, existing.Id);
// 5. 读取关联数据并返回
var items = await orderRepository.GetItemsAsync(existing.Id, tenantId, cancellationToken);
var histories = await orderRepository.GetStatusHistoryAsync(existing.Id, tenantId, cancellationToken);
var refunds = await orderRepository.GetRefundsAsync(existing.Id, tenantId, cancellationToken);
return MapToDto(existing, items, histories, refunds);
}

View File

@@ -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, "订阅更新失败");

View File

@@ -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,

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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, "订阅生成失败");
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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),

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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, "实名资料保存失败");
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -21,18 +21,20 @@ public sealed class DictionaryAppService(
ITenantProvider tenantProvider,
ILogger<DictionaryAppService> logger) : IDictionaryAppService
{
public async Task<DictionaryGroupDto> CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default)
{
// 1. 规范化编码并确定租户
var normalizedCode = NormalizeCode(request.Code);
var targetTenant = ResolveTargetTenant(request.Scope);
// 2. 校验编码唯一
var existing = await repository.FindGroupByCodeAsync(normalizedCode, cancellationToken);
if (existing != null)
{
throw new BusinessException(ErrorCodes.Conflict, $"字典分组编码 {normalizedCode} 已存在");
}
// 3. 构建分组实体
var group = new DictionaryGroup
{
Id = 0,
@@ -44,6 +46,7 @@ public sealed class DictionaryAppService(
IsEnabled = true
};
// 4. 持久化并返回
await repository.AddGroupAsync(group, cancellationToken);
await repository.SaveChangesAsync(cancellationToken);
logger.LogInformation("创建字典分组:{Code}({Scope})", group.Code, group.Scope);
@@ -52,13 +55,16 @@ public sealed class DictionaryAppService(
public async Task<DictionaryGroupDto> UpdateGroupAsync(long groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default)
{
// 1. 读取分组并校验权限
var group = await RequireGroupAsync(groupId, cancellationToken);
EnsureScopePermission(group.Scope);
// 2. 更新字段
group.Name = request.Name.Trim();
group.Description = request.Description?.Trim();
group.IsEnabled = request.IsEnabled;
// 3. 持久化并失效缓存
await repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken);
logger.LogInformation("更新字典分组:{GroupId}", group.Id);
@@ -67,9 +73,11 @@ public sealed class DictionaryAppService(
public async Task DeleteGroupAsync(long groupId, CancellationToken cancellationToken = default)
{
// 1. 读取分组并校验权限
var group = await RequireGroupAsync(groupId, cancellationToken);
EnsureScopePermission(group.Scope);
// 2. 删除并失效缓存
await repository.RemoveGroupAsync(group, cancellationToken);
await repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken);
@@ -78,10 +86,12 @@ public sealed class DictionaryAppService(
public async Task<IReadOnlyList<DictionaryGroupDto>> SearchGroupsAsync(DictionaryGroupQuery request, CancellationToken cancellationToken = default)
{
// 1. 确定查询范围并校验权限
var tenantId = tenantProvider.GetCurrentTenantId();
var scope = ResolveScopeForQuery(request.Scope, tenantId);
EnsureScopePermission(scope);
// 2. 查询分组及可选项
var groups = await repository.SearchGroupsAsync(scope, cancellationToken);
var includeItems = request.IncludeItems;
var result = new List<DictionaryGroupDto>(groups.Count);
@@ -91,6 +101,7 @@ public sealed class DictionaryAppService(
IReadOnlyList<DictionaryItemDto> items = Array.Empty<DictionaryItemDto>();
if (includeItems)
{
// 查询分组下字典项
var itemEntities = await repository.GetItemsByGroupIdAsync(group.Id, cancellationToken);
items = itemEntities.Select(MapItem).ToList();
}
@@ -103,9 +114,11 @@ public sealed class DictionaryAppService(
public async Task<DictionaryItemDto> CreateItemAsync(CreateDictionaryItemRequest request, CancellationToken cancellationToken = default)
{
// 1. 校验分组与权限
var group = await RequireGroupAsync(request.GroupId, cancellationToken);
EnsureScopePermission(group.Scope);
// 2. 构建字典项
var item = new DictionaryItem
{
Id = 0,
@@ -119,6 +132,7 @@ public sealed class DictionaryAppService(
IsEnabled = request.IsEnabled
};
// 3. 持久化并失效缓存
await repository.AddItemAsync(item, cancellationToken);
await repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken);
@@ -128,16 +142,19 @@ public sealed class DictionaryAppService(
public async Task<DictionaryItemDto> UpdateItemAsync(long itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default)
{
// 1. 读取字典项与分组并校验权限
var item = await RequireItemAsync(itemId, cancellationToken);
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
EnsureScopePermission(group.Scope);
// 2. 更新字段
item.Value = request.Value.Trim();
item.Description = request.Description?.Trim();
item.SortOrder = request.SortOrder;
item.IsDefault = request.IsDefault;
item.IsEnabled = request.IsEnabled;
// 3. 持久化并失效缓存
await repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken);
logger.LogInformation("更新字典项:{ItemId}", item.Id);
@@ -146,10 +163,12 @@ public sealed class DictionaryAppService(
public async Task DeleteItemAsync(long itemId, CancellationToken cancellationToken = default)
{
// 1. 读取字典项与分组并校验权限
var item = await RequireItemAsync(itemId, cancellationToken);
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
EnsureScopePermission(group.Scope);
// 2. 删除并失效缓存
await repository.RemoveItemAsync(item, cancellationToken);
await repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken);
@@ -158,6 +177,7 @@ public sealed class DictionaryAppService(
public async Task<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>> GetCachedItemsAsync(DictionaryBatchQueryRequest request, CancellationToken cancellationToken = default)
{
// 1. 规范化编码
var normalizedCodes = request.Codes
.Where(code => !string.IsNullOrWhiteSpace(code))
.Select(NormalizeCode)
@@ -169,6 +189,7 @@ public sealed class DictionaryAppService(
return new Dictionary<string, IReadOnlyList<DictionaryItemDto>>(StringComparer.OrdinalIgnoreCase);
}
// 2. 按租户合并系统与业务字典
var tenantId = tenantProvider.GetCurrentTenantId();
var result = new Dictionary<string, IReadOnlyList<DictionaryItemDto>>(StringComparer.OrdinalIgnoreCase);
@@ -190,6 +211,7 @@ public sealed class DictionaryAppService(
private async Task<DictionaryGroup> RequireGroupAsync(long groupId, CancellationToken cancellationToken)
{
// 1. 读取分组,找不到抛异常
var group = await repository.FindGroupByIdAsync(groupId, cancellationToken);
if (group == null)
{
@@ -201,6 +223,7 @@ public sealed class DictionaryAppService(
private async Task<DictionaryItem> RequireItemAsync(long itemId, CancellationToken cancellationToken)
{
// 1. 读取字典项,找不到抛异常
var item = await repository.FindItemByIdAsync(itemId, cancellationToken);
if (item == null)
{
@@ -269,12 +292,14 @@ public sealed class DictionaryAppService(
private async Task<IReadOnlyList<DictionaryItemDto>> GetOrLoadCacheAsync(long tenantId, string code, CancellationToken cancellationToken)
{
// 1. 先查缓存
var cached = await cache.GetAsync(tenantId, code, cancellationToken);
if (cached != null)
{
return cached;
}
// 2. 从仓储加载并写入缓存
var entities = await repository.GetItemsByCodesAsync(new[] { code }, tenantId, includeSystem: false, cancellationToken);
var items = entities
.Where(item => item.IsEnabled && (item.Group?.IsEnabled ?? true))

View File

@@ -15,9 +15,14 @@ public sealed class AssignUserRolesCommandHandler(
{
public async Task<bool> Handle(AssignUserRolesCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
// 2. 覆盖式绑定角色
await userRoleRepository.ReplaceUserRolesAsync(tenantId, request.UserId, request.RoleIds, cancellationToken);
await userRoleRepository.SaveChangesAsync(cancellationToken);
// 3. 返回执行结果
return true;
}
}

View File

@@ -15,9 +15,14 @@ public sealed class BindRolePermissionsCommandHandler(
{
public async Task<bool> Handle(BindRolePermissionsCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
// 2. 覆盖式绑定权限
await rolePermissionRepository.ReplaceRolePermissionsAsync(tenantId, request.RoleId, request.PermissionIds, cancellationToken);
await rolePermissionRepository.SaveChangesAsync(cancellationToken);
// 3. 返回执行结果
return true;
}
}

View File

@@ -26,6 +26,7 @@ public sealed class CopyRoleTemplateCommandHandler(
/// <inheritdoc />
public async Task<RoleDto> Handle(CopyRoleTemplateCommand request, CancellationToken cancellationToken)
{
// 1. 查询模板与模板权限
var template = await roleTemplateRepository.FindByCodeAsync(request.TemplateCode, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {request.TemplateCode} 不存在");
@@ -36,6 +37,7 @@ public sealed class CopyRoleTemplateCommandHandler(
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
// 2. 计算角色名称/编码与描述
var tenantId = tenantProvider.GetCurrentTenantId();
var roleCode = string.IsNullOrWhiteSpace(request.RoleCode) ? template.TemplateCode : request.RoleCode.Trim();
var roleName = string.IsNullOrWhiteSpace(request.RoleName) ? template.Name : request.RoleName.Trim();
@@ -69,7 +71,7 @@ public sealed class CopyRoleTemplateCommandHandler(
await roleRepository.UpdateAsync(role, cancellationToken);
}
// 2. 确保模板权限全部存在,不存在则按模板定义创建。
// 3. 确保模板权限全部存在,不存在则按模板定义创建。
var existingPermissions = await permissionRepository.GetByCodesAsync(tenantId, permissionCodes, cancellationToken);
var permissionMap = existingPermissions.ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase);
@@ -94,7 +96,7 @@ public sealed class CopyRoleTemplateCommandHandler(
await roleRepository.SaveChangesAsync(cancellationToken);
// 3. 绑定缺失的权限,保留租户自定义的已有授权。
// 4. 绑定缺失的权限,保留租户自定义的已有授权。
var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(tenantId, new[] { role.Id }, cancellationToken);
var existingPermissionIds = rolePermissions
.Select(x => x.PermissionId)

View File

@@ -17,7 +17,10 @@ public sealed class CreatePermissionCommandHandler(
{
public async Task<PermissionDto> Handle(CreatePermissionCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
// 2. 构建权限实体
var permission = new Permission
{
TenantId = tenantId,
@@ -26,9 +29,11 @@ public sealed class CreatePermissionCommandHandler(
Description = request.Description
};
// 3. 持久化
await permissionRepository.AddAsync(permission, cancellationToken);
await permissionRepository.SaveChangesAsync(cancellationToken);
// 4. 返回 DTO
return new PermissionDto
{
Id = permission.Id,

View File

@@ -17,7 +17,10 @@ public sealed class CreateRoleCommandHandler(
{
public async Task<RoleDto> Handle(CreateRoleCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
// 2. 构建角色实体
var role = new Role
{
TenantId = tenantId,
@@ -26,9 +29,11 @@ public sealed class CreateRoleCommandHandler(
Description = request.Description
};
// 3. 持久化
await roleRepository.AddAsync(role, cancellationToken);
await roleRepository.SaveChangesAsync(cancellationToken);
// 4. 返回 DTO
return new RoleDto
{
Id = role.Id,

View File

@@ -19,17 +19,20 @@ public sealed class CreateRoleTemplateCommandHandler(IRoleTemplateRepository rol
/// <inheritdoc />
public async Task<RoleTemplateDto> Handle(CreateRoleTemplateCommand request, CancellationToken cancellationToken)
{
// 1. 校验必填
if (string.IsNullOrWhiteSpace(request.TemplateCode) || string.IsNullOrWhiteSpace(request.Name))
{
throw new BusinessException(ErrorCodes.BadRequest, "模板编码与名称不能为空");
}
// 2. 检查编码唯一
var existing = await roleTemplateRepository.FindByCodeAsync(request.TemplateCode, cancellationToken);
if (existing != null)
{
throw new BusinessException(ErrorCodes.Conflict, $"模板编码 {request.TemplateCode} 已存在");
}
// 3. 构建模板实体
var template = new RoleTemplate
{
TemplateCode = request.TemplateCode.Trim(),
@@ -38,12 +41,14 @@ public sealed class CreateRoleTemplateCommandHandler(IRoleTemplateRepository rol
IsActive = request.IsActive
};
// 4. 清洗权限编码
var permissions = request.PermissionCodes
.Where(code => !string.IsNullOrWhiteSpace(code))
.Select(code => code.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
// 5. 持久化并返回 DTO
await roleTemplateRepository.AddAsync(template, permissions, cancellationToken);
await roleTemplateRepository.SaveChangesAsync(cancellationToken);

View File

@@ -15,9 +15,14 @@ public sealed class DeletePermissionCommandHandler(
{
public async Task<bool> Handle(DeletePermissionCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
// 2. 删除权限
await permissionRepository.DeleteAsync(request.PermissionId, tenantId, cancellationToken);
await permissionRepository.SaveChangesAsync(cancellationToken);
// 3. 返回执行结果
return true;
}
}

View File

@@ -15,9 +15,14 @@ public sealed class DeleteRoleCommandHandler(
{
public async Task<bool> Handle(DeleteRoleCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
// 2. 删除角色
await roleRepository.DeleteAsync(request.RoleId, tenantId, cancellationToken);
await roleRepository.SaveChangesAsync(cancellationToken);
// 3. 返回执行结果
return true;
}
}

View File

@@ -12,14 +12,18 @@ public sealed class DeleteRoleTemplateCommandHandler(IRoleTemplateRepository rol
{
public async Task<bool> Handle(DeleteRoleTemplateCommand request, CancellationToken cancellationToken)
{
// 1. 查询模板
var template = await roleTemplateRepository.FindByCodeAsync(request.TemplateCode, cancellationToken);
if (template == null)
{
return false;
}
// 2. 删除并保存
await roleTemplateRepository.DeleteAsync(template.Id, cancellationToken);
await roleTemplateRepository.SaveChangesAsync(cancellationToken);
// 3. 返回执行结果
return true;
}
}

View File

@@ -15,14 +15,18 @@ public sealed class GetRoleTemplateQueryHandler(IRoleTemplateRepository roleTemp
/// <inheritdoc />
public async Task<RoleTemplateDto?> Handle(GetRoleTemplateQuery request, CancellationToken cancellationToken)
{
// 1. 查询模板
var template = await roleTemplateRepository.FindByCodeAsync(request.TemplateCode, cancellationToken);
if (template == null)
{
return null;
}
// 2. 查询模板权限
var permissions = await roleTemplateRepository.GetPermissionsAsync(template.Id, cancellationToken);
var codes = permissions.Select(x => x.PermissionCode).ToArray();
// 3. 返回 DTO
return TemplateMapper.ToDto(template, codes);
}
}

View File

@@ -20,26 +20,22 @@ public sealed class GetUserPermissionsQueryHandler(
ITenantProvider tenantProvider)
: IRequestHandler<GetUserPermissionsQuery, UserPermissionDto?>
{
private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository;
private readonly IUserRoleRepository _userRoleRepository = userRoleRepository;
private readonly IRoleRepository _roleRepository = roleRepository;
private readonly IPermissionRepository _permissionRepository = permissionRepository;
private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
public async Task<UserPermissionDto?> Handle(GetUserPermissionsQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var user = await _identityUserRepository.FindByIdAsync(request.UserId, cancellationToken);
// 1. 获取租户并查询用户
var tenantId = tenantProvider.GetCurrentTenantId();
var user = await identityUserRepository.FindByIdAsync(request.UserId, cancellationToken);
if (user == null || user.TenantId != tenantId)
{
return null;
}
// 2. 解析角色与权限
var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken);
var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken);
// 3. 返回用户权限概览
return new UserPermissionDto
{
UserId = user.Id,
@@ -55,34 +51,39 @@ public sealed class GetUserPermissionsQueryHandler(
private async Task<string[]> ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken)
{
var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
// 1. 查询用户角色关系
var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
if (roleIds.Length == 0)
{
return Array.Empty<string>();
}
var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
// 2. 查询角色编码
var roles = await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
private async Task<string[]> ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken)
{
var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
// 1. 查询用户角色关系
var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
if (roleIds.Length == 0)
{
return Array.Empty<string>();
}
var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
// 2. 查询角色-权限关系
var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
if (permissionIds.Length == 0)
{
return Array.Empty<string>();
}
var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
// 3. 查询权限编码
var permissions = await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
}

View File

@@ -16,9 +16,11 @@ public sealed class ListRoleTemplatesQueryHandler(IRoleTemplateRepository roleTe
/// <inheritdoc />
public async Task<IReadOnlyList<RoleTemplateDto>> Handle(ListRoleTemplatesQuery request, CancellationToken cancellationToken)
{
// 1. 查询模板与权限映射
var templates = await roleTemplateRepository.GetAllAsync(request.IsActive, cancellationToken);
var permissionsMap = await roleTemplateRepository.GetPermissionsAsync(templates.Select(t => t.Id), cancellationToken);
// 2. 排序并映射 DTO
var dtos = templates
.OrderBy(template => template.TemplateCode, StringComparer.OrdinalIgnoreCase)
.Select(template =>
@@ -30,6 +32,7 @@ public sealed class ListRoleTemplatesQueryHandler(IRoleTemplateRepository roleTe
})
.ToArray();
// 3. 返回结果
return dtos;
}
}

View File

@@ -19,9 +19,11 @@ public sealed class SearchPermissionsQueryHandler(
{
public async Task<PagedResult<PermissionDto>> Handle(SearchPermissionsQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文并查询权限
var tenantId = tenantProvider.GetCurrentTenantId();
var permissions = await permissionRepository.SearchAsync(tenantId, request.Keyword, cancellationToken);
// 2. 排序
var sorted = request.SortBy?.ToLowerInvariant() switch
{
"name" => request.SortDescending
@@ -35,11 +37,13 @@ public sealed class SearchPermissionsQueryHandler(
: permissions.OrderBy(x => x.CreatedAt)
};
// 3. 分页
var paged = sorted
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToList();
// 4. 映射 DTO
var items = paged.Select(permission => new PermissionDto
{
Id = permission.Id,
@@ -49,6 +53,7 @@ public sealed class SearchPermissionsQueryHandler(
Description = permission.Description
}).ToList();
// 5. 返回分页结果
return new PagedResult<PermissionDto>(items, request.Page, request.PageSize, permissions.Count);
}
}

View File

@@ -19,9 +19,11 @@ public sealed class SearchRolesQueryHandler(
{
public async Task<PagedResult<RoleDto>> Handle(SearchRolesQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文并查询角色
var tenantId = tenantProvider.GetCurrentTenantId();
var roles = await roleRepository.SearchAsync(tenantId, request.Keyword, cancellationToken);
// 2. 排序
var sorted = request.SortBy?.ToLowerInvariant() switch
{
"name" => request.SortDescending
@@ -32,11 +34,13 @@ public sealed class SearchRolesQueryHandler(
: roles.OrderBy(x => x.CreatedAt)
};
// 3. 分页
var paged = sorted
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToList();
// 4. 映射 DTO
var items = paged.Select(role => new RoleDto
{
Id = role.Id,
@@ -46,6 +50,7 @@ public sealed class SearchRolesQueryHandler(
Description = role.Description
}).ToList();
// 5. 返回分页结果
return new PagedResult<RoleDto>(items, request.Page, request.PageSize, roles.Count);
}
}

View File

@@ -22,25 +22,21 @@ public sealed class SearchUserPermissionsQueryHandler(
ITenantProvider tenantProvider)
: IRequestHandler<SearchUserPermissionsQuery, PagedResult<UserPermissionDto>>
{
private readonly IIdentityUserRepository _identityUserRepository = identityUserRepository;
private readonly IUserRoleRepository _userRoleRepository = userRoleRepository;
private readonly IRoleRepository _roleRepository = roleRepository;
private readonly IPermissionRepository _permissionRepository = permissionRepository;
private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
public async Task<PagedResult<UserPermissionDto>> Handle(SearchUserPermissionsQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var users = await _identityUserRepository.SearchAsync(tenantId, request.Keyword, cancellationToken);
// 1. 获取租户并查询用户
var tenantId = tenantProvider.GetCurrentTenantId();
var users = await identityUserRepository.SearchAsync(tenantId, request.Keyword, cancellationToken);
// 2. 排序与分页
var sorted = SortUsers(users, request.SortBy, request.SortDescending);
var paged = sorted
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToList();
// 3. 解析角色与权限
var resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken);
var items = paged.Select(user => new UserPermissionDto
{
@@ -81,23 +77,27 @@ public sealed class SearchUserPermissionsQueryHandler(
IReadOnlyCollection<Domain.Identity.Entities.IdentityUser> users,
CancellationToken cancellationToken)
{
// 1. 查询用户角色关系
var userIds = users.Select(x => x.Id).ToArray();
var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
var userRoleRelations = await userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray();
// 2. 查询角色信息
var roles = roleIds.Length == 0
? Array.Empty<Domain.Identity.Entities.Role>()
: await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
: await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer<long>.Default);
// 3. 查询角色-权限关系
var rolePermissions = roleIds.Length == 0
? Array.Empty<Domain.Identity.Entities.RolePermission>()
: await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
: await rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
// 4. 查询权限详情
var permissions = permissionIds.Length == 0
? Array.Empty<Domain.Identity.Entities.Permission>()
: await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
: await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
var rolePermissionsLookup = rolePermissions
@@ -107,6 +107,7 @@ public sealed class SearchUserPermissionsQueryHandler(
var result = new Dictionary<long, (string[] roles, string[] permissions)>();
foreach (var userId in userIds)
{
// 5. 聚合用户角色与权限编码
var rolesForUser = userRoleRelations.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).Distinct().ToArray();
var roleCodes = rolesForUser
.Select(rid => roleCodeMap.GetValueOrDefault(rid))

View File

@@ -16,6 +16,7 @@ public sealed class UpdatePermissionCommandHandler(
{
public async Task<PermissionDto?> Handle(UpdatePermissionCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文并查询权限
var tenantId = tenantProvider.GetCurrentTenantId();
var permission = await permissionRepository.FindByIdAsync(request.PermissionId, tenantId, cancellationToken);
if (permission == null)
@@ -23,12 +24,15 @@ public sealed class UpdatePermissionCommandHandler(
return null;
}
// 2. 更新字段
permission.Name = request.Name;
permission.Description = request.Description;
// 3. 持久化
await permissionRepository.UpdateAsync(permission, cancellationToken);
await permissionRepository.SaveChangesAsync(cancellationToken);
// 4. 返回 DTO
return new PermissionDto
{
Id = permission.Id,

View File

@@ -16,6 +16,7 @@ public sealed class UpdateRoleCommandHandler(
{
public async Task<RoleDto?> Handle(UpdateRoleCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文并查询角色
var tenantId = tenantProvider.GetCurrentTenantId();
var role = await roleRepository.FindByIdAsync(request.RoleId, tenantId, cancellationToken);
if (role == null)
@@ -23,12 +24,15 @@ public sealed class UpdateRoleCommandHandler(
return null;
}
// 2. 更新字段
role.Name = request.Name;
role.Description = request.Description;
// 3. 持久化
await roleRepository.UpdateAsync(role, cancellationToken);
await roleRepository.SaveChangesAsync(cancellationToken);
// 4. 返回 DTO
return new RoleDto
{
Id = role.Id,

View File

@@ -10,8 +10,6 @@ namespace TakeoutSaaS.Application.Sms.Contracts;
/// </remarks>
public sealed class SendVerificationCodeRequest(string phoneNumber, string scene, SmsProviderKind? provider = null)
{
/// <summary>
/// 手机号(支持 +86 前缀或纯 11 位)。
/// </summary>

View File

@@ -8,8 +8,6 @@ namespace TakeoutSaaS.Application.Sms.Contracts;
/// </remarks>
public sealed class VerifyVerificationCodeRequest(string phoneNumber, string scene, string code)
{
/// <summary>
/// 手机号。
/// </summary>

View File

@@ -30,6 +30,7 @@ public sealed class VerificationCodeService(
/// <inheritdoc />
public async Task<SendVerificationCodeResponse> SendAsync(SendVerificationCodeRequest request, CancellationToken cancellationToken = default)
{
// 1. 参数校验
if (string.IsNullOrWhiteSpace(request.PhoneNumber))
{
throw new BusinessException(ErrorCodes.BadRequest, "手机号不能为空");
@@ -40,6 +41,7 @@ public sealed class VerificationCodeService(
throw new BusinessException(ErrorCodes.BadRequest, "场景不能为空");
}
// 2. 解析模板与缓存键
var smsOptions = smsOptionsMonitor.CurrentValue;
var codeOptions = codeOptionsMonitor.CurrentValue;
var templateCode = ResolveTemplate(request.Scene, smsOptions);
@@ -48,8 +50,10 @@ public sealed class VerificationCodeService(
var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}";
var cooldownKey = $"{cacheKey}:cooldown";
// 3. 检查冷却期
await EnsureCooldownAsync(cooldownKey, codeOptions.CooldownSeconds, cancellationToken).ConfigureAwait(false);
// 4. 生成验证码并发送短信
var code = GenerateCode(codeOptions.CodeLength);
var variables = new Dictionary<string, string> { { "code", code } };
var sender = senderResolver.Resolve(request.Provider);
@@ -61,6 +65,7 @@ public sealed class VerificationCodeService(
throw new BusinessException(ErrorCodes.InternalServerError, $"短信发送失败:{smsResult.Message}");
}
// 5. 写入验证码与冷却缓存
var expiresAt = DateTimeOffset.UtcNow.AddMinutes(codeOptions.ExpireMinutes);
await cache.SetStringAsync(cacheKey, code, new DistributedCacheEntryOptions
{
@@ -83,11 +88,13 @@ public sealed class VerificationCodeService(
/// <inheritdoc />
public async Task<bool> VerifyAsync(VerifyVerificationCodeRequest request, CancellationToken cancellationToken = default)
{
// 1. 基础校验
if (string.IsNullOrWhiteSpace(request.Code))
{
return false;
}
// 2. 读取验证码
var codeOptions = codeOptionsMonitor.CurrentValue;
var phone = NormalizePhoneNumber(request.PhoneNumber);
var tenantKey = tenantProvider.GetCurrentTenantId() == 0 ? "platform" : tenantProvider.GetCurrentTenantId().ToString();
@@ -99,6 +106,7 @@ public sealed class VerificationCodeService(
return false;
}
// 3. 比对成功后清除缓存
var success = string.Equals(cachedCode, request.Code, StringComparison.Ordinal);
if (success)
{

View File

@@ -10,8 +10,6 @@ namespace TakeoutSaaS.Application.Storage.Contracts;
/// </remarks>
public sealed class DirectUploadRequest(UploadFileType fileType, string fileName, string contentType, long contentLength, string? requestOrigin)
{
/// <summary>
/// 文件类型。
/// </summary>

View File

@@ -17,8 +17,6 @@ public sealed class UploadFileRequest(
long contentLength,
string? requestOrigin)
{
/// <summary>
/// 文件分类。
/// </summary>

View File

@@ -35,11 +35,13 @@ public sealed class FileStorageService(
/// <inheritdoc />
public async Task<FileUploadResponse> UploadAsync(UploadFileRequest request, CancellationToken cancellationToken = default)
{
// 1. 校验请求
if (request is null)
{
throw new BusinessException(ErrorCodes.BadRequest, "上传请求不能为空");
}
// 2. 读取安全配置并校验来源/大小/类型
var options = optionsMonitor.CurrentValue;
var security = options.Security;
ValidateOrigin(request.RequestOrigin, security);
@@ -50,15 +52,18 @@ public sealed class FileStorageService(
var contentType = NormalizeContentType(request.ContentType, extension);
ResetStream(request.Content);
// 3. 生成对象键与元数据
var objectKey = BuildObjectKey(request.FileType, extension);
var metadata = BuildMetadata(request.FileType);
var expires = TimeSpan.FromMinutes(Math.Max(1, security.DefaultUrlExpirationMinutes));
var provider = providerResolver.Resolve();
// 4. 上传到对象存储
var uploadResult = await provider.UploadAsync(
new StorageUploadRequest(objectKey, request.Content, contentType, request.ContentLength, true, expires, metadata),
cancellationToken).ConfigureAwait(false);
// 5. 追加防盗链签名并返回
var finalUrl = AppendAntiLeechToken(uploadResult.SignedUrl ?? uploadResult.Url, objectKey, expires, security);
logger.LogInformation("文件上传成功:{ObjectKey} ({Size} bytes)", objectKey, request.ContentLength);
@@ -73,11 +78,13 @@ public sealed class FileStorageService(
/// <inheritdoc />
public async Task<DirectUploadResponse> CreateDirectUploadAsync(DirectUploadRequest request, CancellationToken cancellationToken = default)
{
// 1. 校验请求
if (request is null)
{
throw new BusinessException(ErrorCodes.BadRequest, "直传请求不能为空");
}
// 2. 校验来源/大小/类型
var options = optionsMonitor.CurrentValue;
var security = options.Security;
ValidateOrigin(request.RequestOrigin, security);
@@ -87,14 +94,17 @@ public sealed class FileStorageService(
ValidateExtension(request.FileType, extension, security);
var contentType = NormalizeContentType(request.ContentType, extension);
// 3. 构建直传参数
var objectKey = BuildObjectKey(request.FileType, extension);
var provider = providerResolver.Resolve();
var expires = TimeSpan.FromMinutes(Math.Max(1, security.DefaultUrlExpirationMinutes));
// 4. 向存储获取直传凭证
var directResult = await provider.CreateDirectUploadAsync(
new StorageDirectUploadRequest(objectKey, contentType, request.ContentLength, expires),
cancellationToken).ConfigureAwait(false);
// 5. 构造直传结果并追加防盗链
var finalDownloadUrl = directResult.SignedDownloadUrl != null
? AppendAntiLeechToken(directResult.SignedDownloadUrl, objectKey, expires, security)
: null;