feat: 商户模块移除租户上下文依赖

This commit is contained in:
2026-01-29 13:52:36 +00:00
parent bb3bb842bc
commit 4dc5b067eb
31 changed files with 218 additions and 268 deletions

View File

@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -22,15 +23,18 @@ public sealed class MerchantCategoriesController(IMediator mediator) : BaseApiCo
/// <summary>
/// 列出所有类目。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>类目列表。</returns>
[HttpGet]
[PermissionAuthorize("merchant_category:read")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantCategoryDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<MerchantCategoryDto>>> List(CancellationToken cancellationToken)
public async Task<ApiResponse<IReadOnlyList<MerchantCategoryDto>>> List(
[FromQuery, Range(1, long.MaxValue)] long tenantId,
CancellationToken cancellationToken)
{
// 1. 查询所有类目
var result = await mediator.Send(new ListMerchantCategoriesQuery(), cancellationToken);
var result = await mediator.Send(new ListMerchantCategoriesQuery { TenantId = tenantId }, cancellationToken);
// 2. 返回类目列表
return ApiResponse<IReadOnlyList<MerchantCategoryDto>>.Ok(result);
@@ -39,15 +43,20 @@ public sealed class MerchantCategoriesController(IMediator mediator) : BaseApiCo
/// <summary>
/// 新增类目。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="command">创建命令。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>创建的类目。</returns>
[HttpPost]
[PermissionAuthorize("merchant_category:create")]
[ProducesResponseType(typeof(ApiResponse<MerchantCategoryDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<MerchantCategoryDto>> Create([FromBody] CreateMerchantCategoryCommand command, CancellationToken cancellationToken)
public async Task<ApiResponse<MerchantCategoryDto>> Create(
[FromQuery, Range(1, long.MaxValue)] long tenantId,
[FromBody] CreateMerchantCategoryCommand command,
CancellationToken cancellationToken)
{
// 1. 创建类目
command = command with { TenantId = tenantId };
var result = await mediator.Send(command, cancellationToken);
// 2. 返回创建结果
@@ -57,6 +66,7 @@ public sealed class MerchantCategoriesController(IMediator mediator) : BaseApiCo
/// <summary>
/// 删除类目。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="categoryId">类目 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>删除结果,未找到则返回错误。</returns>
@@ -64,10 +74,13 @@ public sealed class MerchantCategoriesController(IMediator mediator) : BaseApiCo
[PermissionAuthorize("merchant_category:delete")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<object>> Delete(long categoryId, CancellationToken cancellationToken)
public async Task<ApiResponse<object>> Delete(
[FromQuery, Range(1, long.MaxValue)] long tenantId,
long categoryId,
CancellationToken cancellationToken)
{
// 1. 执行删除
var success = await mediator.Send(new DeleteMerchantCategoryCommand(categoryId), cancellationToken);
var success = await mediator.Send(new DeleteMerchantCategoryCommand(tenantId, categoryId), cancellationToken);
// 2. 返回删除结果或 404
return success
@@ -78,15 +91,20 @@ public sealed class MerchantCategoriesController(IMediator mediator) : BaseApiCo
/// <summary>
/// 批量调整类目排序。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="command">排序命令。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>执行结果。</returns>
[HttpPost("reorder")]
[PermissionAuthorize("merchant_category:update")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
public async Task<ApiResponse<object>> Reorder([FromBody] ReorderMerchantCategoriesCommand command, CancellationToken cancellationToken)
public async Task<ApiResponse<object>> Reorder(
[FromQuery, Range(1, long.MaxValue)] long tenantId,
[FromBody] ReorderMerchantCategoriesCommand command,
CancellationToken cancellationToken)
{
// 1. 执行排序调整
command = command with { TenantId = tenantId };
await mediator.Send(command, cancellationToken);
// 2. 返回成功结果

View File

@@ -2,6 +2,7 @@ using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using System.ComponentModel.DataAnnotations;
using TakeoutSaaS.Application.App.Merchants.Commands;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
@@ -436,10 +437,12 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
[HttpGet("categories")]
[PermissionAuthorize("merchant:read")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<string>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<string>>> Categories(CancellationToken cancellationToken)
public async Task<ApiResponse<IReadOnlyList<string>>> Categories(
[FromQuery, Range(1, long.MaxValue)] long tenantId,
CancellationToken cancellationToken)
{
// 1. 查询可选类目
var result = await mediator.Send(new GetMerchantCategoriesQuery(), cancellationToken);
var result = await mediator.Send(new GetMerchantCategoriesQuery(tenantId), cancellationToken);
// 2. 返回类目列表
return ApiResponse<IReadOnlyList<string>>.Ok(result);

View File

@@ -7,7 +7,27 @@ namespace TakeoutSaaS.Application.App.Merchants.Commands;
/// <summary>
/// 新增商户类目。
/// </summary>
public sealed record CreateMerchantCategoryCommand(
[property: Required, MaxLength(64)] string Name,
int? DisplayOrder,
bool IsActive = true) : IRequest<MerchantCategoryDto>;
public sealed record CreateMerchantCategoryCommand : IRequest<MerchantCategoryDto>
{
/// <summary>
/// 租户 ID。
/// </summary>
[Range(1, long.MaxValue)]
public long TenantId { get; init; }
/// <summary>
/// 类目名称。
/// </summary>
[Required, MaxLength(64)]
public string Name { get; init; } = string.Empty;
/// <summary>
/// 排序(为空则自动追加到末尾)。
/// </summary>
public int? DisplayOrder { get; init; }
/// <summary>
/// 是否启用。
/// </summary>
public bool IsActive { get; init; } = true;
}

View File

@@ -10,6 +10,12 @@ namespace TakeoutSaaS.Application.App.Merchants.Commands;
/// </summary>
public sealed class CreateMerchantCommand : IRequest<MerchantDto>
{
/// <summary>
/// 所属租户 ID。
/// </summary>
[Range(1, long.MaxValue)]
public long TenantId { get; init; }
/// <summary>
/// 品牌名称。
/// </summary>

View File

@@ -6,4 +6,6 @@ namespace TakeoutSaaS.Application.App.Merchants.Commands;
/// <summary>
/// 删除商户类目。
/// </summary>
public sealed record DeleteMerchantCategoryCommand([property: Required] long CategoryId) : IRequest<bool>;
public sealed record DeleteMerchantCategoryCommand(
[property: Range(1, long.MaxValue)] long TenantId,
[property: Required] long CategoryId) : IRequest<bool>;

View File

@@ -6,8 +6,20 @@ namespace TakeoutSaaS.Application.App.Merchants.Commands;
/// <summary>
/// 调整类目排序。
/// </summary>
public sealed record ReorderMerchantCategoriesCommand(
[property: Required, MinLength(1)] IReadOnlyList<MerchantCategoryOrderItem> Items) : IRequest<bool>;
public sealed record ReorderMerchantCategoriesCommand : IRequest<bool>
{
/// <summary>
/// 租户 ID。
/// </summary>
[Range(1, long.MaxValue)]
public long TenantId { get; init; }
/// <summary>
/// 排序条目。
/// </summary>
[Required, MinLength(1)]
public IReadOnlyList<MerchantCategoryOrderItem> Items { get; init; } = [];
}
/// <summary>
/// 类目排序条目。

View File

@@ -8,7 +8,6 @@ using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Ids;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -17,7 +16,6 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// </summary>
public sealed class AddMerchantDocumentCommandHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider,
IIdGenerator idGenerator,
ICurrentUserAccessor currentUserAccessor)
: IRequestHandler<AddMerchantDocumentCommand, MerchantDocumentDto>
@@ -30,15 +28,16 @@ public sealed class AddMerchantDocumentCommandHandler(
/// <returns>证照 DTO。</returns>
public async Task<MerchantDocumentDto> Handle(AddMerchantDocumentCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户并查询商户
var tenantId = tenantProvider.GetCurrentTenantId();
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
// 1. 查询商户并解析租
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
var tenantId = merchant.TenantId;
// 2. 构建证照记录
// 2. (空行后) 构建证照记录并写入租户
var document = new MerchantDocument
{
Id = idGenerator.NextId(),
TenantId = tenantId,
MerchantId = merchant.Id,
DocumentType = request.DocumentType,
Status = MerchantDocumentStatus.Pending,
@@ -48,7 +47,7 @@ public sealed class AddMerchantDocumentCommandHandler(
ExpiresAt = request.ExpiresAt
};
// 3. 持久化与审计
// 3. (空行后) 持久化与审计
await merchantRepository.AddDocumentAsync(document, cancellationToken);
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
{

View File

@@ -5,7 +5,6 @@ using TakeoutSaaS.Domain.Merchants.Entities;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -13,8 +12,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// 创建类目处理器。
/// </summary>
public sealed class CreateMerchantCategoryCommandHandler(
IMerchantCategoryRepository categoryRepository,
ITenantProvider tenantProvider)
IMerchantCategoryRepository categoryRepository)
: IRequestHandler<CreateMerchantCategoryCommand, MerchantCategoryDto>
{
/// <summary>
@@ -25,29 +23,30 @@ public sealed class CreateMerchantCategoryCommandHandler(
/// <returns>类目 DTO。</returns>
public async Task<MerchantCategoryDto> Handle(CreateMerchantCategoryCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
// 1. 解析租户与规范化名称
var tenantId = request.TenantId;
var normalizedName = request.Name.Trim();
// 2. 检查重名
// 2. (空行后) 检查重名
if (await categoryRepository.ExistsAsync(normalizedName, tenantId, cancellationToken))
{
throw new BusinessException(ErrorCodes.Conflict, $"类目“{normalizedName}”已存在");
}
// 3. 计算排序
// 3. (空行后) 计算排序
var categories = await categoryRepository.ListAsync(tenantId, cancellationToken);
var targetOrder = request.DisplayOrder ?? (categories.Count == 0 ? 1 : categories.Max(x => x.DisplayOrder) + 1);
// 4. 构建实体
// 4. (空行后) 构建实体并写入租户
var entity = new MerchantCategory
{
TenantId = tenantId,
Name = normalizedName,
DisplayOrder = targetOrder,
IsActive = request.IsActive
};
// 5. 持久化并返回
// 5. (空行后) 持久化并返回
await categoryRepository.AddAsync(entity, cancellationToken);
await categoryRepository.SaveChangesAsync(cancellationToken);

View File

@@ -20,6 +20,7 @@ public sealed class CreateMerchantCommandHandler(IMerchantRepository merchantRep
// 1. 构建商户实体
var merchant = new Merchant
{
TenantId = request.TenantId,
BrandName = request.BrandName.Trim(),
BrandAlias = request.BrandAlias?.Trim(),
LogoUrl = request.LogoUrl?.Trim(),

View File

@@ -8,7 +8,6 @@ using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Ids;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -17,7 +16,6 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// </summary>
public sealed class CreateMerchantContractCommandHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider,
IIdGenerator idGenerator,
ICurrentUserAccessor currentUserAccessor)
: IRequestHandler<CreateMerchantContractCommand, MerchantContractDto>
@@ -36,15 +34,16 @@ public sealed class CreateMerchantContractCommandHandler(
throw new BusinessException(ErrorCodes.BadRequest, "合同结束时间必须晚于开始时间");
}
// 2. 查询商户
var tenantId = tenantProvider.GetCurrentTenantId();
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
// 2. 查询商户并解析租户
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
var tenantId = merchant.TenantId;
// 3. 构建合同
// 3. (空行后) 构建合同并写入租户
var contract = new MerchantContract
{
Id = idGenerator.NextId(),
TenantId = tenantId,
MerchantId = merchant.Id,
ContractNumber = request.ContractNumber.Trim(),
StartDate = request.StartDate,
@@ -52,7 +51,7 @@ public sealed class CreateMerchantContractCommandHandler(
FileUrl = request.FileUrl.Trim()
};
// 4. 持久化与审计
// 4. (空行后) 持久化与审计
await merchantRepository.AddContractAsync(contract, cancellationToken);
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
{

View File

@@ -1,7 +1,6 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Commands;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -9,8 +8,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// 删除类目处理器。
/// </summary>
public sealed class DeleteMerchantCategoryCommandHandler(
IMerchantCategoryRepository categoryRepository,
ITenantProvider tenantProvider)
IMerchantCategoryRepository categoryRepository)
: IRequestHandler<DeleteMerchantCategoryCommand, bool>
{
/// <summary>
@@ -21,16 +19,15 @@ public sealed class DeleteMerchantCategoryCommandHandler(
/// <returns>执行结果。</returns>
public async Task<bool> Handle(DeleteMerchantCategoryCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
var existing = await categoryRepository.FindByIdAsync(request.CategoryId, tenantId, cancellationToken);
// 1. 查询类目
var existing = await categoryRepository.FindByIdAsync(request.CategoryId, request.TenantId, cancellationToken);
if (existing == null)
{
return false;
}
// 2. 删除并保存
// 2. (空行后) 删除并保存
await categoryRepository.RemoveAsync(existing, cancellationToken);
await categoryRepository.SaveChangesAsync(cancellationToken);
return true;

View File

@@ -2,7 +2,6 @@ using MediatR;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.App.Merchants.Commands;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -11,23 +10,21 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// </summary>
public sealed class DeleteMerchantCommandHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider,
ILogger<DeleteMerchantCommandHandler> logger)
: IRequestHandler<DeleteMerchantCommand, bool>
{
/// <inheritdoc />
public async Task<bool> Handle(DeleteMerchantCommand request, CancellationToken cancellationToken)
{
// 1. 校验存在性
var tenantId = tenantProvider.GetCurrentTenantId();
var existing = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken);
// 1. 校验存在性(跨租户)
var existing = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken);
if (existing == null)
{
return false;
}
// 2. 删除
await merchantRepository.DeleteMerchantAsync(request.MerchantId, tenantId, cancellationToken);
// 2. (空行后) 删除
await merchantRepository.DeleteMerchantAsync(request.MerchantId, existing.TenantId, cancellationToken);
await merchantRepository.SaveChangesAsync(cancellationToken);
logger.LogInformation("删除商户 {MerchantId}", request.MerchantId);

View File

@@ -1,15 +1,11 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Domain.Merchants.Services;
using TakeoutSaaS.Domain.Stores.Repositories;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -20,36 +16,24 @@ public sealed class ExportMerchantPdfQueryHandler(
IMerchantRepository merchantRepository,
IStoreRepository storeRepository,
ITenantRepository tenantRepository,
IMerchantExportService exportService,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService)
IMerchantExportService exportService)
: IRequestHandler<ExportMerchantPdfQuery, byte[]>
{
public async Task<byte[]> Handle(ExportMerchantPdfQuery request, CancellationToken cancellationToken)
{
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
var merchant = isSuperAdmin
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
// 1. 查询商户(跨租户)
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken);
if (merchant == null)
{
throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
}
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止导出其他租户商户");
}
// 2. (空行后) 查询关联数据
var stores = await storeRepository.GetByMerchantIdAsync(merchant.Id, merchant.TenantId, cancellationToken);
var auditLogs = await merchantRepository.GetAuditLogsAsync(merchant.Id, merchant.TenantId, cancellationToken);
var tenant = await tenantRepository.FindByIdAsync(merchant.TenantId, cancellationToken);
// 3. (空行后) 导出 PDF
return await exportService.ExportToPdfAsync(merchant, tenant?.Name, stores, auditLogs, cancellationToken);
}
}

View File

@@ -1,13 +1,9 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -15,10 +11,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// 商户审核历史处理器。
/// </summary>
public sealed class GetMerchantAuditHistoryQueryHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService)
IMerchantRepository merchantRepository)
: IRequestHandler<GetMerchantAuditHistoryQuery, IReadOnlyList<MerchantAuditLogDto>>
{
/// <inheritdoc />
@@ -26,24 +19,14 @@ public sealed class GetMerchantAuditHistoryQueryHandler(
GetMerchantAuditHistoryQuery request,
CancellationToken cancellationToken)
{
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
var merchant = isSuperAdmin
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
// 1. 查询商户(跨租户)
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken);
if (merchant == null)
{
throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
}
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止访问其他租户的商户审核历史");
}
// 2. (空行后) 查询审核历史
var logs = await merchantRepository.GetAuditLogsAsync(merchant.Id, merchant.TenantId, cancellationToken);
return logs.Select(MerchantMapping.ToDto).ToList();
}

View File

@@ -3,7 +3,6 @@ using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -11,8 +10,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// 读取商户审核日志。
/// </summary>
public sealed class GetMerchantAuditLogsQueryHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider)
IMerchantRepository merchantRepository)
: IRequestHandler<GetMerchantAuditLogsQuery, PagedResult<MerchantAuditLogDto>>
{
/// <summary>
@@ -23,19 +21,25 @@ public sealed class GetMerchantAuditLogsQueryHandler(
/// <returns>分页结果。</returns>
public async Task<PagedResult<MerchantAuditLogDto>> Handle(GetMerchantAuditLogsQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文并查询日志
var tenantId = tenantProvider.GetCurrentTenantId();
var logs = await merchantRepository.GetAuditLogsAsync(request.MerchantId, tenantId, cancellationToken);
// 1. 查询商户并解析租户
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken);
if (merchant is null)
{
return new PagedResult<MerchantAuditLogDto>([], request.Page, request.PageSize, 0);
}
// 2. (空行后) 查询日志
var logs = await merchantRepository.GetAuditLogsAsync(request.MerchantId, merchant.TenantId, cancellationToken);
var total = logs.Count;
// 2. 分页映射
// 3. (空行后) 分页映射
var paged = logs
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.Select(MerchantMapping.ToDto)
.ToList();
// 3. 返回结果
// 4. (空行后) 返回结果
return new PagedResult<MerchantAuditLogDto>(paged, request.Page, request.PageSize, total);
}
}

View File

@@ -2,28 +2,26 @@ using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// <summary>
/// 获取商户详情查询处理器。
/// </summary>
public sealed class GetMerchantByIdQueryHandler(IMerchantRepository merchantRepository, ITenantProvider tenantProvider)
public sealed class GetMerchantByIdQueryHandler(IMerchantRepository merchantRepository)
: IRequestHandler<GetMerchantByIdQuery, MerchantDto?>
{
/// <inheritdoc />
public async Task<MerchantDto?> Handle(GetMerchantByIdQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken);
// 1. 查询商户(跨租户)
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken);
if (merchant == null)
{
return null;
}
// 2. 返回 DTO
// 2. (空行后) 返回 DTO
return new MerchantDto
{
Id = merchant.Id,

View File

@@ -1,7 +1,6 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -9,8 +8,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// 读取可选类目。
/// </summary>
public sealed class GetMerchantCategoriesQueryHandler(
IMerchantCategoryRepository categoryRepository,
ITenantProvider tenantProvider)
IMerchantCategoryRepository categoryRepository)
: IRequestHandler<GetMerchantCategoriesQuery, IReadOnlyList<string>>
{
/// <summary>
@@ -21,11 +19,10 @@ public sealed class GetMerchantCategoriesQueryHandler(
/// <returns>类目名称集合。</returns>
public async Task<IReadOnlyList<string>> Handle(GetMerchantCategoriesQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文并读取类目
var tenantId = tenantProvider.GetCurrentTenantId();
var categories = await categoryRepository.ListAsync(tenantId, cancellationToken);
// 1. 读取类目列表
var categories = await categoryRepository.ListAsync(request.TenantId, cancellationToken);
// 2. 过滤启用类目并去重
// 2. (空行后) 过滤启用类目并去重
return categories
.Where(x => x.IsActive)
.Select(x => x.Name.Trim())

View File

@@ -1,13 +1,9 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -15,10 +11,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// 商户变更历史处理器。
/// </summary>
public sealed class GetMerchantChangeHistoryQueryHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService)
IMerchantRepository merchantRepository)
: IRequestHandler<GetMerchantChangeHistoryQuery, IReadOnlyList<MerchantChangeLogDto>>
{
/// <inheritdoc />
@@ -26,24 +19,14 @@ public sealed class GetMerchantChangeHistoryQueryHandler(
GetMerchantChangeHistoryQuery request,
CancellationToken cancellationToken)
{
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
var merchant = isSuperAdmin
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
// 1. 查询商户(跨租户)
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken);
if (merchant == null)
{
throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
}
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止访问其他租户的商户变更历史");
}
// 2. (空行后) 查询变更历史
var logs = await merchantRepository.GetChangeLogsAsync(merchant.Id, merchant.TenantId, request.FieldName, cancellationToken);
return logs.Select(MerchantMapping.ToDto).ToList();
}

View File

@@ -4,7 +4,6 @@ using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -12,8 +11,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// 查询合同列表。
/// </summary>
public sealed class GetMerchantContractsQueryHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider)
IMerchantRepository merchantRepository)
: IRequestHandler<GetMerchantContractsQuery, IReadOnlyList<MerchantContractDto>>
{
/// <summary>
@@ -24,13 +22,12 @@ public sealed class GetMerchantContractsQueryHandler(
/// <returns>合同 DTO 列表。</returns>
public async Task<IReadOnlyList<MerchantContractDto>> Handle(GetMerchantContractsQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文并校验商户存在
var tenantId = tenantProvider.GetCurrentTenantId();
_ = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
// 1. 校验商户存在并解析租户
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
// 2. 查询合同列表
var contracts = await merchantRepository.GetContractsAsync(request.MerchantId, tenantId, cancellationToken);
// 2. (空行后) 查询合同列表
var contracts = await merchantRepository.GetContractsAsync(request.MerchantId, merchant.TenantId, cancellationToken);
return MerchantMapping.ToContractDtos(contracts);
}
}

View File

@@ -1,15 +1,11 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Domain.Stores.Repositories;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -19,10 +15,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
public sealed class GetMerchantDetailQueryHandler(
IMerchantRepository merchantRepository,
IStoreRepository storeRepository,
ITenantRepository tenantRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService)
ITenantRepository tenantRepository)
: IRequestHandler<GetMerchantDetailQuery, MerchantDetailDto>
{
/// <summary>
@@ -33,31 +26,19 @@ public sealed class GetMerchantDetailQueryHandler(
/// <returns>商户详情 DTO。</returns>
public async Task<MerchantDetailDto> Handle(GetMerchantDetailQuery request, CancellationToken cancellationToken)
{
// 1. 获取权限与商户
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
var merchant = isSuperAdmin
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
// 1. 查询商户(跨租户)
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken);
if (merchant == null)
{
throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
}
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止访问其他租户的商户");
}
// 2. 查询门店与租户信息
// 2. (空行后) 查询门店与租户信息
var stores = await storeRepository.GetByMerchantIdAsync(merchant.Id, merchant.TenantId, cancellationToken);
var storeDtos = MerchantMapping.ToStoreDtos(stores);
var tenant = await tenantRepository.FindByIdAsync(merchant.TenantId, cancellationToken);
// 3. 返回明细 DTO
// 3. (空行后) 返回明细 DTO
return MerchantMapping.ToDetailDto(merchant, tenant?.Name, storeDtos);
}
}

View File

@@ -4,7 +4,6 @@ using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -12,8 +11,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// 查询证照列表。
/// </summary>
public sealed class GetMerchantDocumentsQueryHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider)
IMerchantRepository merchantRepository)
: IRequestHandler<GetMerchantDocumentsQuery, IReadOnlyList<MerchantDocumentDto>>
{
/// <summary>
@@ -24,13 +22,12 @@ public sealed class GetMerchantDocumentsQueryHandler(
/// <returns>证照 DTO 列表。</returns>
public async Task<IReadOnlyList<MerchantDocumentDto>> Handle(GetMerchantDocumentsQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文并校验商户存在
var tenantId = tenantProvider.GetCurrentTenantId();
_ = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
// 1. 校验商户存在并解析租户
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
// 2. 查询证照列表
var documents = await merchantRepository.GetDocumentsAsync(request.MerchantId, tenantId, cancellationToken);
// 2. (空行后) 查询证照列表
var documents = await merchantRepository.GetDocumentsAsync(request.MerchantId, merchant.TenantId, cancellationToken);
return MerchantMapping.ToDocumentDtos(documents);
}
}

View File

@@ -1,16 +1,10 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Domain.Stores.Repositories;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -20,10 +14,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
public sealed class GetMerchantListQueryHandler(
IMerchantRepository merchantRepository,
IStoreRepository storeRepository,
ITenantRepository tenantRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService)
ITenantRepository tenantRepository)
: IRequestHandler<GetMerchantListQuery, PagedResult<MerchantListItemDto>>
{
/// <inheritdoc />
@@ -31,21 +22,9 @@ public sealed class GetMerchantListQueryHandler(
GetMerchantListQuery request,
CancellationToken cancellationToken)
{
// 1. 校验跨租户访问权限
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户查询商户");
}
var effectiveTenantId = isSuperAdmin ? request.TenantId : currentTenantId;
// 2. 查询商户列表
// 1. 查询商户列表
var merchants = await merchantRepository.SearchAsync(
effectiveTenantId,
request.TenantId,
request.Status,
request.OperatingMode,
request.Keyword,
@@ -56,7 +35,7 @@ public sealed class GetMerchantListQueryHandler(
return new PagedResult<MerchantListItemDto>(Array.Empty<MerchantListItemDto>(), request.Page, request.PageSize, 0);
}
// 3. 排序 & 分页
// 2. (空行后) 排序 & 分页
var sorted = ApplySorting(merchants, request.SortBy, request.SortOrder);
var total = sorted.Count;
var paged = sorted
@@ -69,16 +48,16 @@ public sealed class GetMerchantListQueryHandler(
return new PagedResult<MerchantListItemDto>(Array.Empty<MerchantListItemDto>(), request.Page, request.PageSize, total);
}
// 4. 批量查询租户名称
// 3. (空行后) 批量查询租户名称
var tenantIds = paged.Select(x => x.TenantId).Distinct().ToArray();
var tenants = await tenantRepository.FindByIdsAsync(tenantIds, cancellationToken);
var tenantLookup = tenants.ToDictionary(x => x.Id, x => x.Name);
// 5. 批量查询门店数量
// 4. (空行后) 批量查询门店数量
var merchantIds = paged.Select(x => x.Id).ToArray();
var storeCounts = await storeRepository.GetStoreCountsAsync(effectiveTenantId, merchantIds, cancellationToken);
var storeCounts = await storeRepository.GetStoreCountsAsync(request.TenantId, merchantIds, cancellationToken);
// 6. 组装 DTO
// 5. (空行后) 组装 DTO
var items = paged.Select(merchant =>
{
var tenantName = tenantLookup.TryGetValue(merchant.TenantId, out var name) ? name : null;

View File

@@ -2,7 +2,6 @@ using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -10,8 +9,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// 列出类目。
/// </summary>
public sealed class ListMerchantCategoriesQueryHandler(
IMerchantCategoryRepository categoryRepository,
ITenantProvider tenantProvider)
IMerchantCategoryRepository categoryRepository)
: IRequestHandler<ListMerchantCategoriesQuery, IReadOnlyList<MerchantCategoryDto>>
{
/// <summary>
@@ -22,11 +20,10 @@ public sealed class ListMerchantCategoriesQueryHandler(
/// <returns>类目 DTO 列表。</returns>
public async Task<IReadOnlyList<MerchantCategoryDto>> Handle(ListMerchantCategoriesQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户上下文
var tenantId = tenantProvider.GetCurrentTenantId();
var categories = await categoryRepository.ListAsync(tenantId, cancellationToken);
// 1. 查询类目列表
var categories = await categoryRepository.ListAsync(request.TenantId, cancellationToken);
// 2. 映射 DTO
// 2. (空行后) 映射 DTO
return MerchantMapping.ToCategoryDtos(categories);
}
}

View File

@@ -3,7 +3,6 @@ using TakeoutSaaS.Application.App.Merchants.Commands;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -11,8 +10,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// 类目排序处理器。
/// </summary>
public sealed class ReorderMerchantCategoriesCommandHandler(
IMerchantCategoryRepository categoryRepository,
ITenantProvider tenantProvider)
IMerchantCategoryRepository categoryRepository)
: IRequestHandler<ReorderMerchantCategoriesCommand, bool>
{
/// <summary>
@@ -23,12 +21,11 @@ public sealed class ReorderMerchantCategoriesCommandHandler(
/// <returns>执行结果。</returns>
public async Task<bool> Handle(ReorderMerchantCategoriesCommand request, CancellationToken cancellationToken)
{
// 1. 获取租户并查询类目
var tenantId = tenantProvider.GetCurrentTenantId();
var categories = await categoryRepository.ListAsync(tenantId, cancellationToken);
// 1. 查询类目列表
var categories = await categoryRepository.ListAsync(request.TenantId, cancellationToken);
var map = categories.ToDictionary(x => x.Id);
// 2. 更新排序
// 2. (空行后) 更新排序
foreach (var item in request.Items)
{
if (!map.TryGetValue(item.CategoryId, out var category))
@@ -39,7 +36,7 @@ public sealed class ReorderMerchantCategoriesCommandHandler(
category.DisplayOrder = item.DisplayOrder;
}
// 3. 持久化
// 3. (空行后) 持久化
await categoryRepository.UpdateRangeAsync(map.Values, cancellationToken);
await categoryRepository.SaveChangesAsync(cancellationToken);
return true;

View File

@@ -7,7 +7,6 @@ using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -16,7 +15,6 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// </summary>
public sealed class ReviewMerchantDocumentCommandHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor)
: IRequestHandler<ReviewMerchantDocumentCommand, MerchantDocumentDto>
{
@@ -28,23 +26,27 @@ public sealed class ReviewMerchantDocumentCommandHandler(
/// <returns>证照 DTO。</returns>
public async Task<MerchantDocumentDto> Handle(ReviewMerchantDocumentCommand request, CancellationToken cancellationToken)
{
// 1. 读取证照
var tenantId = tenantProvider.GetCurrentTenantId();
// 1. 查询商户并解析租户
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
var tenantId = merchant.TenantId;
// 2. (空行后) 读取证照
var document = await merchantRepository.FindDocumentByIdAsync(request.MerchantId, tenantId, request.DocumentId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "证照不存在");
// 2. 若状态无变化且备注相同,直接返回
// 3. (空行后) 若状态无变化且备注相同,直接返回
var targetStatus = request.Approve ? MerchantDocumentStatus.Approved : MerchantDocumentStatus.Rejected;
if (document.Status == targetStatus && document.Remarks == request.Remarks)
{
return MerchantMapping.ToDto(document);
}
// 3. 更新状态
// 4. (空行后) 更新状态
document.Status = targetStatus;
document.Remarks = request.Remarks;
// 4. 持久化与审计
// 5. (空行后) 持久化与审计
await merchantRepository.UpdateDocumentAsync(document, cancellationToken);
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
{
@@ -58,7 +60,7 @@ public sealed class ReviewMerchantDocumentCommandHandler(
}, cancellationToken);
await merchantRepository.SaveChangesAsync(cancellationToken);
// 5. 返回 DTO
// 6. (空行后) 返回 DTO
return MerchantMapping.ToDto(document);
}

View File

@@ -3,7 +3,6 @@ using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -11,25 +10,28 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// 商户列表查询处理器。
/// </summary>
public sealed class SearchMerchantsQueryHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider)
IMerchantRepository merchantRepository)
: IRequestHandler<SearchMerchantsQuery, PagedResult<MerchantDto>>
{
/// <inheritdoc />
public async Task<PagedResult<MerchantDto>> Handle(SearchMerchantsQuery request, CancellationToken cancellationToken)
{
// 1. 获取租户并查询商户
var tenantId = tenantProvider.GetCurrentTenantId();
var merchants = await merchantRepository.SearchAsync(tenantId, request.Status, cancellationToken);
// 1. 查询商户列表(可选租户过滤)
var merchants = await merchantRepository.SearchAsync(
request.TenantId,
request.Status,
operatingMode: null,
keyword: null,
cancellationToken);
// 2. 排序与分页
// 2. (空行后) 排序与分页
var sorted = ApplySorting(merchants, request.SortBy, request.SortDescending);
var paged = sorted
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToList();
// 3. 映射 DTO
// 3. (空行后) 映射 DTO
var items = paged.Select(merchant => new MerchantDto
{
Id = merchant.Id,
@@ -45,7 +47,7 @@ public sealed class SearchMerchantsQueryHandler(
CreatedAt = merchant.CreatedAt
}).ToList();
// 4. 返回分页结果
// 4. (空行后) 返回分页结果
return new PagedResult<MerchantDto>(items, request.Page, request.PageSize, merchants.Count);
}

View File

@@ -2,8 +2,6 @@ using MediatR;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.App.Merchants.Commands;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Domain.Merchants.Entities;
using TakeoutSaaS.Domain.Merchants.Enums;
using TakeoutSaaS.Domain.Merchants.Repositories;
@@ -12,7 +10,6 @@ using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -23,9 +20,7 @@ public sealed class UpdateMerchantCommandHandler(
IMerchantRepository merchantRepository,
IStoreRepository storeRepository,
ITenantRepository tenantRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService,
ILogger<UpdateMerchantCommandHandler> logger)
: IRequestHandler<UpdateMerchantCommand, UpdateMerchantResultDto?>
{
@@ -37,27 +32,15 @@ public sealed class UpdateMerchantCommandHandler(
throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空");
}
// 1. 获取操作者权限
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
// 2. 读取商户信息
var merchant = isSuperAdmin
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
// 1. 读取商户信息(跨租户)
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken);
if (merchant == null)
{
return null;
}
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
{
return null;
}
// 3. 规范化输入
// 2. (空行后) 规范化输入
var name = NormalizeRequired(request.Name, "商户名称");
var contactPhone = NormalizeRequired(request.ContactPhone, "联系电话");
var licenseNumber = NormalizeOptional(request.LicenseNumber);
@@ -78,7 +61,7 @@ public sealed class UpdateMerchantCommandHandler(
TrackChange("contactPhone", merchant.ContactPhone, contactPhone, isCritical: false);
TrackChange("contactEmail", merchant.ContactEmail, contactEmail, isCritical: false);
// 4. 写入字段
// 3. (空行后) 写入字段
merchant.BrandName = name;
merchant.BusinessLicenseNumber = licenseNumber;
merchant.LegalPerson = legalRepresentative;
@@ -103,7 +86,7 @@ public sealed class UpdateMerchantCommandHandler(
merchant.FrozenAt = null;
}
// 5. 持久化日志与数据
// 4. (空行后) 持久化日志与数据
await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
foreach (var log in changes)
{
@@ -135,7 +118,7 @@ public sealed class UpdateMerchantCommandHandler(
logger.LogInformation("更新商户 {MerchantId} - {Name}", merchant.Id, merchant.BrandName);
// 6. 返回更新结果
// 5. (空行后) 返回更新结果
var stores = await storeRepository.GetByMerchantIdAsync(merchant.Id, merchant.TenantId, cancellationToken);
var tenant = await tenantRepository.FindByIdAsync(merchant.TenantId, cancellationToken);
var detail = MerchantMapping.ToDetailDto(merchant, tenant?.Name, MerchantMapping.ToStoreDtos(stores));

View File

@@ -7,7 +7,6 @@ using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -16,7 +15,6 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// </summary>
public sealed class UpdateMerchantContractStatusCommandHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor)
: IRequestHandler<UpdateMerchantContractStatusCommand, MerchantContractDto>
{
@@ -28,12 +26,16 @@ public sealed class UpdateMerchantContractStatusCommandHandler(
/// <returns>合同 DTO。</returns>
public async Task<MerchantContractDto> Handle(UpdateMerchantContractStatusCommand request, CancellationToken cancellationToken)
{
// 1. 查询合同
var tenantId = tenantProvider.GetCurrentTenantId();
// 1. 查询商户并解析租户
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
var tenantId = merchant.TenantId;
// 2. (空行后) 查询合同
var contract = await merchantRepository.FindContractByIdAsync(request.MerchantId, tenantId, request.ContractId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "合同不存在");
// 2. 更新状态
// 3. (空行后) 更新状态
if (request.Status == ContractStatus.Active)
{
contract.Status = ContractStatus.Active;
@@ -50,7 +52,7 @@ public sealed class UpdateMerchantContractStatusCommandHandler(
contract.Status = request.Status;
}
// 3. 持久化与审计
// 4. (空行后) 持久化与审计
await merchantRepository.UpdateContractAsync(contract, cancellationToken);
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
{

View File

@@ -5,4 +5,4 @@ namespace TakeoutSaaS.Application.App.Merchants.Queries;
/// <summary>
/// 获取商户可选类目。
/// </summary>
public sealed record GetMerchantCategoriesQuery() : IRequest<IReadOnlyList<string>>;
public sealed record GetMerchantCategoriesQuery(long TenantId) : IRequest<IReadOnlyList<string>>;

View File

@@ -6,4 +6,10 @@ namespace TakeoutSaaS.Application.App.Merchants.Queries;
/// <summary>
/// 管理端获取完整类目列表。
/// </summary>
public sealed record ListMerchantCategoriesQuery() : IRequest<IReadOnlyList<MerchantCategoryDto>>;
public sealed record ListMerchantCategoriesQuery : IRequest<IReadOnlyList<MerchantCategoryDto>>
{
/// <summary>
/// 租户 ID。
/// </summary>
public long TenantId { get; init; }
}

View File

@@ -10,6 +10,11 @@ namespace TakeoutSaaS.Application.App.Merchants.Queries;
/// </summary>
public sealed class SearchMerchantsQuery : IRequest<PagedResult<MerchantDto>>
{
/// <summary>
/// 租户过滤(为空表示跨租户查询)。
/// </summary>
public long? TenantId { get; init; }
/// <summary>
/// 按状态过滤。
/// </summary>