diff --git a/TakeoutSaaS.sln b/TakeoutSaaS.sln index 1c33d44..08719a4 100644 --- a/TakeoutSaaS.sln +++ b/TakeoutSaaS.sln @@ -31,10 +31,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Authoriz EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Tenancy", "src\Modules\TakeoutSaaS.Module.Tenancy\TakeoutSaaS.Module.Tenancy.csproj", "{5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.MiniApi", "src\Api\TakeoutSaaS.MiniApi\TakeoutSaaS.MiniApi.csproj", "{12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.UserApi", "src\Api\TakeoutSaaS.UserApi\TakeoutSaaS.UserApi.csproj", "{1C0BCC51-AF18-44F3-A1E6-A693F74276B5}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Shared.Kernel", "TakeoutSaaS.BuildingBlocks\src\Core\TakeoutSaaS.Shared.Kernel\TakeoutSaaS.Shared.Kernel.csproj", "{BBC99B58-ECA8-42C3-9070-9AA058D778D3}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Storage", "src\Modules\TakeoutSaaS.Module.Storage\TakeoutSaaS.Module.Storage.csproj", "{05058F44-6FB7-43AF-8648-8BF538E283EF}" @@ -55,6 +51,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Application.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Integration.Tests", "tests\TakeoutSaaS.Integration.Tests\TakeoutSaaS.Integration.Tests.csproj", "{8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.TenantApi", "src\Api\TakeoutSaaS.TenantApi\TakeoutSaaS.TenantApi.csproj", "{F53E274A-838A-477A-8D29-6EEB0DBD62CD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -149,30 +147,6 @@ Global {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Release|x64.Build.0 = Release|Any CPU {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Release|x86.ActiveCfg = Release|Any CPU {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Release|x86.Build.0 = Release|Any CPU - {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Debug|x64.ActiveCfg = Debug|Any CPU - {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Debug|x64.Build.0 = Debug|Any CPU - {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Debug|x86.ActiveCfg = Debug|Any CPU - {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Debug|x86.Build.0 = Debug|Any CPU - {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Release|Any CPU.Build.0 = Release|Any CPU - {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Release|x64.ActiveCfg = Release|Any CPU - {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Release|x64.Build.0 = Release|Any CPU - {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Release|x86.ActiveCfg = Release|Any CPU - {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D}.Release|x86.Build.0 = Release|Any CPU - {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Debug|x64.ActiveCfg = Debug|Any CPU - {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Debug|x64.Build.0 = Debug|Any CPU - {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Debug|x86.ActiveCfg = Debug|Any CPU - {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Debug|x86.Build.0 = Debug|Any CPU - {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|Any CPU.Build.0 = Release|Any CPU - {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|x64.ActiveCfg = Release|Any CPU - {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|x64.Build.0 = Release|Any CPU - {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|x86.ActiveCfg = Release|Any CPU - {1C0BCC51-AF18-44F3-A1E6-A693F74276B5}.Release|x86.Build.0 = Release|Any CPU {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Debug|Any CPU.Build.0 = Debug|Any CPU {BBC99B58-ECA8-42C3-9070-9AA058D778D3}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -281,6 +255,18 @@ Global {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Release|x64.Build.0 = Release|Any CPU {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Release|x86.ActiveCfg = Release|Any CPU {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Release|x86.Build.0 = Release|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|x64.Build.0 = Debug|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|x86.ActiveCfg = Debug|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|x86.Build.0 = Debug|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Release|Any CPU.Build.0 = Release|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Release|x64.ActiveCfg = Release|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Release|x64.Build.0 = Release|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Release|x86.ActiveCfg = Release|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -299,8 +285,6 @@ Global {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {6CB8487D-5C74-487C-9D84-E57838BDA015} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} - {12ECF33A-D5E3-4F8B-A9D9-60F7F55B869D} = {81034408-37C8-1011-444E-4C15C2FADA8E} - {1C0BCC51-AF18-44F3-A1E6-A693F74276B5} = {81034408-37C8-1011-444E-4C15C2FADA8E} {BBC99B58-ECA8-42C3-9070-9AA058D778D3} = {8D626EA8-CB54-BC41-363A-217881BEBA6E} {05058F44-6FB7-43AF-8648-8BF538E283EF} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} {5C12177E-6C25-4F78-BFD4-AA073CFC0650} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} @@ -310,5 +294,6 @@ Global {38011EC3-7EC3-40E4-B9B2-E631966B350B} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} {2601637E-777A-4FA2-81BA-1AFE32E961FF} = {0AB3BF05-4346-4AA6-1389-037BE0695223} {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {F53E274A-838A-477A-8D29-6EEB0DBD62CD} = {81034408-37C8-1011-444E-4C15C2FADA8E} EndGlobalSection EndGlobal diff --git a/src/Api/TakeoutSaaS.MiniApi/Contracts/Requests/FileUploadFormRequest.cs b/src/Api/TakeoutSaaS.MiniApi/Contracts/Requests/FileUploadFormRequest.cs deleted file mode 100644 index 8cc3e95..0000000 --- a/src/Api/TakeoutSaaS.MiniApi/Contracts/Requests/FileUploadFormRequest.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Http; - -namespace TakeoutSaaS.MiniApi.Contracts.Requests; - -/// -/// 文件上传表单请求。 -/// -public sealed record FileUploadFormRequest -{ - /// - /// 上传文件。 - /// - [Required] - public required IFormFile File { get; init; } - /// - /// 上传类型。 - /// - public string? Type { get; init; } -} diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/.gitkeep b/src/Api/TakeoutSaaS.MiniApi/Controllers/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs deleted file mode 100644 index 11e7d10..0000000 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/AuthController.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Application.Identity.Contracts; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.MiniApi.Controllers; - -/// -/// 小程序登录认证 -/// -/// 提供小程序端的微信登录与 Token 刷新能力。 -/// 小程序认证服务 -[ApiVersion("1.0")] -[Authorize] -[Route("api/mini/v{version:apiVersion}/auth")] -public sealed class AuthController(IMiniAuthService authService) : BaseApiController -{ - /// - /// 微信登录 - /// - /// 微信登录请求。 - /// 取消标记。 - /// 包含访问令牌与刷新令牌的响应。 - [HttpPost("wechat/login")] - [AllowAnonymous] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> LoginWithWeChat([FromBody] WeChatLoginRequest request, CancellationToken cancellationToken) - { - // 1. 调用认证服务完成微信登录 - var response = await authService.LoginWithWeChatAsync(request, cancellationToken); - - // 2. 返回访问与刷新令牌 - return ApiResponse.Ok(response); - } - - /// - /// 刷新 Token - /// - /// 刷新令牌请求。 - /// 取消标记。 - /// 新的访问令牌与刷新令牌。 - [HttpPost("refresh")] - [AllowAnonymous] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) - { - // 1. 调用认证服务刷新 Token - var response = await authService.RefreshTokenAsync(request, cancellationToken); - - // 2. 返回新的令牌 - return ApiResponse.Ok(response); - } -} diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/FilesController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/FilesController.cs deleted file mode 100644 index c06c440..0000000 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/FilesController.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using TakeoutSaaS.Application.Storage.Abstractions; -using TakeoutSaaS.Application.Storage.Contracts; -using TakeoutSaaS.Application.Storage.Extensions; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; -using TakeoutSaaS.MiniApi.Contracts.Requests; - -namespace TakeoutSaaS.MiniApi.Controllers; - -/// -/// 小程序文件上传。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/mini/v{version:apiVersion}/files")] -public sealed class FilesController(IFileStorageService fileStorageService) : BaseApiController -{ - /// - /// 上传图片或文件。 - /// - /// 表单请求,包含文件与类型。 - /// 取消标记。 - /// 上传结果,包含访问链接等信息。 - [HttpPost("upload")] - [Consumes("multipart/form-data")] - [RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] - public async Task> Upload([FromForm] FileUploadFormRequest request, CancellationToken cancellationToken) - { - // 1. 校验文件有效性 - if (request.File is null || request.File.Length == 0) - { - return ApiResponse.Error(ErrorCodes.BadRequest, "文件不能为空"); - } - // 2. 解析上传类型 - if (!UploadFileTypeParser.TryParse(request.Type, out var uploadType)) - { - return ApiResponse.Error(ErrorCodes.BadRequest, "上传类型不合法"); - } - // 3. 提取请求来源 - var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault(); - await using var stream = request.File.OpenReadStream(); - // 4. 调用存储服务执行上传 - var result = await fileStorageService.UploadAsync( - new UploadFileRequest(uploadType, stream, request.File.FileName, request.File.ContentType ?? string.Empty, request.File.Length, origin), - cancellationToken); - // 5. 返回上传结果 - return ApiResponse.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/MenusController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/MenusController.cs deleted file mode 100644 index 549db99..0000000 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/MenusController.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using TakeoutSaaS.Application.App.Products.Dto; -using TakeoutSaaS.Application.App.Products.Queries; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.MiniApi.Controllers; - -/// -/// 小程序端菜单查询。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/mini/v{version:apiVersion}/stores/{storeId:long}/menu")] -public sealed class MenusController(IMediator mediator) : BaseApiController -{ - /// - /// 获取门店菜单(含分类与商品详情)。 - /// - [HttpGet] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> GetMenu(long storeId, [FromQuery] DateTime? updatedAfter, CancellationToken cancellationToken) - { - // 1. 组装查询 - var query = new GetStoreMenuQuery - { - StoreId = storeId, - UpdatedAfter = updatedAfter - }; - // 2. 拉取菜单 - var result = await mediator.Send(query, cancellationToken); - return ApiResponse.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/PickupSlotsController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/PickupSlotsController.cs deleted file mode 100644 index 66ce98d..0000000 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/PickupSlotsController.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using TakeoutSaaS.Application.App.Stores.Dto; -using TakeoutSaaS.Application.App.Stores.Queries; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.MiniApi.Controllers; - -/// -/// 小程序端自提档期查询。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/mini/v{version:apiVersion}/stores/{storeId:long}/pickup-slots")] -public sealed class PickupSlotsController(IMediator mediator) : BaseApiController -{ - /// - /// 获取指定日期可用档期。 - /// - [HttpGet] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> GetSlots(long storeId, [FromQuery] DateTime date, CancellationToken cancellationToken) - { - var result = await mediator.Send(new GetAvailablePickupSlotsQuery { StoreId = storeId, Date = date }, cancellationToken); - return ApiResponse>.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/TablesController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/TablesController.cs deleted file mode 100644 index 504e35a..0000000 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/TablesController.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using TakeoutSaaS.Application.App.Stores.Dto; -using TakeoutSaaS.Application.App.Stores.Queries; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.MiniApi.Controllers; - -/// -/// 桌码上下文。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/mini/v{version:apiVersion}/tables")] -public sealed class TablesController(IMediator mediator) : BaseApiController -{ - /// - /// 解析桌码并返回上下文。 - /// - [HttpGet("{code}/context")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> GetContext(string code, CancellationToken cancellationToken) - { - var result = await mediator.Send(new GetStoreTableContextQuery { TableCode = code }, cancellationToken); - return result is null - ? ApiResponse.Error(ErrorCodes.NotFound, "桌码不存在") - : ApiResponse.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.MiniApi/Dockerfile b/src/Api/TakeoutSaaS.MiniApi/Dockerfile deleted file mode 100644 index f737a92..0000000 --- a/src/Api/TakeoutSaaS.MiniApi/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj -RUN dotnet publish src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj -c Release -o /app/publish - -FROM mcr.microsoft.com/dotnet/aspnet:10.0 -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 7701 -ENV ASPNETCORE_URLS=http://+:7701 -ENTRYPOINT ["dotnet", "TakeoutSaaS.MiniApi.dll"] diff --git a/src/Api/TakeoutSaaS.MiniApi/Program.cs b/src/Api/TakeoutSaaS.MiniApi/Program.cs deleted file mode 100644 index 399f187..0000000 --- a/src/Api/TakeoutSaaS.MiniApi/Program.cs +++ /dev/null @@ -1,176 +0,0 @@ -using FluentValidation; -using Microsoft.AspNetCore.Cors.Infrastructure; -using Microsoft.Extensions.Caching.StackExchangeRedis; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; -using Serilog; -using TakeoutSaaS.Application.Messaging.Extensions; -using TakeoutSaaS.Application.Sms.Extensions; -using TakeoutSaaS.Application.Storage.Extensions; -using TakeoutSaaS.Module.Messaging.Extensions; -using TakeoutSaaS.Module.Sms.Extensions; -using TakeoutSaaS.Module.Storage.Extensions; -using TakeoutSaaS.Module.Tenancy.Extensions; -using TakeoutSaaS.Shared.Abstractions.Ids; -using TakeoutSaaS.Shared.Kernel.Ids; -using TakeoutSaaS.Shared.Web.Extensions; -using TakeoutSaaS.Shared.Web.Swagger; -using System.Reflection; - -// 1. 创建构建器与日志模板 -var builder = WebApplication.CreateBuilder(args); -const string logTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [TraceId:{TraceId}] [SpanId:{SpanId}] [Service:{Service}] {SourceContext} {Message:lj}{NewLine}{Exception}"; -var isDevelopment = builder.Environment.IsDevelopment(); - -// 2. 注册雪花 ID 生成器与 Serilog -builder.Services.AddSingleton(_ => new SnowflakeIdGenerator()); -builder.Host.UseSerilog((_, _, configuration) => -{ - configuration - .Enrich.FromLogContext() - .Enrich.WithProperty("Service", "MiniApi") - .WriteTo.Console(outputTemplate: logTemplate) - .WriteTo.File( - "logs/mini-api-.log", - rollingInterval: RollingInterval.Day, - retainedFileCountLimit: 7, - shared: true, - outputTemplate: logTemplate); -}); - -// 3. 注册通用 Web 能力,开发环境启用 Swagger -builder.Services.AddSharedWebCore(); -if (isDevelopment) -{ - builder.Services.AddSharedSwagger(options => - { - options.Title = "外卖SaaS - 小程序端"; - options.Description = "小程序 API 文档"; - options.EnableAuthorization = true; - }); -} -builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); - -// 4. 注册 Redis 分布式缓存,供验证码等功能使用 -var redisConnection = builder.Configuration.GetValue("Redis"); -builder.Services.AddStackExchangeRedisCache(options => -{ - options.Configuration = redisConnection; -}); - -// 4. 注册多租户与业务模块 -builder.Services.AddTenantResolution(builder.Configuration); -builder.Services.AddStorageModule(builder.Configuration); -builder.Services.AddStorageApplication(); -builder.Services.AddSmsModule(builder.Configuration); -builder.Services.AddSmsApplication(builder.Configuration); -builder.Services.AddMessagingModule(builder.Configuration); -builder.Services.AddMessagingApplication(); -builder.Services.AddHealthChecks(); - -// 5. 配置 OpenTelemetry 采集 -var otelSection = builder.Configuration.GetSection("Otel"); -var otelEndpoint = otelSection.GetValue("Endpoint"); -var useConsoleExporter = otelSection.GetValue("UseConsoleExporter") ?? builder.Environment.IsDevelopment(); -builder.Services.AddOpenTelemetry() - .ConfigureResource(resource => resource.AddService( - serviceName: "TakeoutSaaS.MiniApi", - serviceVersion: "1.0.0", - serviceInstanceId: Environment.MachineName)) - .WithTracing(tracing => - { - tracing - .SetSampler(new ParentBasedSampler(new AlwaysOnSampler())) - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddEntityFrameworkCoreInstrumentation(); - - if (!string.IsNullOrWhiteSpace(otelEndpoint)) - { - tracing.AddOtlpExporter(exporter => - { - exporter.Endpoint = new Uri(otelEndpoint); - }); - } - - if (useConsoleExporter) - { - tracing.AddConsoleExporter(); - } - }) - .WithMetrics(metrics => - { - metrics - .AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation() - .AddPrometheusExporter(); - - if (!string.IsNullOrWhiteSpace(otelEndpoint)) - { - metrics.AddOtlpExporter(exporter => - { - exporter.Endpoint = new Uri(otelEndpoint); - }); - } - - if (useConsoleExporter) - { - metrics.AddConsoleExporter(); - } - }); - -// 6. 配置 CORS -var miniOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Mini"); -builder.Services.AddCors(options => -{ - options.AddPolicy("MiniApiCors", policy => - { - ConfigureCorsPolicy(policy, miniOrigins); - }); -}); - -// 7. 构建应用并配置中间件管道 -var app = builder.Build(); - -app.UseCors("MiniApiCors"); -app.UseTenantResolution(); -app.UseSharedWebCore(); -if (app.Environment.IsDevelopment()) -{ - app.UseSharedSwagger(); -} - -app.MapHealthChecks("/healthz"); -app.MapPrometheusScrapingEndpoint(); -app.MapControllers(); -app.Run(); - -// 8. 解析配置中的 CORS 域名 -static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionKey) -{ - var origins = configuration.GetSection(sectionKey).Get(); - return origins? - .Where(origin => !string.IsNullOrWhiteSpace(origin)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray() ?? []; -} - -// 9. 构建 CORS 策略 -static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins) -{ - if (origins.Length == 0) - { - policy.AllowAnyOrigin(); - } - else - { - policy.WithOrigins(origins) - .AllowCredentials(); - } - - policy - .AllowAnyHeader() - .AllowAnyMethod(); -} diff --git a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj deleted file mode 100644 index 653f6a0..0000000 Binary files a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj and /dev/null differ diff --git a/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json b/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json deleted file mode 100644 index 7fd933a..0000000 --- a/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "Database": { - "DataSources": { - "AppDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", - "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" - ], - "CommandTimeoutSeconds": 30, - "MaxRetryCount": 3, - "MaxRetryDelaySeconds": 5 - }, - "IdentityDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", - "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" - ], - "CommandTimeoutSeconds": 30, - "MaxRetryCount": 3, - "MaxRetryDelaySeconds": 5 - }, - "DictionaryDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", - "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" - ], - "CommandTimeoutSeconds": 30, - "MaxRetryCount": 3, - "MaxRetryDelaySeconds": 5 - }, - "LogsDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", - "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" - ], - "CommandTimeoutSeconds": 30, - "MaxRetryCount": 3, - "MaxRetryDelaySeconds": 5 - } - } - }, - "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false", - "Identity": { - "Jwt": { - "Issuer": "takeout-saas", - "Audience": "takeout-saas-clients", - "Secret": "psZEx_O##]Mq(W.1$?8Aia*LM03sXGGx", - "AccessTokenExpirationMinutes": 120, - "RefreshTokenExpirationMinutes": 10080 - }, - "LoginRateLimit": { - "WindowSeconds": 60, - "MaxAttempts": 5 - }, - "RefreshTokenStore": { - "Prefix": "identity:refresh:" - } - }, - "Dictionary": { - "Cache": { - "SlidingExpiration": "00:30:00" - } - }, - "Tenancy": { - "TenantIdHeaderName": "X-Tenant-Id", - "TenantCodeHeaderName": "X-Tenant-Code", - "IgnoredPaths": [ - "/health" - ], - "RootDomain": "" - }, - "Storage": { - "Provider": "TencentCos", - "CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", - "TencentCos": { - "SecretId": "AKID52mHageV8ZnnY5NRL3Xq270fAcw2vb5R", - "SecretKey": "B8sPitsiEXcS4ScaMvGMErFOL3ZqsgFa", - "Region": "ap-beijing", - "Bucket": "saas2025-1388556178", - "Endpoint": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", - "CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", - "UseHttps": true, - "ForcePathStyle": false - }, - "QiniuKodo": { - "AccessKey": "QINIU_ACCESS_KEY", - "SecretKey": "QINIU_SECRET_KEY", - "Bucket": "takeout-files", - "DownloadDomain": "", - "Endpoint": "", - "UseHttps": true, - "SignedUrlExpirationMinutes": 30 - }, - "AliyunOss": { - "AccessKeyId": "OSS_ACCESS_KEY_ID", - "AccessKeySecret": "OSS_ACCESS_KEY_SECRET", - "Endpoint": "https://oss-cn-hangzhou.aliyuncs.com", - "Bucket": "takeout-files", - "CdnBaseUrl": "", - "UseHttps": true - }, - "Security": { - "MaxFileSizeBytes": 10485760, - "AllowedImageExtensions": [ - ".jpg", - ".jpeg", - ".png", - ".webp", - ".gif" - ], - "AllowedFileExtensions": [ - ".jpg", - ".jpeg", - ".png", - ".webp", - ".gif", - ".pdf" - ], - "DefaultUrlExpirationMinutes": 30, - "EnableRefererValidation": true, - "AllowedReferers": [ - "https://admin.example.com", - "https://miniapp.example.com" - ], - "AntiLeechTokenSecret": "ReplaceWithARandomToken" - } - }, - "Sms": { - "Provider": "Tencent", - "DefaultSignName": "外卖SaaS", - "UseMock": true, - "Tencent": { - "SecretId": "TENCENT_SMS_SECRET_ID", - "SecretKey": "TENCENT_SMS_SECRET_KEY", - "SdkAppId": "1400000000", - "SignName": "外卖SaaS", - "Region": "ap-beijing", - "Endpoint": "https://sms.tencentcloudapi.com" - }, - "Aliyun": { - "AccessKeyId": "ALIYUN_SMS_AK", - "AccessKeySecret": "ALIYUN_SMS_SK", - "Endpoint": "dysmsapi.aliyuncs.com", - "SignName": "外卖SaaS", - "Region": "cn-hangzhou" - }, - "SceneTemplates": { - "login": "LOGIN_TEMPLATE_ID", - "register": "REGISTER_TEMPLATE_ID", - "reset": "RESET_TEMPLATE_ID" - }, - "VerificationCode": { - "CodeLength": 6, - "ExpireMinutes": 5, - "CooldownSeconds": 60, - "CachePrefix": "sms:code" - } - }, - "RabbitMQ": { - "Host": "49.232.6.45", - "Port": 5672, - "Username": "Admin", - "Password": "MsuMshk112233", - "VirtualHost": "/", - "Exchange": "takeout.events", - "ExchangeType": "topic", - "PrefetchCount": 20 - }, - "Otel": { - "Endpoint": "", - "Sampling": "ParentBasedAlwaysOn", - "UseConsoleExporter": true - } -} - diff --git a/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json b/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json deleted file mode 100644 index 7fd933a..0000000 --- a/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "Database": { - "DataSources": { - "AppDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", - "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" - ], - "CommandTimeoutSeconds": 30, - "MaxRetryCount": 3, - "MaxRetryDelaySeconds": 5 - }, - "IdentityDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", - "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" - ], - "CommandTimeoutSeconds": 30, - "MaxRetryCount": 3, - "MaxRetryDelaySeconds": 5 - }, - "DictionaryDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", - "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" - ], - "CommandTimeoutSeconds": 30, - "MaxRetryCount": 3, - "MaxRetryDelaySeconds": 5 - }, - "LogsDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", - "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" - ], - "CommandTimeoutSeconds": 30, - "MaxRetryCount": 3, - "MaxRetryDelaySeconds": 5 - } - } - }, - "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false", - "Identity": { - "Jwt": { - "Issuer": "takeout-saas", - "Audience": "takeout-saas-clients", - "Secret": "psZEx_O##]Mq(W.1$?8Aia*LM03sXGGx", - "AccessTokenExpirationMinutes": 120, - "RefreshTokenExpirationMinutes": 10080 - }, - "LoginRateLimit": { - "WindowSeconds": 60, - "MaxAttempts": 5 - }, - "RefreshTokenStore": { - "Prefix": "identity:refresh:" - } - }, - "Dictionary": { - "Cache": { - "SlidingExpiration": "00:30:00" - } - }, - "Tenancy": { - "TenantIdHeaderName": "X-Tenant-Id", - "TenantCodeHeaderName": "X-Tenant-Code", - "IgnoredPaths": [ - "/health" - ], - "RootDomain": "" - }, - "Storage": { - "Provider": "TencentCos", - "CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", - "TencentCos": { - "SecretId": "AKID52mHageV8ZnnY5NRL3Xq270fAcw2vb5R", - "SecretKey": "B8sPitsiEXcS4ScaMvGMErFOL3ZqsgFa", - "Region": "ap-beijing", - "Bucket": "saas2025-1388556178", - "Endpoint": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", - "CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", - "UseHttps": true, - "ForcePathStyle": false - }, - "QiniuKodo": { - "AccessKey": "QINIU_ACCESS_KEY", - "SecretKey": "QINIU_SECRET_KEY", - "Bucket": "takeout-files", - "DownloadDomain": "", - "Endpoint": "", - "UseHttps": true, - "SignedUrlExpirationMinutes": 30 - }, - "AliyunOss": { - "AccessKeyId": "OSS_ACCESS_KEY_ID", - "AccessKeySecret": "OSS_ACCESS_KEY_SECRET", - "Endpoint": "https://oss-cn-hangzhou.aliyuncs.com", - "Bucket": "takeout-files", - "CdnBaseUrl": "", - "UseHttps": true - }, - "Security": { - "MaxFileSizeBytes": 10485760, - "AllowedImageExtensions": [ - ".jpg", - ".jpeg", - ".png", - ".webp", - ".gif" - ], - "AllowedFileExtensions": [ - ".jpg", - ".jpeg", - ".png", - ".webp", - ".gif", - ".pdf" - ], - "DefaultUrlExpirationMinutes": 30, - "EnableRefererValidation": true, - "AllowedReferers": [ - "https://admin.example.com", - "https://miniapp.example.com" - ], - "AntiLeechTokenSecret": "ReplaceWithARandomToken" - } - }, - "Sms": { - "Provider": "Tencent", - "DefaultSignName": "外卖SaaS", - "UseMock": true, - "Tencent": { - "SecretId": "TENCENT_SMS_SECRET_ID", - "SecretKey": "TENCENT_SMS_SECRET_KEY", - "SdkAppId": "1400000000", - "SignName": "外卖SaaS", - "Region": "ap-beijing", - "Endpoint": "https://sms.tencentcloudapi.com" - }, - "Aliyun": { - "AccessKeyId": "ALIYUN_SMS_AK", - "AccessKeySecret": "ALIYUN_SMS_SK", - "Endpoint": "dysmsapi.aliyuncs.com", - "SignName": "外卖SaaS", - "Region": "cn-hangzhou" - }, - "SceneTemplates": { - "login": "LOGIN_TEMPLATE_ID", - "register": "REGISTER_TEMPLATE_ID", - "reset": "RESET_TEMPLATE_ID" - }, - "VerificationCode": { - "CodeLength": 6, - "ExpireMinutes": 5, - "CooldownSeconds": 60, - "CachePrefix": "sms:code" - } - }, - "RabbitMQ": { - "Host": "49.232.6.45", - "Port": 5672, - "Username": "Admin", - "Password": "MsuMshk112233", - "VirtualHost": "/", - "Exchange": "takeout.events", - "ExchangeType": "topic", - "PrefetchCount": 20 - }, - "Otel": { - "Endpoint": "", - "Sampling": "ParentBasedAlwaysOn", - "UseConsoleExporter": true - } -} - diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/AuthController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/AuthController.cs new file mode 100644 index 0000000..58302e9 --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/AuthController.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; + +namespace TakeoutSaaS.TenantApi.Controllers; + +/// +/// 租户管理端登录认证。 +/// +/// 仅允许租户管理员登录获取 Token。 +[ApiVersion("1.0")] +[Authorize] +[Route("api/tenant/v{version:apiVersion}/auth")] +public sealed class AuthController(IAdminAuthService authService) : BaseApiController +{ + /// + /// 账号密码登录。 + /// + /// 登录请求。 + /// 取消标记。 + /// 访问令牌与刷新令牌。 + [HttpPost("login")] + [AllowAnonymous] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Login([FromBody] AdminLoginRequest request, CancellationToken cancellationToken) + { + // 1. 调用认证服务登录 + var response = await authService.LoginAsync(request, cancellationToken); + + // 2. 返回令牌 + return ApiResponse.Ok(response); + } + + /// + /// 简化登录:支持使用“账号@手机号”自动解析租户后登录。 + /// + /// 登录请求。 + /// 取消标记。 + /// 访问令牌与刷新令牌。 + [HttpPost("login/simple")] + [AllowAnonymous] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> LoginSimple([FromBody] AdminLoginRequest request, CancellationToken cancellationToken) + { + // 1. 调用认证服务完成简化登录 + var response = await authService.LoginSimpleAsync(request, cancellationToken); + + // 2. 返回令牌 + return ApiResponse.Ok(response); + } + + /// + /// 刷新 Token。 + /// + /// 刷新令牌请求。 + /// 取消标记。 + /// 新的访问令牌与刷新令牌。 + [HttpPost("refresh")] + [AllowAnonymous] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) + { + // 1. 刷新 Token + var response = await authService.RefreshTokenAsync(request, cancellationToken); + + // 2. 返回新的令牌 + return ApiResponse.Ok(response); + } +} diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/HealthController.cs similarity index 62% rename from src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs rename to src/Api/TakeoutSaaS.TenantApi/Controllers/HealthController.cs index e19775c..b4aca57 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs +++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/HealthController.cs @@ -3,26 +3,26 @@ using Microsoft.AspNetCore.Mvc; using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Web.Api; -namespace TakeoutSaaS.MiniApi.Controllers; +namespace TakeoutSaaS.TenantApi.Controllers; /// -/// 小程序端 - 健康检查。 +/// 租户管理端 - 健康检查。 /// [ApiVersion("1.0")] [AllowAnonymous] -[Route("api/mini/v{version:apiVersion}/[controller]")] -public class HealthController : BaseApiController +[Route("api/tenant/v{version:apiVersion}/[controller]")] +public sealed class HealthController : BaseApiController { /// /// 获取服务健康状态。 /// - /// 健康状态 + /// 健康状态。 [HttpGet] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public ApiResponse Get() { // 1. 构造健康状态 - var payload = new { status = "OK", service = "MiniApi", time = DateTime.UtcNow }; + var payload = new { status = "OK", service = "TenantApi", time = DateTime.UtcNow }; // 2. 返回健康响应 return ApiResponse.Ok(payload); diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/MeController.cs similarity index 78% rename from src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs rename to src/Api/TakeoutSaaS.TenantApi/Controllers/MeController.cs index 7411644..dc06827 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Controllers/MeController.cs +++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/MeController.cs @@ -7,20 +7,18 @@ using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Web.Api; using TakeoutSaaS.Shared.Web.Security; -namespace TakeoutSaaS.MiniApi.Controllers; +namespace TakeoutSaaS.TenantApi.Controllers; /// -/// 当前用户信息 +/// 当前租户管理员信息。 /// -/// 提供小程序端当前用户档案查询。 -/// 小程序认证服务 [ApiVersion("1.0")] -[Authorize] -[Route("api/mini/v{version:apiVersion}/me")] -public sealed class MeController(IMiniAuthService authService) : BaseApiController +[Authorize(Roles = "tenant-admin")] +[Route("api/tenant/v{version:apiVersion}/me")] +public sealed class MeController(IAdminAuthService authService) : BaseApiController { /// - /// 获取用户档案 + /// 获取当前用户档案。 /// /// 取消标记。 /// 当前用户档案信息。 diff --git a/src/Api/TakeoutSaaS.TenantApi/Dockerfile b/src/Api/TakeoutSaaS.TenantApi/Dockerfile new file mode 100644 index 0000000..3fc8c2e --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Dockerfile @@ -0,0 +1,12 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src +COPY . . +RUN dotnet restore src/Api/TakeoutSaaS.TenantApi/TakeoutSaaS.TenantApi.csproj +RUN dotnet publish src/Api/TakeoutSaaS.TenantApi/TakeoutSaaS.TenantApi.csproj -c Release -o /app/publish + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app/publish . +EXPOSE 7903 +ENV ASPNETCORE_URLS=http://+:7903 +ENTRYPOINT ["dotnet", "TakeoutSaaS.TenantApi.dll"] diff --git a/src/Api/TakeoutSaaS.UserApi/Program.cs b/src/Api/TakeoutSaaS.TenantApi/Program.cs similarity index 53% rename from src/Api/TakeoutSaaS.UserApi/Program.cs rename to src/Api/TakeoutSaaS.TenantApi/Program.cs index 25bd2a7..128d909 100644 --- a/src/Api/TakeoutSaaS.UserApi/Program.cs +++ b/src/Api/TakeoutSaaS.TenantApi/Program.cs @@ -1,13 +1,21 @@ +using Asp.Versioning; +using Asp.Versioning.ApiExplorer; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Shared.Abstractions.Security; +using TakeoutSaaS.Shared.Abstractions.Serialization; +using TakeoutSaaS.Shared.Web.Filters; +using TakeoutSaaS.Shared.Web.Security; using Microsoft.AspNetCore.Cors.Infrastructure; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Serilog; +using TakeoutSaaS.Application.App.Extensions; +using TakeoutSaaS.Application.Identity.Extensions; +using TakeoutSaaS.Infrastructure.App.Extensions; using TakeoutSaaS.Infrastructure.Identity.Extensions; -using TakeoutSaaS.Module.Dictionary.Extensions; +using TakeoutSaaS.Module.Authorization.Extensions; using TakeoutSaaS.Module.Tenancy.Extensions; -using TakeoutSaaS.Shared.Abstractions.Ids; -using TakeoutSaaS.Shared.Kernel.Ids; using TakeoutSaaS.Shared.Web.Extensions; using TakeoutSaaS.Shared.Web.Swagger; @@ -16,48 +24,83 @@ var builder = WebApplication.CreateBuilder(args); const string logTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [TraceId:{TraceId}] [SpanId:{SpanId}] [Service:{Service}] {SourceContext} {Message:lj}{NewLine}{Exception}"; var isDevelopment = builder.Environment.IsDevelopment(); -// 2. 注册雪花 ID 生成器与 Serilog -builder.Services.AddSingleton(_ => new SnowflakeIdGenerator()); +// 2. 配置 Serilog builder.Host.UseSerilog((_, _, configuration) => { configuration .Enrich.FromLogContext() - .Enrich.WithProperty("Service", "UserApi") + .Enrich.WithProperty("Service", "TenantApi") .WriteTo.Console(outputTemplate: logTemplate) .WriteTo.File( - "logs/user-api-.log", + "logs/tenant-api-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7, shared: true, outputTemplate: logTemplate); }); -// 3. 注册通用 Web 能力,开发环境启用 Swagger -builder.Services.AddSharedWebCore(); +// 3. 注册 Web Core(控制器、API 版本化、模型验证、雪花 ID 序列化等) +builder.Services.AddHttpContextAccessor(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddScoped(); +builder.Services + .AddControllers(options => + { + options.Filters.Add(); + options.Filters.Add(); + }) + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.Add(new SnowflakeIdJsonConverter()); + options.JsonSerializerOptions.Converters.Add(new NullableSnowflakeIdJsonConverter()); + }); +builder.Services.Configure(options => +{ + options.SuppressModelStateInvalidFilter = true; +}); +var apiVersioningBuilder = builder.Services.AddApiVersioning(options => +{ + options.AssumeDefaultVersionWhenUnspecified = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + options.ReportApiVersions = true; +}); +apiVersioningBuilder.AddApiExplorer(setup => +{ + setup.GroupNameFormat = "'v'VVV"; + setup.SubstituteApiVersionInUrl = true; +}); + +// 4. (空行后) 开发环境启用 Swagger(含 JWT 鉴权按钮) if (isDevelopment) { builder.Services.AddSharedSwagger(options => { - options.Title = "外卖SaaS - 用户端"; - options.Description = "C 端用户 API 文档"; + options.Title = "外卖SaaS - 租户管理端"; + options.Description = "租户管理员 API 文档"; options.EnableAuthorization = true; }); } -// 4. 注册多租户与健康检查 +// 5. 注册多租户解析、鉴权授权与权限策略 builder.Services.AddTenantResolution(builder.Configuration); builder.Services.AddJwtAuthentication(builder.Configuration); builder.Services.AddAuthorization(); -builder.Services.AddDictionaryModule(builder.Configuration); +builder.Services.AddPermissionAuthorization(); builder.Services.AddHealthChecks(); -// 5. 配置 OpenTelemetry 采集 +// 6. 注册应用层与基础设施(仅租户侧所需) +builder.Services.AddAppApplication(); +builder.Services.AddIdentityApplication(enableMiniSupport: false); +builder.Services.AddAppInfrastructure(builder.Configuration); +builder.Services.AddIdentityInfrastructure(builder.Configuration, enableMiniFeatures: false, enableAdminSeed: false); + +// 7. 配置 OpenTelemetry 采集 var otelSection = builder.Configuration.GetSection("Otel"); var otelEndpoint = otelSection.GetValue("Endpoint"); var useConsoleExporter = otelSection.GetValue("UseConsoleExporter") ?? builder.Environment.IsDevelopment(); builder.Services.AddOpenTelemetry() .ConfigureResource(resource => resource.AddService( - serviceName: "TakeoutSaaS.UserApi", + serviceName: "TakeoutSaaS.TenantApi", serviceVersion: "1.0.0", serviceInstanceId: Environment.MachineName)) .WithTracing(tracing => @@ -67,7 +110,7 @@ builder.Services.AddOpenTelemetry() .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddEntityFrameworkCoreInstrumentation(); - + // 1. (空行后) 配置 OTLP 导出 if (!string.IsNullOrWhiteSpace(otelEndpoint)) { tracing.AddOtlpExporter(exporter => @@ -75,7 +118,7 @@ builder.Services.AddOpenTelemetry() exporter.Endpoint = new Uri(otelEndpoint); }); } - + // 2. (空行后) 配置 Console 导出 if (useConsoleExporter) { tracing.AddConsoleExporter(); @@ -88,7 +131,7 @@ builder.Services.AddOpenTelemetry() .AddHttpClientInstrumentation() .AddRuntimeInstrumentation() .AddPrometheusExporter(); - + // 1. (空行后) 配置 OTLP 导出 if (!string.IsNullOrWhiteSpace(otelEndpoint)) { metrics.AddOtlpExporter(exporter => @@ -96,42 +139,50 @@ builder.Services.AddOpenTelemetry() exporter.Endpoint = new Uri(otelEndpoint); }); } - + // 2. (空行后) 配置 Console 导出 if (useConsoleExporter) { metrics.AddConsoleExporter(); } }); -// 6. 配置 CORS -var userOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:User"); +// 8. 配置 CORS +var tenantOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Tenant"); builder.Services.AddCors(options => { - options.AddPolicy("UserApiCors", policy => + options.AddPolicy("TenantApiCors", policy => { - ConfigureCorsPolicy(policy, userOrigins); + ConfigureCorsPolicy(policy, tenantOrigins); }); }); -// 7. 构建应用并配置中间件管道 +// 9. 构建应用并配置中间件管道 var app = builder.Build(); +app.UseCors("TenantApiCors"); -app.UseCors("UserApiCors"); -app.UseTenantResolution(); -app.UseSharedWebCore(); +// 1. (空行后) 先完成身份认证,确保租户解析优先使用 Token Claim app.UseAuthentication(); + +// 2. (空行后) 解析并注入租户上下文(已认证请求不允许 Header 覆盖) +app.UseTenantResolution(); + +// 3. (空行后) 通用 Web Core 中间件(异常、ProblemDetails、日志等) +app.UseSharedWebCore(); + +// 4. (空行后) 执行授权 app.UseAuthorization(); + +// 5. (空行后) 开发环境启用 Swagger if (app.Environment.IsDevelopment()) { app.UseSharedSwagger(); } - app.MapHealthChecks("/healthz"); app.MapPrometheusScrapingEndpoint(); app.MapControllers(); app.Run(); -// 8. 解析配置中的 CORS 域名 +// 10. 解析配置中的 CORS 域名 static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionKey) { var origins = configuration.GetSection(sectionKey).Get(); @@ -141,7 +192,7 @@ static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionK .ToArray() ?? []; } -// 9. 构建 CORS 策略 +// 10. 构建 CORS 策略 static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins) { if (origins.Length == 0) @@ -153,7 +204,7 @@ static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins) policy.WithOrigins(origins) .AllowCredentials(); } - + // 1. (空行后) 放行通用 Header 与 Method policy .AllowAnyHeader() .AllowAnyMethod(); diff --git a/src/Api/TakeoutSaaS.MiniApi/Properties/launchSettings.json b/src/Api/TakeoutSaaS.TenantApi/Properties/launchSettings.json similarity index 68% rename from src/Api/TakeoutSaaS.MiniApi/Properties/launchSettings.json rename to src/Api/TakeoutSaaS.TenantApi/Properties/launchSettings.json index a89a59c..78189ba 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Properties/launchSettings.json +++ b/src/Api/TakeoutSaaS.TenantApi/Properties/launchSettings.json @@ -1,12 +1,12 @@ { "profiles": { - "TakeoutSaaS.MiniApi": { + "TakeoutSaaS.TenantApi": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "http://localhost:2681" + "applicationUrl": "http://localhost:2683" } } -} \ No newline at end of file +} diff --git a/src/Api/TakeoutSaaS.TenantApi/TakeoutSaaS.TenantApi.csproj b/src/Api/TakeoutSaaS.TenantApi/TakeoutSaaS.TenantApi.csproj new file mode 100644 index 0000000..781f2b0 --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/TakeoutSaaS.TenantApi.csproj @@ -0,0 +1,30 @@ + + + net10.0 + enable + enable + true + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json b/src/Api/TakeoutSaaS.TenantApi/appsettings.Development.json similarity index 75% rename from src/Api/TakeoutSaaS.UserApi/appsettings.Development.json rename to src/Api/TakeoutSaaS.TenantApi/appsettings.Development.json index 0fe565e..928ab03 100644 --- a/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.TenantApi/appsettings.Development.json @@ -19,15 +19,6 @@ "MaxRetryCount": 3, "MaxRetryDelaySeconds": 5 }, - "DictionaryDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", - "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" - ], - "CommandTimeoutSeconds": 30, - "MaxRetryCount": 3, - "MaxRetryDelaySeconds": 5 - }, "LogsDatabase": { "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ @@ -39,7 +30,9 @@ } } }, - "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false", + "ConnectionStrings": { + "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false" + }, "Identity": { "Jwt": { "Issuer": "takeout-saas", @@ -56,23 +49,21 @@ "Prefix": "identity:refresh:" } }, - "Dictionary": { - "Cache": { - "SlidingExpiration": "00:30:00" - } - }, "Tenancy": { "TenantIdHeaderName": "X-Tenant-Id", "TenantCodeHeaderName": "X-Tenant-Code", "IgnoredPaths": [ - "/health" + "/health", + "/healthz" ], "RootDomain": "" }, + "Cors": { + "Tenant": [] + }, "Otel": { "Endpoint": "", "Sampling": "ParentBasedAlwaysOn", "UseConsoleExporter": true } } - diff --git a/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json b/src/Api/TakeoutSaaS.TenantApi/appsettings.Production.json similarity index 74% rename from src/Api/TakeoutSaaS.UserApi/appsettings.Production.json rename to src/Api/TakeoutSaaS.TenantApi/appsettings.Production.json index 0fe565e..00e5b40 100644 --- a/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json +++ b/src/Api/TakeoutSaaS.TenantApi/appsettings.Production.json @@ -19,15 +19,6 @@ "MaxRetryCount": 3, "MaxRetryDelaySeconds": 5 }, - "DictionaryDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", - "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" - ], - "CommandTimeoutSeconds": 30, - "MaxRetryCount": 3, - "MaxRetryDelaySeconds": 5 - }, "LogsDatabase": { "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ @@ -39,7 +30,9 @@ } } }, - "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false", + "ConnectionStrings": { + "Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false" + }, "Identity": { "Jwt": { "Issuer": "takeout-saas", @@ -56,23 +49,21 @@ "Prefix": "identity:refresh:" } }, - "Dictionary": { - "Cache": { - "SlidingExpiration": "00:30:00" - } - }, "Tenancy": { "TenantIdHeaderName": "X-Tenant-Id", "TenantCodeHeaderName": "X-Tenant-Code", "IgnoredPaths": [ - "/health" + "/health", + "/healthz" ], "RootDomain": "" }, + "Cors": { + "Tenant": [] + }, "Otel": { "Endpoint": "", "Sampling": "ParentBasedAlwaysOn", - "UseConsoleExporter": true + "UseConsoleExporter": false } } - diff --git a/src/Api/TakeoutSaaS.UserApi/Controllers/DictionaryController.cs b/src/Api/TakeoutSaaS.UserApi/Controllers/DictionaryController.cs deleted file mode 100644 index 3f3405d..0000000 --- a/src/Api/TakeoutSaaS.UserApi/Controllers/DictionaryController.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; -using TakeoutSaaS.Application.Dictionary.Contracts; -using TakeoutSaaS.Application.Dictionary.Models; -using TakeoutSaaS.Application.Dictionary.Services; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.UserApi.Controllers; - -/// -/// 字典查询接口。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/user/v{version:apiVersion}/dictionary")] -public sealed class DictionaryController(DictionaryQueryService queryService) : BaseApiController -{ - /// - /// 获取指定字典分组的合并结果。 - /// - [HttpGet("{code}")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> GetByCode(string code, CancellationToken cancellationToken) - { - Response.Headers[HeaderNames.CacheControl] = "max-age=1800"; - var result = await queryService.GetMergedDictionaryAsync(code, cancellationToken); - return ApiResponse>.Ok(result); - } - - /// - /// 批量获取字典分组。 - /// - [HttpPost("batch")] - [ProducesResponseType(typeof(ApiResponse>>), StatusCodes.Status200OK)] - public async Task>>> BatchGet( - [FromBody] DictionaryBatchQueryRequest request, - CancellationToken cancellationToken) - { - if (request.Codes.Count > 20) - { - throw new BusinessException(ErrorCodes.ValidationFailed, "最多支持 20 个字典编码"); - } - - var result = await queryService.BatchGetDictionariesAsync(request.Codes, cancellationToken); - return ApiResponse>>.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.UserApi/Controllers/HealthController.cs b/src/Api/TakeoutSaaS.UserApi/Controllers/HealthController.cs deleted file mode 100644 index 08fe92b..0000000 --- a/src/Api/TakeoutSaaS.UserApi/Controllers/HealthController.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.UserApi.Controllers; - -/// -/// 用户端 - 健康检查。 -/// -[ApiVersion("1.0")] -[AllowAnonymous] -[Route("api/user/v{version:apiVersion}/[controller]")] -public class HealthController : BaseApiController -{ - /// - /// 获取服务健康状态。 - /// - /// 健康状态 - [HttpGet] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public ApiResponse Get() - { - // 1. 构造健康状态 - var payload = new { status = "OK", service = "UserApi", time = DateTime.UtcNow }; - - // 2. 返回健康响应 - return ApiResponse.Ok(payload); - } -} diff --git a/src/Api/TakeoutSaaS.UserApi/Dockerfile b/src/Api/TakeoutSaaS.UserApi/Dockerfile deleted file mode 100644 index 22e0ab8..0000000 --- a/src/Api/TakeoutSaaS.UserApi/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY . . -RUN dotnet restore src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj -RUN dotnet publish src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj -c Release -o /app/publish - -FROM mcr.microsoft.com/dotnet/aspnet:10.0 -WORKDIR /app -COPY --from=build /app/publish . -EXPOSE 7901 -ENV ASPNETCORE_URLS=http://+:7901 -ENTRYPOINT ["dotnet", "TakeoutSaaS.UserApi.dll"] diff --git a/src/Api/TakeoutSaaS.UserApi/Properties/launchSettings.json b/src/Api/TakeoutSaaS.UserApi/Properties/launchSettings.json deleted file mode 100644 index bb5c543..0000000 --- a/src/Api/TakeoutSaaS.UserApi/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "TakeoutSaaS.UserApi": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:2682" - } - } -} \ No newline at end of file diff --git a/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj b/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj deleted file mode 100644 index ed127bd..0000000 Binary files a/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj and /dev/null differ diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ExportMerchantPdfQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ExportMerchantPdfQueryHandler.cs index 4519892..13d2252 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ExportMerchantPdfQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/ExportMerchantPdfQueryHandler.cs @@ -1,14 +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; @@ -21,31 +18,19 @@ public sealed class ExportMerchantPdfQueryHandler( IStoreRepository storeRepository, ITenantRepository tenantRepository, IMerchantExportService exportService, - ITenantProvider tenantProvider, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService) + ITenantProvider tenantProvider) : IRequestHandler { public async Task 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); + var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken); if (merchant == null) { throw new BusinessException(ErrorCodes.NotFound, "商户不存在"); } - if (!isSuperAdmin && merchant.TenantId != currentTenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "禁止导出其他租户商户"); - } - 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); diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantAuditHistoryQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantAuditHistoryQueryHandler.cs index 905cb54..3423417 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantAuditHistoryQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantAuditHistoryQueryHandler.cs @@ -1,12 +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; @@ -16,9 +13,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers; /// public sealed class GetMerchantAuditHistoryQueryHandler( IMerchantRepository merchantRepository, - ITenantProvider tenantProvider, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService) + ITenantProvider tenantProvider) : IRequestHandler> { /// @@ -27,23 +22,13 @@ public sealed class GetMerchantAuditHistoryQueryHandler( 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); + var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken); if (merchant == null) { throw new BusinessException(ErrorCodes.NotFound, "商户不存在"); } - if (!isSuperAdmin && merchant.TenantId != currentTenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "禁止访问其他租户的商户审核历史"); - } - var logs = await merchantRepository.GetAuditLogsAsync(merchant.Id, merchant.TenantId, cancellationToken); return logs.Select(MerchantMapping.ToDto).ToList(); } diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantChangeHistoryQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantChangeHistoryQueryHandler.cs index 35d3be5..7054a62 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantChangeHistoryQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantChangeHistoryQueryHandler.cs @@ -1,12 +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; @@ -16,9 +13,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers; /// public sealed class GetMerchantChangeHistoryQueryHandler( IMerchantRepository merchantRepository, - ITenantProvider tenantProvider, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService) + ITenantProvider tenantProvider) : IRequestHandler> { /// @@ -27,23 +22,13 @@ public sealed class GetMerchantChangeHistoryQueryHandler( 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); + var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken); if (merchant == null) { throw new BusinessException(ErrorCodes.NotFound, "商户不存在"); } - if (!isSuperAdmin && merchant.TenantId != currentTenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "禁止访问其他租户的商户变更历史"); - } - var logs = await merchantRepository.GetChangeLogsAsync(merchant.Id, merchant.TenantId, request.FieldName, cancellationToken); return logs.Select(MerchantMapping.ToDto).ToList(); } diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantDetailQueryHandler.cs index 69a6bcf..236ea6b 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantDetailQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantDetailQueryHandler.cs @@ -1,14 +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; @@ -20,9 +17,7 @@ public sealed class GetMerchantDetailQueryHandler( IMerchantRepository merchantRepository, IStoreRepository storeRepository, ITenantRepository tenantRepository, - ITenantProvider tenantProvider, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService) + ITenantProvider tenantProvider) : IRequestHandler { /// @@ -33,25 +28,15 @@ public sealed class GetMerchantDetailQueryHandler( /// 商户详情 DTO。 public async Task Handle(GetMerchantDetailQuery request, CancellationToken cancellationToken) { - // 1. 获取权限与商户 + // 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); + var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, 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 storeDtos = MerchantMapping.ToStoreDtos(stores); diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantListQueryHandler.cs index 4d0018e..f56f870 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantListQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/GetMerchantListQueryHandler.cs @@ -1,15 +1,12 @@ 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; @@ -21,9 +18,7 @@ public sealed class GetMerchantListQueryHandler( IMerchantRepository merchantRepository, IStoreRepository storeRepository, ITenantRepository tenantRepository, - ITenantProvider tenantProvider, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService) + ITenantProvider tenantProvider) : IRequestHandler> { /// @@ -31,17 +26,14 @@ public sealed class GetMerchantListQueryHandler( GetMerchantListQuery request, CancellationToken cancellationToken) { - // 1. 校验跨租户访问权限 + // 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) + if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId) { throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户查询商户"); } - var effectiveTenantId = isSuperAdmin ? request.TenantId : currentTenantId; + var effectiveTenantId = currentTenantId; // 2. 查询商户列表 var merchants = await merchantRepository.SearchAsync( diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs index 4626003..1a0eb65 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs @@ -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; @@ -25,7 +23,6 @@ public sealed class UpdateMerchantCommandHandler( ITenantRepository tenantRepository, ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService, ILogger logger) : IRequestHandler { @@ -39,24 +36,15 @@ public sealed class UpdateMerchantCommandHandler( // 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); + var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken); if (merchant == null) { return null; } - if (!isSuperAdmin && merchant.TenantId != currentTenantId) - { - return null; - } - // 3. 规范化输入 var name = NormalizeRequired(request.Name, "商户名称"); var contactPhone = NormalizeRequired(request.ContactPhone, "联系电话"); diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/StoreTenantAccess.cs b/src/Application/TakeoutSaaS.Application/App/Stores/StoreTenantAccess.cs index f63f77c..60377a2 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/StoreTenantAccess.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/StoreTenantAccess.cs @@ -1,45 +1,12 @@ using Microsoft.AspNetCore.Http; -using System; -using System.Linq; namespace TakeoutSaaS.Application.App.Stores; internal static class StoreTenantAccess { - private const string PermissionClaimType = "permission"; - private const string ViewAllStoresPermission = "store:read:all"; - private static readonly string[] PlatformRoleCodes = - { - "super-admin", - "SUPER_ADMIN", - "PlatformAdmin", - "platform-admin" - }; - public static bool ShouldIgnoreTenantFilter(IHttpContextAccessor httpContextAccessor) { - var httpContext = httpContextAccessor.HttpContext; - if (httpContext == null) - { - return false; - } - - var user = httpContext.User; - if (user?.Identity?.IsAuthenticated != true) - { - return false; - } - - if (PlatformRoleCodes.Any(user.IsInRole)) - { - return true; - } - - var permissions = user.FindAll(PermissionClaimType) - .Select(c => c.Value?.Trim()) - .Where(value => !string.IsNullOrWhiteSpace(value)) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - - return permissions.Contains(ViewAllStoresPermission); + // 1. 租户管理端不允许跨租户访问门店数据 + return false; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/SubscriptionTenantAccess.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/SubscriptionTenantAccess.cs index 356ada2..0458f4a 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/SubscriptionTenantAccess.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/SubscriptionTenantAccess.cs @@ -1,13 +1,9 @@ using Microsoft.AspNetCore.Http; -using System.Security.Claims; namespace TakeoutSaaS.Application.App.Subscriptions; internal static class SubscriptionTenantAccess { - private const string PermissionClaimType = "permission"; - private const string PlatformAdminRole = "PlatformAdmin"; - public static bool ShouldIgnoreTenantFilter(IHttpContextAccessor httpContextAccessor) { var httpContext = httpContextAccessor.HttpContext; @@ -16,24 +12,7 @@ internal static class SubscriptionTenantAccess // Background jobs / out-of-request execution should process across tenants. return true; } - - var user = httpContext.User; - if (user?.Identity?.IsAuthenticated != true) - { - return false; - } - - if (user.IsInRole(PlatformAdminRole)) - { - return true; - } - - var permissions = user.FindAll(PermissionClaimType) - .Select(c => c.Value?.Trim()) - .Where(v => !string.IsNullOrWhiteSpace(v)) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - - // Platform-level tenant permissions imply cross-tenant visibility. - return permissions.Contains("tenant:read"); + // (空行后) 请求上下文下强制不允许跨租户 + return false; } } diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAccessHelper.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAccessHelper.cs deleted file mode 100644 index 7b5b104..0000000 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAccessHelper.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace TakeoutSaaS.Application.Dictionary.Services; - -internal static class DictionaryAccessHelper -{ - internal static bool IsPlatformAdmin(IHttpContextAccessor httpContextAccessor) - { - var user = httpContextAccessor.HttpContext?.User; - if (user?.Identity?.IsAuthenticated != true) - { - return false; - } - - return user.IsInRole("PlatformAdmin") || - user.IsInRole("platform-admin") || - user.IsInRole("super-admin") || - user.IsInRole("SUPER_ADMIN"); - } -} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs index 2f62030..b657b92 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; -using Microsoft.AspNetCore.Http; using System.Security.Cryptography; using TakeoutSaaS.Application.Dictionary.Abstractions; using TakeoutSaaS.Application.Dictionary.Contracts; @@ -20,7 +19,6 @@ public sealed class DictionaryAppService( IDictionaryRepository repository, IDictionaryCache cache, ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, ILogger logger) : IDictionaryAppService { /// @@ -356,17 +354,20 @@ public sealed class DictionaryAppService( private void EnsureScopePermission(DictionaryScope scope) { var tenantId = tenantProvider.GetCurrentTenantId(); - if (scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor)) + + // 1. (空行后) 租户端不允许操作系统字典 + if (scope == DictionaryScope.System && tenantId != 0) { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); + throw new BusinessException(ErrorCodes.Forbidden, "租户端不允许操作系统字典"); } } private void EnsurePlatformTenant(long tenantId) { - if (tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor)) + // 1. (空行后) 系统字典只能在平台租户(TenantId=0)上下文中操作 + if (tenantId != 0) { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); + throw new BusinessException(ErrorCodes.Forbidden, "租户端不允许操作系统字典"); } } diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryCommandService.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryCommandService.cs index fc4e65d..c34a8db 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryCommandService.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryCommandService.cs @@ -1,6 +1,5 @@ using System.Security.Cryptography; using Microsoft.Extensions.Logging; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.Dictionary.Abstractions; using TakeoutSaaS.Application.Dictionary.Contracts; using TakeoutSaaS.Application.Dictionary.Models; @@ -22,7 +21,6 @@ public sealed class DictionaryCommandService( IDictionaryItemRepository itemRepository, IDictionaryHybridCache cache, ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, ILogger logger) { /// @@ -231,14 +229,16 @@ public sealed class DictionaryCommandService( var tenantId = tenantProvider.GetCurrentTenantId(); if (scope == DictionaryScope.System) { - if (tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor)) + // 1. (空行后) 租户端禁止写入系统字典 + if (tenantId != 0) { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可创建系统字典"); + throw new BusinessException(ErrorCodes.Forbidden, "租户端不允许创建系统字典"); } return 0; } + // 2. (空行后) 业务字典必须在租户上下文中创建 if (tenantId == 0) { throw new BusinessException(ErrorCodes.BadRequest, "业务字典必须在租户上下文中创建"); @@ -250,11 +250,14 @@ public sealed class DictionaryCommandService( private void EnsureGroupAccess(DictionaryGroup group) { var tenantId = tenantProvider.GetCurrentTenantId(); - if (group.Scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor)) + + // 1. (空行后) 租户端不允许操作系统字典 + if (group.Scope == DictionaryScope.System && tenantId != 0) { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); + throw new BusinessException(ErrorCodes.Forbidden, "租户端不允许操作系统字典"); } + // 2. (空行后) 业务字典必须属于当前租户 if (group.Scope == DictionaryScope.Business && tenantId != group.TenantId) { throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他租户字典"); diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryImportExportService.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryImportExportService.cs index ae62ad7..9e085a7 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryImportExportService.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryImportExportService.cs @@ -14,7 +14,6 @@ using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; using Microsoft.Extensions.Logging; -using Microsoft.AspNetCore.Http; namespace TakeoutSaaS.Application.Dictionary.Services; @@ -30,7 +29,6 @@ public sealed class DictionaryImportExportService( IDictionaryHybridCache cache, ITenantProvider tenantProvider, ICurrentUserAccessor currentUser, - IHttpContextAccessor httpContextAccessor, ILogger logger) { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); @@ -426,11 +424,14 @@ public sealed class DictionaryImportExportService( private void EnsureGroupAccess(DictionaryGroup group) { var tenantId = tenantProvider.GetCurrentTenantId(); - if (group.Scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor)) + + // 1. (空行后) 租户端不允许操作系统字典 + if (group.Scope == DictionaryScope.System && tenantId != 0) { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); + throw new BusinessException(ErrorCodes.Forbidden, "租户端不允许操作系统字典"); } + // 2. (空行后) 业务字典必须属于当前租户 if (group.Scope == DictionaryScope.Business && tenantId != group.TenantId) { throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他租户字典"); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs index 5a56dc0..1ad6573 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs @@ -31,24 +31,18 @@ public sealed class BatchIdentityUserOperationCommandHandler( /// public async Task Handle(BatchIdentityUserOperationCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 + // 1. 获取当前租户与操作者档案 var currentTenantId = tenantProvider.GetCurrentTenantId(); var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - // 2. 校验跨租户访问权限 - if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) + // 2. 校验跨租户访问 + if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId) { throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户批量操作用户"); } - if (isSuperAdmin && !request.TenantId.HasValue) - { - throw new BusinessException(ErrorCodes.BadRequest, "批量操作必须指定租户"); - } - // 3. 解析用户 ID 列表 - var tenantId = request.TenantId ?? currentTenantId; + var tenantId = currentTenantId; var userIds = ParseIds(request.UserIds, "用户"); if (userIds.Length == 0) { @@ -63,7 +57,7 @@ public sealed class BatchIdentityUserOperationCommandHandler( // 4. 查询目标用户集合 var includeDeleted = request.Operation == IdentityUserBatchOperation.Restore; - var users = await identityUserRepository.GetForUpdateByIdsAsync(tenantId, userIds, includeDeleted, isSuperAdmin, cancellationToken); + var users = await identityUserRepository.GetForUpdateByIdsAsync(tenantId, userIds, includeDeleted, false, cancellationToken); var usersById = users.ToDictionary(user => user.Id, user => user, EqualityComparer.Default); // 5. 预计算租户管理员约束 @@ -85,7 +79,7 @@ public sealed class BatchIdentityUserOperationCommandHandler( IncludeDeleted = false, Page = 1, PageSize = 1 - }, isSuperAdmin, cancellationToken)).Total; + }, false, cancellationToken)).Total; var remainingActiveAdmins = activeAdminCount; // 6. 执行批量操作 diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs index 0390a09..41d5135 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs @@ -28,27 +28,24 @@ public sealed class ChangeIdentityUserStatusCommandHandler( /// public async Task Handle(ChangeIdentityUserStatusCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 + // 1. 获取当前租户与操作者档案 var currentTenantId = tenantProvider.GetCurrentTenantId(); var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - // 2. 校验跨租户访问权限 - if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) + // 2. 校验跨租户访问 + if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId) { throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户修改用户状态"); } // 3. 查询用户实体 - var user = isSuperAdmin - ? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken) - : await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); + var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { return false; } - if (!isSuperAdmin && user.TenantId != currentTenantId) + if (user.TenantId != currentTenantId) { return false; } @@ -56,7 +53,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler( // 4. 校验租户管理员保留规则 if (request.Status == IdentityUserStatus.Disabled && user.Status == IdentityUserStatus.Active) { - await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken); + await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, cancellationToken); } // 5. 更新状态 @@ -114,7 +111,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler( return true; } - private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, bool ignoreTenantFilter, CancellationToken cancellationToken) + private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, CancellationToken cancellationToken) { // 1. 获取租户管理员角色 var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken); @@ -140,7 +137,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler( Page = 1, PageSize = 1 }; - var result = await identityUserRepository.SearchPagedAsync(filter, ignoreTenantFilter, cancellationToken); + var result = await identityUserRepository.SearchPagedAsync(filter, false, cancellationToken); if (result.Total <= 1) { throw new BusinessException(ErrorCodes.Conflict, "至少保留一个管理员"); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs index 00d38c9..b031905 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs @@ -36,19 +36,18 @@ public sealed class CreateIdentityUserCommandHandler( /// public async Task Handle(CreateIdentityUserCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 + // 1. 获取当前租户与操作者档案 var currentTenantId = tenantProvider.GetCurrentTenantId(); var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - // 2. 校验跨租户访问权限 - if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) + // 2. 校验跨租户访问 + if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId) { throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户创建用户"); } // 3. 规范化输入并准备校验 - var tenantId = isSuperAdmin ? request.TenantId ?? currentTenantId : currentTenantId; + var tenantId = currentTenantId; var account = request.Account.Trim(); var displayName = request.DisplayName.Trim(); var phone = string.IsNullOrWhiteSpace(request.Phone) ? null : request.Phone.Trim(); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs index e452305..e874644 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs @@ -28,27 +28,24 @@ public sealed class DeleteIdentityUserCommandHandler( /// public async Task Handle(DeleteIdentityUserCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 + // 1. 获取当前租户与操作者档案 var currentTenantId = tenantProvider.GetCurrentTenantId(); var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - // 2. 校验跨租户访问权限 - if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) + // 2. 校验跨租户访问 + if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId) { throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户删除用户"); } // 3. 查询用户实体 - var user = isSuperAdmin - ? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken) - : await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); + var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { return false; } - if (!isSuperAdmin && user.TenantId != currentTenantId) + if (user.TenantId != currentTenantId) { return false; } @@ -56,7 +53,7 @@ public sealed class DeleteIdentityUserCommandHandler( // 4. 校验租户管理员保留规则 if (user.Status == IdentityUserStatus.Active) { - await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken); + await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, cancellationToken); } // 5. 构建操作日志消息 @@ -88,7 +85,7 @@ public sealed class DeleteIdentityUserCommandHandler( return true; } - private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, bool ignoreTenantFilter, CancellationToken cancellationToken) + private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, CancellationToken cancellationToken) { // 1. 获取租户管理员角色 var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken); @@ -114,7 +111,7 @@ public sealed class DeleteIdentityUserCommandHandler( Page = 1, PageSize = 1 }; - var result = await identityUserRepository.SearchPagedAsync(filter, ignoreTenantFilter, cancellationToken); + var result = await identityUserRepository.SearchPagedAsync(filter, false, cancellationToken); if (result.Total <= 1) { throw new BusinessException(ErrorCodes.Conflict, "至少保留一个管理员"); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetIdentityUserDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetIdentityUserDetailQueryHandler.cs index 87d1aad..ef73439 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetIdentityUserDetailQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetIdentityUserDetailQueryHandler.cs @@ -1,11 +1,9 @@ using MediatR; -using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -19,30 +17,24 @@ public sealed class GetIdentityUserDetailQueryHandler( IRoleRepository roleRepository, IRolePermissionRepository rolePermissionRepository, IPermissionRepository permissionRepository, - ITenantProvider tenantProvider, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService) + ITenantProvider tenantProvider) : IRequestHandler { /// public async Task Handle(GetIdentityUserDetailQuery request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 + // 1. 获取当前租户 var currentTenantId = tenantProvider.GetCurrentTenantId(); - var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); // 2. 查询用户实体 IdentityUser? user; if (request.IncludeDeleted) { - user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, isSuperAdmin, cancellationToken); + user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, false, cancellationToken); } else { - user = isSuperAdmin - ? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken) - : await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); + user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); } if (user == null) @@ -50,7 +42,7 @@ public sealed class GetIdentityUserDetailQueryHandler( return null; } - if (!isSuperAdmin && user.TenantId != currentTenantId) + if (user.TenantId != currentTenantId) { return null; } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs index 6f57f76..d1a7eed 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs @@ -28,27 +28,24 @@ public sealed class ResetIdentityUserPasswordCommandHandler( /// public async Task Handle(ResetIdentityUserPasswordCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 + // 1. 获取当前租户与操作者档案 var currentTenantId = tenantProvider.GetCurrentTenantId(); var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - // 2. 校验跨租户访问权限 - if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) + // 2. 校验跨租户访问 + if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId) { throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码"); } // 3. 查询用户实体 - var user = isSuperAdmin - ? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken) - : await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); + var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); } - if (!isSuperAdmin && user.TenantId != currentTenantId) + if (user.TenantId != currentTenantId) { throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码"); } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs index 525341a..5d70955 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs @@ -25,25 +25,24 @@ public sealed class RestoreIdentityUserCommandHandler( /// public async Task Handle(RestoreIdentityUserCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 + // 1. 获取当前租户与操作者档案 var currentTenantId = tenantProvider.GetCurrentTenantId(); var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - // 2. 校验跨租户访问权限 - if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) + // 2. 校验跨租户访问 + if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId) { throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户恢复用户"); } // 3. 查询用户实体(包含已删除) - var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, isSuperAdmin, cancellationToken); + var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, false, cancellationToken); if (user == null) { return false; } - if (!isSuperAdmin && user.TenantId != currentTenantId) + if (user.TenantId != currentTenantId) { return false; } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchIdentityUsersQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchIdentityUsersQueryHandler.cs index 887136e..95717cc 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchIdentityUsersQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchIdentityUsersQueryHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; using TakeoutSaaS.Domain.Identity.Entities; @@ -8,7 +7,6 @@ using TakeoutSaaS.Domain.Identity.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.Identity.Handlers; @@ -20,21 +18,17 @@ public sealed class SearchIdentityUsersQueryHandler( IIdentityUserRepository identityUserRepository, IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, - ITenantProvider tenantProvider, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService) + ITenantProvider tenantProvider) : IRequestHandler> { /// public async Task> Handle(SearchIdentityUsersQuery request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 + // 1. 获取当前租户 var currentTenantId = tenantProvider.GetCurrentTenantId(); - var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - // 2. 校验跨租户访问权限 - if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) + // 2. 校验跨租户访问 + if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId) { throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户查询用户"); } @@ -42,7 +36,7 @@ public sealed class SearchIdentityUsersQueryHandler( // 3. 组装查询过滤条件 var filter = new IdentityUserSearchFilter { - TenantId = isSuperAdmin ? request.TenantId : currentTenantId, + TenantId = currentTenantId, Keyword = request.Keyword, Status = request.Status, RoleId = request.RoleId, @@ -58,7 +52,7 @@ public sealed class SearchIdentityUsersQueryHandler( }; // 4. 执行分页查询 - var (items, total) = await identityUserRepository.SearchPagedAsync(filter, isSuperAdmin, cancellationToken); + var (items, total) = await identityUserRepository.SearchPagedAsync(filter, false, cancellationToken); if (items.Count == 0) { return new PagedResult(Array.Empty(), request.Page, request.PageSize, total); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs index fefa002..81a812b 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs @@ -31,27 +31,24 @@ public sealed class UpdateIdentityUserCommandHandler( /// public async Task Handle(UpdateIdentityUserCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 + // 1. 获取当前租户与操作者档案 var currentTenantId = tenantProvider.GetCurrentTenantId(); var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - // 2. 校验跨租户访问权限 - if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) + // 2. 校验跨租户访问 + if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId) { throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户更新用户"); } // 3. 获取用户实体 - var user = isSuperAdmin - ? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken) - : await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); + var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { return null; } - if (!isSuperAdmin && user.TenantId != currentTenantId) + if (user.TenantId != currentTenantId) { return null; } diff --git a/src/Application/TakeoutSaaS.Application/Identity/IdentityUserAccess.cs b/src/Application/TakeoutSaaS.Application/Identity/IdentityUserAccess.cs deleted file mode 100644 index 1d64fc6..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/IdentityUserAccess.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Frozen; -using System.Linq; -using TakeoutSaaS.Application.Identity.Contracts; - -namespace TakeoutSaaS.Application.Identity; - -internal static class IdentityUserAccess -{ - private static readonly FrozenSet SuperAdminRoleCodes = new[] - { - "super-admin", - "SUPER_ADMIN", - "PlatformAdmin", - "platform-admin" - }.ToFrozenSet(StringComparer.OrdinalIgnoreCase); - - internal static bool IsSuperAdmin(CurrentUserProfile profile) - => profile.Roles.Any(role => SuperAdminRoleCodes.Contains(role)); -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs index 9691306..7863b3d 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs @@ -29,14 +29,7 @@ public sealed class AdminAuthService( ITenantContextAccessor tenantContextAccessor, ITenantRepository tenantRepository) : IAdminAuthService { - private readonly ITenantProvider _tenantProvider = tenantProvider; - private readonly ITenantContextAccessor _tenantContextAccessor = tenantContextAccessor; - private readonly ITenantRepository _tenantRepository = tenantRepository; - private readonly IUserRoleRepository _userRoleRepository = userRoleRepository; - private readonly IRoleRepository _roleRepository = roleRepository; - private readonly IPermissionRepository _permissionRepository = permissionRepository; - private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository; - private readonly IMenuRepository _menuRepository = menuRepository; + private const string TenantAdminRoleCode = "tenant-admin"; /// /// 管理后台登录:验证账号密码并生成令牌。 @@ -47,8 +40,15 @@ public sealed class AdminAuthService( /// 账号或密码错误时抛出 public async Task LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default) { + // 0. 强制要求租户上下文(严格多租户隔离) + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识,请使用 账号@手机号 登录或在 Header 指定租户"); + } + // 1. 根据账号查找用户 - var user = await userRepository.FindByAccountAsync(request.Account, cancellationToken) + var user = await userRepository.FindByAccountAsync(currentTenantId, request.Account, cancellationToken) ?? throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误"); // 2. 校验账号状态 @@ -114,27 +114,27 @@ public sealed class AdminAuthService( throw new BusinessException(ErrorCodes.BadRequest, "账号格式错误,应为 账号@手机号"); } - var tenantId = await _tenantRepository.FindTenantIdByContactPhoneAsync(phonePart, cancellationToken); + var tenantId = await tenantRepository.FindTenantIdByContactPhoneAsync(phonePart, cancellationToken); if (!tenantId.HasValue || tenantId.Value == 0) { throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误"); } - var originalTenant = _tenantContextAccessor.Current; - _tenantContextAccessor.Current = new TenantContext(tenantId.Value, null, "login:simple:contact_phone"); + var originalTenant = tenantContextAccessor.Current; + tenantContextAccessor.Current = new TenantContext(tenantId.Value, null, "login:simple:contact_phone"); try { return await LoginAsync(new AdminLoginRequest { Account = accountPart, Password = request.Password }, cancellationToken); } finally { - _tenantContextAccessor.Current = originalTenant; + tenantContextAccessor.Current = originalTenant; } } } // 3. 未携带手机号时,要求外部已解析租户(Header/Host 等) - if (_tenantProvider.GetCurrentTenantId() == 0) + if (tenantProvider.GetCurrentTenantId() == 0) { throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识,请使用 账号@手机号 登录"); } @@ -163,6 +163,9 @@ public sealed class AdminAuthService( var user = await userRepository.FindByIdAsync(descriptor.UserId, cancellationToken) ?? throw new BusinessException(ErrorCodes.Unauthorized, "用户不存在"); + // 2.1 校验租户上下文与用户租户一致 + EnsureTenantMatched(user.TenantId); + // 3. 撤销旧刷新令牌(防止重复使用) await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken); @@ -197,8 +200,8 @@ public sealed class AdminAuthService( // 1. 读取档案以获取权限 var profile = await GetProfileAsync(userId, cancellationToken); // 2. 读取菜单定义 - var tenantId = _tenantProvider.GetCurrentTenantId(); - var definitions = await _menuRepository.GetByTenantAsync(tenantId, cancellationToken); + var tenantId = tenantProvider.GetCurrentTenantId(); + var definitions = await menuRepository.GetByTenantAsync(tenantId, cancellationToken); // 3. 生成菜单树 var menu = BuildMenuTree(definitions, profile.Permissions); @@ -210,7 +213,7 @@ public sealed class AdminAuthService( /// public async Task GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default) { - var tenantId = _tenantProvider.GetCurrentTenantId(); + var tenantId = tenantProvider.GetCurrentTenantId(); var user = await userRepository.FindByIdAsync(userId, cancellationToken); if (user == null || user.TenantId != tenantId) { @@ -244,7 +247,7 @@ public sealed class AdminAuthService( bool sortDescending, CancellationToken cancellationToken = default) { - var tenantId = _tenantProvider.GetCurrentTenantId(); + var tenantId = tenantProvider.GetCurrentTenantId(); var users = await userRepository.SearchAsync(tenantId, keyword, cancellationToken); var sorted = sortBy?.ToLowerInvariant() switch @@ -285,8 +288,10 @@ public sealed class AdminAuthService( { var tenantId = user.TenantId; var roles = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken); + // 1. 强制仅允许租户管理员登录(平台不允许超级管理员) + EnsureTenantAdmin(tenantId, roles); + // 2. 加载权限并返回档案 var permissions = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken); - return new CurrentUserProfile { UserId = user.Id, @@ -300,6 +305,38 @@ public sealed class AdminAuthService( }; } + private void EnsureTenantMatched(long userTenantId) + { + // 1. 读取当前租户 + var currentTenantId = tenantProvider.GetCurrentTenantId(); + if (currentTenantId <= 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识,请在 Header 指定租户"); + } + + // 2. 校验租户一致 + if (currentTenantId != userTenantId) + { + throw new BusinessException(ErrorCodes.Unauthorized, "RefreshToken 无效或已过期"); + } + } + + private static void EnsureTenantAdmin(long tenantId, IReadOnlyCollection roles) + { + // 1. 租户 ID 必须有效 + if (tenantId <= 0) + { + throw new BusinessException(ErrorCodes.Forbidden, "仅允许租户管理员登录"); + } + + // 2. 必须具备租户管理员角色 + var isTenantAdmin = roles.Any(role => string.Equals(role, TenantAdminRoleCode, StringComparison.OrdinalIgnoreCase)); + if (!isTenantAdmin) + { + throw new BusinessException(ErrorCodes.Forbidden, "仅允许租户管理员登录"); + } + } + private async Task ResetLockedUserAsync(long userId, CancellationToken cancellationToken) { // 1. 获取可更新实体 @@ -495,34 +532,34 @@ public sealed class AdminAuthService( private async Task ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken) { - var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); if (roleIds.Length == 0) { return Array.Empty(); } - var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + var roles = await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } private async Task ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken) { - var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); if (roleIds.Length == 0) { return Array.Empty(); } - var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); if (permissionIds.Length == 0) { return Array.Empty(); } - var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + var permissions = await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } @@ -532,22 +569,22 @@ public sealed class AdminAuthService( CancellationToken cancellationToken) { 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(); var roles = roleIds.Length == 0 ? Array.Empty() - : await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + : await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer.Default); var rolePermissions = roleIds.Length == 0 ? Array.Empty() - : await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + : await rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); var permissions = permissionIds.Length == 0 ? Array.Empty() - : await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + : await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer.Default); var rolePermissionsLookup = rolePermissions diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs index ecac706..c354be7 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs @@ -11,11 +11,21 @@ public interface IIdentityUserRepository /// /// 根据账号获取后台用户。 /// + /// 为保证多租户隔离,优先使用带租户参数的重载方法。 /// 账号。 /// 取消标记。 /// 后台用户或 null。 Task FindByAccountAsync(string account, CancellationToken cancellationToken = default); + /// + /// 根据租户与账号获取后台用户。 + /// + /// 租户 ID。 + /// 账号。 + /// 取消标记。 + /// 后台用户或 null。 + Task FindByAccountAsync(long tenantId, string account, CancellationToken cancellationToken = default); + /// /// 判断账号是否存在。 /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index 54ca514..d402a15 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -26,7 +26,6 @@ using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Infrastructure.App.Persistence.Configurations; -using Microsoft.AspNetCore.Http; namespace TakeoutSaaS.Infrastructure.App.Persistence; @@ -37,9 +36,8 @@ public sealed class TakeoutAppDbContext( DbContextOptions options, ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, - IIdGenerator? idGenerator = null, - IHttpContextAccessor? httpContextAccessor = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor) + IIdGenerator? idGenerator = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { /// /// 租户聚合根。 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs index 177a576..69cebbb 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs @@ -111,16 +111,16 @@ internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDb return explicitDir; } + // 1. (空行后) 尝试从当前目录定位解决方案根目录 var currentDir = Directory.GetCurrentDirectory(); var solutionRoot = LocateSolutionRoot(currentDir); + // 2. (空行后) 依次尝试常见 appsettings 目录(仅保留租户管理端 TenantApi) var candidateDirs = new[] { currentDir, solutionRoot, - solutionRoot is null ? null : Path.Combine(solutionRoot, "src", "Api", "TakeoutSaaS.AdminApi"), - solutionRoot is null ? null : Path.Combine(solutionRoot, "src", "Api", "TakeoutSaaS.UserApi"), - solutionRoot is null ? null : Path.Combine(solutionRoot, "src", "Api", "TakeoutSaaS.MiniApi") + solutionRoot is null ? null : Path.Combine(solutionRoot, "src", "Api", "TakeoutSaaS.TenantApi") }.Where(dir => !string.IsNullOrWhiteSpace(dir)); foreach (var dir in candidateDirs) diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs index 3d10e96..e657c3c 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs @@ -5,7 +5,6 @@ using TakeoutSaaS.Shared.Abstractions.Entities; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; -using Microsoft.AspNetCore.Http; namespace TakeoutSaaS.Infrastructure.Common.Persistence; @@ -16,18 +15,9 @@ public abstract class TenantAwareDbContext( DbContextOptions options, ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, - IIdGenerator? idGenerator = null, - IHttpContextAccessor? httpContextAccessor = null) : AppDbContext(options, currentUserAccessor, idGenerator) + IIdGenerator? idGenerator = null) : AppDbContext(options, currentUserAccessor, idGenerator) { private readonly ITenantProvider _tenantProvider = tenantProvider ?? throw new ArgumentNullException(nameof(tenantProvider)); - private readonly IHttpContextAccessor? _httpContextAccessor = httpContextAccessor; - private static readonly string[] PlatformRoleCodes = - { - "super-admin", - "SUPER_ADMIN", - "PlatformAdmin", - "platform-admin" - }; /// /// 当前请求租户 ID。 @@ -86,22 +76,8 @@ public abstract class TenantAwareDbContext( { if (entry.State == EntityState.Added && entry.Entity.TenantId == 0 && tenantId != 0) { - if (!IsPlatformAdmin()) - { - entry.Entity.TenantId = tenantId; - } + entry.Entity.TenantId = tenantId; } } } - - private bool IsPlatformAdmin() - { - var user = _httpContextAccessor?.HttpContext?.User; - if (user?.Identity?.IsAuthenticated != true) - { - return false; - } - - return PlatformRoleCodes.Any(user.IsInRole); - } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs index 0ff9fff..0339ac7 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs @@ -7,7 +7,6 @@ using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; -using Microsoft.AspNetCore.Http; namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence; @@ -18,9 +17,8 @@ public sealed class DictionaryDbContext( DbContextOptions options, ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, - IIdGenerator? idGenerator = null, - IHttpContextAccessor? httpContextAccessor = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor) + IIdGenerator? idGenerator = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { /// /// 字典分组集合。 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs index a1a9aa5..8e78b18 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs @@ -18,6 +18,24 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde public Task FindByAccountAsync(string account, CancellationToken cancellationToken = default) => dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Account == account, cancellationToken); + /// + /// 根据租户与账号获取后台用户。 + /// + /// 租户 ID。 + /// 账号。 + /// 取消标记。 + /// 后台用户或 null。 + public Task FindByAccountAsync(long tenantId, string account, CancellationToken cancellationToken = default) + { + // 1. 标准化账号 + var normalized = account.Trim(); + + // 2. 查询用户(强制租户隔离) + return dbContext.IdentityUsers + .AsNoTracking() + .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Account == normalized, cancellationToken); + } + /// /// 判断账号是否存在。 /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs index 33656c8..9d0467b 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs @@ -6,7 +6,6 @@ using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; -using Microsoft.AspNetCore.Http; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; @@ -17,9 +16,8 @@ public sealed class IdentityDbContext( DbContextOptions options, ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, - IIdGenerator? idGenerator = null, - IHttpContextAccessor? httpContextAccessor = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor) + IIdGenerator? idGenerator = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { /// /// 管理后台用户集合。 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs index 37918e4..a34444d 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs @@ -7,7 +7,6 @@ using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; -using Microsoft.AspNetCore.Http; namespace TakeoutSaaS.Infrastructure.Logs.Persistence; @@ -18,9 +17,8 @@ public sealed class TakeoutLogsDbContext( DbContextOptions options, ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, - IIdGenerator? idGenerator = null, - IHttpContextAccessor? httpContextAccessor = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor) + IIdGenerator? idGenerator = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { /// /// 租户审计日志集合。 diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs index 46c9770..e686b7f 100644 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs @@ -90,7 +90,14 @@ public sealed class TenantResolutionMiddleware( { var request = context.Request; - // 1. Header 中的租户 ID + // 1. Token Claim(已认证请求必须以 Claim 为准,避免 Header 覆盖导致跨租户访问) + var claim = context.User?.FindFirst("tenant_id"); + if (claim != null && long.TryParse(claim.Value, out var claimTenant)) + { + return new TenantContext(claimTenant, null, "claim:tenant_id"); + } + + // 2. Header 中的租户 ID if (!string.IsNullOrWhiteSpace(options.TenantIdHeaderName) && request.Headers.TryGetValue(options.TenantIdHeaderName, out var tenantHeader) && long.TryParse(tenantHeader.FirstOrDefault(), out var headerTenantId)) @@ -98,7 +105,7 @@ public sealed class TenantResolutionMiddleware( return new TenantContext(headerTenantId, null, $"header:{options.TenantIdHeaderName}"); } - // 2. Header 中的租户编码 + // 3. Header 中的租户编码 if (!string.IsNullOrWhiteSpace(options.TenantCodeHeaderName) && request.Headers.TryGetValue(options.TenantCodeHeaderName, out var codeHeader)) { @@ -109,7 +116,7 @@ public sealed class TenantResolutionMiddleware( } } - // 3. Host 映射/子域名解析 + // 4. Host 映射/子域名解析 var host = request.Host.Host; if (!string.IsNullOrWhiteSpace(host)) { @@ -125,13 +132,6 @@ public sealed class TenantResolutionMiddleware( } } - // 4. Token Claim - var claim = context.User?.FindFirst("tenant_id"); - if (claim != null && long.TryParse(claim.Value, out var claimTenant)) - { - return new TenantContext(claimTenant, null, "claim:tenant_id"); - } - return TenantContext.Empty; } diff --git a/tests/TakeoutSaaS.Integration.Tests/App/Dictionary/DictionaryApiTests.cs b/tests/TakeoutSaaS.Integration.Tests/App/Dictionary/DictionaryApiTests.cs index dde9318..96c6d80 100644 --- a/tests/TakeoutSaaS.Integration.Tests/App/Dictionary/DictionaryApiTests.cs +++ b/tests/TakeoutSaaS.Integration.Tests/App/Dictionary/DictionaryApiTests.cs @@ -444,7 +444,6 @@ public sealed class DictionaryApiTests new DictionaryItemRepository(context), cache, tenantProvider, - new HttpContextAccessor { HttpContext = new DefaultHttpContext() }, NullLogger.Instance); } @@ -496,7 +495,6 @@ public sealed class DictionaryApiTests cache, tenantProvider, currentUser, - new HttpContextAccessor { HttpContext = new DefaultHttpContext() }, NullLogger.Instance); }