diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/MerchantController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/MerchantController.cs new file mode 100644 index 0000000..ea5ac1f --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/MerchantController.cs @@ -0,0 +1,35 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Merchants.Dto; +using TakeoutSaaS.Application.App.Merchants.Queries; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.TenantApi.Controllers; + +/// +/// 租户端商户中心。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/tenant/v{version:apiVersion}/merchant")] +public sealed class MerchantController(IMediator mediator) : BaseApiController +{ + /// + /// 获取当前登录用户对应的商户中心信息。 + /// + /// 取消标记。 + /// 商户中心聚合信息。 + [HttpGet("info")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] + public async Task> GetInfo(CancellationToken cancellationToken) + { + // 1. 查询当前商户中心信息 + var info = await mediator.Send(new GetCurrentMerchantCenterQuery(), cancellationToken); + + // 2. 返回聚合信息 + return ApiResponse.Ok(info); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/CurrentMerchantCenterDto.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/CurrentMerchantCenterDto.cs new file mode 100644 index 0000000..094843f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Dto/CurrentMerchantCenterDto.cs @@ -0,0 +1,39 @@ +using TakeoutSaaS.Application.App.Stores.Dto; + +namespace TakeoutSaaS.Application.App.Merchants.Dto; + +/// +/// 当前租户商户中心聚合信息。 +/// +public sealed class CurrentMerchantCenterDto +{ + /// + /// 商户主体详情。 + /// + public MerchantDetailDto Merchant { get; init; } = new(); + + /// + /// 商户证照列表。 + /// + public IReadOnlyList Documents { get; init; } = []; + + /// + /// 商户合同列表。 + /// + public IReadOnlyList Contracts { get; init; } = []; + + /// + /// 商户员工列表。 + /// + public IReadOnlyList Staffs { get; init; } = []; + + /// + /// 商户审核日志列表。 + /// + public IReadOnlyList AuditLogs { get; init; } = []; + + /// + /// 商户变更日志列表。 + /// + public IReadOnlyList ChangeLogs { get; init; } = []; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetCurrentMerchantCenterQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetCurrentMerchantCenterQueryHandler.cs new file mode 100644 index 0000000..af8db2a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetCurrentMerchantCenterQueryHandler.cs @@ -0,0 +1,115 @@ +using MediatR; +using TakeoutSaaS.Application.App.Merchants.Dto; +using TakeoutSaaS.Application.App.Merchants.Queries; +using TakeoutSaaS.Application.App.Stores; +using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Identity.Repositories; +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; + +/// +/// 获取当前商户中心信息查询处理器。 +/// +public sealed class GetCurrentMerchantCenterQueryHandler( + ICurrentUserAccessor currentUserAccessor, + ITenantProvider tenantProvider, + IIdentityUserRepository identityUserRepository, + IMerchantRepository merchantRepository, + IStoreRepository storeRepository, + ITenantRepository tenantRepository) + : IRequestHandler +{ + /// + /// 获取当前登录用户可访问的商户中心完整信息。 + /// + /// 查询请求。 + /// 取消标记。 + /// 商户中心聚合信息。 + public async Task Handle(GetCurrentMerchantCenterQuery request, CancellationToken cancellationToken) + { + // 1. 校验登录上下文 + var currentUserId = currentUserAccessor.UserId; + if (currentUserId <= 0) + { + throw new BusinessException(ErrorCodes.Unauthorized, "未登录或登录已过期"); + } + + // 2. 校验租户上下文 + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识,请在 Header 指定租户"); + } + + // 3. 校验当前用户归属 + var currentUser = await identityUserRepository.FindByIdAsync(currentUserId, cancellationToken) + ?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); + if (currentUser.TenantId != currentTenantId) + { + throw new BusinessException(ErrorCodes.Unauthorized, "无权访问当前租户的商户信息"); + } + + // 4. 解析当前用户可访问商户 + var merchantId = await ResolveMerchantIdAsync(currentUser.MerchantId, currentTenantId, cancellationToken); + + // 5. 读取商户主体与租户信息 + var merchant = await merchantRepository.FindByIdAsync(merchantId, currentTenantId, cancellationToken) + ?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在"); + var tenant = await tenantRepository.FindByIdAsync(currentTenantId, cancellationToken); + + // 6. 读取商户关联数据 + var stores = await storeRepository.GetByMerchantIdAsync(merchantId, currentTenantId, cancellationToken); + var staffs = await merchantRepository.GetStaffAsync(merchantId, currentTenantId, cancellationToken); + var documents = await merchantRepository.GetDocumentsAsync(merchantId, currentTenantId, cancellationToken); + var contracts = await merchantRepository.GetContractsAsync(merchantId, currentTenantId, cancellationToken); + var auditLogs = await merchantRepository.GetAuditLogsAsync(merchantId, currentTenantId, cancellationToken); + var changeLogs = await merchantRepository.GetChangeLogsAsync(merchantId, currentTenantId, null, cancellationToken); + + // 7. 组装并返回聚合结果 + var storeDtos = MerchantMapping.ToStoreDtos(stores); + var staffDtos = staffs.Select(StoreMapping.ToDto).ToList(); + return new CurrentMerchantCenterDto + { + Merchant = MerchantMapping.ToDetailDto(merchant, tenant?.Name, storeDtos), + Staffs = staffDtos, + Documents = MerchantMapping.ToDocumentDtos(documents), + Contracts = MerchantMapping.ToContractDtos(contracts), + AuditLogs = auditLogs.Select(MerchantMapping.ToDto).ToList(), + ChangeLogs = changeLogs.Select(MerchantMapping.ToDto).ToList() + }; + } + + /// + /// 解析当前用户对应的商户标识。 + /// + /// 当前用户绑定的商户 ID。 + /// 当前租户 ID。 + /// 取消标记。 + /// 商户 ID。 + private async Task ResolveMerchantIdAsync(long? currentUserMerchantId, long tenantId, CancellationToken cancellationToken) + { + // 1. 优先使用用户显式绑定的商户 + if (currentUserMerchantId is > 0) + { + return currentUserMerchantId.Value; + } + + // 2. 兜底读取租户下最新商户 + var merchants = await merchantRepository.SearchAsync(tenantId, status: null, cancellationToken); + var merchantId = merchants.FirstOrDefault()?.Id; + if (!merchantId.HasValue || merchantId.Value <= 0) + { + throw new BusinessException(ErrorCodes.NotFound, "当前租户尚未创建商户信息"); + } + + // 3. 返回商户标识 + return merchantId.Value; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetCurrentMerchantCenterQuery.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetCurrentMerchantCenterQuery.cs new file mode 100644 index 0000000..f4c0ace --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Queries/GetCurrentMerchantCenterQuery.cs @@ -0,0 +1,9 @@ +using MediatR; +using TakeoutSaaS.Application.App.Merchants.Dto; + +namespace TakeoutSaaS.Application.App.Merchants.Queries; + +/// +/// 获取当前登录用户可访问的商户中心聚合信息。 +/// +public sealed record GetCurrentMerchantCenterQuery : IRequest;