From 4f8424adb6393dee3a5a461c517a2eddcf4ae359 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Thu, 29 Jan 2026 23:24:44 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20AdminApi=20=E5=89=94=E9=99=A4?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E4=BE=A7=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TakeoutSaaS.sln | 47 ---- .../Requests/FileUploadFormRequest.cs | 6 + .../Controllers/AppAnnouncementsController.cs | 120 ---------- .../Controllers/AuthController.cs | 40 ---- .../Controllers/FilesController.cs | 15 +- .../PlatformAnnouncementsController.cs | 31 +-- .../PublicTenantPackagesController.cs | 39 ---- .../PublicTenantSubscriptionsController.cs | 49 ---- .../Controllers/PublicTenantsController.cs | 76 ------ .../Controllers/RoleTemplatesController.cs | 46 ---- .../Controllers/StoreAuditsController.cs | 37 +-- .../StoreQualificationsController.cs | 23 +- .../TenantAnnouncementsController.cs | 42 +--- .../Controllers/TenantsController.cs | 44 ---- .../Controllers/UserPermissionsController.cs | 75 ------ src/Api/TakeoutSaaS.AdminApi/Dockerfile | 1 - .../Filters/TenantRouteContextFilter.cs | 66 ------ src/Api/TakeoutSaaS.AdminApi/Program.cs | 24 -- .../TakeoutSaaS.AdminApi.csproj | 1 - .../Handlers/AdjustInventoryCommandHandler.cs | 21 +- .../Handlers/DeductInventoryCommandHandler.cs | 6 +- .../GetInventoryBatchesQueryHandler.cs | 7 +- .../Handlers/GetInventoryItemQueryHandler.cs | 7 +- .../Handlers/LockInventoryCommandHandler.cs | 6 +- ...easeExpiredInventoryLocksCommandHandler.cs | 7 +- .../ReleaseInventoryCommandHandler.cs | 6 +- .../UpsertInventoryBatchCommandHandler.cs | 15 +- .../CreateTenantAdminResetLinkTokenCommand.cs | 15 -- .../Commands/ImpersonateTenantCommand.cs | 16 -- .../Commands/MarkAnnouncementAsReadCommand.cs | 22 -- .../Commands/PublishAnnouncementCommand.cs | 6 + .../Commands/RevokeAnnouncementCommand.cs | 6 + .../Commands/SelfRegisterTenantCommand.cs | 46 ---- .../App/Tenants/Dto/SelfRegisterResultDto.cs | 47 ---- .../App/Tenants/Dto/TenantProgressDto.cs | 42 ---- ...TenantAdminResetLinkTokenCommandHandler.cs | 94 -------- .../CreateTenantManuallyCommandHandler.cs | 81 +++---- .../GetAnnouncementByIdQueryHandler.cs | 48 +--- .../GetPublicTenantPackagesQueryHandler.cs | 36 --- .../Handlers/GetTenantProgressQueryHandler.cs | 39 ---- .../GetTenantsAnnouncementsQueryHandler.cs | 63 +---- .../GetUnreadAnnouncementsQueryHandler.cs | 76 ------ .../ImpersonateTenantCommandHandler.cs | 109 --------- .../MarkAnnouncementAsReadCommandHandler.cs | 100 -------- .../PublishAnnouncementCommandHandler.cs | 4 +- .../RevokeAnnouncementCommandHandler.cs | 4 +- .../SelfRegisterTenantCommandHandler.cs | 142 ------------ .../Queries/GetAnnouncementByIdQuery.cs | 2 +- .../Queries/GetPublicTenantPackagesQuery.cs | 21 -- .../Tenants/Queries/GetTenantProgressQuery.cs | 17 -- .../Queries/GetUnreadAnnouncementsQuery.cs | 21 -- .../AnnouncementTargetContextFactory.cs | 58 ----- .../App/Tenants/Targeting/TargetTypeFilter.cs | 218 ------------------ .../SelfRegisterTenantCommandValidator.cs | 22 -- .../Contracts/CreateDictionaryGroupRequest.cs | 5 + .../Contracts/DictionaryBatchQueryRequest.cs | 5 + .../Contracts/DictionaryGroupQuery.cs | 5 + .../Services/DictionaryAppService.cs | 42 +--- .../Services/DictionaryCommandService.cs | 40 +--- .../Services/DictionaryImportExportService.cs | 43 ---- .../Services/DictionaryQueryService.cs | 51 ++-- .../Abstractions/IAdminAuthService.cs | 11 - .../Identity/Abstractions/IMiniAuthService.cs | 13 -- .../Abstractions/IWeChatAuthService.cs | 36 --- .../Commands/AssignUserRolesCommand.cs | 19 -- .../Commands/CopyRoleTemplateCommand.cs | 11 + .../InitializeRoleTemplatesCommand.cs | 15 -- .../Identity/Contracts/WeChatLoginRequest.cs | 38 --- .../IdentityServiceCollectionExtensions.cs | 8 +- .../Handlers/AssignUserRolesCommandHandler.cs | 38 --- ...atchIdentityUserOperationCommandHandler.cs | 21 +- .../BindRolePermissionsCommandHandler.cs | 20 +- .../ChangeIdentityUserStatusCommandHandler.cs | 27 +-- .../CopyRoleTemplateCommandHandler.cs | 31 ++- .../CreateIdentityUserCommandHandler.cs | 16 +- .../Handlers/CreateRoleCommandHandler.cs | 24 +- .../DeleteIdentityUserCommandHandler.cs | 25 +- .../Handlers/DeleteRoleCommandHandler.cs | 19 +- .../GetIdentityUserDetailQueryHandler.cs | 26 +-- .../GetUserPermissionsQueryHandler.cs | 89 ------- .../InitializeRoleTemplatesCommandHandler.cs | 62 ----- ...ResetIdentityUserPasswordCommandHandler.cs | 27 +-- .../RestoreIdentityUserCommandHandler.cs | 23 +- .../Handlers/RoleDetailQueryHandler.cs | 23 +- .../SearchIdentityUsersQueryHandler.cs | 33 +-- .../Handlers/SearchRolesQueryHandler.cs | 24 +- .../SearchUserPermissionsQueryHandler.cs | 132 ----------- .../UpdateIdentityUserCommandHandler.cs | 31 +-- .../Handlers/UpdateRoleCommandHandler.cs | 22 +- .../Queries/GetUserPermissionsQuery.cs | 15 -- .../Queries/SearchUserPermissionsQuery.cs | 36 --- .../Identity/Services/AdminAuthService.cs | 155 +------------ .../Identity/Services/MiniAuthService.cs | 148 ------------ .../Abstractions/IVerificationCodeService.cs | 19 -- .../Contracts/SendVerificationCodeRequest.cs | 27 --- .../Contracts/SendVerificationCodeResponse.cs | 17 -- .../VerifyVerificationCodeRequest.cs | 25 -- .../SmsServiceCollectionExtensions.cs | 27 --- .../Sms/Options/VerificationCodeOptions.cs | 33 --- .../Sms/Services/VerificationCodeService.cs | 155 ------------- .../Storage/Contracts/DirectUploadRequest.cs | 7 +- .../Storage/Contracts/UploadFileRequest.cs | 6 + .../Storage/Services/FileStorageService.cs | 25 +- .../Repositories/IInventoryRepository.cs | 16 +- .../ITenantAnnouncementReadRepository.cs | 53 ----- .../ITenantAnnouncementRepository.cs | 27 --- .../AppServiceCollectionExtensions.cs | 1 - .../App/Persistence/TakeoutAdminDbContext.cs | 4 +- .../App/Persistence/TakeoutAppDbContext.cs | 7 +- .../TakeoutAppDesignTimeDbContextFactory.cs | 5 +- .../App/Repositories/EfInventoryRepository.cs | 114 ++++++--- .../EfTenantAnnouncementReadRepository.cs | 68 ------ .../EfTenantAnnouncementRepository.cs | 57 ----- .../DesignTimeDbContextFactoryBase.cs | 19 +- .../Persistence/TenantAwareDbContext.cs | 80 ------- .../Persistence/DictionaryDbContext.cs | 5 +- .../DictionaryDesignTimeDbContextFactory.cs | 5 +- .../Extensions/ServiceCollectionExtensions.cs | 16 -- .../Identity/Options/WeChatMiniOptions.cs | 21 -- .../Persistence/IdentityDataSeeder.cs | 58 +++-- .../Identity/Persistence/IdentityDbContext.cs | 7 +- .../IdentityDesignTimeDbContextFactory.cs | 5 +- .../Identity/Services/WeChatAuthService.cs | 79 ------- .../Logs/Persistence/TakeoutLogsDbContext.cs | 4 +- .../TakeoutLogsDesignTimeDbContextFactory.cs | 5 +- .../TenantServiceCollectionExtensions.cs | 34 --- .../TakeoutSaaS.Module.Tenancy.csproj | 16 -- .../TenantContextAccessor.cs | 35 --- .../TenantProvider.cs | 17 -- .../TenantResolutionMiddleware.cs | 177 -------------- .../TenantResolutionOptions.cs | 56 ----- .../GetAnnouncementByIdQueryHandlerTests.cs | 108 ++------- ...etTenantsAnnouncementsQueryHandlerTests.cs | 73 ++---- ...GetUnreadAnnouncementsQueryHandlerTests.cs | 72 ------ .../App/Tenants/AnnouncementWorkflowTests.cs | 33 +-- .../Fixtures/DictionarySqliteTestDatabase.cs | 15 +- .../Fixtures/SqliteTestDatabase.cs | 16 +- .../Fixtures/TestIdGenerator.cs | 20 ++ .../AnnouncementQueryPerformanceTests.cs | 23 +- 139 files changed, 622 insertions(+), 4691 deletions(-) delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/AppAnnouncementsController.cs delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantPackagesController.cs delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantSubscriptionsController.cs delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantsController.cs delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/UserPermissionsController.cs delete mode 100644 src/Api/TakeoutSaaS.AdminApi/Filters/TenantRouteContextFilter.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAdminResetLinkTokenCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ImpersonateTenantCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkAnnouncementAsReadCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Commands/SelfRegisterTenantCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/SelfRegisterResultDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantProgressDto.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAdminResetLinkTokenCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetPublicTenantPackagesQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantProgressQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ImpersonateTenantCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkAnnouncementAsReadCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SelfRegisterTenantCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetPublicTenantPackagesQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantProgressQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetUnreadAnnouncementsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Targeting/AnnouncementTargetContextFactory.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Targeting/TargetTypeFilter.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Tenants/Validators/SelfRegisterTenantCommandValidator.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Identity/Abstractions/IMiniAuthService.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Identity/Abstractions/IWeChatAuthService.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/AssignUserRolesCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/InitializeRoleTemplatesCommand.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Identity/Contracts/WeChatLoginRequest.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/InitializeRoleTemplatesCommandHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Identity/Queries/GetUserPermissionsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Identity/Queries/SearchUserPermissionsQuery.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Sms/Abstractions/IVerificationCodeService.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeRequest.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeResponse.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Sms/Contracts/VerifyVerificationCodeRequest.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Sms/Extensions/SmsServiceCollectionExtensions.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Sms/Options/VerificationCodeOptions.cs delete mode 100644 src/Application/TakeoutSaaS.Application/Sms/Services/VerificationCodeService.cs delete mode 100644 src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantAnnouncementReadRepository.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementReadRepository.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/WeChatMiniOptions.cs delete mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs delete mode 100644 src/Modules/TakeoutSaaS.Module.Tenancy/Extensions/TenantServiceCollectionExtensions.cs delete mode 100644 src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj delete mode 100644 src/Modules/TakeoutSaaS.Module.Tenancy/TenantContextAccessor.cs delete mode 100644 src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs delete mode 100644 src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs delete mode 100644 src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionOptions.cs delete mode 100644 tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandlerTests.cs create mode 100644 tests/TakeoutSaaS.Integration.Tests/Fixtures/TestIdGenerator.cs diff --git a/TakeoutSaaS.sln b/TakeoutSaaS.sln index 8e0b8a3..813aa37 100644 --- a/TakeoutSaaS.sln +++ b/TakeoutSaaS.sln @@ -31,8 +31,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{EC44 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Authorization", "src\Modules\TakeoutSaaS.Module.Authorization\TakeoutSaaS.Module.Authorization.csproj", "{6CB8487D-5C74-487C-9D84-E57838BDA015}" 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.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}" @@ -47,12 +45,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Schedule EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Sms", "src\Modules\TakeoutSaaS.Module.Sms\TakeoutSaaS.Module.Sms.csproj", "{38011EC3-7EC3-40E4-B9B2-E631966B350B}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Application.Tests", "tests\TakeoutSaaS.Application.Tests\TakeoutSaaS.Application.Tests.csproj", "{2601637E-777A-4FA2-81BA-1AFE32E961FF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Integration.Tests", "tests\TakeoutSaaS.Integration.Tests\TakeoutSaaS.Integration.Tests.csproj", "{8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -147,18 +139,6 @@ Global {6CB8487D-5C74-487C-9D84-E57838BDA015}.Release|x64.Build.0 = Release|Any CPU {6CB8487D-5C74-487C-9D84-E57838BDA015}.Release|x86.ActiveCfg = Release|Any CPU {6CB8487D-5C74-487C-9D84-E57838BDA015}.Release|x86.Build.0 = Release|Any CPU - {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Debug|x64.ActiveCfg = Debug|Any CPU - {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Debug|x64.Build.0 = Debug|Any CPU - {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Debug|x86.ActiveCfg = Debug|Any CPU - {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Debug|x86.Build.0 = Debug|Any CPU - {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Release|Any CPU.Build.0 = Release|Any CPU - {5B1DAF2B-C36C-4CB1-9452-81D5D6F79D38}.Release|x64.ActiveCfg = Release|Any CPU - {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 {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 @@ -243,30 +223,6 @@ Global {38011EC3-7EC3-40E4-B9B2-E631966B350B}.Release|x64.Build.0 = Release|Any CPU {38011EC3-7EC3-40E4-B9B2-E631966B350B}.Release|x86.ActiveCfg = Release|Any CPU {38011EC3-7EC3-40E4-B9B2-E631966B350B}.Release|x86.Build.0 = Release|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Debug|x64.ActiveCfg = Debug|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Debug|x64.Build.0 = Debug|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Debug|x86.ActiveCfg = Debug|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Debug|x86.Build.0 = Debug|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Release|Any CPU.Build.0 = Release|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Release|x64.ActiveCfg = Release|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Release|x64.Build.0 = Release|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Release|x86.ActiveCfg = Release|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Release|x86.Build.0 = Release|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Debug|x64.ActiveCfg = Debug|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Debug|x64.Build.0 = Debug|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Debug|x86.ActiveCfg = Debug|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Debug|x86.Build.0 = Debug|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Release|Any CPU.Build.0 = Release|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Release|x64.ActiveCfg = Release|Any CPU - {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -285,7 +241,6 @@ Global {80B45C7D-9423-400A-8279-40D95BFEBC9D} = {9048EB7F-3875-A59E-E36B-5BD4C6F2A282} {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} {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} @@ -293,7 +248,5 @@ Global {FE49A9E7-1228-45BA-9B71-337AA353FE98} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} {9C2F510E-4054-482D-AFD3-D2E374D60304} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} {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} EndGlobalSection EndGlobal diff --git a/src/Api/TakeoutSaaS.AdminApi/Contracts/Requests/FileUploadFormRequest.cs b/src/Api/TakeoutSaaS.AdminApi/Contracts/Requests/FileUploadFormRequest.cs index 056faa3..15f3d85 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Contracts/Requests/FileUploadFormRequest.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Contracts/Requests/FileUploadFormRequest.cs @@ -13,6 +13,12 @@ public sealed record FileUploadFormRequest /// [Required] public required IFormFile File { get; init; } + + /// + /// 租户 ID(0 表示平台)。 + /// + [Required] + public long? TenantId { get; init; } /// /// 上传类型。 /// diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/AppAnnouncementsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/AppAnnouncementsController.cs deleted file mode 100644 index eec915a..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/AppAnnouncementsController.cs +++ /dev/null @@ -1,120 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Swashbuckle.AspNetCore.Annotations; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.AdminApi.Controllers; - -/// -/// 应用端公告(面向已认证用户)。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/app/announcements")] -public sealed class AppAnnouncementsController(IMediator mediator) : BaseApiController -{ - /// - /// 获取当前用户可见的公告列表(已发布/有效期内)。 - /// - /// - /// 示例: - /// - /// GET /api/app/announcements?page=1&pageSize=20 - /// Header: Authorization: Bearer <JWT> - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "items": [], - /// "page": 1, - /// "pageSize": 20, - /// "totalCount": 0 - /// } - /// } - /// - /// - [HttpGet] - [SwaggerOperation(Summary = "获取可见公告列表", Description = "仅返回已发布且在有效期内的公告(含平台公告)。")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] - public async Task>> List([FromQuery] GetTenantsAnnouncementsQuery query, CancellationToken cancellationToken) - { - var request = query with - { - Status = AnnouncementStatus.Published, - IsActive = true, - OnlyEffective = true - }; - - var result = await mediator.Send(request, cancellationToken); - return ApiResponse>.Ok(result); - } - - /// - /// 获取当前用户未读公告。 - /// - /// - /// 示例: - /// - /// GET /api/app/announcements/unread?page=1&pageSize=20 - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "items": [], - /// "page": 1, - /// "pageSize": 20, - /// "totalCount": 0 - /// } - /// } - /// - /// - [HttpGet("unread")] - [SwaggerOperation(Summary = "获取未读公告", Description = "仅返回未读且在有效期内的已发布公告。")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] - public async Task>> GetUnread([FromQuery] GetUnreadAnnouncementsQuery query, CancellationToken cancellationToken) - { - var result = await mediator.Send(query, cancellationToken); - return ApiResponse>.Ok(result); - } - - /// - /// 标记公告已读。 - /// - /// - /// 示例: - /// - /// POST /api/app/announcements/900123456789012345/mark-read - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "id": "900123456789012345", - /// "isRead": true - /// } - /// } - /// - /// - [HttpPost("{announcementId:long}/mark-read")] - [SwaggerOperation(Summary = "标记公告已读", Description = "仅已发布且可见的公告允许标记已读。")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] - public async Task> MarkRead(long announcementId, CancellationToken cancellationToken) - { - var result = await mediator.Send(new MarkAnnouncementAsReadCommand { AnnouncementId = announcementId }, cancellationToken); - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") - : ApiResponse.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs index 2545af7..b68236f 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/AuthController.cs @@ -154,44 +154,4 @@ public sealed class AuthController(IAdminAuthService authService, IMediator medi var menu = await authService.GetMenuTreeAsync(userId, cancellationToken); return ApiResponse>.Ok(menu); } - - /// - /// 查询指定用户的角色与权限概览(当前租户范围)。 - /// - /// - /// 示例: - /// - /// GET /api/admin/v1/auth/permissions/900123456789012346 - /// Header: Authorization: Bearer <JWT> - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "userId": "900123456789012346", - /// "tenantId": "100000000000000001", - /// "merchantId": "200000000000000001", - /// "account": "ops.manager", - /// "displayName": "运营经理", - /// "roles": ["OpsManager", "Reporter"], - /// "permissions": ["delivery:read", "order:read", "payment:read"], - /// "createdAt": "2025-12-01T08:30:00Z" - /// } - /// } - /// - /// - /// 目标用户 ID。 - /// 取消标记。 - /// 用户权限概览,未找到则返回 404。 - [HttpGet("permissions/{userId:long}")] - [PermissionAuthorize("identity:permission:read")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> GetUserPermissions(long userId, CancellationToken cancellationToken) - { - var result = await authService.GetUserPermissionsAsync(userId, cancellationToken); - return result is null - ? ApiResponse.Error(ErrorCodes.NotFound, "用户不存在或不属于当前租户") - : ApiResponse.Ok(result); - } } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/FilesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/FilesController.cs index 35f57f5..717e50a 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/FilesController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/FilesController.cs @@ -34,19 +34,24 @@ public sealed class FilesController(IFileStorageService fileStorageService) : Ba { return ApiResponse.Error(ErrorCodes.BadRequest, "文件不能为空"); } - // 2. 解析上传类型 + // 2. 校验租户标识 + if (!request.TenantId.HasValue || request.TenantId.Value < 0) + { + return ApiResponse.Error(ErrorCodes.BadRequest, "TenantId 不能为空"); + } + // 3. 解析上传类型 if (!UploadFileTypeParser.TryParse(request.Type, out var uploadType)) { return ApiResponse.Error(ErrorCodes.BadRequest, "上传类型不合法"); } - // 3. 提取请求来源 + // 4. 提取请求来源 var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault(); await using var stream = request.File.OpenReadStream(); - // 4. 调用存储服务执行上传 + // 5. 调用存储服务执行上传 var result = await fileStorageService.UploadAsync( - new UploadFileRequest(uploadType, stream, request.File.FileName, request.File.ContentType ?? string.Empty, request.File.Length, origin), + new UploadFileRequest(uploadType, request.TenantId.Value, stream, request.File.FileName, request.File.ContentType ?? string.Empty, request.File.Length, origin), cancellationToken); - // 5. 返回上传结果 + // 6. 返回上传结果 return ApiResponse.Ok(result); } } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PlatformAnnouncementsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PlatformAnnouncementsController.cs index e2af709..2eb65e5 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/PlatformAnnouncementsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/PlatformAnnouncementsController.cs @@ -9,7 +9,6 @@ using TakeoutSaaS.Application.App.Tenants.Queries; using TakeoutSaaS.Domain.Tenants.Enums; using TakeoutSaaS.Module.Authorization.Attributes; using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Shared.Web.Api; namespace TakeoutSaaS.AdminApi.Controllers; @@ -19,9 +18,8 @@ namespace TakeoutSaaS.AdminApi.Controllers; /// [ApiVersion("1.0")] [Authorize] -[Route("api/platform/announcements")] [Route("api/admin/v{version:apiVersion}/platform/announcements")] -public sealed class PlatformAnnouncementsController(IMediator mediator, ITenantContextAccessor tenantContextAccessor) : BaseApiController +public sealed class PlatformAnnouncementsController(IMediator mediator) : BaseApiController { /// /// 创建平台公告。 @@ -101,7 +99,7 @@ public sealed class PlatformAnnouncementsController(IMediator mediator, ITenantC public async Task>> List([FromQuery] GetTenantsAnnouncementsQuery query, CancellationToken cancellationToken) { var request = query with { TenantId = 0 }; - var result = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken)); + var result = await mediator.Send(request, cancellationToken); return ApiResponse>.Ok(result); } @@ -133,8 +131,7 @@ public sealed class PlatformAnnouncementsController(IMediator mediator, ITenantC [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] public async Task> Detail(long announcementId, CancellationToken cancellationToken) { - var result = await ExecuteAsPlatformAsync(() => - mediator.Send(new GetAnnouncementByIdQuery { TenantId = 0, AnnouncementId = announcementId }, cancellationToken)); + var result = await mediator.Send(new GetAnnouncementByIdQuery { TenantId = 0, AnnouncementId = announcementId }, cancellationToken); return result is null ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") @@ -215,8 +212,8 @@ public sealed class PlatformAnnouncementsController(IMediator mediator, ITenantC [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] public async Task> Publish(long announcementId, [FromBody, Required] PublishAnnouncementCommand command, CancellationToken cancellationToken) { - command = command with { AnnouncementId = announcementId }; - var result = await ExecuteAsPlatformAsync(() => mediator.Send(command, cancellationToken)); + command = command with { TenantId = 0, AnnouncementId = announcementId }; + var result = await mediator.Send(command, cancellationToken); return result is null ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") @@ -254,25 +251,11 @@ public sealed class PlatformAnnouncementsController(IMediator mediator, ITenantC [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] public async Task> Revoke(long announcementId, [FromBody, Required] RevokeAnnouncementCommand command, CancellationToken cancellationToken) { - command = command with { AnnouncementId = announcementId }; - var result = await ExecuteAsPlatformAsync(() => mediator.Send(command, cancellationToken)); + command = command with { TenantId = 0, AnnouncementId = announcementId }; + var result = await mediator.Send(command, cancellationToken); return result is null ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") : ApiResponse.Ok(result); } - - private async Task ExecuteAsPlatformAsync(Func> action) - { - var original = tenantContextAccessor.Current; - tenantContextAccessor.Current = new TenantContext(0, null, "platform"); - try - { - return await action(); - } - finally - { - tenantContextAccessor.Current = original; - } - } } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantPackagesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantPackagesController.cs deleted file mode 100644 index 716608d..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantPackagesController.cs +++ /dev/null @@ -1,39 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.RateLimiting; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.AdminApi.Controllers; - -/// -/// 公共租户套餐查询接口。 -/// -[ApiVersion("1.0")] -[AllowAnonymous] -[EnableRateLimiting("public-self-service")] -[Route("api/public/v{version:apiVersion}/tenant-packages")] -public sealed class PublicTenantPackagesController(IMediator mediator) : BaseApiController -{ - /// - /// 分页获取已启用的租户套餐。 - /// - /// 分页参数。 - /// 取消标记。 - /// 启用套餐的分页列表。 - [HttpGet] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> List( - [FromQuery, Required] GetPublicTenantPackagesQuery query, - CancellationToken cancellationToken) - { - // 1. 执行查询 - var result = await mediator.Send(query, cancellationToken); - // 2. 返回结果 - return ApiResponse>.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantSubscriptionsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantSubscriptionsController.cs deleted file mode 100644 index 3952b9b..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantSubscriptionsController.cs +++ /dev/null @@ -1,49 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.RateLimiting; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.AdminApi.Controllers; - -/// -/// 公域租户订阅自助接口(需登录,无权限校验)。 -/// -[ApiVersion("1.0")] -[Authorize] -[EnableRateLimiting("public-self-service")] -[Route("api/public/v{version:apiVersion}/tenants")] -public sealed class PublicTenantSubscriptionsController(IMediator mediator) : BaseApiController -{ - /// - /// 初次绑定租户订阅(默认 0 个月)。 - /// - /// 租户 ID。 - /// 绑定请求。 - /// 取消标记。 - /// 绑定后的订阅信息。 - [HttpPost("{tenantId:long}/subscriptions/initial")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] - public async Task> BindInitialSubscription( - long tenantId, - [FromBody, Required] BindInitialTenantSubscriptionCommand body, - CancellationToken cancellationToken) - { - // 1. 合并路由租户标识 - var command = body with { TenantId = tenantId }; - - // 2. 执行初次订阅绑定 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回绑定结果 - return ApiResponse.Ok(result); - } -} - diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantsController.cs deleted file mode 100644 index 05e7764..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/PublicTenantsController.cs +++ /dev/null @@ -1,76 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.RateLimiting; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.AdminApi.Controllers; - -/// -/// 公域租户自助入住接口。 -/// -[ApiVersion("1.0")] -[AllowAnonymous] -[EnableRateLimiting("public-self-service")] -[Route("api/public/v{version:apiVersion}/tenants")] -public sealed class PublicTenantsController(IMediator mediator) : BaseApiController -{ - /// - /// 自助注册租户并生成初始管理员。 - /// - /// 自助注册命令。 - /// 取消标记。 - /// 注册结果(含临时密码)。 - [HttpPost("self-register")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> SelfRegister( - [FromBody, Required] SelfRegisterTenantCommand command, - CancellationToken cancellationToken) - { - // 1. 执行自助注册 - var result = await mediator.Send(command, cancellationToken); - return ApiResponse.Ok(result); - } - - /// - /// 自助提交或更新实名资料。 - /// - /// 租户 ID。 - /// 实名资料。 - /// 取消标记。 - /// 实名资料结果。 - [HttpPost("{tenantId:long}/verification")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> SubmitVerification( - long tenantId, - [FromBody, Required] SubmitTenantVerificationCommand command, - CancellationToken cancellationToken) - { - // 1. 绑定租户 ID - var merged = command with { TenantId = tenantId }; - // 2. 提交实名 - var result = await mediator.Send(merged, cancellationToken); - return ApiResponse.Ok(result); - } - - /// - /// 查询租户入住进度。 - /// - /// 租户 ID。 - /// 取消标记。 - /// 入住进度。 - [HttpGet("{tenantId:long}/status")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Progress(long tenantId, CancellationToken cancellationToken) - { - // 1. 查询进度 - var query = new GetTenantProgressQuery { TenantId = tenantId }; - var result = await mediator.Send(query, cancellationToken); - return ApiResponse.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/RoleTemplatesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/RoleTemplatesController.cs index f5853ac..b92f486 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/RoleTemplatesController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/RoleTemplatesController.cs @@ -168,50 +168,4 @@ public sealed class RoleTemplatesController(IMediator mediator) : BaseApiControl return ApiResponse.Ok(result); } - /// - /// 为当前租户批量初始化预置角色模板。 - /// - /// 初始化命令。 - /// 取消标记。 - /// 生成的租户角色列表。 - [HttpPost("init")] - [PermissionAuthorize("identity:role:create")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> Initialize( - [FromBody] InitializeRoleTemplatesCommand? command, - CancellationToken cancellationToken) - { - // 1. 确保命令存在 - command ??= new InitializeRoleTemplatesCommand(); - - // 2. 初始化模板到租户 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回新建的角色列表 - return ApiResponse>.Ok(result); - } - - /// - /// 将单个模板初始化到当前租户。 - /// - /// 模板编码。 - /// 取消标记。 - /// 生成的角色列表。 - [HttpPost("{templateCode}/initialize-tenant")] - [PermissionAuthorize("identity:role:create")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> InitializeSingle(string templateCode, CancellationToken cancellationToken) - { - // 1. 构造初始化命令 - var command = new InitializeRoleTemplatesCommand - { - TemplateCodes = new[] { templateCode } - }; - - // 2. 初始化模板到租户 - var result = await mediator.Send(command, cancellationToken); - - // 3. 返回生成的角色列表 - return ApiResponse>.Ok(result); - } } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreAuditsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreAuditsController.cs index ab376b0..9351f3a 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreAuditsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreAuditsController.cs @@ -8,7 +8,6 @@ using TakeoutSaaS.Application.App.StoreAudits.Queries; using TakeoutSaaS.Module.Authorization.Attributes; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Shared.Web.Api; namespace TakeoutSaaS.AdminApi.Controllers; @@ -18,9 +17,8 @@ namespace TakeoutSaaS.AdminApi.Controllers; /// [ApiVersion("1.0")] [Authorize] -[Route("api/platform/store-audits")] [Route("api/admin/v{version:apiVersion}/platform/store-audits")] -public sealed class StoreAuditsController(IMediator mediator, ITenantContextAccessor tenantContextAccessor) : BaseApiController +public sealed class StoreAuditsController(IMediator mediator) : BaseApiController { /// /// 查询待审核门店列表。 @@ -34,7 +32,7 @@ public sealed class StoreAuditsController(IMediator mediator, ITenantContextAcce CancellationToken cancellationToken) { // 1. 查询待审核门店列表 - var result = await ExecuteAsPlatformAsync(() => mediator.Send(query, cancellationToken)); + var result = await mediator.Send(query, cancellationToken); // 2. 返回分页结果 return ApiResponse>.Ok(result); @@ -53,8 +51,7 @@ public sealed class StoreAuditsController(IMediator mediator, ITenantContextAcce public async Task> GetDetail(long storeId, CancellationToken cancellationToken) { // 1. 获取审核详情 - var result = await ExecuteAsPlatformAsync(() => - mediator.Send(new GetStoreAuditDetailQuery { StoreId = storeId }, cancellationToken)); + var result = await mediator.Send(new GetStoreAuditDetailQuery { StoreId = storeId }, cancellationToken); // 2. 返回详情或未找到 return result is null @@ -79,7 +76,7 @@ public sealed class StoreAuditsController(IMediator mediator, ITenantContextAcce { // 1. 执行审核通过 var request = command with { StoreId = storeId }; - var result = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken)); + var result = await mediator.Send(request, cancellationToken); // 2. 返回结果 return ApiResponse.Ok(result); @@ -102,7 +99,7 @@ public sealed class StoreAuditsController(IMediator mediator, ITenantContextAcce { // 1. 执行审核驳回 var request = command with { StoreId = storeId }; - var result = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken)); + var result = await mediator.Send(request, cancellationToken); // 2. 返回结果 return ApiResponse.Ok(result); @@ -132,7 +129,7 @@ public sealed class StoreAuditsController(IMediator mediator, ITenantContextAcce Page = page, PageSize = pageSize }; - var result = await ExecuteAsPlatformAsync(() => mediator.Send(query, cancellationToken)); + var result = await mediator.Send(query, cancellationToken); // 2. 返回分页结果 return ApiResponse>.Ok(result); @@ -152,7 +149,7 @@ public sealed class StoreAuditsController(IMediator mediator, ITenantContextAcce CancellationToken cancellationToken) { // 1. 执行统计查询 - var result = await ExecuteAsPlatformAsync(() => mediator.Send(query, cancellationToken)); + var result = await mediator.Send(query, cancellationToken); // 2. 返回统计结果 return ApiResponse.Ok(result); @@ -175,7 +172,7 @@ public sealed class StoreAuditsController(IMediator mediator, ITenantContextAcce { // 1. 执行强制关闭 var request = command with { StoreId = storeId }; - var result = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken)); + var result = await mediator.Send(request, cancellationToken); // 2. 返回结果 return ApiResponse.Ok(result); @@ -198,25 +195,9 @@ public sealed class StoreAuditsController(IMediator mediator, ITenantContextAcce { // 1. 执行解除强制关闭 var request = command with { StoreId = storeId }; - var result = await ExecuteAsPlatformAsync(() => mediator.Send(request, cancellationToken)); + var result = await mediator.Send(request, cancellationToken); // 2. 返回结果 return ApiResponse.Ok(result); } - - private async Task ExecuteAsPlatformAsync(Func> action) - { - var original = tenantContextAccessor.Current; - tenantContextAccessor.Current = new TenantContext(0, null, "platform"); - - // 1. (空行后) 切换到平台上下文执行 - try - { - return await action(); - } - finally - { - tenantContextAccessor.Current = original; - } - } } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreQualificationsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreQualificationsController.cs index ce56b25..a4f4edb 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreQualificationsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoreQualificationsController.cs @@ -5,7 +5,6 @@ using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Application.App.Stores.Queries; using TakeoutSaaS.Module.Authorization.Attributes; using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Shared.Web.Api; namespace TakeoutSaaS.AdminApi.Controllers; @@ -15,11 +14,9 @@ namespace TakeoutSaaS.AdminApi.Controllers; /// [ApiVersion("1.0")] [Authorize] -[Route("api/platform/store-qualifications")] [Route("api/admin/v{version:apiVersion}/platform/store-qualifications")] public sealed class StoreQualificationsController( - IMediator mediator, - ITenantContextAccessor tenantContextAccessor) + IMediator mediator) : BaseApiController { /// @@ -36,25 +33,9 @@ public sealed class StoreQualificationsController( CancellationToken cancellationToken) { // 1. 查询资质预警 - var result = await ExecuteAsPlatformAsync(() => mediator.Send(query, cancellationToken)); + var result = await mediator.Send(query, cancellationToken); // 2. (空行后) 返回结果 return ApiResponse.Ok(result); } - - private async Task ExecuteAsPlatformAsync(Func> action) - { - var original = tenantContextAccessor.Current; - tenantContextAccessor.Current = new TenantContext(0, null, "platform"); - - // 1. (空行后) 切换到平台上下文执行 - try - { - return await action(); - } - finally - { - tenantContextAccessor.Current = original; - } - } } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantAnnouncementsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantAnnouncementsController.cs index b4aa7c2..0bc4a42 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantAnnouncementsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantAnnouncementsController.cs @@ -244,7 +244,7 @@ public sealed class TenantAnnouncementsController(IMediator mediator) : BaseApiC } // 2. (空行后) 发布公告 - command = command with { AnnouncementId = announcementId }; + command = command with { TenantId = tenantId, AnnouncementId = announcementId }; var result = await mediator.Send(command, cancellationToken); return result is null ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") @@ -289,7 +289,7 @@ public sealed class TenantAnnouncementsController(IMediator mediator) : BaseApiC } // 2. (空行后) 撤销公告 - command = command with { AnnouncementId = announcementId }; + command = command with { TenantId = tenantId, AnnouncementId = announcementId }; var result = await mediator.Send(command, cancellationToken); return result is null ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") @@ -329,42 +329,4 @@ public sealed class TenantAnnouncementsController(IMediator mediator) : BaseApiC return ApiResponse.Ok(result); } - /// - /// 标记公告已读。 - /// - /// - /// 示例: - /// - /// POST /api/admin/v1/tenants/100000000000000001/announcements/900123456789012345/read - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "id": "900123456789012345", - /// "isRead": true - /// } - /// } - /// - /// - [HttpPost("{announcementId:long}/read")] - [PermissionAuthorize("tenant-announcement:read")] - [SwaggerOperation(Summary = "标记公告已读", Description = "需要权限:tenant-announcement:read")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)] - public async Task> MarkRead(long tenantId, long announcementId, CancellationToken cancellationToken) - { - // 1. 校验租户标识 - if (tenantId <= 0) - { - return ApiResponse.Error(StatusCodes.Status400BadRequest, "租户标识无效"); - } - - // 2. (空行后) 标记已读 - var result = await mediator.Send(new MarkAnnouncementAsReadCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken); - return result is null - ? ApiResponse.Error(StatusCodes.Status404NotFound, "公告不存在") - : ApiResponse.Ok(result); - } } diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs index b42b168..9beaf81 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs @@ -5,7 +5,6 @@ using System.ComponentModel.DataAnnotations; using TakeoutSaaS.Application.App.Tenants.Commands; using TakeoutSaaS.Application.App.Tenants.Dto; using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Module.Authorization.Attributes; using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Web.Api; @@ -348,49 +347,6 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController return ApiResponse>.Ok(result); } - /// - /// 伪装登录租户(仅平台超级管理员可用)。 - /// - /// 目标租户主管理员的令牌对。 - [HttpPost("{tenantId:long}/impersonate")] - [PermissionAuthorize("tenant:read")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> Impersonate(long tenantId, CancellationToken cancellationToken) - { - // 1. 执行伪装登录 - var result = await mediator.Send(new ImpersonateTenantCommand { TenantId = tenantId }, cancellationToken); - - // 2. 返回令牌 - return ApiResponse.Ok(result); - } - - /// - /// 生成租户主管理员重置密码链接(仅平台超级管理员可用)。 - /// - /// 链接默认 24 小时有效且仅可使用一次。 - /// 重置密码链接。 - [HttpPost("{tenantId:long}/admin/reset-link")] - [PermissionAuthorize("tenant:read")] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] - public async Task> CreateAdminResetLink(long tenantId, CancellationToken cancellationToken) - { - // 1. 生成一次性令牌 - var token = await mediator.Send(new CreateTenantAdminResetLinkTokenCommand { TenantId = tenantId }, cancellationToken); - - // 2. 解析前端来源(优先 Origin,避免拼成 AdminApi 域名) - var origin = Request.Headers.Origin.ToString(); - if (string.IsNullOrWhiteSpace(origin)) - { - origin = $"{Request.Scheme}://{Request.Host}"; - } - - origin = origin.TrimEnd('/'); - var resetUrl = $"{origin}/#/auth/reset-password?token={Uri.EscapeDataString(token)}"; - - // 3. 返回链接 - return ApiResponse.Ok(data: resetUrl); - } - /// /// 配额校验并占用额度(门店/账号/短信/配送)。 /// diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/UserPermissionsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/UserPermissionsController.cs deleted file mode 100644 index c6a4a04..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/UserPermissionsController.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Application.Identity.Contracts; -using TakeoutSaaS.Application.Identity.Queries; -using TakeoutSaaS.Module.Authorization.Attributes; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Web.Api; - -namespace TakeoutSaaS.AdminApi.Controllers; - -/// -/// 用户权限洞察接口。 -/// -[ApiVersion("1.0")] -[Authorize] -[Route("api/admin/v{version:apiVersion}/users/permissions")] -public sealed class UserPermissionsController(IAdminAuthService authService) : BaseApiController -{ - /// - /// 分页查询当前租户用户的角色与权限概览。 - /// - /// - /// 示例: - /// - /// GET /api/admin/v1/users/permissions?keyword=ops&page=1&pageSize=20&sortBy=createdAt&sortDescending=true - /// Header: Authorization: Bearer <JWT> - /// 响应: - /// { - /// "success": true, - /// "code": 200, - /// "data": { - /// "items": [ - /// { - /// "userId": "900123456789012346", - /// "tenantId": "100000000000000001", - /// "merchantId": "200000000000000001", - /// "account": "ops.manager", - /// "displayName": "运营经理", - /// "roles": ["OpsManager", "Reporter"], - /// "permissions": ["delivery:read", "order:read", "payment:read"], - /// "createdAt": "2025-12-01T08:30:00Z" - /// } - /// ], - /// "page": 1, - /// "pageSize": 20, - /// "totalCount": 1, - /// "totalPages": 1 - /// } - /// } - /// - /// - /// 搜索条件。 - /// 取消标记。 - /// 分页的用户权限概览。 - [HttpGet] - [PermissionAuthorize("identity:permission:read")] - [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> Search( - [FromQuery] SearchUserPermissionsQuery query, - CancellationToken cancellationToken) - { - // 1. 查询当前租户的用户权限概览 - var result = await authService.SearchUserPermissionsAsync( - query.Keyword, - query.Page, - query.PageSize, - query.SortBy, - query.SortDescending, - cancellationToken); - - // 2. 返回分页结果 - return ApiResponse>.Ok(result); - } -} diff --git a/src/Api/TakeoutSaaS.AdminApi/Dockerfile b/src/Api/TakeoutSaaS.AdminApi/Dockerfile index 72754e9..81c8fea 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Dockerfile +++ b/src/Api/TakeoutSaaS.AdminApi/Dockerfile @@ -19,7 +19,6 @@ COPY ["src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csp COPY ["src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj", "src/Modules/TakeoutSaaS.Module.Scheduler/"] COPY ["src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj", "src/Modules/TakeoutSaaS.Module.Sms/"] COPY ["src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj", "src/Modules/TakeoutSaaS.Module.Storage/"] -COPY ["src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj", "src/Modules/TakeoutSaaS.Module.Tenancy/"] RUN dotnet restore "src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj" diff --git a/src/Api/TakeoutSaaS.AdminApi/Filters/TenantRouteContextFilter.cs b/src/Api/TakeoutSaaS.AdminApi/Filters/TenantRouteContextFilter.cs deleted file mode 100644 index bd30521..0000000 --- a/src/Api/TakeoutSaaS.AdminApi/Filters/TenantRouteContextFilter.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Filters; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.AdminApi.Filters; - -/// -/// 路由租户上下文过滤器:当路由中包含 tenantId 时,将其注入租户上下文,便于下游使用 与 EF 租户过滤器。 -/// -/// -/// 初始化过滤器。 -/// -/// 租户上下文访问器。 -public sealed class TenantRouteContextFilter(ITenantContextAccessor tenantContextAccessor) : IAsyncActionFilter -{ - /// - /// 在 Action 执行前将路由 tenantId 写入租户上下文,并在结束后恢复。 - /// - /// Action 执行上下文。 - /// 下一个执行委托。 - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - // 1. 解析路由 tenantId(仅当 > 0 才视为有效) - if (!TryGetRouteTenantId(context, out var tenantId)) - { - await next(); - return; - } - - // 2. (空行后) 备份并覆盖租户上下文 - var original = tenantContextAccessor.Current; - var current = new TenantContext(tenantId, null, "route:tenantId"); - tenantContextAccessor.Current = current; - context.HttpContext.Items[TenantConstants.HttpContextItemKey] = current; - - // 3. (空行后) 执行后续管道并在 finally 中恢复 - try - { - await next(); - } - finally - { - tenantContextAccessor.Current = original; - - if (original is null) - { - context.HttpContext.Items.Remove(TenantConstants.HttpContextItemKey); - } - else - { - context.HttpContext.Items[TenantConstants.HttpContextItemKey] = original; - } - } - } - - private static bool TryGetRouteTenantId(ActionExecutingContext context, out long tenantId) - { - if (!context.RouteData.Values.TryGetValue("tenantId", out var value) || value is null) - { - tenantId = 0; - return false; - } - - return long.TryParse(value.ToString(), out tenantId) && tenantId > 0; - } -} - diff --git a/src/Api/TakeoutSaaS.AdminApi/Program.cs b/src/Api/TakeoutSaaS.AdminApi/Program.cs index 3a289f8..89c027a 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Program.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Program.cs @@ -1,17 +1,13 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Cors.Infrastructure; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Serilog; -using System.Threading.RateLimiting; -using TakeoutSaaS.AdminApi.Filters; using TakeoutSaaS.Application.App.Extensions; using TakeoutSaaS.Application.Identity.Extensions; using TakeoutSaaS.Application.Messaging.Extensions; -using TakeoutSaaS.Application.Sms.Extensions; using TakeoutSaaS.Application.Storage.Extensions; using TakeoutSaaS.Infrastructure.App.Extensions; using TakeoutSaaS.Infrastructure.Identity.Extensions; @@ -22,7 +18,6 @@ using TakeoutSaaS.Module.Messaging.Extensions; using TakeoutSaaS.Module.Scheduler.Extensions; using TakeoutSaaS.Module.Sms.Extensions; using TakeoutSaaS.Module.Storage.Extensions; -using TakeoutSaaS.Module.Tenancy.Extensions; using TakeoutSaaS.Shared.Web.Extensions; using TakeoutSaaS.Shared.Web.Swagger; @@ -63,13 +58,6 @@ if (isDevelopment) }); } -// 4.1 (空行后) 路由租户注入:以路由 tenantId 写入租户上下文 -builder.Services.AddScoped(); -builder.Services.Configure(options => -{ - options.Filters.AddService(); -}); - // 5. 注册领域与基础设施模块 builder.Services.AddIdentityApplication(); builder.Services.AddIdentityInfrastructure(builder.Configuration, enableAdminSeed: true); @@ -78,12 +66,10 @@ builder.Services.AddAppApplication(); builder.Services.AddJwtAuthentication(builder.Configuration); builder.Services.AddAuthorization(); builder.Services.AddPermissionAuthorization(); -builder.Services.AddTenantResolution(builder.Configuration); builder.Services.AddDictionaryModule(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.AddOperationLogOutbox(builder.Configuration); @@ -92,13 +78,6 @@ builder.Services.AddHealthChecks(); builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; - options.AddFixedWindowLimiter("public-self-service", limiterOptions => - { - limiterOptions.PermitLimit = 10; - limiterOptions.Window = TimeSpan.FromMinutes(1); - limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; - limiterOptions.QueueLimit = 2; - }); }); // 6. 配置 OpenTelemetry 采集 @@ -167,9 +146,6 @@ app.UseCors("AdminApiCors"); app.UseRateLimiter(); app.UseSharedWebCore(); app.UseAuthentication(); - -// 8.1 (空行后) 解析租户:在认证后才能读取 Token Claim(tenant_id) -app.UseTenantResolution(); app.UseAuthorization(); if (app.Environment.IsDevelopment()) { diff --git a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj index 212123a..47f4749 100644 --- a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj +++ b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj @@ -35,6 +35,5 @@ - diff --git a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/AdjustInventoryCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/AdjustInventoryCommandHandler.cs index 0590589..293ab2c 100644 --- a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/AdjustInventoryCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/AdjustInventoryCommandHandler.cs @@ -4,9 +4,9 @@ using TakeoutSaaS.Application.App.Inventory.Commands; using TakeoutSaaS.Application.App.Inventory.Dto; using TakeoutSaaS.Domain.Inventory.Entities; using TakeoutSaaS.Domain.Inventory.Repositories; +using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Inventory.Handlers; @@ -15,7 +15,7 @@ namespace TakeoutSaaS.Application.App.Inventory.Handlers; /// public sealed class AdjustInventoryCommandHandler( IInventoryRepository inventoryRepository, - ITenantProvider tenantProvider, + IStoreRepository storeRepository, ILogger logger) : IRequestHandler { @@ -23,9 +23,10 @@ public sealed class AdjustInventoryCommandHandler( public async Task Handle(AdjustInventoryCommand request, CancellationToken cancellationToken) { // 1. 读取库存 - var tenantId = tenantProvider.GetCurrentTenantId(); - var item = await inventoryRepository.GetForUpdateAsync(tenantId, request.StoreId, request.ProductSkuId, cancellationToken); + var item = await inventoryRepository.GetForUpdateAsync(null, request.StoreId, request.ProductSkuId, cancellationToken); + // 2. 初始化或校验存在性 + long tenantId; if (item is null) { if (request.QuantityDelta < 0) @@ -33,6 +34,14 @@ public sealed class AdjustInventoryCommandHandler( throw new BusinessException(ErrorCodes.NotFound, "库存不存在,无法扣减"); } + // 2.1 查询门店以获取 TenantId + var store = await storeRepository.FindByIdAsync(request.StoreId, null, cancellationToken); + if (store is null) + { + throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); + } + tenantId = store.TenantId; + // 初始化库存记录 item = new InventoryItem { @@ -46,6 +55,10 @@ public sealed class AdjustInventoryCommandHandler( }; await inventoryRepository.AddItemAsync(item, cancellationToken); } + else + { + tenantId = item.TenantId; + } // 3. 应用调整 var newQuantity = item.QuantityOnHand + request.QuantityDelta; diff --git a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/DeductInventoryCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/DeductInventoryCommandHandler.cs index 0cbde90..419b845 100644 --- a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/DeductInventoryCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/DeductInventoryCommandHandler.cs @@ -5,7 +5,6 @@ using TakeoutSaaS.Application.App.Inventory.Dto; using TakeoutSaaS.Domain.Inventory.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Inventory.Handlers; @@ -14,7 +13,6 @@ namespace TakeoutSaaS.Application.App.Inventory.Handlers; /// public sealed class DeductInventoryCommandHandler( IInventoryRepository inventoryRepository, - ITenantProvider tenantProvider, ILogger logger) : IRequestHandler { @@ -22,12 +20,12 @@ public sealed class DeductInventoryCommandHandler( public async Task Handle(DeductInventoryCommand request, CancellationToken cancellationToken) { // 1. 读取库存 - var tenantId = tenantProvider.GetCurrentTenantId(); - var item = await inventoryRepository.GetForUpdateAsync(tenantId, request.StoreId, request.ProductSkuId, cancellationToken); + var item = await inventoryRepository.GetForUpdateAsync(null, request.StoreId, request.ProductSkuId, cancellationToken); if (item is null) { throw new BusinessException(ErrorCodes.NotFound, "库存不存在"); } + var tenantId = item.TenantId; // 1.1 幂等:若锁记录已扣减/释放则直接返回 if (!string.IsNullOrWhiteSpace(request.IdempotencyKey)) diff --git a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/GetInventoryBatchesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/GetInventoryBatchesQueryHandler.cs index 3796fda..23f0938 100644 --- a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/GetInventoryBatchesQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/GetInventoryBatchesQueryHandler.cs @@ -2,7 +2,6 @@ using MediatR; using TakeoutSaaS.Application.App.Inventory.Dto; using TakeoutSaaS.Application.App.Inventory.Queries; using TakeoutSaaS.Domain.Inventory.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Inventory.Handlers; @@ -10,16 +9,14 @@ namespace TakeoutSaaS.Application.App.Inventory.Handlers; /// 库存批次查询处理器。 /// public sealed class GetInventoryBatchesQueryHandler( - IInventoryRepository inventoryRepository, - ITenantProvider tenantProvider) + IInventoryRepository inventoryRepository) : IRequestHandler> { /// public async Task> Handle(GetInventoryBatchesQuery request, CancellationToken cancellationToken) { // 1. 读取批次 - var tenantId = tenantProvider.GetCurrentTenantId(); - var batches = await inventoryRepository.GetBatchesAsync(tenantId, request.StoreId, request.ProductSkuId, cancellationToken); + var batches = await inventoryRepository.GetBatchesAsync(null, request.StoreId, request.ProductSkuId, cancellationToken); // 2. 映射 return batches.Select(InventoryMapping.ToDto).ToList(); } diff --git a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/GetInventoryItemQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/GetInventoryItemQueryHandler.cs index f9a940c..e240039 100644 --- a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/GetInventoryItemQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/GetInventoryItemQueryHandler.cs @@ -2,7 +2,6 @@ using MediatR; using TakeoutSaaS.Application.App.Inventory.Dto; using TakeoutSaaS.Application.App.Inventory.Queries; using TakeoutSaaS.Domain.Inventory.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Inventory.Handlers; @@ -10,16 +9,14 @@ namespace TakeoutSaaS.Application.App.Inventory.Handlers; /// 查询库存处理器。 /// public sealed class GetInventoryItemQueryHandler( - IInventoryRepository inventoryRepository, - ITenantProvider tenantProvider) + IInventoryRepository inventoryRepository) : IRequestHandler { /// public async Task Handle(GetInventoryItemQuery request, CancellationToken cancellationToken) { // 1. 读取库存 - var tenantId = tenantProvider.GetCurrentTenantId(); - var item = await inventoryRepository.FindBySkuAsync(tenantId, request.StoreId, request.ProductSkuId, cancellationToken); + var item = await inventoryRepository.FindBySkuAsync(null, request.StoreId, request.ProductSkuId, cancellationToken); // 2. 返回 DTO return item is null ? null : InventoryMapping.ToDto(item); } diff --git a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/LockInventoryCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/LockInventoryCommandHandler.cs index 42f69fa..bfbdd10 100644 --- a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/LockInventoryCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/LockInventoryCommandHandler.cs @@ -5,7 +5,6 @@ using TakeoutSaaS.Application.App.Inventory.Dto; using TakeoutSaaS.Domain.Inventory.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Inventory.Handlers; @@ -14,7 +13,6 @@ namespace TakeoutSaaS.Application.App.Inventory.Handlers; /// public sealed class LockInventoryCommandHandler( IInventoryRepository inventoryRepository, - ITenantProvider tenantProvider, ILogger logger) : IRequestHandler { @@ -22,12 +20,12 @@ public sealed class LockInventoryCommandHandler( public async Task Handle(LockInventoryCommand request, CancellationToken cancellationToken) { // 1. 读取库存 - var tenantId = tenantProvider.GetCurrentTenantId(); - var item = await inventoryRepository.GetForUpdateAsync(tenantId, request.StoreId, request.ProductSkuId, cancellationToken); + var item = await inventoryRepository.GetForUpdateAsync(null, request.StoreId, request.ProductSkuId, cancellationToken); if (item is null) { throw new BusinessException(ErrorCodes.NotFound, "库存不存在"); } + var tenantId = item.TenantId; // 1.1 幂等处理 var existingLock = await inventoryRepository.FindLockByKeyAsync(tenantId, request.IdempotencyKey, cancellationToken); diff --git a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/ReleaseExpiredInventoryLocksCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/ReleaseExpiredInventoryLocksCommandHandler.cs index 0ef2884..37a5f28 100644 --- a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/ReleaseExpiredInventoryLocksCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/ReleaseExpiredInventoryLocksCommandHandler.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging; using TakeoutSaaS.Application.App.Inventory.Commands; using TakeoutSaaS.Domain.Inventory.Enums; using TakeoutSaaS.Domain.Inventory.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Inventory.Handlers; @@ -12,7 +11,6 @@ namespace TakeoutSaaS.Application.App.Inventory.Handlers; /// public sealed class ReleaseExpiredInventoryLocksCommandHandler( IInventoryRepository inventoryRepository, - ITenantProvider tenantProvider, ILogger logger) : IRequestHandler { @@ -20,9 +18,8 @@ public sealed class ReleaseExpiredInventoryLocksCommandHandler( public async Task Handle(ReleaseExpiredInventoryLocksCommand request, CancellationToken cancellationToken) { // 1. 查询过期锁 - var tenantId = tenantProvider.GetCurrentTenantId(); var now = DateTime.UtcNow; - var expiredLocks = await inventoryRepository.FindExpiredLocksAsync(tenantId, now, cancellationToken); + var expiredLocks = await inventoryRepository.FindExpiredLocksAsync(null, now, cancellationToken); if (expiredLocks.Count == 0) { return 0; @@ -32,7 +29,7 @@ public sealed class ReleaseExpiredInventoryLocksCommandHandler( var affected = 0; foreach (var lockRecord in expiredLocks) { - var item = await inventoryRepository.GetForUpdateAsync(tenantId, lockRecord.StoreId, lockRecord.ProductSkuId, cancellationToken); + var item = await inventoryRepository.GetForUpdateAsync(lockRecord.TenantId, lockRecord.StoreId, lockRecord.ProductSkuId, cancellationToken); if (item is null) { continue; diff --git a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/ReleaseInventoryCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/ReleaseInventoryCommandHandler.cs index b159471..4873073 100644 --- a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/ReleaseInventoryCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/ReleaseInventoryCommandHandler.cs @@ -5,7 +5,6 @@ using TakeoutSaaS.Application.App.Inventory.Dto; using TakeoutSaaS.Domain.Inventory.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Inventory.Handlers; @@ -14,7 +13,6 @@ namespace TakeoutSaaS.Application.App.Inventory.Handlers; /// public sealed class ReleaseInventoryCommandHandler( IInventoryRepository inventoryRepository, - ITenantProvider tenantProvider, ILogger logger) : IRequestHandler { @@ -22,12 +20,12 @@ public sealed class ReleaseInventoryCommandHandler( public async Task Handle(ReleaseInventoryCommand request, CancellationToken cancellationToken) { // 1. 读取库存 - var tenantId = tenantProvider.GetCurrentTenantId(); - var item = await inventoryRepository.GetForUpdateAsync(tenantId, request.StoreId, request.ProductSkuId, cancellationToken); + var item = await inventoryRepository.GetForUpdateAsync(null, request.StoreId, request.ProductSkuId, cancellationToken); if (item is null) { throw new BusinessException(ErrorCodes.NotFound, "库存不存在"); } + var tenantId = item.TenantId; // 1.1 幂等处理:若提供键且锁记录不存在,直接视为已释放 if (!string.IsNullOrWhiteSpace(request.IdempotencyKey)) diff --git a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/UpsertInventoryBatchCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/UpsertInventoryBatchCommandHandler.cs index dea621d..211030d 100644 --- a/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/UpsertInventoryBatchCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Inventory/Handlers/UpsertInventoryBatchCommandHandler.cs @@ -6,7 +6,6 @@ using TakeoutSaaS.Domain.Inventory.Entities; using TakeoutSaaS.Domain.Inventory.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Inventory.Handlers; @@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Inventory.Handlers; /// public sealed class UpsertInventoryBatchCommandHandler( IInventoryRepository inventoryRepository, - ITenantProvider tenantProvider, ILogger logger) : IRequestHandler { @@ -23,14 +21,21 @@ public sealed class UpsertInventoryBatchCommandHandler( public async Task Handle(UpsertInventoryBatchCommand request, CancellationToken cancellationToken) { // 1. 读取批次 - var tenantId = tenantProvider.GetCurrentTenantId(); - var batch = await inventoryRepository.GetBatchForUpdateAsync(tenantId, request.StoreId, request.ProductSkuId, request.BatchNumber, cancellationToken); + var batch = await inventoryRepository.GetBatchForUpdateAsync(null, request.StoreId, request.ProductSkuId, request.BatchNumber, cancellationToken); + // 2. 创建或更新 if (batch is null) { + // 2.1 查询库存以获取 TenantId + var item = await inventoryRepository.FindBySkuAsync(null, request.StoreId, request.ProductSkuId, cancellationToken); + if (item is null) + { + throw new BusinessException(ErrorCodes.NotFound, "库存不存在,无法新增批次"); + } + batch = new InventoryBatch { - TenantId = tenantId, + TenantId = item.TenantId, StoreId = request.StoreId, ProductSkuId = request.ProductSkuId, BatchNumber = request.BatchNumber, diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAdminResetLinkTokenCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAdminResetLinkTokenCommand.cs deleted file mode 100644 index 0005a64..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/CreateTenantAdminResetLinkTokenCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 生成租户主管理员重置链接令牌命令(平台超级管理员使用)。 -/// -public sealed record CreateTenantAdminResetLinkTokenCommand : IRequest -{ - /// - /// 目标租户 ID。 - /// - public required long TenantId { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ImpersonateTenantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ImpersonateTenantCommand.cs deleted file mode 100644 index 1ea701a..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/ImpersonateTenantCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.Identity.Contracts; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 伪装登录租户命令(平台超级管理员使用)。 -/// -public sealed record ImpersonateTenantCommand : IRequest -{ - /// - /// 目标租户 ID。 - /// - public required long TenantId { get; init; } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkAnnouncementAsReadCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkAnnouncementAsReadCommand.cs deleted file mode 100644 index 286e207..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/MarkAnnouncementAsReadCommand.cs +++ /dev/null @@ -1,22 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 标记公告已读命令。 -/// -public sealed record MarkAnnouncementAsReadCommand : IRequest -{ - /// - /// 租户 ID(雪花算法,兼容旧调用,实际以当前租户为准)。 - /// - public long TenantId { get; init; } - - /// - /// 公告 ID。 - /// - [Range(1, long.MaxValue)] - public long AnnouncementId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/PublishAnnouncementCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/PublishAnnouncementCommand.cs index a56c6ba..ce3281a 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/PublishAnnouncementCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/PublishAnnouncementCommand.cs @@ -9,6 +9,12 @@ namespace TakeoutSaaS.Application.App.Tenants.Commands; /// public sealed record PublishAnnouncementCommand : IRequest { + /// + /// 租户 ID(0 表示平台公告)。 + /// + [Range(0, long.MaxValue)] + public long TenantId { get; init; } + /// /// 公告 ID。 /// diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/RevokeAnnouncementCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/RevokeAnnouncementCommand.cs index fc5628d..09b1a79 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/RevokeAnnouncementCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/RevokeAnnouncementCommand.cs @@ -9,6 +9,12 @@ namespace TakeoutSaaS.Application.App.Tenants.Commands; /// public sealed record RevokeAnnouncementCommand : IRequest { + /// + /// 租户 ID(0 表示平台公告)。 + /// + [Range(0, long.MaxValue)] + public long TenantId { get; init; } + /// /// 公告 ID。 /// diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/SelfRegisterTenantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/SelfRegisterTenantCommand.cs deleted file mode 100644 index cddd7ed..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/SelfRegisterTenantCommand.cs +++ /dev/null @@ -1,46 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Commands; - -/// -/// 自助注册租户命令。 -/// -public sealed record SelfRegisterTenantCommand : IRequest -{ - /// - /// 初始管理员账号。 - /// - [Required] - [StringLength(64)] - [RegularExpression("^[A-Za-z0-9]+$", ErrorMessage = "登录账号仅允许大小写字母和数字")] - public string AdminAccount { get; init; } = string.Empty; - - /// - /// 初始管理员展示名称。 - /// - [StringLength(64)] - public string? AdminDisplayName { get; init; } - - /// - /// 初始管理员邮箱。 - /// - [EmailAddress] - [StringLength(128)] - public string? AdminEmail { get; init; } - - /// - /// 初始管理员手机号。 - /// - [Required] - [StringLength(32)] - public string AdminPhone { get; init; } = string.Empty; - - /// - /// 初始管理员登录密码(前端自定义)。 - /// - [Required] - [StringLength(128, MinimumLength = 8)] - public string AdminPassword { get; init; } = string.Empty; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/SelfRegisterResultDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/SelfRegisterResultDto.cs deleted file mode 100644 index 024e485..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/SelfRegisterResultDto.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Tenants.Dto; - -/// -/// 自助注册结果 DTO。 -/// -public sealed class SelfRegisterResultDto -{ - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 租户编码。 - /// - public string Code { get; init; } = string.Empty; - - /// - /// 初始状态。 - /// - public TenantStatus Status { get; init; } = TenantStatus.PendingReview; - - /// - /// 当前实名状态。 - /// - public TenantVerificationStatus VerificationStatus { get; init; } = TenantVerificationStatus.Draft; - - /// - /// 订阅开始时间。 - /// - public DateTime? EffectiveFrom { get; init; } - - /// - /// 订阅到期时间。 - /// - public DateTime? EffectiveTo { get; init; } - - /// - /// 初始管理员账号。 - /// - public string AdminAccount { get; init; } = string.Empty; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantProgressDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantProgressDto.cs deleted file mode 100644 index 25e5807..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantProgressDto.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Text.Json.Serialization; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Shared.Abstractions.Serialization; - -namespace TakeoutSaaS.Application.App.Tenants.Dto; - -/// -/// 租户入住进度 DTO。 -/// -public sealed class TenantProgressDto -{ - /// - /// 租户 ID。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - - /// - /// 租户编码。 - /// - public string Code { get; init; } = string.Empty; - - /// - /// 当前租户状态。 - /// - public TenantStatus Status { get; init; } - - /// - /// 实名审核状态。 - /// - public TenantVerificationStatus VerificationStatus { get; init; } - - /// - /// 当前订阅开始时间。 - /// - public DateTime? EffectiveFrom { get; init; } - - /// - /// 当前订阅到期时间。 - /// - public DateTime? EffectiveTo { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAdminResetLinkTokenCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAdminResetLinkTokenCommandHandler.cs deleted file mode 100644 index 788dfa3..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAdminResetLinkTokenCommandHandler.cs +++ /dev/null @@ -1,94 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -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.Tenants.Handlers; - -/// -/// 生成租户主管理员重置链接令牌处理器(平台超级管理员使用)。 -/// -public sealed class CreateTenantAdminResetLinkTokenCommandHandler( - ITenantRepository tenantRepository, - ITenantProvider tenantProvider, - ITenantContextAccessor tenantContextAccessor, - IIdentityUserRepository identityUserRepository, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService, - IAdminPasswordResetTokenStore tokenStore) - : IRequestHandler -{ - private const long PlatformRootTenantId = 1000000000001; - - /// - public async Task Handle(CreateTenantAdminResetLinkTokenCommand request, CancellationToken cancellationToken) - { - // 1. 校验仅允许平台超级管理员执行 - var currentTenantId = tenantProvider.GetCurrentTenantId(); - if (currentTenantId != PlatformRootTenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台超级管理员可生成重置链接"); - } - - // 2. 校验租户存在且存在主管理员 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 2.1 若缺少主管理员则自动回填(兼容历史数据) - if (!tenant.PrimaryOwnerUserId.HasValue || tenant.PrimaryOwnerUserId.Value == 0) - { - var originalContextForFix = tenantContextAccessor.Current; - tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "admin:reset-link:fix-owner"); - try - { - var users = await identityUserRepository.SearchAsync(tenant.Id, keyword: null, cancellationToken); - var ownerCandidate = users.OrderBy(x => x.CreatedAt).FirstOrDefault(); - if (ownerCandidate == null) - { - throw new BusinessException(ErrorCodes.BadRequest, "该租户未配置主管理员账号,且未找到可用管理员账号"); - } - - tenant.PrimaryOwnerUserId = ownerCandidate.Id; - await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - } - finally - { - tenantContextAccessor.Current = originalContextForFix; - } - } - - // 3. 签发一次性重置令牌(默认 24 小时有效) - var token = await tokenStore.IssueAsync(tenant.PrimaryOwnerUserId.Value, DateTime.UtcNow.AddHours(24), cancellationToken); - - // 4. 写入审计日志 - var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) - ? $"user:{currentUserAccessor.UserId}" - : operatorProfile.DisplayName; - - var auditLog = new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.AdminResetLinkIssued, - Title = "生成重置链接", - Description = $"操作者:{operatorName},目标用户ID:{tenant.PrimaryOwnerUserId.Value}", - OperatorId = currentUserAccessor.UserId, - OperatorName = operatorName, - PreviousStatus = tenant.Status, - CurrentStatus = tenant.Status - }; - await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 5. 返回令牌 - return token; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs index 8db8ad0..e2d9fd5 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs @@ -14,7 +14,6 @@ using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; @@ -25,11 +24,11 @@ public sealed class CreateTenantManuallyCommandHandler( ITenantRepository tenantRepository, ITenantPackageRepository tenantPackageRepository, IIdentityUserRepository identityUserRepository, + IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, IPasswordHasher passwordHasher, IIdGenerator idGenerator, IMediator mediator, - ITenantContextAccessor tenantContextAccessor, ICurrentUserAccessor currentUserAccessor, ILogger logger) : IRequestHandler @@ -208,56 +207,44 @@ public sealed class CreateTenantManuallyCommandHandler( await tenantRepository.UpsertVerificationProfileAsync(verification, cancellationToken); await tenantRepository.SaveChangesAsync(cancellationToken); - // 12. 临时切换租户上下文,保证身份与权限写入正确 - var previousContext = tenantContextAccessor.Current; - tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "manual-create"); - try + // 12. 创建租户管理员账号(Portal=Tenant) + var adminUser = new IdentityUser { - // 13. 创建租户管理员账号 - var adminUser = new IdentityUser - { - Portal = PortalType.Tenant, - TenantId = tenant.Id, - Account = normalizedAccount, - DisplayName = request.AdminDisplayName.Trim(), - PasswordHash = string.Empty, - Phone = string.IsNullOrWhiteSpace(request.ContactPhone) ? null : request.ContactPhone.Trim(), - Email = string.IsNullOrWhiteSpace(request.ContactEmail) ? null : request.ContactEmail.Trim(), - MerchantId = request.AdminMerchantId, - Avatar = request.AdminAvatar - }; - adminUser.PasswordHash = passwordHasher.HashPassword(adminUser, request.AdminPassword); - await identityUserRepository.AddAsync(adminUser, cancellationToken); - await identityUserRepository.SaveChangesAsync(cancellationToken); + Portal = PortalType.Tenant, + TenantId = tenant.Id, + Account = normalizedAccount, + DisplayName = request.AdminDisplayName.Trim(), + PasswordHash = string.Empty, + Phone = string.IsNullOrWhiteSpace(request.ContactPhone) ? null : request.ContactPhone.Trim(), + Email = string.IsNullOrWhiteSpace(request.ContactEmail) ? null : request.ContactEmail.Trim(), + MerchantId = request.AdminMerchantId, + Avatar = request.AdminAvatar + }; + adminUser.PasswordHash = passwordHasher.HashPassword(adminUser, request.AdminPassword); + await identityUserRepository.AddAsync(adminUser, cancellationToken); + await identityUserRepository.SaveChangesAsync(cancellationToken); - // 14. 初始化租户管理员角色模板并绑定角色 - await mediator.Send(new InitializeRoleTemplatesCommand - { - TemplateCodes = new[] { "tenant-admin" } - }, cancellationToken); - - var tenantAdminRole = await roleRepository.FindByCodeAsync(PortalType.Tenant, tenant.Id, "tenant-admin", cancellationToken); - if (tenantAdminRole != null) - { - await mediator.Send(new AssignUserRolesCommand - { - UserId = adminUser.Id, - RoleIds = new[] { tenantAdminRole.Id } - }, cancellationToken); - } - - // 15. 回写租户所有者账号 - tenant.PrimaryOwnerUserId = adminUser.Id; - await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - } - finally + // 13. 初始化租户管理员角色并绑定到用户(不依赖租户上下文) + await mediator.Send(new CopyRoleTemplateCommand { - // 16. 恢复上下文 - tenantContextAccessor.Current = previousContext; + Portal = PortalType.Tenant, + TenantId = tenant.Id, + TemplateCode = "tenant-admin" + }, cancellationToken); + + var tenantAdminRole = await roleRepository.FindByCodeAsync(PortalType.Tenant, tenant.Id, "tenant-admin", cancellationToken); + if (tenantAdminRole != null) + { + await userRoleRepository.ReplaceUserRolesAsync(PortalType.Tenant, tenant.Id, adminUser.Id, new[] { tenantAdminRole.Id }, cancellationToken); + await userRoleRepository.SaveChangesAsync(cancellationToken); } - // 17. 返回创建结果 + // 14. 回写租户所有者账号 + tenant.PrimaryOwnerUserId = adminUser.Id; + await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); + await tenantRepository.SaveChangesAsync(cancellationToken); + + // 15. 返回创建结果 logger.LogInformation("已后台手动创建租户 {TenantCode}", tenant.Code); return new TenantDetailDto diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetAnnouncementByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetAnnouncementByIdQueryHandler.cs index bdf0e81..5a28125 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetAnnouncementByIdQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetAnnouncementByIdQueryHandler.cs @@ -1,11 +1,7 @@ using MediatR; using TakeoutSaaS.Application.App.Tenants.Dto; using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Application.App.Tenants.Targeting; -using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; @@ -13,12 +9,7 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// 公告详情查询处理器。 /// public sealed class GetAnnouncementByIdQueryHandler( - ITenantAnnouncementRepository announcementRepository, - ITenantAnnouncementReadRepository readRepository, - ITenantProvider tenantProvider, - ICurrentUserAccessor? currentUserAccessor = null, - IAdminAuthService? adminAuthService = null, - IMiniAuthService? miniAuthService = null) + ITenantAnnouncementRepository announcementRepository) : IRequestHandler { /// @@ -29,43 +20,14 @@ public sealed class GetAnnouncementByIdQueryHandler( /// 公告 DTO 或 null。 public async Task Handle(GetAnnouncementByIdQuery request, CancellationToken cancellationToken) { - var tenantId = tenantProvider.GetCurrentTenantId(); - - // 1. 查询公告主体(含平台公告) - var announcement = await announcementRepository.FindByIdInScopeAsync(tenantId, request.AnnouncementId, cancellationToken); + // 1. 查询公告主体 + var announcement = await announcementRepository.FindByIdAsync(request.TenantId, request.AnnouncementId, cancellationToken); if (announcement == null) { return null; } - // 2. 目标受众过滤 - var targetContext = await AnnouncementTargetContextFactory.BuildAsync( - tenantProvider, - currentUserAccessor, - adminAuthService, - miniAuthService, - cancellationToken); - - if (!TargetTypeFilter.IsMatch(announcement, targetContext)) - { - return null; - } - - // 3. 优先查用户级已读 - var userId = targetContext.UserId; - var reads = await readRepository.GetByAnnouncementAsync( - tenantId, - new[] { announcement.Id }, - userId == 0 ? null : userId, - cancellationToken); - - if (reads.Count == 0) - { - var tenantReads = await readRepository.GetByAnnouncementAsync(tenantId, new[] { announcement.Id }, null, cancellationToken); - reads = tenantReads; - } - - var readRecord = reads.FirstOrDefault(); - return announcement.ToDto(readRecord != null, readRecord?.ReadAt); + // 2. (空行后) 映射 DTO(管理端不返回已读信息) + return announcement.ToDto(false, null); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetPublicTenantPackagesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetPublicTenantPackagesQueryHandler.cs deleted file mode 100644 index a974f4c..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetPublicTenantPackagesQueryHandler.cs +++ /dev/null @@ -1,36 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 公共场景分页查询启用套餐处理器。 -/// -public sealed class GetPublicTenantPackagesQueryHandler(ITenantPackageRepository packageRepository) - : IRequestHandler> -{ - /// - public async Task> Handle(GetPublicTenantPackagesQuery request, CancellationToken cancellationToken) - { - // 1. 仅查询公共可选购套餐(已发布 + 对外可见 + 允许新购 + 启用) - var packages = await packageRepository.SearchPublicPurchasableAsync(cancellationToken); - // 2. 规范化分页参数 - var pageIndex = request.Page <= 0 ? 1 : request.Page; - var size = request.PageSize <= 0 ? 20 : request.PageSize; - // 3. 执行排序、分页与映射 - var ordered = packages - .OrderBy(x => x.SortOrder) - .ThenByDescending(x => x.CreatedAt) - .ToList(); - var items = ordered - .Skip((pageIndex - 1) * size) - .Take(size) - .Select(x => x.ToDto()) - .ToList(); - // 4. 返回分页结果 - return new PagedResult(items, pageIndex, size, ordered.Count); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantProgressQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantProgressQueryHandler.cs deleted file mode 100644 index 25eaca6..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantProgressQueryHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 租户入住进度查询处理器。 -/// -public sealed class GetTenantProgressQueryHandler(ITenantRepository tenantRepository) - : IRequestHandler -{ - /// - public async Task Handle(GetTenantProgressQuery request, CancellationToken cancellationToken) - { - // 1. 查询租户 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 2. 查询订阅与实名 - var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken); - var verification = await tenantRepository.GetVerificationProfileAsync(request.TenantId, cancellationToken); - - // 3. 组装进度信息 - return new TenantProgressDto - { - TenantId = tenant.Id, - Code = tenant.Code, - Status = tenant.Status, - VerificationStatus = verification?.Status ?? TenantVerificationStatus.Draft, - EffectiveFrom = subscription?.EffectiveFrom ?? tenant.EffectiveFrom, - EffectiveTo = subscription?.EffectiveTo ?? tenant.EffectiveTo - }; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandler.cs index 22b85d6..cd9a74a 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandler.cs @@ -1,12 +1,8 @@ using MediatR; using TakeoutSaaS.Application.App.Tenants.Dto; using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Application.App.Tenants.Targeting; -using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; @@ -14,12 +10,7 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// 公告分页查询处理器。 /// public sealed class GetTenantsAnnouncementsQueryHandler( - ITenantAnnouncementRepository announcementRepository, - ITenantAnnouncementReadRepository announcementReadRepository, - ITenantProvider tenantProvider, - ICurrentUserAccessor? currentUserAccessor = null, - IAdminAuthService? adminAuthService = null, - IMiniAuthService? miniAuthService = null) + ITenantAnnouncementRepository announcementRepository) : IRequestHandler> { /// @@ -30,7 +21,7 @@ public sealed class GetTenantsAnnouncementsQueryHandler( /// 分页结果。 public async Task> Handle(GetTenantsAnnouncementsQuery request, CancellationToken cancellationToken) { - var tenantId = tenantProvider.GetCurrentTenantId(); + var tenantId = request.TenantId; var effectiveAt = request.OnlyEffective == true ? DateTime.UtcNow : (DateTime?)null; // 计算分页参数 @@ -64,62 +55,20 @@ public sealed class GetTenantsAnnouncementsQueryHandler( .ToList(); } - // 3. 目标受众过滤(在内存中,但数据量已大幅减少) - var targetContext = await AnnouncementTargetContextFactory.BuildAsync( - tenantProvider, - currentUserAccessor, - adminAuthService, - miniAuthService, - cancellationToken); - + // 3. 按租户隔离(仅返回 request.TenantId 对应的公告) var filtered = announcements - .Where(a => TargetTypeFilter.IsMatch(a, targetContext)) + .Where(x => x.TenantId == tenantId) .ToList(); - // 注意:由于目标受众过滤可能移除记录,filtered.Count 可能小于请求的 size - // 这是可接受的,因为精确计算总数代价高昂 - // 4. 分页(数据已在数据库层排序,这里只需 Skip/Take) var pageItems = filtered .Skip((page - 1) * size) .Take(size) .ToList(); - // 5. 构建已读映射 - var announcementIds = pageItems.Select(x => x.Id).ToArray(); - var userId = targetContext.UserId; - - var readMap = new Dictionary(); - if (announcementIds.Length > 0) - { - var reads = new List(); - if (userId != 0) - { - var userReads = await announcementReadRepository.GetByAnnouncementAsync(tenantId, announcementIds, userId, cancellationToken); - reads.AddRange(userReads); - } - - var tenantReads = await announcementReadRepository.GetByAnnouncementAsync(tenantId, announcementIds, null, cancellationToken); - reads.AddRange(tenantReads); - - foreach (var read in reads.OrderByDescending(x => x.ReadAt)) - { - if (readMap.ContainsKey(read.AnnouncementId) && read.UserId.HasValue) - { - continue; - } - - readMap[read.AnnouncementId] = (true, read.ReadAt); - } - } - - // 6. 映射 DTO 并带上已读状态 + // 5. 映射 DTO var items = pageItems - .Select(a => - { - readMap.TryGetValue(a.Id, out var read); - return a.ToDto(read.isRead, read.readAt); - }) + .Select(a => a.ToDto(false, null)) .ToList(); // 注意:由于我们使用了估算的 limit,总数是 filtered.Count 而不是数据库中的实际总数 diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandler.cs deleted file mode 100644 index fc5a445..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandler.cs +++ /dev/null @@ -1,76 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Application.App.Tenants.Targeting; -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 未读公告查询处理器。 -/// -public sealed class GetUnreadAnnouncementsQueryHandler( - ITenantAnnouncementRepository announcementRepository, - ITenantProvider tenantProvider, - ICurrentUserAccessor? currentUserAccessor = null, - IAdminAuthService? adminAuthService = null, - IMiniAuthService? miniAuthService = null) - : IRequestHandler> -{ - /// - public async Task> Handle(GetUnreadAnnouncementsQuery request, CancellationToken cancellationToken) - { - var tenantId = tenantProvider.GetCurrentTenantId(); - var userId = currentUserAccessor?.UserId ?? 0; - var now = DateTime.UtcNow; - - // 1. 查询未读公告(已发布/启用/有效期内) - var announcements = await announcementRepository.SearchUnreadAsync( - tenantId, - userId == 0 ? null : userId, - AnnouncementStatus.Published, - true, - now, - cancellationToken); - - announcements = announcements - .Where(x => x.ScheduledPublishAt == null || x.ScheduledPublishAt <= now) - .ToList(); - - // 2. 目标受众过滤 - var targetContext = await AnnouncementTargetContextFactory.BuildAsync( - tenantProvider, - currentUserAccessor, - adminAuthService, - miniAuthService, - cancellationToken); - - var filtered = announcements - .Where(a => TargetTypeFilter.IsMatch(a, targetContext)) - .ToList(); - - // 3. 排序与分页 - var ordered = filtered - .OrderByDescending(x => x.Priority) - .ThenByDescending(x => x.EffectiveFrom) - .ToList(); - - var page = request.Page <= 0 ? 1 : request.Page; - var size = request.PageSize <= 0 ? 20 : request.PageSize; - var pageItems = ordered - .Skip((page - 1) * size) - .Take(size) - .ToList(); - - var items = pageItems - .Select(x => x.ToDto(false, null)) - .ToList(); - - return new PagedResult(items, page, size, ordered.Count); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ImpersonateTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ImpersonateTenantCommandHandler.cs deleted file mode 100644 index 55c60ca..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ImpersonateTenantCommandHandler.cs +++ /dev/null @@ -1,109 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Application.Identity.Contracts; -using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -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.Tenants.Handlers; - -/// -/// 伪装登录租户处理器(平台超级管理员使用)。 -/// -public sealed class ImpersonateTenantCommandHandler( - ITenantRepository tenantRepository, - ITenantProvider tenantProvider, - ITenantContextAccessor tenantContextAccessor, - IIdentityUserRepository identityUserRepository, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService, - IJwtTokenService jwtTokenService) - : IRequestHandler -{ - private const long PlatformRootTenantId = 1000000000001; - - /// - public async Task Handle(ImpersonateTenantCommand request, CancellationToken cancellationToken) - { - // 1. 校验仅允许平台超级管理员执行 - var currentTenantId = tenantProvider.GetCurrentTenantId(); - if (currentTenantId != PlatformRootTenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台超级管理员可执行伪装登录"); - } - - // 2. 读取操作者信息(在平台租户上下文内) - var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) - ? $"user:{currentUserAccessor.UserId}" - : operatorProfile.DisplayName; - - // 2. 校验租户存在且存在主管理员 - var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); - - // 2.1 若缺少主管理员则自动回填(兼容历史数据) - if (!tenant.PrimaryOwnerUserId.HasValue || tenant.PrimaryOwnerUserId.Value == 0) - { - var originalContextForFix = tenantContextAccessor.Current; - tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "admin:impersonate:fix-owner"); - try - { - var users = await identityUserRepository.SearchAsync(tenant.Id, keyword: null, cancellationToken); - var ownerCandidate = users.OrderBy(x => x.CreatedAt).FirstOrDefault(); - if (ownerCandidate == null) - { - throw new BusinessException(ErrorCodes.BadRequest, "该租户未配置主管理员账号,且未找到可用管理员账号"); - } - - tenant.PrimaryOwnerUserId = ownerCandidate.Id; - await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - } - finally - { - tenantContextAccessor.Current = originalContextForFix; - } - } - - // 3. 进入目标租户上下文以读取租户内用户(避免多租户查询过滤导致找不到用户) - var originalTenantContext = tenantContextAccessor.Current; - tenantContextAccessor.Current = new TenantContext(tenant.Id, null, "admin:impersonate"); - try - { - // 4. 为租户主管理员签发令牌 - var targetProfile = await adminAuthService.GetProfileAsync(tenant.PrimaryOwnerUserId.Value, cancellationToken); - var token = await jwtTokenService.CreateTokensAsync(targetProfile, false, cancellationToken); - - // 5. 恢复租户上下文后写入审计日志 - tenantContextAccessor.Current = originalTenantContext; - var auditLog = new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.ImpersonatedLogin, - Title = "伪装登录", - Description = $"操作者:{operatorName},目标账号:{targetProfile.Account}", - OperatorId = currentUserAccessor.UserId, - OperatorName = operatorName, - PreviousStatus = tenant.Status, - CurrentStatus = tenant.Status - }; - await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 6. 返回令牌 - return token; - } - finally - { - // 7. 确保恢复租户上下文 - tenantContextAccessor.Current = originalTenantContext; - } - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkAnnouncementAsReadCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkAnnouncementAsReadCommandHandler.cs deleted file mode 100644 index 124af7d..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkAnnouncementAsReadCommandHandler.cs +++ /dev/null @@ -1,100 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.App.Tenants.Targeting; -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 标记公告已读处理器。 -/// -public sealed class MarkAnnouncementAsReadCommandHandler( - ITenantAnnouncementRepository announcementRepository, - ITenantAnnouncementReadRepository readRepository, - ITenantProvider tenantProvider, - ICurrentUserAccessor? currentUserAccessor = null, - IAdminAuthService? adminAuthService = null, - IMiniAuthService? miniAuthService = null) - : IRequestHandler -{ - /// - /// 标记公告已读。 - /// - /// 标记命令。 - /// 取消标记。 - /// 公告 DTO 或 null。 - public async Task Handle(MarkAnnouncementAsReadCommand request, CancellationToken cancellationToken) - { - var tenantId = tenantProvider.GetCurrentTenantId(); - - // 1. 查询公告(含平台公告) - var announcement = await announcementRepository.FindByIdInScopeAsync(tenantId, request.AnnouncementId, cancellationToken); - if (announcement == null) - { - return null; - } - - // 2. 仅允许已发布且在有效期内的公告标记已读 - var now = DateTime.UtcNow; - if (announcement.Status != AnnouncementStatus.Published) - { - return null; - } - - if (announcement.EffectiveFrom > now || (announcement.EffectiveTo.HasValue && announcement.EffectiveTo.Value < now)) - { - return null; - } - - if (announcement.ScheduledPublishAt.HasValue && announcement.ScheduledPublishAt.Value > now) - { - return null; - } - - // 3. 目标受众过滤 - var targetContext = await AnnouncementTargetContextFactory.BuildAsync( - tenantProvider, - currentUserAccessor, - adminAuthService, - miniAuthService, - cancellationToken); - - if (!TargetTypeFilter.IsMatch(announcement, targetContext)) - { - return null; - } - - // 4. 确定用户标识 - var userId = targetContext.UserId == 0 ? (long?)null : targetContext.UserId; - var existing = await readRepository.FindAsync(tenantId, announcement.Id, userId, cancellationToken); - - if (existing == null && userId.HasValue) - { - existing = await readRepository.FindAsync(tenantId, announcement.Id, null, cancellationToken); - } - - // 5. 如未读则写入已读记录 - if (existing == null) - { - var record = new TenantAnnouncementRead - { - TenantId = tenantId, - AnnouncementId = announcement.Id, - UserId = userId, - ReadAt = now - }; - - await readRepository.AddAsync(record, cancellationToken); - await readRepository.SaveChangesAsync(cancellationToken); - existing = record; - } - - return announcement.ToDto(true, existing.ReadAt); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/PublishAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/PublishAnnouncementCommandHandler.cs index 1162527..5561031 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/PublishAnnouncementCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/PublishAnnouncementCommandHandler.cs @@ -7,7 +7,6 @@ using TakeoutSaaS.Domain.Tenants.Events; using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; @@ -16,7 +15,6 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// public sealed class PublishAnnouncementCommandHandler( ITenantAnnouncementRepository announcementRepository, - ITenantProvider tenantProvider, IEventPublisher eventPublisher) : IRequestHandler { @@ -29,7 +27,7 @@ public sealed class PublishAnnouncementCommandHandler( } // 1. 查询公告 - var tenantId = tenantProvider.GetCurrentTenantId(); + var tenantId = request.TenantId; var announcement = await announcementRepository.FindByIdAsync(tenantId, request.AnnouncementId, cancellationToken); if (announcement == null) { diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RevokeAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RevokeAnnouncementCommandHandler.cs index 01a57d5..27c4c46 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RevokeAnnouncementCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RevokeAnnouncementCommandHandler.cs @@ -7,7 +7,6 @@ using TakeoutSaaS.Domain.Tenants.Events; using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; @@ -16,7 +15,6 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// public sealed class RevokeAnnouncementCommandHandler( ITenantAnnouncementRepository announcementRepository, - ITenantProvider tenantProvider, IEventPublisher eventPublisher) : IRequestHandler { @@ -29,7 +27,7 @@ public sealed class RevokeAnnouncementCommandHandler( } // 1. 查询公告 - var tenantId = tenantProvider.GetCurrentTenantId(); + var tenantId = request.TenantId; var announcement = await announcementRepository.FindByIdAsync(tenantId, request.AnnouncementId, cancellationToken); if (announcement == null) { diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SelfRegisterTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SelfRegisterTenantCommandHandler.cs deleted file mode 100644 index 6f573c9..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SelfRegisterTenantCommandHandler.cs +++ /dev/null @@ -1,142 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Identity; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Application.Identity.Commands; -using TakeoutSaaS.Domain.Identity.Entities; -using TakeoutSaaS.Domain.Identity.Enums; -using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Ids; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.App.Tenants.Handlers; - -/// -/// 自助注册租户处理器。 -/// -public sealed class SelfRegisterTenantCommandHandler( - ITenantRepository tenantRepository, - IIdentityUserRepository identityUserRepository, - IRoleRepository roleRepository, - IPasswordHasher passwordHasher, - IIdGenerator idGenerator, - IMediator mediator, - ITenantContextAccessor tenantContextAccessor) - : IRequestHandler -{ - /// - public async Task Handle(SelfRegisterTenantCommand request, CancellationToken cancellationToken) - { - // 1. 唯一性校验 - var normalizedAccount = request.AdminAccount.Trim(); - if (await identityUserRepository.ExistsByAccountAsync(normalizedAccount, cancellationToken)) - { - throw new BusinessException(ErrorCodes.Conflict, $"账号 {normalizedAccount} 已存在"); - } - - // 1.2 校验手机号唯一性 - var normalizedPhone = request.AdminPhone.Trim(); - if (await tenantRepository.ExistsByContactPhoneAsync(normalizedPhone, cancellationToken)) - { - throw new BusinessException(ErrorCodes.Conflict, $"手机号 {normalizedPhone} 已注册"); - } - - // 2. 生成租户标识与编码 - var tenantId = idGenerator.NextId(); - var tenantCode = $"t{tenantId}"; - - // 3. 构建租户(无订阅,待审核) - var tenant = new Tenant - { - Id = tenantId, - Code = tenantCode, - Name = normalizedAccount, - ShortName = string.IsNullOrWhiteSpace(request.AdminDisplayName) ? normalizedAccount : request.AdminDisplayName!.Trim(), - ContactName = string.IsNullOrWhiteSpace(request.AdminDisplayName) ? normalizedAccount : request.AdminDisplayName!.Trim(), - ContactPhone = normalizedPhone, - ContactEmail = request.AdminEmail, - Status = TenantStatus.PendingReview, - EffectiveFrom = null, - EffectiveTo = null - }; - - // 4. 写入审计日志 - var auditLog = new TenantAuditLog - { - TenantId = tenant.Id, - Action = TenantAuditAction.RegistrationSubmitted, - Title = "自助注册", - Description = "自助注册提交,等待补充资料与审核" - }; - - // 5. 持久化租户与审计 - await tenantRepository.AddTenantAsync(tenant, cancellationToken); - await tenantRepository.AddAuditLogAsync(auditLog, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 6. 临时切换租户上下文,保证身份与权限写入正确 - var previousContext = tenantContextAccessor.Current; - tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "self-register"); - try - { - // 7. 使用用户自设密码创建管理员 - var adminUser = new IdentityUser - { - Portal = PortalType.Tenant, - TenantId = tenant.Id, - Account = normalizedAccount, - DisplayName = string.IsNullOrWhiteSpace(request.AdminDisplayName) ? normalizedAccount : request.AdminDisplayName!.Trim(), - PasswordHash = string.Empty, - Phone = normalizedPhone, - Email = string.IsNullOrWhiteSpace(request.AdminEmail) ? null : request.AdminEmail.Trim() - }; - adminUser.PasswordHash = passwordHasher.HashPassword(adminUser, request.AdminPassword); - await identityUserRepository.AddAsync(adminUser, cancellationToken); - await identityUserRepository.SaveChangesAsync(cancellationToken); - - // 7.1 回填主管理员标识,确保后续伪装登录/重置管理员等能力可用 - tenant.PrimaryOwnerUserId = adminUser.Id; - await tenantRepository.UpdateTenantAsync(tenant, cancellationToken); - await tenantRepository.SaveChangesAsync(cancellationToken); - - // 8. 初始化租户管理员角色模板 - await mediator.Send(new InitializeRoleTemplatesCommand - { - TemplateCodes = new[] { "tenant-admin" } - }, cancellationToken); - - // 9. 绑定租户管理员角色 - var tenantAdminRole = await roleRepository.FindByCodeAsync(PortalType.Tenant, tenant.Id, "tenant-admin", cancellationToken); - if (tenantAdminRole != null) - { - await mediator.Send(new AssignUserRolesCommand - { - UserId = adminUser.Id, - RoleIds = new[] { tenantAdminRole.Id } - }, cancellationToken); - } - - // 10. 返回注册结果 - return new SelfRegisterResultDto - { - TenantId = tenant.Id, - Code = tenant.Code, - Status = tenant.Status, - VerificationStatus = TenantVerificationStatus.Draft, - EffectiveFrom = tenant.EffectiveFrom, - EffectiveTo = tenant.EffectiveTo, - AdminAccount = adminUser.Account - }; - } - finally - { - // 11. 恢复上下文 - tenantContextAccessor.Current = previousContext; - } - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetAnnouncementByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetAnnouncementByIdQuery.cs index e889dff..94845a3 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetAnnouncementByIdQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetAnnouncementByIdQuery.cs @@ -9,7 +9,7 @@ namespace TakeoutSaaS.Application.App.Tenants.Queries; public sealed record GetAnnouncementByIdQuery : IRequest { /// - /// 租户 ID(雪花算法,兼容旧调用,实际以当前租户为准)。 + /// 租户 ID(0 表示平台公告)。 /// public long TenantId { get; init; } diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetPublicTenantPackagesQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetPublicTenantPackagesQuery.cs deleted file mode 100644 index 3c56e95..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetPublicTenantPackagesQuery.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Tenants.Queries; - -/// -/// 公共场景分页查询启用套餐。 -/// -public sealed record GetPublicTenantPackagesQuery : IRequest> -{ - /// - /// 页码。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页条数。 - /// - public int PageSize { get; init; } = 20; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantProgressQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantProgressQuery.cs deleted file mode 100644 index 214e600..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetTenantProgressQuery.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MediatR; -using System.ComponentModel.DataAnnotations; -using TakeoutSaaS.Application.App.Tenants.Dto; - -namespace TakeoutSaaS.Application.App.Tenants.Queries; - -/// -/// 租户入住进度查询。 -/// -public sealed record GetTenantProgressQuery : IRequest -{ - /// - /// 租户 ID(雪花算法)。 - /// - [Required] - public long TenantId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetUnreadAnnouncementsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetUnreadAnnouncementsQuery.cs deleted file mode 100644 index d0972a7..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Queries/GetUnreadAnnouncementsQuery.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.App.Tenants.Dto; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.App.Tenants.Queries; - -/// -/// 查询未读公告。 -/// -public sealed record GetUnreadAnnouncementsQuery : IRequest> -{ - /// - /// 页码(从 1 开始)。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页条数。 - /// - public int PageSize { get; init; } = 20; -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Targeting/AnnouncementTargetContextFactory.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Targeting/AnnouncementTargetContextFactory.cs deleted file mode 100644 index a8e61fd..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Targeting/AnnouncementTargetContextFactory.cs +++ /dev/null @@ -1,58 +0,0 @@ -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Application.Identity.Contracts; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.App.Tenants.Targeting; - -/// -/// 目标受众上下文构建器。 -/// -internal static class AnnouncementTargetContextFactory -{ - /// - /// 构建当前用户的目标上下文。 - /// - public static async Task BuildAsync( - ITenantProvider tenantProvider, - ICurrentUserAccessor? currentUserAccessor, - IAdminAuthService? adminAuthService, - IMiniAuthService? miniAuthService, - CancellationToken cancellationToken) - { - var tenantId = tenantProvider.GetCurrentTenantId(); - var userId = currentUserAccessor?.UserId ?? 0; - long? merchantId = null; - IReadOnlyCollection roles = Array.Empty(); - IReadOnlyCollection permissions = Array.Empty(); - - if (userId != 0) - { - CurrentUserProfile? profile = null; - if (adminAuthService != null) - { - profile = await adminAuthService.GetProfileAsync(userId, cancellationToken); - } - else if (miniAuthService != null) - { - profile = await miniAuthService.GetProfileAsync(userId, cancellationToken); - } - - if (profile != null) - { - merchantId = profile.MerchantId; - roles = profile.Roles ?? Array.Empty(); - permissions = profile.Permissions ?? Array.Empty(); - } - } - - return new AnnouncementTargetContext - { - TenantId = tenantId, - UserId = userId, - MerchantId = merchantId, - Roles = roles, - Permissions = permissions - }; - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Targeting/TargetTypeFilter.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Targeting/TargetTypeFilter.cs deleted file mode 100644 index 5642549..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Targeting/TargetTypeFilter.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System.Text.Json; -using TakeoutSaaS.Domain.Tenants.Entities; - -namespace TakeoutSaaS.Application.App.Tenants.Targeting; - -/// -/// 目标受众过滤器。 -/// -public static class TargetTypeFilter -{ - private static readonly JsonSerializerOptions Options = new() - { - PropertyNameCaseInsensitive = true - }; - - /// - /// 判断公告是否匹配当前用户上下文。 - /// - /// 公告实体。 - /// 目标上下文。 - /// 是否匹配。 - public static bool IsMatch(TenantAnnouncement announcement, AnnouncementTargetContext context) - { - if (announcement == null) - { - return false; - } - - var targetType = announcement.TargetType?.Trim(); - if (string.IsNullOrWhiteSpace(targetType)) - { - return true; - } - - var normalized = targetType.ToUpperInvariant(); - var parsed = TryParseParameters(announcement.TargetParameters, out var payload); - - return normalized switch - { - "ALL" => announcement.TenantId == 0 - ? ApplyPayloadConstraints(payload, parsed, context, allowEmpty: true) - : announcement.TenantId == context.TenantId - && ApplyPayloadConstraints(payload, parsed, context, allowEmpty: true), - "ALL_TENANTS" => ApplyPayloadConstraints(payload, parsed, context, allowEmpty: true), - "TENANT_ALL" => announcement.TenantId == context.TenantId - && ApplyPayloadConstraints(payload, parsed, context, allowEmpty: true), - "SPECIFIC_TENANTS" => RequireTenantMatch(payload, parsed, context) - && ApplyPayloadConstraints(payload, parsed, context, allowEmpty: false), - "USERS" or "SPECIFIC_USERS" or "USER_IDS" => RequireUserMatch(payload, parsed, context) - && ApplyPayloadConstraints(payload, parsed, context, allowEmpty: false), - "ROLES" or "ROLE" => RequireRoleMatch(payload, parsed, context) - && ApplyPayloadConstraints(payload, parsed, context, allowEmpty: false), - "PERMISSIONS" or "PERMISSION" => RequirePermissionMatch(payload, parsed, context) - && ApplyPayloadConstraints(payload, parsed, context, allowEmpty: false), - "MERCHANTS" or "MERCHANT_IDS" => RequireMerchantMatch(payload, parsed, context) - && ApplyPayloadConstraints(payload, parsed, context, allowEmpty: false), - _ => ApplyPayloadConstraints(payload, parsed, context, allowEmpty: false) - }; - } - - private static bool RequireTenantMatch(TargetParametersPayload payload, bool parsed, AnnouncementTargetContext context) - => parsed && payload.TenantIds is { Length: > 0 } && payload.TenantIds.Contains(context.TenantId); - - private static bool RequireUserMatch(TargetParametersPayload payload, bool parsed, AnnouncementTargetContext context) - => parsed && payload.UserIds is { Length: > 0 } && context.UserId != 0 && payload.UserIds.Contains(context.UserId); - - private static bool RequireMerchantMatch(TargetParametersPayload payload, bool parsed, AnnouncementTargetContext context) - => parsed && payload.MerchantIds is { Length: > 0 } && context.MerchantId.HasValue && payload.MerchantIds.Contains(context.MerchantId.Value); - - private static bool RequireRoleMatch(TargetParametersPayload payload, bool parsed, AnnouncementTargetContext context) - => parsed && payload.Roles is { Length: > 0 } && Intersects(payload.Roles, context.Roles); - - private static bool RequirePermissionMatch(TargetParametersPayload payload, bool parsed, AnnouncementTargetContext context) - => parsed && payload.Permissions is { Length: > 0 } && Intersects(payload.Permissions, context.Permissions); - - private static bool ApplyPayloadConstraints( - TargetParametersPayload payload, - bool parsed, - AnnouncementTargetContext context, - bool allowEmpty) - { - if (!parsed) - { - return false; - } - - if (!payload.HasConstraints) - { - return allowEmpty; - } - - if (payload.TenantIds is { Length: > 0 } && !payload.TenantIds.Contains(context.TenantId)) - { - return false; - } - - if (payload.UserIds is { Length: > 0 }) - { - if (context.UserId == 0 || !payload.UserIds.Contains(context.UserId)) - { - return false; - } - } - - if (payload.MerchantIds is { Length: > 0 }) - { - if (!context.MerchantId.HasValue || !payload.MerchantIds.Contains(context.MerchantId.Value)) - { - return false; - } - } - - if (payload.Roles is { Length: > 0 } && !Intersects(payload.Roles, context.Roles)) - { - return false; - } - - if (payload.Permissions is { Length: > 0 } && !Intersects(payload.Permissions, context.Permissions)) - { - return false; - } - - if (payload.Departments is { Length: > 0 } && !Intersects(payload.Departments, context.Departments)) - { - return false; - } - - return true; - } - - private static bool TryParseParameters(string? json, out TargetParametersPayload payload) - { - payload = new TargetParametersPayload(); - - if (string.IsNullOrWhiteSpace(json)) - { - return true; - } - - try - { - payload = JsonSerializer.Deserialize(json, Options) ?? new TargetParametersPayload(); - return true; - } - catch (JsonException) - { - return false; - } - } - - private static bool Intersects(IEnumerable left, IEnumerable right) - { - var set = new HashSet(right ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); - foreach (var value in left ?? Array.Empty()) - { - if (set.Contains(value)) - { - return true; - } - } - - return false; - } - - private sealed class TargetParametersPayload - { - public long[]? TenantIds { get; init; } - public long[]? UserIds { get; init; } - public long[]? MerchantIds { get; init; } - public string[]? Roles { get; init; } - public string[]? Permissions { get; init; } - public string[]? Departments { get; init; } - - public bool HasConstraints - => (TenantIds?.Length ?? 0) > 0 - || (UserIds?.Length ?? 0) > 0 - || (MerchantIds?.Length ?? 0) > 0 - || (Roles?.Length ?? 0) > 0 - || (Permissions?.Length ?? 0) > 0 - || (Departments?.Length ?? 0) > 0; - } -} - -/// -/// 目标受众上下文。 -/// -public sealed record AnnouncementTargetContext -{ - /// - /// 租户 ID。 - /// - public long TenantId { get; init; } - - /// - /// 用户 ID。 - /// - public long UserId { get; init; } - - /// - /// 商户 ID(可选)。 - /// - public long? MerchantId { get; init; } - - /// - /// 角色集合。 - /// - public IReadOnlyCollection Roles { get; init; } = Array.Empty(); - - /// - /// 权限集合。 - /// - public IReadOnlyCollection Permissions { get; init; } = Array.Empty(); - - /// - /// 部门集合(可选)。 - /// - public IReadOnlyCollection Departments { get; init; } = Array.Empty(); -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/SelfRegisterTenantCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/SelfRegisterTenantCommandValidator.cs deleted file mode 100644 index 411d834..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/SelfRegisterTenantCommandValidator.cs +++ /dev/null @@ -1,22 +0,0 @@ -using FluentValidation; -using TakeoutSaaS.Application.App.Tenants.Commands; - -namespace TakeoutSaaS.Application.App.Tenants.Validators; - -/// -/// 自助注册租户命令验证器。 -/// -public sealed class SelfRegisterTenantCommandValidator : AbstractValidator -{ - /// - /// 初始化验证规则。 - /// - public SelfRegisterTenantCommandValidator() - { - RuleFor(x => x.AdminAccount) - .NotEmpty() - .MaximumLength(64) - .Matches("^[A-Za-z0-9]+$") - .WithMessage("登录账号仅允许大小写字母和数字"); - } -} diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryGroupRequest.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryGroupRequest.cs index 17de073..ef89bf4 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryGroupRequest.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/CreateDictionaryGroupRequest.cs @@ -8,6 +8,11 @@ namespace TakeoutSaaS.Application.Dictionary.Contracts; /// public sealed class CreateDictionaryGroupRequest { + /// + /// 租户 ID(仅当 Scope=Business 时必填)。 + /// + public long? TenantId { get; set; } + /// /// 分组编码。 /// diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryBatchQueryRequest.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryBatchQueryRequest.cs index cc5e2c6..3d524e6 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryBatchQueryRequest.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryBatchQueryRequest.cs @@ -7,6 +7,11 @@ namespace TakeoutSaaS.Application.Dictionary.Contracts; /// public sealed class DictionaryBatchQueryRequest { + /// + /// 租户 ID(为空或 0 表示仅读取系统字典)。 + /// + public long? TenantId { get; init; } + /// /// 分组编码集合。 /// diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryGroupQuery.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryGroupQuery.cs index c7afa74..35dd21a 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryGroupQuery.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Contracts/DictionaryGroupQuery.cs @@ -7,6 +7,11 @@ namespace TakeoutSaaS.Application.Dictionary.Contracts; /// public sealed class DictionaryGroupQuery { + /// + /// 租户 ID(仅当 Scope=Business 时需要;Scope=System 时忽略)。 + /// + public long? TenantId { get; set; } + /// /// 作用域过滤。 /// diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs index 2f62030..8d080d2 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs @@ -9,7 +9,6 @@ using TakeoutSaaS.Domain.Dictionary.Enums; using TakeoutSaaS.Domain.Dictionary.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Dictionary.Services; @@ -19,8 +18,6 @@ namespace TakeoutSaaS.Application.Dictionary.Services; public sealed class DictionaryAppService( IDictionaryRepository repository, IDictionaryCache cache, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, ILogger logger) : IDictionaryAppService { /// @@ -33,7 +30,7 @@ public sealed class DictionaryAppService( { // 1. 规范化编码并确定租户 var normalizedCode = NormalizeCode(request.Code); - var targetTenant = ResolveTargetTenant(request.Scope); + var targetTenant = ResolveTargetTenant(request.Scope, request.TenantId); // 2. 校验编码唯一 var existing = await repository.FindGroupByCodeAsync(normalizedCode, cancellationToken); @@ -74,7 +71,6 @@ public sealed class DictionaryAppService( { // 1. 读取分组并校验权限 var group = await RequireGroupAsync(groupId, cancellationToken); - EnsureScopePermission(group.Scope); if (request.RowVersion == null || request.RowVersion.Length == 0) { @@ -116,7 +112,6 @@ public sealed class DictionaryAppService( { // 1. 读取分组并校验权限 var group = await RequireGroupAsync(groupId, cancellationToken); - EnsureScopePermission(group.Scope); // 2. 删除并失效缓存 await repository.RemoveGroupAsync(group, cancellationToken); @@ -134,9 +129,8 @@ public sealed class DictionaryAppService( public async Task> SearchGroupsAsync(DictionaryGroupQuery request, CancellationToken cancellationToken = default) { // 1. 确定查询范围并校验权限 - var tenantId = tenantProvider.GetCurrentTenantId(); + var tenantId = request.TenantId ?? 0; var scope = ResolveScopeForQuery(request.Scope, tenantId); - EnsureScopePermission(scope); // 2. 查询分组及可选项 var groups = await repository.SearchGroupsAsync(scope, cancellationToken); @@ -169,7 +163,6 @@ public sealed class DictionaryAppService( { // 1. 校验分组与权限 var group = await RequireGroupAsync(request.GroupId, cancellationToken); - EnsureScopePermission(group.Scope); // 2. 构建字典项 var item = new DictionaryItem @@ -206,7 +199,6 @@ public sealed class DictionaryAppService( // 1. 读取字典项与分组并校验权限 var item = await RequireItemAsync(itemId, cancellationToken); var group = await RequireGroupAsync(item.GroupId, cancellationToken); - EnsureScopePermission(group.Scope); if (request.RowVersion == null || request.RowVersion.Length == 0) { @@ -251,7 +243,6 @@ public sealed class DictionaryAppService( // 1. 读取字典项与分组并校验权限 var item = await RequireItemAsync(itemId, cancellationToken); var group = await RequireGroupAsync(item.GroupId, cancellationToken); - EnsureScopePermission(group.Scope); // 2. 删除并失效缓存 await repository.RemoveItemAsync(item, cancellationToken); @@ -281,7 +272,7 @@ public sealed class DictionaryAppService( } // 2. 按租户合并系统与业务字典 - var tenantId = tenantProvider.GetCurrentTenantId(); + var tenantId = request.TenantId ?? 0; var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var code in normalizedCodes) @@ -324,21 +315,19 @@ public sealed class DictionaryAppService( return item; } - private long ResolveTargetTenant(DictionaryScope scope) + private static long ResolveTargetTenant(DictionaryScope scope, long? tenantId) { - var tenantId = tenantProvider.GetCurrentTenantId(); if (scope == DictionaryScope.System) { - EnsurePlatformTenant(tenantId); return 0; } - if (tenantId == 0) + if (!tenantId.HasValue || tenantId.Value <= 0) { - throw new BusinessException(ErrorCodes.BadRequest, "业务参数需指定租户"); + throw new BusinessException(ErrorCodes.ValidationFailed, "业务参数需指定租户"); } - return tenantId; + return tenantId.Value; } private static string NormalizeCode(string code) => code.Trim().ToLowerInvariant(); @@ -353,23 +342,6 @@ public sealed class DictionaryAppService( return tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business; } - private void EnsureScopePermission(DictionaryScope scope) - { - var tenantId = tenantProvider.GetCurrentTenantId(); - if (scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor)) - { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); - } - } - - private void EnsurePlatformTenant(long tenantId) - { - if (tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor)) - { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); - } - } - private async Task InvalidateCacheAsync(DictionaryGroup group, CancellationToken cancellationToken) { await cache.RemoveAsync(group.TenantId, group.Code, cancellationToken); diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryCommandService.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryCommandService.cs index fc4e65d..ddfc24b 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; @@ -10,7 +9,6 @@ using TakeoutSaaS.Domain.Dictionary.Repositories; using TakeoutSaaS.Domain.Dictionary.ValueObjects; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Dictionary.Services; @@ -21,8 +19,6 @@ public sealed class DictionaryCommandService( IDictionaryGroupRepository groupRepository, IDictionaryItemRepository itemRepository, IDictionaryHybridCache cache, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, ILogger logger) { /// @@ -30,7 +26,7 @@ public sealed class DictionaryCommandService( /// public async Task CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default) { - var targetTenantId = ResolveTargetTenant(request.Scope); + var targetTenantId = ResolveTargetTenant(request.Scope, request.TenantId); var code = new DictionaryCode(request.Code); var existing = await groupRepository.GetByCodeAsync(targetTenantId, code, cancellationToken); @@ -68,7 +64,6 @@ public sealed class DictionaryCommandService( public async Task UpdateGroupAsync(long groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default) { var group = await RequireGroupAsync(groupId, cancellationToken); - EnsureGroupAccess(group); EnsureRowVersion(request.RowVersion, group.RowVersion, "字典分组"); @@ -103,8 +98,6 @@ public sealed class DictionaryCommandService( return false; } - EnsureGroupAccess(group); - var items = await itemRepository.GetByGroupIdAsync(group.TenantId, group.Id, cancellationToken); foreach (var item in items) { @@ -125,7 +118,6 @@ public sealed class DictionaryCommandService( public async Task CreateItemAsync(CreateDictionaryItemRequest request, CancellationToken cancellationToken = default) { var group = await RequireGroupAsync(request.GroupId, cancellationToken); - EnsureGroupAccess(group); var items = await itemRepository.GetByGroupIdAsync(group.TenantId, group.Id, cancellationToken); var normalizedKey = request.Key.Trim(); @@ -168,7 +160,6 @@ public sealed class DictionaryCommandService( { var item = await RequireItemAsync(itemId, cancellationToken); var group = await RequireGroupAsync(item.GroupId, cancellationToken); - EnsureGroupAccess(group); EnsureRowVersion(request.RowVersion, item.RowVersion, "字典项"); @@ -216,7 +207,6 @@ public sealed class DictionaryCommandService( } var group = await RequireGroupAsync(item.GroupId, cancellationToken); - EnsureGroupAccess(group); await itemRepository.RemoveAsync(item, cancellationToken); await groupRepository.SaveChangesAsync(cancellationToken); @@ -226,39 +216,19 @@ public sealed class DictionaryCommandService( return true; } - private long ResolveTargetTenant(DictionaryScope scope) + private static long ResolveTargetTenant(DictionaryScope scope, long? tenantId) { - var tenantId = tenantProvider.GetCurrentTenantId(); if (scope == DictionaryScope.System) { - if (tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor)) - { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可创建系统字典"); - } - return 0; } - if (tenantId == 0) + if (!tenantId.HasValue || tenantId.Value <= 0) { - throw new BusinessException(ErrorCodes.BadRequest, "业务字典必须在租户上下文中创建"); + throw new BusinessException(ErrorCodes.ValidationFailed, "业务字典必须指定 TenantId"); } - return tenantId; - } - - private void EnsureGroupAccess(DictionaryGroup group) - { - var tenantId = tenantProvider.GetCurrentTenantId(); - if (group.Scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor)) - { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); - } - - if (group.Scope == DictionaryScope.Business && tenantId != group.TenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他租户字典"); - } + return tenantId.Value; } private static void EnsureRowVersion(byte[]? requestVersion, byte[] entityVersion, string resourceName) diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryImportExportService.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryImportExportService.cs index ae62ad7..72d520c 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryImportExportService.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryImportExportService.cs @@ -12,9 +12,7 @@ using TakeoutSaaS.Domain.Dictionary.ValueObjects; using TakeoutSaaS.Shared.Abstractions.Constants; 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; @@ -28,9 +26,7 @@ public sealed class DictionaryImportExportService( IDictionaryItemRepository itemRepository, IDictionaryImportLogRepository importLogRepository, IDictionaryHybridCache cache, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUser, - IHttpContextAccessor httpContextAccessor, ILogger logger) { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); @@ -41,7 +37,6 @@ public sealed class DictionaryImportExportService( public async Task ExportToCsvAsync(long groupId, Stream output, CancellationToken cancellationToken = default) { var group = await RequireGroupAsync(groupId, cancellationToken); - EnsureGroupReadable(group); var items = await ResolveExportItemsAsync(group, cancellationToken); await WriteCsvAsync(group, items, output, cancellationToken); @@ -53,7 +48,6 @@ public sealed class DictionaryImportExportService( public async Task ExportToJsonAsync(long groupId, Stream output, CancellationToken cancellationToken = default) { var group = await RequireGroupAsync(groupId, cancellationToken); - EnsureGroupReadable(group); var items = await ResolveExportItemsAsync(group, cancellationToken); var payload = items.Select(item => new DictionaryExportRow @@ -96,7 +90,6 @@ public sealed class DictionaryImportExportService( { var stopwatch = Stopwatch.StartNew(); var group = await RequireGroupAsync(request.GroupId, cancellationToken); - EnsureGroupWritable(group); var errors = new List(); var validRows = new List(rows.Count); @@ -210,14 +203,6 @@ public sealed class DictionaryImportExportService( private async Task> ResolveExportItemsAsync(DictionaryGroup group, CancellationToken cancellationToken) { - var tenantId = tenantProvider.GetCurrentTenantId(); - - if (group.Scope == DictionaryScope.System && tenantId != 0) - { - var mergedItems = await itemRepository.GetMergedItemsAsync(tenantId, group.Id, includeOverrides: true, cancellationToken); - return mergedItems.Select(DictionaryMapper.ToItemDto).ToList(); - } - var items = await itemRepository.GetByGroupIdAsync(group.TenantId, group.Id, cancellationToken); return items.Select(DictionaryMapper.ToItemDto).ToList(); } @@ -423,34 +408,6 @@ public sealed class DictionaryImportExportService( return group; } - private void EnsureGroupAccess(DictionaryGroup group) - { - var tenantId = tenantProvider.GetCurrentTenantId(); - if (group.Scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor)) - { - throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); - } - - if (group.Scope == DictionaryScope.Business && tenantId != group.TenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他租户字典"); - } - } - - private void EnsureGroupReadable(DictionaryGroup group) - { - var tenantId = tenantProvider.GetCurrentTenantId(); - if (tenantId != 0 && group.Scope == DictionaryScope.Business && group.TenantId != tenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "无权访问其他租户字典"); - } - } - - private void EnsureGroupWritable(DictionaryGroup group) - { - EnsureGroupAccess(group); - } - private static DictionaryImportResultDto.ImportError CreateError(int rowNumber, string field, string message) => new() { diff --git a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryQueryService.cs b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryQueryService.cs index 3d33d0d..a338b56 100644 --- a/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryQueryService.cs +++ b/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryQueryService.cs @@ -8,7 +8,6 @@ using TakeoutSaaS.Domain.Dictionary.ValueObjects; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Dictionary.Services; @@ -18,9 +17,7 @@ namespace TakeoutSaaS.Application.Dictionary.Services; public sealed class DictionaryQueryService( IDictionaryGroupRepository groupRepository, IDictionaryItemRepository itemRepository, - DictionaryMergeService mergeService, - IDictionaryHybridCache cache, - ITenantProvider tenantProvider) + IDictionaryHybridCache cache) { private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(30); @@ -31,8 +28,21 @@ public sealed class DictionaryQueryService( DictionaryGroupQuery query, CancellationToken cancellationToken = default) { - var tenantId = tenantProvider.GetCurrentTenantId(); + // 1. 解析查询租户与作用域 + var tenantId = query.TenantId ?? 0; + if (query.Scope == DictionaryScope.Business && tenantId <= 0) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "Scope=Business 时必须指定 TenantId"); + } + + // 2. (空行后) 确定作用域与目标租户 var scope = query.Scope ?? (tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business); + if (scope == DictionaryScope.System) + { + tenantId = 0; + } + + // 3. (空行后) 构建缓存键并加载分页数据 var sortDescending = string.Equals(query.SortOrder, "desc", StringComparison.OrdinalIgnoreCase); var targetTenant = scope == DictionaryScope.System ? 0 : tenantId; @@ -118,7 +128,6 @@ public sealed class DictionaryQueryService( return null; } - EnsureGroupReadable(group); return DictionaryMapper.ToGroupDto(group); } @@ -139,7 +148,6 @@ public sealed class DictionaryQueryService( throw new BusinessException(ErrorCodes.NotFound, "字典分组不存在"); } - EnsureGroupReadable(group); var items = await itemRepository.GetByGroupIdAsync(group.TenantId, groupId, token); return items .Where(item => item.IsEnabled) @@ -162,7 +170,8 @@ public sealed class DictionaryQueryService( throw new BusinessException(ErrorCodes.ValidationFailed, "字典编码格式不正确"); } - var tenantId = tenantProvider.GetCurrentTenantId(); + // 1. 管理端默认读取系统字典(TenantId=0) + var tenantId = 0; var normalized = new DictionaryCode(code); var cacheKey = DictionaryCacheKeys.BuildDictionaryKey(tenantId, normalized); @@ -177,17 +186,12 @@ public sealed class DictionaryQueryService( return Array.Empty(); } - if (tenantId == 0) - { - var systemItems = await itemRepository.GetByGroupIdAsync(0, systemGroup.Id, token); - return systemItems - .Where(item => item.IsEnabled) - .OrderBy(item => item.SortOrder) - .Select(DictionaryMapper.ToItemDto) - .ToList(); - } - - return await mergeService.MergeItemsAsync(tenantId, systemGroup.Id, token); + var systemItems = await itemRepository.GetByGroupIdAsync(0, systemGroup.Id, token); + return systemItems + .Where(item => item.IsEnabled) + .OrderBy(item => item.SortOrder) + .Select(DictionaryMapper.ToItemDto) + .ToList(); }, cancellationToken); @@ -227,15 +231,6 @@ public sealed class DictionaryQueryService( return result; } - private void EnsureGroupReadable(DictionaryGroup group) - { - var tenantId = tenantProvider.GetCurrentTenantId(); - if (tenantId != 0 && group.Scope == DictionaryScope.Business && group.TenantId != tenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "无权访问其他租户字典"); - } - } - private sealed class DictionaryGroupPage { public IReadOnlyList Items { get; init; } = Array.Empty(); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs index 0fd945f..ee6935c 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IAdminAuthService.cs @@ -1,5 +1,4 @@ using TakeoutSaaS.Application.Identity.Contracts; -using TakeoutSaaS.Shared.Abstractions.Results; namespace TakeoutSaaS.Application.Identity.Abstractions; @@ -28,16 +27,6 @@ public interface IAdminAuthService /// Task GetProfileAsync(long userId, CancellationToken cancellationToken = default); - /// - /// 获取用户权限概览。 - /// - Task GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default); - - /// - /// 搜索用户权限概览列表。 - /// - Task> SearchUserPermissionsAsync(string? keyword, int page, int pageSize, string? sortBy, bool sortDescending, CancellationToken cancellationToken = default); - /// /// 获取当前用户可见菜单树。 /// diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IMiniAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IMiniAuthService.cs deleted file mode 100644 index 8c5250a..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IMiniAuthService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using TakeoutSaaS.Application.Identity.Contracts; - -namespace TakeoutSaaS.Application.Identity.Abstractions; - -/// -/// 小程序认证服务。 -/// -public interface IMiniAuthService -{ - Task LoginWithWeChatAsync(WeChatLoginRequest request, CancellationToken cancellationToken = default); - Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default); - Task GetProfileAsync(long userId, CancellationToken cancellationToken = default); -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IWeChatAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IWeChatAuthService.cs deleted file mode 100644 index d3465ef..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IWeChatAuthService.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace TakeoutSaaS.Application.Identity.Abstractions; - -/// -/// 微信 code2Session 服务契约。 -/// -public interface IWeChatAuthService -{ - /// - /// 调用微信接口完成 code2Session 交换。 - /// - /// 临时登录凭证 code。 - /// 取消标记。 - /// 会话信息。 - Task Code2SessionAsync(string code, CancellationToken cancellationToken = default); -} - -/// -/// 微信会话信息。 -/// -public sealed class WeChatSessionInfo -{ - /// - /// OpenId。 - /// - public string OpenId { get; init; } = string.Empty; - - /// - /// UnionId。 - /// - public string? UnionId { get; init; } - - /// - /// 会话密钥。 - /// - public string SessionKey { get; init; } = string.Empty; -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/AssignUserRolesCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/AssignUserRolesCommand.cs deleted file mode 100644 index c52bbff..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Commands/AssignUserRolesCommand.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MediatR; - -namespace TakeoutSaaS.Application.Identity.Commands; - -/// -/// 为用户分配角色(覆盖式)。 -/// -public sealed record AssignUserRolesCommand : IRequest -{ - /// - /// 用户 ID。 - /// - public long UserId { get; init; } - - /// - /// 角色 ID 集合。 - /// - public long[] RoleIds { get; init; } = Array.Empty(); -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/CopyRoleTemplateCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/CopyRoleTemplateCommand.cs index efaa1a3..a7129a0 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Commands/CopyRoleTemplateCommand.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/CopyRoleTemplateCommand.cs @@ -1,5 +1,6 @@ using MediatR; using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Domain.Identity.Enums; namespace TakeoutSaaS.Application.Identity.Commands; @@ -8,6 +9,16 @@ namespace TakeoutSaaS.Application.Identity.Commands; /// public sealed record CopyRoleTemplateCommand : IRequest { + /// + /// 目标 Portal。 + /// + public PortalType Portal { get; init; } + + /// + /// 目标租户 ID(Portal=Tenant 时必填;Portal=Admin 时必须为空)。 + /// + public long? TenantId { get; init; } + /// /// 模板编码。 /// diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/InitializeRoleTemplatesCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/InitializeRoleTemplatesCommand.cs deleted file mode 100644 index 7eb39f9..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Commands/InitializeRoleTemplatesCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.Identity.Contracts; - -namespace TakeoutSaaS.Application.Identity.Commands; - -/// -/// 批量为当前租户初始化角色模板。 -/// -public sealed record InitializeRoleTemplatesCommand : IRequest> -{ - /// - /// 需要初始化的模板编码列表(为空则全部)。 - /// - public IReadOnlyCollection? TemplateCodes { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/WeChatLoginRequest.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/WeChatLoginRequest.cs deleted file mode 100644 index 79ae8d4..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/WeChatLoginRequest.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace TakeoutSaaS.Application.Identity.Contracts; - -/// -/// 微信小程序登录请求。 -/// -public sealed class WeChatLoginRequest -{ - /// - /// wx.login 返回的临时 code。 - /// - [Required] - [MaxLength(128)] - public string Code { get; set; } = string.Empty; - - /// - /// 用户昵称。 - /// - [MaxLength(64)] - public string? Nickname { get; set; } - - /// - /// 头像地址。 - /// - [MaxLength(256)] - public string? Avatar { get; set; } - - /// - /// 加密用户数据。 - /// - public string? EncryptedData { get; set; } - - /// - /// 加密向量。 - /// - public string? Iv { get; set; } -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Extensions/IdentityServiceCollectionExtensions.cs b/src/Application/TakeoutSaaS.Application/Identity/Extensions/IdentityServiceCollectionExtensions.cs index c5df667..6aa50b9 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Extensions/IdentityServiceCollectionExtensions.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Extensions/IdentityServiceCollectionExtensions.cs @@ -13,16 +13,10 @@ public static class IdentityServiceCollectionExtensions /// 注册身份认证相关应用服务 /// /// 服务集合 - /// 是否注册小程序认证服务 - public static IServiceCollection AddIdentityApplication(this IServiceCollection services, bool enableMiniSupport = false) + public static IServiceCollection AddIdentityApplication(this IServiceCollection services) { services.AddScoped(); - if (enableMiniSupport) - { - services.AddScoped(); - } - return services; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs deleted file mode 100644 index 405d8e9..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.Identity.Commands; -using TakeoutSaaS.Domain.Identity.Enums; -using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.Identity.Handlers; - -/// -/// 用户角色分配处理器。 -/// -public sealed class AssignUserRolesCommandHandler( - IUserRoleRepository userRoleRepository, - ITenantProvider tenantProvider) - : IRequestHandler -{ - /// - /// 处理用户角色分配请求。 - /// - /// 分配命令。 - /// 取消标记。 - /// 执行结果。 - public async Task Handle(AssignUserRolesCommand request, CancellationToken cancellationToken) - { - // 1. 固定为租户侧用户分配角色 - var portal = PortalType.Tenant; - - // 2. 获取租户上下文 - var tenantId = tenantProvider.GetCurrentTenantId(); - - // 3. 覆盖式绑定角色 - await userRoleRepository.ReplaceUserRolesAsync(portal, tenantId, request.UserId, request.RoleIds, cancellationToken); - await userRoleRepository.SaveChangesAsync(cancellationToken); - - // 4. 返回执行结果 - return true; - } -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs index f5c0a26..7ff4d3a 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs @@ -11,7 +11,6 @@ using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -22,7 +21,6 @@ public sealed class BatchIdentityUserOperationCommandHandler( IIdentityUserRepository identityUserRepository, IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, IIdentityOperationLogPublisher operationLogPublisher) @@ -31,24 +29,17 @@ public sealed class BatchIdentityUserOperationCommandHandler( /// public async Task Handle(BatchIdentityUserOperationCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 - var currentTenantId = tenantProvider.GetCurrentTenantId(); + // 1. 获取操作者档案(用于操作日志) 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 <= 0) { - throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户批量操作用户"); + throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空"); } - if (isSuperAdmin && !request.TenantId.HasValue) - { - throw new BusinessException(ErrorCodes.BadRequest, "批量操作必须指定租户"); - } - - // 3. 解析用户 ID 列表 - var tenantId = request.TenantId ?? currentTenantId; + // 3. (空行后) 解析用户 ID 列表 + var tenantId = request.TenantId.Value; var userIds = ParseIds(request.UserIds, "用户"); if (userIds.Length == 0) { diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs index f2e5915..a4641e1 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs @@ -2,7 +2,8 @@ using MediatR; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -10,8 +11,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 绑定角色权限处理器。 /// public sealed class BindRolePermissionsCommandHandler( - IRolePermissionRepository rolePermissionRepository, - ITenantProvider tenantProvider) + IRolePermissionRepository rolePermissionRepository) : IRequestHandler { /// @@ -25,10 +25,16 @@ public sealed class BindRolePermissionsCommandHandler( // 1. 固定绑定租户侧角色权限 var portal = PortalType.Tenant; - // 2. 获取租户上下文 - var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 校验租户参数 + if (!request.TenantId.HasValue || request.TenantId.Value <= 0) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空"); + } - // 3. 覆盖式绑定权限 + // 3. (空行后) 获取租户标识 + var tenantId = request.TenantId.Value; + + // 4. (空行后) 覆盖式绑定权限 var distinctPermissionIds = request.PermissionIds .Where(id => id > 0) .Distinct() @@ -37,7 +43,7 @@ public sealed class BindRolePermissionsCommandHandler( await rolePermissionRepository.ReplaceRolePermissionsAsync(portal, tenantId, request.RoleId, distinctPermissionIds, cancellationToken); await rolePermissionRepository.SaveChangesAsync(cancellationToken); - // 4. 返回执行结果 + // 5. 返回执行结果 return true; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs index bf9163e..05f85b7 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs @@ -8,7 +8,6 @@ using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -19,7 +18,6 @@ public sealed class ChangeIdentityUserStatusCommandHandler( IIdentityUserRepository identityUserRepository, IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, IIdentityOperationLogPublisher operationLogPublisher) @@ -28,30 +26,17 @@ public sealed class ChangeIdentityUserStatusCommandHandler( /// public async Task Handle(ChangeIdentityUserStatusCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 - var currentTenantId = tenantProvider.GetCurrentTenantId(); + // 1. 获取操作者档案(用于操作日志) var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - // 2. 校验跨租户访问权限 - if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户修改用户状态"); - } - - // 3. 查询用户实体 + // 2. (空行后) 查询用户实体 var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { return false; } - if (!isSuperAdmin && user.TenantId != currentTenantId) - { - return false; - } - - // 4. 校验租户管理员保留规则(仅租户侧用户适用) + // 3. 校验租户管理员保留规则(仅租户侧用户适用) if (user.Portal == PortalType.Tenant && request.Status == IdentityUserStatus.Disabled && user.Status == IdentityUserStatus.Active @@ -60,7 +45,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler( await EnsureNotLastActiveTenantAdminAsync(user.TenantId.Value, user.Id, cancellationToken); } - // 5. 更新状态 + // 4. 更新状态 var previousStatus = user.Status; switch (request.Status) { @@ -81,7 +66,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler( throw new BusinessException(ErrorCodes.BadRequest, "无效的用户状态"); } - // 6. 构建操作日志消息 + // 5. 构建操作日志消息 var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) ? operatorProfile.Account : operatorProfile.DisplayName; @@ -108,7 +93,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler( Success = true }; - // 7. 写入 Outbox 并保存变更 + // 6. 写入 Outbox 并保存变更 await operationLogPublisher.PublishAsync(logMessage, cancellationToken); await identityUserRepository.SaveChangesAsync(cancellationToken); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CopyRoleTemplateCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CopyRoleTemplateCommandHandler.cs index 91518ae..36f8074 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CopyRoleTemplateCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CopyRoleTemplateCommandHandler.cs @@ -6,7 +6,6 @@ using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -17,14 +16,24 @@ public sealed class CopyRoleTemplateCommandHandler( IRoleTemplateRepository roleTemplateRepository, IRoleRepository roleRepository, IPermissionRepository permissionRepository, - IRolePermissionRepository rolePermissionRepository, - ITenantProvider tenantProvider) + IRolePermissionRepository rolePermissionRepository) : IRequestHandler { /// public async Task Handle(CopyRoleTemplateCommand request, CancellationToken cancellationToken) { - // 1. 查询模板与模板权限 + // 1. 校验 Portal 与 TenantId 参数 + if (request.Portal == PortalType.Admin && request.TenantId is not null) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "Portal=Admin 时 TenantId 必须为空"); + } + + if (request.Portal == PortalType.Tenant && (!request.TenantId.HasValue || request.TenantId.Value <= 0)) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "Portal=Tenant 时 TenantId 必须大于 0"); + } + + // 2. 查询模板与模板权限 var template = await roleTemplateRepository.FindByCodeAsync(request.TemplateCode, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {request.TemplateCode} 不存在"); @@ -35,16 +44,16 @@ public sealed class CopyRoleTemplateCommandHandler( .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); - // 2. 计算角色名称/编码与描述 - var tenantId = tenantProvider.GetCurrentTenantId(); + // 3. 计算角色名称/编码与描述 + var portal = request.Portal; + var tenantId = request.TenantId; - // 3. 固定复制为租户侧角色 - var portal = PortalType.Tenant; + // 4. (空行后) 解析目标角色信息 var roleCode = string.IsNullOrWhiteSpace(request.RoleCode) ? template.TemplateCode : request.RoleCode.Trim(); var roleName = string.IsNullOrWhiteSpace(request.RoleName) ? template.Name : request.RoleName.Trim(); var roleDescription = request.Description ?? template.Description; - // 4. 准备或更新角色主体(幂等创建)。 + // 5. 准备或更新角色主体(幂等创建)。 var role = await roleRepository.FindByCodeAsync(portal, tenantId, roleCode, cancellationToken); if (role is null) { @@ -73,7 +82,7 @@ public sealed class CopyRoleTemplateCommandHandler( await roleRepository.UpdateAsync(role, cancellationToken); } - // 5. 确保模板权限全部存在,不存在则按模板定义创建。 + // 6. 确保模板权限全部存在,不存在则按模板定义创建。 var existingPermissions = await permissionRepository.GetByCodesAsync(permissionCodes, cancellationToken); var permissionMap = existingPermissions.ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase); @@ -97,7 +106,7 @@ public sealed class CopyRoleTemplateCommandHandler( await roleRepository.SaveChangesAsync(cancellationToken); - // 6. 绑定缺失的权限,保留租户自定义的已有授权。 + // 7. 绑定缺失的权限,保留租户自定义的已有授权。 var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, new[] { role.Id }, cancellationToken); var existingPermissionIds = rolePermissions .Select(x => x.PermissionId) diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs index 839dc06..071526a 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs @@ -13,7 +13,6 @@ using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -25,7 +24,6 @@ public sealed class CreateIdentityUserCommandHandler( IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, IPasswordHasher passwordHasher, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, IIdentityOperationLogPublisher operationLogPublisher, @@ -36,19 +34,17 @@ public sealed class CreateIdentityUserCommandHandler( /// public async Task Handle(CreateIdentityUserCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 - var currentTenantId = tenantProvider.GetCurrentTenantId(); + // 1. 获取操作者档案(用于操作日志) 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 <= 0) { - throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户创建用户"); + throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空"); } - // 3. 规范化输入并准备校验 - var tenantId = isSuperAdmin ? request.TenantId ?? currentTenantId : currentTenantId; + // 3. (空行后) 规范化输入并准备校验 + var tenantId = request.TenantId.Value; 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/CreateRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs index 9126878..0acd571 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs @@ -6,7 +6,6 @@ using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -14,8 +13,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 创建角色处理器。 /// public sealed class CreateRoleCommandHandler( - IRoleRepository roleRepository, - ITenantProvider tenantProvider) + IRoleRepository roleRepository) : IRequestHandler { /// @@ -28,11 +26,17 @@ public sealed class CreateRoleCommandHandler( { // 1. 固定创建租户侧角色 var portal = PortalType.Tenant; - - // 2. 获取租户上下文 - var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId(); - // 3. 归一化输入并校验唯一 + // 2. (空行后) 校验租户参数 + if (!request.TenantId.HasValue || request.TenantId.Value <= 0) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空"); + } + + // 3. (空行后) 获取租户标识 + var tenantId = request.TenantId.Value; + + // 4. (空行后) 归一化输入并校验唯一 var name = request.Name?.Trim() ?? string.Empty; var code = request.Code?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(code)) @@ -46,7 +50,7 @@ public sealed class CreateRoleCommandHandler( throw new BusinessException(ErrorCodes.Conflict, "角色编码已存在"); } - // 4. 构建角色实体 + // 5. 构建角色实体 var role = new Role { Portal = portal, @@ -56,11 +60,11 @@ public sealed class CreateRoleCommandHandler( Description = request.Description }; - // 5. 持久化 + // 6. 持久化 await roleRepository.AddAsync(role, cancellationToken); await roleRepository.SaveChangesAsync(cancellationToken); - // 6. 返回 DTO + // 7. 返回 DTO return new RoleDto { Id = role.Id, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs index dd0cb86..677c3e2 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs @@ -8,7 +8,6 @@ using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -19,7 +18,6 @@ public sealed class DeleteIdentityUserCommandHandler( IIdentityUserRepository identityUserRepository, IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, IIdentityOperationLogPublisher operationLogPublisher) @@ -28,36 +26,23 @@ public sealed class DeleteIdentityUserCommandHandler( /// public async Task Handle(DeleteIdentityUserCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 - var currentTenantId = tenantProvider.GetCurrentTenantId(); + // 1. 获取操作者档案(用于操作日志) var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - // 2. 校验跨租户访问权限 - if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户删除用户"); - } - - // 3. 查询用户实体 + // 2. (空行后) 查询用户实体 var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { return false; } - if (!isSuperAdmin && user.TenantId != currentTenantId) - { - return false; - } - - // 4. 校验租户管理员保留规则(仅租户侧用户适用) + // 3. 校验租户管理员保留规则(仅租户侧用户适用) if (user.Portal == PortalType.Tenant && user.Status == IdentityUserStatus.Active && user.TenantId.HasValue) { await EnsureNotLastActiveTenantAdminAsync(user.TenantId.Value, user.Id, cancellationToken); } - // 5. 构建操作日志消息 + // 4. 构建操作日志消息 var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) ? operatorProfile.Account : operatorProfile.DisplayName; @@ -78,7 +63,7 @@ public sealed class DeleteIdentityUserCommandHandler( Success = true }; - // 6. 软删除用户并写入 Outbox + // 5. 软删除用户并写入 Outbox await identityUserRepository.RemoveAsync(user, cancellationToken); await operationLogPublisher.PublishAsync(logMessage, cancellationToken); await identityUserRepository.SaveChangesAsync(cancellationToken); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs index 414f711..874cc7b 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs @@ -2,7 +2,8 @@ using MediatR; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -10,8 +11,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 删除角色处理器。 /// public sealed class DeleteRoleCommandHandler( - IRoleRepository roleRepository, - ITenantProvider tenantProvider) + IRoleRepository roleRepository) : IRequestHandler { /// @@ -25,14 +25,19 @@ public sealed class DeleteRoleCommandHandler( // 1. 固定删除租户侧角色 var portal = PortalType.Tenant; - // 2. 获取租户上下文 - var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 校验租户参数 + if (!request.TenantId.HasValue || request.TenantId.Value <= 0) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空"); + } + + // 3. (空行后) 获取租户标识并删除角色 + var tenantId = request.TenantId.Value; - // 3. 删除角色 await roleRepository.DeleteAsync(portal, tenantId, request.RoleId, cancellationToken); await roleRepository.SaveChangesAsync(cancellationToken); - // 4. 返回执行结果 + // 4. (空行后) 返回执行结果 return true; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetIdentityUserDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetIdentityUserDetailQueryHandler.cs index b23662a..a08dad6 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetIdentityUserDetailQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetIdentityUserDetailQueryHandler.cs @@ -1,12 +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; @@ -18,21 +15,13 @@ public sealed class GetIdentityUserDetailQueryHandler( IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, IRolePermissionRepository rolePermissionRepository, - IPermissionRepository permissionRepository, - ITenantProvider tenantProvider, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService) + IPermissionRepository permissionRepository) : IRequestHandler { /// public async Task Handle(GetIdentityUserDetailQuery request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 - var currentTenantId = tenantProvider.GetCurrentTenantId(); - var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - - // 2. 查询用户实体 + // 1. 查询用户实体 var user = request.IncludeDeleted ? await identityUserRepository.GetForUpdateIncludingDeletedAsync(request.UserId, cancellationToken) : await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); @@ -42,16 +31,11 @@ public sealed class GetIdentityUserDetailQueryHandler( return null; } - if (!isSuperAdmin && user.TenantId != currentTenantId) - { - return null; - } - - // 3. 加载角色与权限 + // 2. 加载角色与权限 var portal = user.Portal; var tenantId = user.TenantId; - // 4. 查询用户角色关系 + // 3. 查询用户角色关系 var roleRelations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, user.Id, cancellationToken); var roleIds = roleRelations.Select(x => x.RoleId).Distinct().ToArray(); var roles = roleIds.Length == 0 @@ -75,7 +59,7 @@ public sealed class GetIdentityUserDetailQueryHandler( .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); - // 5. 组装详情 DTO + // 4. 组装详情 DTO var now = DateTime.UtcNow; return new UserDetailDto { diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs deleted file mode 100644 index cefd895..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs +++ /dev/null @@ -1,89 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.Identity.Contracts; -using TakeoutSaaS.Application.Identity.Queries; -using TakeoutSaaS.Domain.Identity.Enums; -using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.Identity.Handlers; - -/// -/// 按用户 ID 获取权限概览处理器。 -/// -public sealed class GetUserPermissionsQueryHandler( - IIdentityUserRepository identityUserRepository, - IUserRoleRepository userRoleRepository, - IRoleRepository roleRepository, - IPermissionRepository permissionRepository, - IRolePermissionRepository rolePermissionRepository, - ITenantProvider tenantProvider) - : IRequestHandler -{ - /// - public async Task Handle(GetUserPermissionsQuery request, CancellationToken cancellationToken) - { - // 1. 获取租户并查询用户 - var portal = PortalType.Tenant; - var tenantId = tenantProvider.GetCurrentTenantId(); - var user = await identityUserRepository.FindByIdAsync(request.UserId, cancellationToken); - if (user == null || user.TenantId != tenantId) - { - return null; - } - - // 2. 解析角色与权限 - var roleCodes = await ResolveUserRolesAsync(portal, tenantId, user.Id, cancellationToken); - var permissionCodes = await ResolveUserPermissionsAsync(portal, tenantId, user.Id, cancellationToken); - - // 3. 返回用户权限概览 - return new UserPermissionDto - { - UserId = user.Id, - TenantId = user.TenantId, - MerchantId = user.MerchantId, - Account = user.Account, - DisplayName = user.DisplayName, - Roles = roleCodes, - Permissions = permissionCodes, - CreatedAt = user.CreatedAt - }; - } - - private async Task ResolveUserRolesAsync(PortalType portal, long tenantId, long userId, CancellationToken cancellationToken) - { - // 1. 查询用户角色关系 - var relations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, userId, cancellationToken); - var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); - if (roleIds.Length == 0) - { - return Array.Empty(); - } - - // 2. 查询角色编码 - var roles = await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken); - return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); - } - - private async Task ResolveUserPermissionsAsync(PortalType portal, long tenantId, long userId, CancellationToken cancellationToken) - { - // 1. 查询用户角色关系 - var relations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, userId, cancellationToken); - var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); - if (roleIds.Length == 0) - { - return Array.Empty(); - } - - // 2. 查询角色-权限关系 - var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken); - var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); - if (permissionIds.Length == 0) - { - return Array.Empty(); - } - - // 3. 查询权限编码 - var permissions = await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken); - return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); - } -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/InitializeRoleTemplatesCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/InitializeRoleTemplatesCommandHandler.cs deleted file mode 100644 index 7ec7a0e..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/InitializeRoleTemplatesCommandHandler.cs +++ /dev/null @@ -1,62 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.Identity.Commands; -using TakeoutSaaS.Application.Identity.Contracts; -using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Application.Identity.Handlers; - -/// -/// 租户角色模板批量初始化处理器。 -/// -public sealed class InitializeRoleTemplatesCommandHandler( - IRoleTemplateRepository roleTemplateRepository, - IMediator mediator) - : IRequestHandler> -{ - /// - public async Task> Handle(InitializeRoleTemplatesCommand request, CancellationToken cancellationToken) - { - // 1. 解析需要初始化的模板编码,默认取全部模板。 - var requestedCodes = request.TemplateCodes? - .Where(code => !string.IsNullOrWhiteSpace(code)) - .Select(code => code.Trim()) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - var availableTemplates = await roleTemplateRepository.GetAllAsync(true, cancellationToken); - var availableCodes = availableTemplates.Select(t => t.TemplateCode).ToHashSet(StringComparer.OrdinalIgnoreCase); - - var targetCodes = requestedCodes?.Length > 0 - ? requestedCodes - : availableTemplates.Select(template => template.TemplateCode).ToArray(); - - if (targetCodes.Length == 0) - { - return Array.Empty(); - } - - foreach (var code in targetCodes) - { - if (!availableCodes.Contains(code)) - { - throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {code} 不存在或未启用"); - } - } - - // 2. 逐个复制模板,幂等写入角色与权限。 - var roles = new List(targetCodes.Length); - foreach (var templateCode in targetCodes) - { - var role = await mediator.Send(new CopyRoleTemplateCommand - { - TemplateCode = templateCode - }, cancellationToken); - - roles.Add(role); - } - - return roles; - } -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs index 6289d70..6b04abb 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs @@ -9,7 +9,6 @@ using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -19,7 +18,6 @@ namespace TakeoutSaaS.Application.Identity.Handlers; public sealed class ResetIdentityUserPasswordCommandHandler( IAdminPasswordResetTokenStore tokenStore, IIdentityUserRepository identityUserRepository, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, IIdentityOperationLogPublisher operationLogPublisher) @@ -28,34 +26,21 @@ public sealed class ResetIdentityUserPasswordCommandHandler( /// public async Task Handle(ResetIdentityUserPasswordCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 - var currentTenantId = tenantProvider.GetCurrentTenantId(); + // 1. 获取操作者档案(用于操作日志) var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - // 2. 校验跨租户访问权限 - if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码"); - } - - // 3. 查询用户实体 + // 2. (空行后) 查询用户实体 var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); } - if (!isSuperAdmin && user.TenantId != currentTenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码"); - } - - // 4. 签发重置令牌(1 小时有效) + // 3. 签发重置令牌(1 小时有效) var expiresAt = DateTime.UtcNow.AddHours(1); var token = await tokenStore.IssueAsync(user.Id, expiresAt, cancellationToken); - // 5. 标记用户需重置密码 + // 4. 标记用户需重置密码 user.MustChangePassword = true; user.FailedLoginCount = 0; user.LockedUntil = null; @@ -64,7 +49,7 @@ public sealed class ResetIdentityUserPasswordCommandHandler( user.Status = IdentityUserStatus.Active; } - // 6. 构建操作日志消息 + // 5. 构建操作日志消息 var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) ? operatorProfile.Account : operatorProfile.DisplayName; @@ -85,7 +70,7 @@ public sealed class ResetIdentityUserPasswordCommandHandler( Success = true }; - // 7. 写入 Outbox 并保存变更 + // 6. 写入 Outbox 并保存变更 await operationLogPublisher.PublishAsync(logMessage, cancellationToken); await identityUserRepository.SaveChangesAsync(cancellationToken); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs index b00725f..4f98d3e 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs @@ -7,7 +7,6 @@ using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -16,7 +15,6 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// public sealed class RestoreIdentityUserCommandHandler( IIdentityUserRepository identityUserRepository, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, IIdentityOperationLogPublisher operationLogPublisher) @@ -25,35 +23,22 @@ public sealed class RestoreIdentityUserCommandHandler( /// public async Task Handle(RestoreIdentityUserCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 - var currentTenantId = tenantProvider.GetCurrentTenantId(); + // 1. 获取操作者档案(用于操作日志) var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - // 2. 校验跨租户访问权限 - if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户恢复用户"); - } - - // 3. 查询用户实体(包含已删除) + // 2. (空行后) 查询用户实体(包含已删除) var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(request.UserId, cancellationToken); if (user == null) { return false; } - if (!isSuperAdmin && user.TenantId != currentTenantId) - { - return false; - } - if (!user.DeletedAt.HasValue) { return false; } - // 4. 构建操作日志消息 + // 3. (空行后) 构建操作日志消息 var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) ? operatorProfile.Account : operatorProfile.DisplayName; @@ -74,7 +59,7 @@ public sealed class RestoreIdentityUserCommandHandler( Success = true }; - // 5. 恢复软删除状态并写入 Outbox + // 4. 恢复软删除状态并写入 Outbox user.DeletedAt = null; user.DeletedBy = null; await operationLogPublisher.PublishAsync(logMessage, cancellationToken); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs index 104c7d6..179295e 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs @@ -3,7 +3,8 @@ using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -13,8 +14,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; public sealed class RoleDetailQueryHandler( IRoleRepository roleRepository, IRolePermissionRepository rolePermissionRepository, - IPermissionRepository permissionRepository, - ITenantProvider tenantProvider) + IPermissionRepository permissionRepository) : IRequestHandler { /// @@ -23,24 +23,30 @@ public sealed class RoleDetailQueryHandler( // 1. 固定查询租户侧角色详情 var portal = PortalType.Tenant; - // 2. 获取租户上下文并查询角色 - var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 校验租户参数 + if (!request.TenantId.HasValue || request.TenantId.Value <= 0) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空"); + } + + // 3. (空行后) 获取租户标识并查询角色 + var tenantId = request.TenantId.Value; var role = await roleRepository.FindByIdAsync(portal, tenantId, request.RoleId, cancellationToken); if (role is null) { return null; } - // 3. 查询角色权限关系 + // 4. 查询角色权限关系 var relations = await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, new[] { role.Id }, cancellationToken); var permissionIds = relations.Select(x => x.PermissionId).ToArray(); - // 4. 拉取权限实体 + // 5. 拉取权限实体 var permissions = permissionIds.Length == 0 ? Array.Empty() : await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken); - // 5. 映射 DTO + // 6. 映射 DTO var permissionDtos = permissions .Select(x => new PermissionDto { @@ -55,6 +61,7 @@ public sealed class RoleDetailQueryHandler( }) .ToList(); + // 7. (空行后) 返回角色详情 return new RoleDetailDto { Id = role.Id, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchIdentityUsersQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchIdentityUsersQueryHandler.cs index 5043b29..8a7d121 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchIdentityUsersQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchIdentityUsersQueryHandler.cs @@ -1,15 +1,10 @@ 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.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; @@ -19,30 +14,16 @@ namespace TakeoutSaaS.Application.Identity.Handlers; public sealed class SearchIdentityUsersQueryHandler( IIdentityUserRepository identityUserRepository, IUserRoleRepository userRoleRepository, - IRoleRepository roleRepository, - ITenantProvider tenantProvider, - ICurrentUserAccessor currentUserAccessor, - IAdminAuthService adminAuthService) + IRoleRepository roleRepository) : IRequestHandler> { /// public async Task> Handle(SearchIdentityUsersQuery request, CancellationToken cancellationToken) { - // 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) - { - throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户查询用户"); - } - - // 3. 组装查询过滤条件 + // 1. 组装查询过滤条件 var filter = new IdentityUserSearchFilter { - TenantId = isSuperAdmin ? request.TenantId : currentTenantId, + TenantId = request.TenantId, Keyword = request.Keyword, Status = request.Status, RoleId = request.RoleId, @@ -57,17 +38,17 @@ public sealed class SearchIdentityUsersQueryHandler( SortDescending = request.SortDescending }; - // 4. 执行分页查询 + // 2. 执行分页查询 var (items, total) = await identityUserRepository.SearchPagedAsync(filter, cancellationToken); if (items.Count == 0) { return new PagedResult(Array.Empty(), request.Page, request.PageSize, total); } - // 5. 加载角色编码映射 + // 3. 加载角色编码映射 var roleCodesLookup = await ResolveRoleCodesAsync(items, userRoleRepository, roleRepository, cancellationToken); - // 6. 组装 DTO + // 4. 组装 DTO var now = DateTime.UtcNow; var dtos = items.Select(user => new UserListItemDto { @@ -87,7 +68,7 @@ public sealed class SearchIdentityUsersQueryHandler( LastLoginAt = user.LastLoginAt }).ToList(); - // 7. 返回分页结果 + // 5. 返回分页结果 return new PagedResult(dtos, request.Page, request.PageSize, total); } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs index 8868ae8..4f3f84e 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs @@ -3,8 +3,9 @@ using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -12,8 +13,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 角色分页查询处理器。 /// public sealed class SearchRolesQueryHandler( - IRoleRepository roleRepository, - ITenantProvider tenantProvider) + IRoleRepository roleRepository) : IRequestHandler> { /// @@ -27,11 +27,17 @@ public sealed class SearchRolesQueryHandler( // 1. 固定查询租户侧角色 var portal = PortalType.Tenant; - // 2. 获取租户上下文并查询角色 - var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 校验租户参数 + if (!request.TenantId.HasValue || request.TenantId.Value <= 0) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空"); + } + + // 3. (空行后) 获取租户标识并查询角色 + var tenantId = request.TenantId.Value; var roles = await roleRepository.SearchAsync(portal, tenantId, request.Keyword, cancellationToken); - // 3. 排序 + // 4. 排序 var sorted = request.SortBy?.ToLowerInvariant() switch { "name" => request.SortDescending @@ -42,13 +48,13 @@ public sealed class SearchRolesQueryHandler( : roles.OrderBy(x => x.CreatedAt) }; - // 4. 分页 + // 5. 分页 var paged = sorted .Skip((request.Page - 1) * request.PageSize) .Take(request.PageSize) .ToList(); - // 5. 映射 DTO + // 6. 映射 DTO var items = paged.Select(role => new RoleDto { Id = role.Id, @@ -59,7 +65,7 @@ public sealed class SearchRolesQueryHandler( Description = role.Description }).ToList(); - // 6. 返回分页结果 + // 7. 返回分页结果 return new PagedResult(items, request.Page, request.PageSize, roles.Count); } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs deleted file mode 100644 index c99912e..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs +++ /dev/null @@ -1,132 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.Identity.Contracts; -using TakeoutSaaS.Application.Identity.Queries; -using TakeoutSaaS.Domain.Identity.Enums; -using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.Identity.Handlers; - -/// -/// 租户用户权限分页查询处理器。 -/// -public sealed class SearchUserPermissionsQueryHandler( - IIdentityUserRepository identityUserRepository, - IUserRoleRepository userRoleRepository, - IRoleRepository roleRepository, - IPermissionRepository permissionRepository, - IRolePermissionRepository rolePermissionRepository, - ITenantProvider tenantProvider) - : IRequestHandler> -{ - /// - public async Task> Handle(SearchUserPermissionsQuery request, CancellationToken cancellationToken) - { - // 1. 获取租户并查询用户 - var portal = PortalType.Tenant; - var tenantId = tenantProvider.GetCurrentTenantId(); - var users = await identityUserRepository.SearchAsync(tenantId, request.Keyword, cancellationToken); - - // 2. 排序与分页 - var sorted = SortUsers(users, request.SortBy, request.SortDescending); - var paged = sorted - .Skip((request.Page - 1) * request.PageSize) - .Take(request.PageSize) - .ToList(); - - // 3. 解析角色与权限 - var resolved = await ResolveRolesAndPermissionsAsync(portal, tenantId, paged, cancellationToken); - var items = paged.Select(user => new UserPermissionDto - { - UserId = user.Id, - TenantId = user.TenantId, - MerchantId = user.MerchantId, - Account = user.Account, - DisplayName = user.DisplayName, - Roles = resolved[user.Id].roles, - Permissions = resolved[user.Id].permissions, - CreatedAt = user.CreatedAt - }).ToList(); - - return new PagedResult(items, request.Page, request.PageSize, users.Count); - } - - private static IOrderedEnumerable SortUsers( - IReadOnlyCollection users, - string? sortBy, - bool sortDescending) - { - return sortBy?.ToLowerInvariant() switch - { - "account" => sortDescending - ? users.OrderByDescending(x => x.Account) - : users.OrderBy(x => x.Account), - "displayname" => sortDescending - ? users.OrderByDescending(x => x.DisplayName) - : users.OrderBy(x => x.DisplayName), - _ => sortDescending - ? users.OrderByDescending(x => x.CreatedAt) - : users.OrderBy(x => x.CreatedAt) - }; - } - - private async Task> ResolveRolesAndPermissionsAsync( - PortalType portal, - long tenantId, - IReadOnlyCollection users, - CancellationToken cancellationToken) - { - // 1. 查询用户角色关系 - var userIds = users.Select(x => x.Id).ToArray(); - var userRoleRelations = await userRoleRepository.GetByUserIdsAsync(portal, tenantId, userIds, cancellationToken); - var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray(); - - // 2. 查询角色信息 - var roles = roleIds.Length == 0 - ? Array.Empty() - : await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken); - var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer.Default); - - // 3. 查询角色-权限关系 - var rolePermissions = roleIds.Length == 0 - ? Array.Empty() - : await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken); - var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); - - // 4. 查询权限详情 - var permissions = permissionIds.Length == 0 - ? Array.Empty() - : await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken); - var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer.Default); - - var rolePermissionsLookup = rolePermissions - .GroupBy(rp => rp.RoleId) - .ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer.Default); - - var result = new Dictionary(); - foreach (var userId in userIds) - { - // 5. 聚合用户角色与权限编码 - var rolesForUser = userRoleRelations.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).Distinct().ToArray(); - var roleCodes = rolesForUser - .Select(rid => roleCodeMap.GetValueOrDefault(rid)) - .Where(c => !string.IsNullOrWhiteSpace(c)) - .Select(c => c!) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - var permissionCodes = rolesForUser - .SelectMany(rid => rolePermissionsLookup.GetValueOrDefault(rid) ?? Array.Empty()) - .Select(pid => permissionCodeMap.GetValueOrDefault(pid)) - .Where(code => !string.IsNullOrWhiteSpace(code)) - .Select(code => code!) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - result[userId] = (roleCodes, permissionCodes); - } - - return result; - } -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs index 1ee0f1c..02c0e0c 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs @@ -11,7 +11,6 @@ using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -22,7 +21,6 @@ public sealed class UpdateIdentityUserCommandHandler( IIdentityUserRepository identityUserRepository, IUserRoleRepository userRoleRepository, IRoleRepository roleRepository, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, IIdentityOperationLogPublisher operationLogPublisher, @@ -32,30 +30,17 @@ public sealed class UpdateIdentityUserCommandHandler( /// public async Task Handle(UpdateIdentityUserCommand request, CancellationToken cancellationToken) { - // 1. 获取操作者档案并判断权限 - var currentTenantId = tenantProvider.GetCurrentTenantId(); + // 1. 获取操作者档案(用于操作日志) var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken); - var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); - // 2. 校验跨租户访问权限 - if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId) - { - throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户更新用户"); - } - - // 3. 获取用户实体 + // 2. (空行后) 获取用户实体 var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { return null; } - if (!isSuperAdmin && user.TenantId != currentTenantId) - { - return null; - } - - // 4. 规范化输入并校验唯一性 + // 3. (空行后) 规范化输入并校验唯一性 var portal = user.Portal; var tenantId = user.TenantId; if (portal == PortalType.Tenant && (!tenantId.HasValue || tenantId.Value == 0)) @@ -93,14 +78,14 @@ public sealed class UpdateIdentityUserCommandHandler( } } - // 5. 更新用户字段 + // 4. 更新用户字段 user.DisplayName = displayName; user.Phone = phone; user.Email = email; user.Avatar = string.IsNullOrWhiteSpace(request.Avatar) ? null : request.Avatar.Trim(); user.RowVersion = request.RowVersion; - // 6. 构建操作日志消息 + // 5. 构建操作日志消息 var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) ? operatorProfile.Account : operatorProfile.DisplayName; @@ -128,7 +113,7 @@ public sealed class UpdateIdentityUserCommandHandler( Success = true }; - // 7. 持久化用户更新并写入 Outbox + // 6. 持久化用户更新并写入 Outbox try { await operationLogPublisher.PublishAsync(logMessage, cancellationToken); @@ -139,13 +124,13 @@ public sealed class UpdateIdentityUserCommandHandler( throw new BusinessException(ErrorCodes.Conflict, "用户数据已被修改,请刷新后重试"); } - // 8. 覆盖角色绑定(仅当显式传入时) + // 7. 覆盖角色绑定(仅当显式传入时) if (roleIds != null) { await userRoleRepository.ReplaceUserRolesAsync(portal, tenantId, user.Id, roleIds, cancellationToken); } - // 9. 返回用户详情 + // 8. 返回用户详情 return await mediator.Send(new GetIdentityUserDetailQuery { UserId = user.Id }, cancellationToken); } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs index 53a95c5..d37fd1d 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs @@ -3,7 +3,8 @@ using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -11,8 +12,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 更新角色处理器。 /// public sealed class UpdateRoleCommandHandler( - IRoleRepository roleRepository, - ITenantProvider tenantProvider) + IRoleRepository roleRepository) : IRequestHandler { /// @@ -26,23 +26,29 @@ public sealed class UpdateRoleCommandHandler( // 1. 固定更新租户侧角色 var portal = PortalType.Tenant; - // 2. 获取租户上下文并查询角色 - var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId(); + // 2. (空行后) 校验租户参数 + if (!request.TenantId.HasValue || request.TenantId.Value <= 0) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 不能为空"); + } + + // 3. (空行后) 获取租户标识并查询角色 + var tenantId = request.TenantId.Value; var role = await roleRepository.FindByIdAsync(portal, tenantId, request.RoleId, cancellationToken); if (role == null) { return null; } - // 3. 更新字段 + // 4. 更新字段 role.Name = request.Name; role.Description = request.Description; - // 4. 持久化 + // 5. 持久化 await roleRepository.UpdateAsync(role, cancellationToken); await roleRepository.SaveChangesAsync(cancellationToken); - // 5. 返回 DTO + // 6. 返回 DTO return new RoleDto { Id = role.Id, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Queries/GetUserPermissionsQuery.cs b/src/Application/TakeoutSaaS.Application/Identity/Queries/GetUserPermissionsQuery.cs deleted file mode 100644 index 84b11d2..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Queries/GetUserPermissionsQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.Identity.Contracts; - -namespace TakeoutSaaS.Application.Identity.Queries; - -/// -/// 按用户 ID 获取角色/权限概览。 -/// -public sealed class GetUserPermissionsQuery : IRequest -{ - /// - /// 用户 ID(雪花)。 - /// - public long UserId { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchUserPermissionsQuery.cs b/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchUserPermissionsQuery.cs deleted file mode 100644 index 1a6d557..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchUserPermissionsQuery.cs +++ /dev/null @@ -1,36 +0,0 @@ -using MediatR; -using TakeoutSaaS.Application.Identity.Contracts; -using TakeoutSaaS.Shared.Abstractions.Results; - -namespace TakeoutSaaS.Application.Identity.Queries; - -/// -/// 按租户分页查询用户的角色/权限概览。 -/// -public sealed class SearchUserPermissionsQuery : IRequest> -{ - /// - /// 关键字(账号或展示名称)。 - /// - public string? Keyword { get; init; } - - /// - /// 页码,从 1 开始。 - /// - public int Page { get; init; } = 1; - - /// - /// 每页条数。 - /// - public int PageSize { get; init; } = 20; - - /// - /// 排序字段(account/displayName/createdAt)。 - /// - public string? SortBy { get; init; } - - /// - /// 是否倒序。 - /// - public bool SortDescending { get; init; } = true; -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs index 0c782ff..24c9302 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs @@ -6,8 +6,6 @@ using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Services; @@ -23,8 +21,7 @@ public sealed class AdminAuthService( IMenuRepository menuRepository, IPasswordHasher passwordHasher, IJwtTokenService jwtTokenService, - IRefreshTokenStore refreshTokenStore, - ITenantProvider tenantProvider) : IAdminAuthService + IRefreshTokenStore refreshTokenStore) : IAdminAuthService { /// /// 管理后台登录:验证账号密码并生成令牌。 @@ -159,92 +156,6 @@ public sealed class AdminAuthService( return menu; } - /// - /// 获取指定用户的权限概览(校验当前租户)。 - /// - public async Task GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default) - { - var tenantId = tenantProvider.GetCurrentTenantId(); - var user = await userRepository.FindByIdAsync(userId, cancellationToken); - if (user == null || user.TenantId != tenantId) - { - return null; - } - - // 1. 解析角色集合 - var roleCodes = await ResolveUserRolesAsync(user.Portal, user.TenantId, user.Id, cancellationToken); - // 2. (空行后) 解析权限集合 - var permissionCodes = await ResolveUserPermissionsAsync(user.Portal, user.TenantId, user.Id, cancellationToken); - - // 3. (空行后) 返回概览 - return new UserPermissionDto - { - UserId = user.Id, - TenantId = user.TenantId, - MerchantId = user.MerchantId, - Account = user.Account, - DisplayName = user.DisplayName, - Roles = roleCodes, - Permissions = permissionCodes, - CreatedAt = user.CreatedAt - }; - } - - /// - /// 按租户分页查询用户权限概览。 - /// - public async Task> SearchUserPermissionsAsync( - string? keyword, - int page, - int pageSize, - string? sortBy, - bool sortDescending, - CancellationToken cancellationToken = default) - { - // 1. 获取当前租户 - var tenantId = tenantProvider.GetCurrentTenantId(); - // 2. (空行后) 查询用户列表 - var users = await userRepository.SearchAsync(tenantId, keyword, cancellationToken); - - // 3. (空行后) 排序 - var sorted = sortBy?.ToLowerInvariant() switch - { - "account" => sortDescending - ? users.OrderByDescending(x => x.Account) - : users.OrderBy(x => x.Account), - "displayname" => sortDescending - ? users.OrderByDescending(x => x.DisplayName) - : users.OrderBy(x => x.DisplayName), - _ => sortDescending - ? users.OrderByDescending(x => x.CreatedAt) - : users.OrderBy(x => x.CreatedAt) - }; - - // 4. (空行后) 分页 - var paged = sorted - .Skip((page - 1) * pageSize) - .Take(pageSize) - .ToList(); - - // 5. (空行后) 解析角色与权限 - var resolved = await ResolveRolesAndPermissionsAsync(PortalType.Tenant, tenantId, paged, cancellationToken); - // 6. (空行后) 映射为 DTO - var items = paged.Select(user => new UserPermissionDto - { - UserId = user.Id, - TenantId = user.TenantId, - MerchantId = user.MerchantId, - Account = user.Account, - DisplayName = user.DisplayName, - Roles = resolved[user.Id].roles, - Permissions = resolved[user.Id].permissions, - CreatedAt = user.CreatedAt - }).ToList(); - - // 7. (空行后) 返回分页结果 - return new PagedResult(items, page, pageSize, users.Count); - } - private async Task BuildProfileAsync(IdentityUser user, CancellationToken cancellationToken) { // 1. 解析角色集合 @@ -495,68 +406,4 @@ public sealed class AdminAuthService( var permissions = await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken); return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } - - private async Task> ResolveRolesAndPermissionsAsync( - PortalType portal, - long? tenantId, - IReadOnlyCollection users, - CancellationToken cancellationToken) - { - // 1. 读取用户-角色关系 - var userIds = users.Select(x => x.Id).ToArray(); - var userRoleRelations = await userRoleRepository.GetByUserIdsAsync(portal, tenantId, userIds, cancellationToken); - var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray(); - - // 2. (空行后) 读取角色定义 - var roles = roleIds.Length == 0 - ? Array.Empty() - : await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken); - var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer.Default); - - // 3. (空行后) 读取角色-权限关系 - var rolePermissions = roleIds.Length == 0 - ? Array.Empty() - : await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken); - - // 4. (空行后) 读取权限定义 - var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); - var permissions = permissionIds.Length == 0 - ? Array.Empty() - : await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken); - var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer.Default); - - // 5. (空行后) 构建 Role -> PermissionId[] 映射 - var rolePermissionsLookup = rolePermissions - .GroupBy(rp => rp.RoleId) - .ToDictionary(g => g.Key, g => g.Select(rp => rp.PermissionId).ToArray(), comparer: EqualityComparer.Default); - - // 6. (空行后) 按用户聚合角色码与权限码 - var result = new Dictionary(); - foreach (var userId in userIds) - { - // 6.1 解析用户角色码 - var rolesForUser = userRoleRelations.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).Distinct().ToArray(); - var roleCodes = rolesForUser - .Select(rid => roleCodeMap.GetValueOrDefault(rid)) - .Where(c => !string.IsNullOrWhiteSpace(c)) - .Select(c => c!) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - // 6.2 (空行后) 解析用户权限码 - var permissionCodes = rolesForUser - .SelectMany(rid => rolePermissionsLookup.GetValueOrDefault(rid) ?? Array.Empty()) - .Select(pid => permissionCodeMap.GetValueOrDefault(pid)) - .Where(code => !string.IsNullOrWhiteSpace(code)) - .Select(code => code!) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - // 6.3 (空行后) 写入结果 - result[userId] = (roleCodes, permissionCodes); - } - - // 7. (空行后) 返回聚合结果 - return result; - } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs deleted file mode 100644 index 8234d7e..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/MiniAuthService.cs +++ /dev/null @@ -1,148 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System.Net; -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Application.Identity.Contracts; -using TakeoutSaaS.Domain.Identity.Entities; -using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.Identity.Services; - -/// -/// 小程序认证服务实现。 -/// -public sealed class MiniAuthService( - IWeChatAuthService weChatAuthService, - IMiniUserRepository miniUserRepository, - IJwtTokenService jwtTokenService, - IRefreshTokenStore refreshTokenStore, - ILoginRateLimiter rateLimiter, - IHttpContextAccessor httpContextAccessor, - ITenantProvider tenantProvider) : IMiniAuthService -{ - /// - /// 微信小程序登录:通过微信 code 获取用户信息并生成令牌。 - /// - /// 微信登录请求 - /// 取消令牌 - /// 令牌响应 - /// 获取微信用户信息失败、缺少租户标识时抛出 - public async Task LoginWithWeChatAsync(WeChatLoginRequest request, CancellationToken cancellationToken = default) - { - // 1. 限流检查(基于 IP 地址) - var throttleKey = BuildThrottleKey(); - await rateLimiter.EnsureAllowedAsync(throttleKey, cancellationToken); - - // 2. 通过微信 code 获取 session(OpenId、UnionId、SessionKey) - var session = await weChatAuthService.Code2SessionAsync(request.Code, cancellationToken); - if (string.IsNullOrWhiteSpace(session.OpenId)) - { - throw new BusinessException(ErrorCodes.Unauthorized, "获取微信用户信息失败"); - } - - // 3. 获取当前租户 ID(多租户支持) - var tenantId = tenantProvider.GetCurrentTenantId(); - if (tenantId == 0) - { - throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识"); - } - - // 4. 获取或创建小程序用户(如果 OpenId 已存在则返回现有用户,否则创建新用户) - var (user, isNew) = await GetOrBindMiniUserAsync(session.OpenId, session.UnionId, request.Nickname, request.Avatar, tenantId, cancellationToken); - - // 5. 登录成功后重置限流计数 - await rateLimiter.ResetAsync(throttleKey, cancellationToken); - - // 6. 构建用户档案并生成令牌 - var profile = BuildProfile(user); - return await jwtTokenService.CreateTokensAsync(profile, isNew, cancellationToken); - } - - /// - /// 刷新访问令牌:使用刷新令牌获取新的访问令牌和刷新令牌。 - /// - /// 刷新令牌请求 - /// 取消令牌 - /// 新的令牌响应 - /// 刷新令牌无效、已过期或用户不存在时抛出 - public async Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default) - { - // 1. 验证刷新令牌(检查是否存在、是否过期、是否已撤销) - var descriptor = await refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken); - if (descriptor == null || descriptor.ExpiresAt <= DateTime.UtcNow || descriptor.Revoked) - { - throw new BusinessException(ErrorCodes.Unauthorized, "RefreshToken 无效或已过期"); - } - - // 2. 根据用户 ID 查找用户 - var user = await miniUserRepository.FindByIdAsync(descriptor.UserId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.Unauthorized, "用户不存在"); - - // 3. 撤销旧刷新令牌(防止重复使用) - await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken); - - // 4. 生成新的令牌对 - var profile = BuildProfile(user); - return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken); - } - - /// - /// 获取用户档案。 - /// - /// 用户 ID - /// 取消令牌 - /// 用户档案 - /// 用户不存在时抛出 - public async Task GetProfileAsync(long userId, CancellationToken cancellationToken = default) - { - var user = await miniUserRepository.FindByIdAsync(userId, cancellationToken) - ?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); - - return BuildProfile(user); - } - - /// - /// 获取或绑定小程序用户:如果 OpenId 已存在则返回现有用户,否则创建新用户。 - /// - /// 微信 OpenId - /// 微信 UnionId(可选) - /// 昵称 - /// 头像地址(可选) - /// 租户 ID - /// 取消令牌 - /// 用户实体和是否为新用户的元组 - private async Task<(MiniUser user, bool isNew)> GetOrBindMiniUserAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken) - { - // 检查用户是否已存在 - var existing = await miniUserRepository.FindByOpenIdAsync(openId, cancellationToken); - if (existing != null) - { - return (existing, false); - } - - // 创建新用户 - var created = await miniUserRepository.CreateOrUpdateAsync(openId, unionId, nickname, avatar, tenantId, cancellationToken); - return (created, true); - } - - private static CurrentUserProfile BuildProfile(MiniUser user) - => new() - { - UserId = user.Id, - Account = user.OpenId, - DisplayName = user.Nickname, - TenantId = user.TenantId, - MerchantId = null, - Roles = Array.Empty(), - Permissions = Array.Empty(), - Avatar = user.Avatar - }; - - private string BuildThrottleKey() - { - var ip = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress ?? IPAddress.Loopback; - return $"mini-login:{ip}"; - } -} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Abstractions/IVerificationCodeService.cs b/src/Application/TakeoutSaaS.Application/Sms/Abstractions/IVerificationCodeService.cs deleted file mode 100644 index 4eab7cc..0000000 --- a/src/Application/TakeoutSaaS.Application/Sms/Abstractions/IVerificationCodeService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using TakeoutSaaS.Application.Sms.Contracts; - -namespace TakeoutSaaS.Application.Sms.Abstractions; - -/// -/// 短信验证码服务抽象。 -/// -public interface IVerificationCodeService -{ - /// - /// 发送验证码。 - /// - Task SendAsync(SendVerificationCodeRequest request, CancellationToken cancellationToken = default); - - /// - /// 校验验证码。 - /// - Task VerifyAsync(VerifyVerificationCodeRequest request, CancellationToken cancellationToken = default); -} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeRequest.cs b/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeRequest.cs deleted file mode 100644 index 5a1bca6..0000000 --- a/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeRequest.cs +++ /dev/null @@ -1,27 +0,0 @@ -using TakeoutSaaS.Module.Sms; - -namespace TakeoutSaaS.Application.Sms.Contracts; - -/// -/// 发送验证码请求。 -/// -/// -/// 创建发送请求。 -/// -public sealed class SendVerificationCodeRequest(string phoneNumber, string scene, SmsProviderKind? provider = null) -{ - /// - /// 手机号(支持 +86 前缀或纯 11 位)。 - /// - public string PhoneNumber { get; } = phoneNumber; - - /// - /// 业务场景(如 login/register/reset)。 - /// - public string Scene { get; } = scene; - - /// - /// 指定服务商,未指定则使用默认配置。 - /// - public SmsProviderKind? Provider { get; } = provider; -} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeResponse.cs b/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeResponse.cs deleted file mode 100644 index e9f84f1..0000000 --- a/src/Application/TakeoutSaaS.Application/Sms/Contracts/SendVerificationCodeResponse.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace TakeoutSaaS.Application.Sms.Contracts; - -/// -/// 发送验证码响应。 -/// -public sealed class SendVerificationCodeResponse -{ - /// - /// 过期时间。 - /// - public DateTimeOffset ExpiresAt { get; set; } - - /// - /// 请求标识。 - /// - public string? RequestId { get; set; } -} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Contracts/VerifyVerificationCodeRequest.cs b/src/Application/TakeoutSaaS.Application/Sms/Contracts/VerifyVerificationCodeRequest.cs deleted file mode 100644 index 9eb1262..0000000 --- a/src/Application/TakeoutSaaS.Application/Sms/Contracts/VerifyVerificationCodeRequest.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace TakeoutSaaS.Application.Sms.Contracts; - -/// -/// 校验验证码请求。 -/// -/// -/// 创建校验请求。 -/// -public sealed class VerifyVerificationCodeRequest(string phoneNumber, string scene, string code) -{ - /// - /// 手机号。 - /// - public string PhoneNumber { get; } = phoneNumber; - - /// - /// 业务场景。 - /// - public string Scene { get; } = scene; - - /// - /// 填写的验证码。 - /// - public string Code { get; } = code; -} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Extensions/SmsServiceCollectionExtensions.cs b/src/Application/TakeoutSaaS.Application/Sms/Extensions/SmsServiceCollectionExtensions.cs deleted file mode 100644 index 5a4d7c7..0000000 --- a/src/Application/TakeoutSaaS.Application/Sms/Extensions/SmsServiceCollectionExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using TakeoutSaaS.Application.Sms.Abstractions; -using TakeoutSaaS.Application.Sms.Options; -using TakeoutSaaS.Application.Sms.Services; - -namespace TakeoutSaaS.Application.Sms.Extensions; - -/// -/// 短信应用服务注册扩展。 -/// -public static class SmsServiceCollectionExtensions -{ - /// - /// 注册短信验证码应用服务。 - /// - public static IServiceCollection AddSmsApplication(this IServiceCollection services, IConfiguration configuration) - { - services.AddOptions() - .Bind(configuration.GetSection("Sms:VerificationCode")) - .ValidateDataAnnotations() - .ValidateOnStart(); - - services.AddScoped(); - return services; - } -} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Options/VerificationCodeOptions.cs b/src/Application/TakeoutSaaS.Application/Sms/Options/VerificationCodeOptions.cs deleted file mode 100644 index fd49271..0000000 --- a/src/Application/TakeoutSaaS.Application/Sms/Options/VerificationCodeOptions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace TakeoutSaaS.Application.Sms.Options; - -/// -/// 验证码发送配置。 -/// -public sealed class VerificationCodeOptions -{ - /// - /// 验证码位数,默认 6。 - /// - [Range(4, 10)] - public int CodeLength { get; set; } = 6; - - /// - /// 过期时间(分钟)。 - /// - [Range(1, 60)] - public int ExpireMinutes { get; set; } = 5; - - /// - /// 发送冷却时间(秒),用于防止频繁请求。 - /// - [Range(10, 300)] - public int CooldownSeconds { get; set; } = 60; - - /// - /// 缓存前缀。 - /// - [Required] - public string CachePrefix { get; set; } = "sms:code"; -} diff --git a/src/Application/TakeoutSaaS.Application/Sms/Services/VerificationCodeService.cs b/src/Application/TakeoutSaaS.Application/Sms/Services/VerificationCodeService.cs deleted file mode 100644 index 1fbac0f..0000000 --- a/src/Application/TakeoutSaaS.Application/Sms/Services/VerificationCodeService.cs +++ /dev/null @@ -1,155 +0,0 @@ -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.Security.Cryptography; -using System.Text; -using TakeoutSaaS.Application.Sms.Abstractions; -using TakeoutSaaS.Application.Sms.Contracts; -using TakeoutSaaS.Application.Sms.Options; -using TakeoutSaaS.Module.Sms.Abstractions; -using TakeoutSaaS.Module.Sms.Models; -using TakeoutSaaS.Module.Sms.Options; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.Sms.Services; - -/// -/// 短信验证码服务实现。 -/// -public sealed class VerificationCodeService( - ISmsSenderResolver senderResolver, - IOptionsMonitor smsOptionsMonitor, - IOptionsMonitor codeOptionsMonitor, - ITenantProvider tenantProvider, - IDistributedCache cache, - ILogger logger) : IVerificationCodeService -{ - /// - public async Task SendAsync(SendVerificationCodeRequest request, CancellationToken cancellationToken = default) - { - // 1. 参数校验 - if (string.IsNullOrWhiteSpace(request.PhoneNumber)) - { - throw new BusinessException(ErrorCodes.BadRequest, "手机号不能为空"); - } - - if (string.IsNullOrWhiteSpace(request.Scene)) - { - throw new BusinessException(ErrorCodes.BadRequest, "场景不能为空"); - } - - // 2. 解析模板与缓存键 - var smsOptions = smsOptionsMonitor.CurrentValue; - var codeOptions = codeOptionsMonitor.CurrentValue; - var templateCode = ResolveTemplate(request.Scene, smsOptions); - var phone = NormalizePhoneNumber(request.PhoneNumber); - var tenantKey = tenantProvider.GetCurrentTenantId() == 0 ? "platform" : tenantProvider.GetCurrentTenantId().ToString(); - var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}"; - var cooldownKey = $"{cacheKey}:cooldown"; - - // 3. 检查冷却期 - await EnsureCooldownAsync(cooldownKey, codeOptions.CooldownSeconds, cancellationToken).ConfigureAwait(false); - - // 4. 生成验证码并发送短信 - var code = GenerateCode(codeOptions.CodeLength); - var variables = new Dictionary { { "code", code } }; - var sender = senderResolver.Resolve(request.Provider); - - var smsRequest = new SmsSendRequest(phone, templateCode, variables, smsOptions.DefaultSignName); - var smsResult = await sender.SendAsync(smsRequest, cancellationToken).ConfigureAwait(false); - if (!smsResult.Success) - { - throw new BusinessException(ErrorCodes.InternalServerError, $"短信发送失败:{smsResult.Message}"); - } - - // 5. 写入验证码与冷却缓存 - var expiresAt = DateTimeOffset.UtcNow.AddMinutes(codeOptions.ExpireMinutes); - await cache.SetStringAsync(cacheKey, code, new DistributedCacheEntryOptions - { - AbsoluteExpiration = expiresAt - }, cancellationToken).ConfigureAwait(false); - - await cache.SetStringAsync(cooldownKey, "1", new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(codeOptions.CooldownSeconds) - }, cancellationToken).ConfigureAwait(false); - - logger.LogInformation("发送验证码成功,Phone:{Phone} Scene:{Scene} Tenant:{Tenant}", phone, request.Scene, tenantKey); - return new SendVerificationCodeResponse - { - ExpiresAt = expiresAt, - RequestId = smsResult.RequestId - }; - } - - /// - public async Task VerifyAsync(VerifyVerificationCodeRequest request, CancellationToken cancellationToken = default) - { - // 1. 基础校验 - if (string.IsNullOrWhiteSpace(request.Code)) - { - return false; - } - - // 2. 读取验证码 - var codeOptions = codeOptionsMonitor.CurrentValue; - var phone = NormalizePhoneNumber(request.PhoneNumber); - var tenantKey = tenantProvider.GetCurrentTenantId() == 0 ? "platform" : tenantProvider.GetCurrentTenantId().ToString(); - var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}"; - - var cachedCode = await cache.GetStringAsync(cacheKey, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrWhiteSpace(cachedCode)) - { - return false; - } - - // 3. 比对成功后清除缓存 - var success = string.Equals(cachedCode, request.Code, StringComparison.Ordinal); - if (success) - { - await cache.RemoveAsync(cacheKey, cancellationToken).ConfigureAwait(false); - } - - return success; - } - - private static string ResolveTemplate(string scene, SmsOptions options) - { - if (options.SceneTemplates.TryGetValue(scene, out var template) && !string.IsNullOrWhiteSpace(template)) - { - return template; - } - - throw new BusinessException(ErrorCodes.BadRequest, $"未配置场景 {scene} 的短信模板"); - } - - private static string NormalizePhoneNumber(string phone) - { - var trimmed = phone.Trim(); - return trimmed.StartsWith("+", StringComparison.Ordinal) ? trimmed : $"+86{trimmed}"; - } - - private static string GenerateCode(int length) - { - var buffer = new byte[length]; - RandomNumberGenerator.Fill(buffer); - var builder = new StringBuilder(length); - foreach (var b in buffer) - { - builder.Append((b % 10).ToString()); - } - - return builder.ToString()[..length]; - } - - private async Task EnsureCooldownAsync(string cooldownKey, int cooldownSeconds, CancellationToken cancellationToken) - { - var existing = await cache.GetStringAsync(cooldownKey, cancellationToken).ConfigureAwait(false); - if (!string.IsNullOrEmpty(existing)) - { - throw new BusinessException(ErrorCodes.BadRequest, "请求过于频繁,请稍后再试"); - } - } -} diff --git a/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadRequest.cs b/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadRequest.cs index 48e05eb..d718eb4 100644 --- a/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadRequest.cs +++ b/src/Application/TakeoutSaaS.Application/Storage/Contracts/DirectUploadRequest.cs @@ -8,13 +8,18 @@ namespace TakeoutSaaS.Application.Storage.Contracts; /// /// 创建直传请求。 /// -public sealed class DirectUploadRequest(UploadFileType fileType, string fileName, string contentType, long contentLength, string? requestOrigin) +public sealed class DirectUploadRequest(UploadFileType fileType, long tenantId, string fileName, string contentType, long contentLength, string? requestOrigin) { /// /// 文件类型。 /// public UploadFileType FileType { get; } = fileType; + /// + /// 租户 ID(0 表示平台)。 + /// + public long TenantId { get; } = tenantId; + /// /// 文件名。 /// diff --git a/src/Application/TakeoutSaaS.Application/Storage/Contracts/UploadFileRequest.cs b/src/Application/TakeoutSaaS.Application/Storage/Contracts/UploadFileRequest.cs index 43e12f2..776e40b 100644 --- a/src/Application/TakeoutSaaS.Application/Storage/Contracts/UploadFileRequest.cs +++ b/src/Application/TakeoutSaaS.Application/Storage/Contracts/UploadFileRequest.cs @@ -10,6 +10,7 @@ namespace TakeoutSaaS.Application.Storage.Contracts; /// public sealed class UploadFileRequest( UploadFileType fileType, + long tenantId, Stream content, string fileName, string contentType, @@ -21,6 +22,11 @@ public sealed class UploadFileRequest( /// public UploadFileType FileType { get; } = fileType; + /// + /// 租户 ID(0 表示平台)。 + /// + public long TenantId { get; } = tenantId; + /// /// 文件流。 /// diff --git a/src/Application/TakeoutSaaS.Application/Storage/Services/FileStorageService.cs b/src/Application/TakeoutSaaS.Application/Storage/Services/FileStorageService.cs index 17eb087..40cb21b 100644 --- a/src/Application/TakeoutSaaS.Application/Storage/Services/FileStorageService.cs +++ b/src/Application/TakeoutSaaS.Application/Storage/Services/FileStorageService.cs @@ -12,7 +12,6 @@ using TakeoutSaaS.Module.Storage.Options; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Storage.Services; @@ -22,7 +21,6 @@ namespace TakeoutSaaS.Application.Storage.Services; public sealed class FileStorageService( IStorageProviderResolver providerResolver, IOptionsMonitor optionsMonitor, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, ILogger logger) : IFileStorageService { @@ -47,8 +45,13 @@ public sealed class FileStorageService( ResetStream(request.Content); // 3. 生成对象键与元数据 - var objectKey = BuildObjectKey(request.FileType, extension); - var metadata = BuildMetadata(request.FileType); + if (request.TenantId < 0) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 无效"); + } + + var objectKey = BuildObjectKey(request.TenantId, request.FileType, extension); + var metadata = BuildMetadata(request.TenantId, request.FileType); var expires = TimeSpan.FromMinutes(Math.Max(1, security.DefaultUrlExpirationMinutes)); var provider = providerResolver.Resolve(); @@ -89,7 +92,12 @@ public sealed class FileStorageService( var contentType = NormalizeContentType(request.ContentType, extension); // 3. 构建直传参数 - var objectKey = BuildObjectKey(request.FileType, extension); + if (request.TenantId < 0) + { + throw new BusinessException(ErrorCodes.ValidationFailed, "TenantId 无效"); + } + + var objectKey = BuildObjectKey(request.TenantId, request.FileType, extension); var provider = providerResolver.Resolve(); var expires = TimeSpan.FromMinutes(Math.Max(1, security.DefaultUrlExpirationMinutes)); @@ -213,9 +221,8 @@ public sealed class FileStorageService( /// /// 生成对象存储的键路径。 /// - private string BuildObjectKey(UploadFileType type, string extension) + private static string BuildObjectKey(long tenantId, UploadFileType type, string extension) { - var tenantId = tenantProvider.GetCurrentTenantId(); var tenantSegment = tenantId == 0 ? "platform" : tenantId.ToString(); var folder = type.ToFolderName(); var now = DateTime.UtcNow; @@ -227,12 +234,12 @@ public sealed class FileStorageService( /// /// 组装对象元数据,便于追踪租户与用户。 /// - private IDictionary BuildMetadata(UploadFileType type) + private IDictionary BuildMetadata(long tenantId, UploadFileType type) { var metadata = new Dictionary { ["x-meta-upload-type"] = type.ToString(), - ["x-meta-tenant-id"] = tenantProvider.GetCurrentTenantId().ToString() + ["x-meta-tenant-id"] = tenantId.ToString() }; if (currentUserAccessor.IsAuthenticated) diff --git a/src/Domain/TakeoutSaaS.Domain/Inventory/Repositories/IInventoryRepository.cs b/src/Domain/TakeoutSaaS.Domain/Inventory/Repositories/IInventoryRepository.cs index bba4734..0e4e6bc 100644 --- a/src/Domain/TakeoutSaaS.Domain/Inventory/Repositories/IInventoryRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Inventory/Repositories/IInventoryRepository.cs @@ -16,17 +16,17 @@ public interface IInventoryRepository /// /// 依据标识查询库存。 /// - Task FindByIdAsync(long inventoryItemId, long tenantId, CancellationToken cancellationToken = default); + Task FindByIdAsync(long inventoryItemId, long? tenantId, CancellationToken cancellationToken = default); /// /// 按门店与 SKU 查询库存(只读)。 /// - Task FindBySkuAsync(long tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default); + Task FindBySkuAsync(long? tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default); /// /// 按门店与 SKU 查询库存(跟踪用于更新)。 /// - Task GetForUpdateAsync(long tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default); + Task GetForUpdateAsync(long? tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default); /// /// 新增库存记录。 @@ -51,7 +51,7 @@ public interface IInventoryRepository /// /// 按幂等键查询锁记录。 /// - Task FindLockByKeyAsync(long tenantId, string idempotencyKey, CancellationToken cancellationToken = default); + Task FindLockByKeyAsync(long? tenantId, string idempotencyKey, CancellationToken cancellationToken = default); /// /// 更新锁状态。 @@ -61,22 +61,22 @@ public interface IInventoryRepository /// /// 查询过期锁定。 /// - Task> FindExpiredLocksAsync(long tenantId, DateTime utcNow, CancellationToken cancellationToken = default); + Task> FindExpiredLocksAsync(long? tenantId, DateTime utcNow, CancellationToken cancellationToken = default); /// /// 查询批次列表。 /// - Task> GetBatchesAsync(long tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default); + Task> GetBatchesAsync(long? tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default); /// /// 批次扣减读取(带排序策略)。 /// - Task> GetBatchesForConsumeAsync(long tenantId, long storeId, long productSkuId, InventoryBatchConsumeStrategy strategy, CancellationToken cancellationToken = default); + Task> GetBatchesForConsumeAsync(long? tenantId, long storeId, long productSkuId, InventoryBatchConsumeStrategy strategy, CancellationToken cancellationToken = default); /// /// 查询批次(跟踪用于更新)。 /// - Task GetBatchForUpdateAsync(long tenantId, long storeId, long productSkuId, string batchNumber, CancellationToken cancellationToken = default); + Task GetBatchForUpdateAsync(long? tenantId, long storeId, long productSkuId, string batchNumber, CancellationToken cancellationToken = default); /// /// 新增批次。 diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantAnnouncementReadRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantAnnouncementReadRepository.cs deleted file mode 100644 index 53af993..0000000 --- a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantAnnouncementReadRepository.cs +++ /dev/null @@ -1,53 +0,0 @@ -using TakeoutSaaS.Domain.Tenants.Entities; - -namespace TakeoutSaaS.Domain.Tenants.Repositories; - -/// -/// 公告已读仓储。 -/// -public interface ITenantAnnouncementReadRepository -{ - /// - /// 按公告查询已读记录。 - /// - /// 租户 ID。 - /// 公告 ID。 - /// 取消标记。 - /// 指定公告的已读列表。 - Task> GetByAnnouncementAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default); - - /// - /// 批量按公告查询已读记录,可选按用户过滤。 - /// - /// 租户 ID。 - /// 公告 ID 集合。 - /// 用户 ID,空则不按用户筛选。 - /// 取消标记。 - /// 匹配条件的已读列表。 - Task> GetByAnnouncementAsync(long tenantId, IEnumerable announcementIds, long? userId, CancellationToken cancellationToken = default); - - /// - /// 查询指定用户对某公告的已读记录。 - /// - /// 租户 ID。 - /// 公告 ID。 - /// 用户 ID。 - /// 取消标记。 - /// 已读记录,未读返回 null。 - Task FindAsync(long tenantId, long announcementId, long? userId, CancellationToken cancellationToken = default); - - /// - /// 新增已读记录。 - /// - /// 已读实体。 - /// 取消标记。 - /// 异步任务。 - Task AddAsync(TenantAnnouncementRead record, CancellationToken cancellationToken = default); - - /// - /// 保存变更。 - /// - /// 取消标记。 - /// 异步任务。 - Task SaveChangesAsync(CancellationToken cancellationToken = default); -} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantAnnouncementRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantAnnouncementRepository.cs index 1b926af..b01af46 100644 --- a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantAnnouncementRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ITenantAnnouncementRepository.cs @@ -36,33 +36,6 @@ public interface ITenantAnnouncementRepository int? limit = null, CancellationToken cancellationToken = default); - /// - /// 按 ID 获取公告(包含平台公告 TenantId=0)。 - /// - /// 租户 ID。 - /// 公告 ID。 - /// 取消标记。 - /// 公告实体或 null。 - Task FindByIdInScopeAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default); - - /// - /// 查询未读公告(包含平台公告 TenantId=0)。 - /// - /// 租户 ID。 - /// 用户 ID。 - /// 公告状态。 - /// 启用状态。 - /// 生效时间点,为空不限制。 - /// 取消标记。 - /// 未读公告集合。 - Task> SearchUnreadAsync( - long tenantId, - long? userId, - AnnouncementStatus? status, - bool? isActive, - DateTime? effectiveAt, - CancellationToken cancellationToken = default); - /// /// 按 ID 获取公告。 /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs index 7831d77..3caeed2 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs @@ -51,7 +51,6 @@ public static class AppServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAdminDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAdminDbContext.cs index 7feeb24..ee6b1ff 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAdminDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAdminDbContext.cs @@ -1,7 +1,6 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.App.Persistence; @@ -10,10 +9,9 @@ namespace TakeoutSaaS.Infrastructure.App.Persistence; /// public sealed class TakeoutAdminDbContext( DbContextOptions options, - ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, IIdGenerator? idGenerator = null) - : TakeoutAppDbContext(options, tenantProvider, currentUserAccessor, idGenerator) + : TakeoutAppDbContext(options, currentUserAccessor, idGenerator) { /// /// 配置实体映射关系(不启用租户过滤)。 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index aee1ee0..19e8506 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -24,7 +24,6 @@ using TakeoutSaaS.Domain.Tenants.Enums; using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Infrastructure.App.Persistence.Configurations; namespace TakeoutSaaS.Infrastructure.App.Persistence; @@ -34,10 +33,9 @@ namespace TakeoutSaaS.Infrastructure.App.Persistence; /// public class TakeoutAppDbContext( DbContextOptions options, - ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, IIdGenerator? idGenerator = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) + : AppDbContext(options, currentUserAccessor, idGenerator) { /// /// 租户聚合根。 @@ -383,9 +381,6 @@ public class TakeoutAppDbContext( { // 1. 构建基础模型(软删除/注释 + 实体映射) OnModelCreatingCore(modelBuilder); - - // 2. (空行后) 应用租户过滤 - ApplyTenantQueryFilters(modelBuilder); } /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs index 9146c16..02d53f8 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs @@ -2,7 +2,6 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.App.Persistence; @@ -24,12 +23,10 @@ internal sealed class TakeoutAppDesignTimeDbContextFactory /// 创建设计时的业务库 DbContext。 /// /// 上下文选项。 - /// 租户提供器。 /// 当前用户访问器。 /// 业务库上下文实例。 protected override TakeoutAppDbContext CreateContext( DbContextOptions options, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor) - => new(options, tenantProvider, currentUserAccessor); + => new(options, currentUserAccessor); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfInventoryRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfInventoryRepository.cs index 1ef37b9..03d33b9 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfInventoryRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfInventoryRepository.cs @@ -15,29 +15,50 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories; public sealed class EfInventoryRepository(TakeoutAdminDbContext context) : IInventoryRepository { /// - public Task FindByIdAsync(long inventoryItemId, long tenantId, CancellationToken cancellationToken = default) + public Task FindByIdAsync(long inventoryItemId, long? tenantId, CancellationToken cancellationToken = default) { - return context.InventoryItems + var query = context.InventoryItems .AsNoTracking() - .Where(x => x.TenantId == tenantId && x.Id == inventoryItemId) - .FirstOrDefaultAsync(cancellationToken); + .Where(x => x.Id == inventoryItemId); + + // 1. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + return query.FirstOrDefaultAsync(cancellationToken); } /// - public Task FindBySkuAsync(long tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default) + public Task FindBySkuAsync(long? tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default) { - return context.InventoryItems + var query = context.InventoryItems .AsNoTracking() - .Where(x => x.TenantId == tenantId && x.StoreId == storeId && x.ProductSkuId == productSkuId) - .FirstOrDefaultAsync(cancellationToken); + .Where(x => x.StoreId == storeId && x.ProductSkuId == productSkuId); + + // 1. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + return query.FirstOrDefaultAsync(cancellationToken); } /// - public Task GetForUpdateAsync(long tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default) + public Task GetForUpdateAsync(long? tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default) { - return context.InventoryItems - .Where(x => x.TenantId == tenantId && x.StoreId == storeId && x.ProductSkuId == productSkuId) - .FirstOrDefaultAsync(cancellationToken); + var query = context.InventoryItems + .Where(x => x.StoreId == storeId && x.ProductSkuId == productSkuId); + + // 1. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + return query.FirstOrDefaultAsync(cancellationToken); } /// @@ -66,11 +87,18 @@ public sealed class EfInventoryRepository(TakeoutAdminDbContext context) : IInve } /// - public Task FindLockByKeyAsync(long tenantId, string idempotencyKey, CancellationToken cancellationToken = default) + public Task FindLockByKeyAsync(long? tenantId, string idempotencyKey, CancellationToken cancellationToken = default) { - return context.InventoryLockRecords - .Where(x => x.TenantId == tenantId && x.IdempotencyKey == idempotencyKey) - .FirstOrDefaultAsync(cancellationToken); + var query = context.InventoryLockRecords + .Where(x => x.IdempotencyKey == idempotencyKey); + + // 1. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + return query.FirstOrDefaultAsync(cancellationToken); } /// @@ -82,19 +110,32 @@ public sealed class EfInventoryRepository(TakeoutAdminDbContext context) : IInve } /// - public async Task> FindExpiredLocksAsync(long tenantId, DateTime utcNow, CancellationToken cancellationToken = default) + public async Task> FindExpiredLocksAsync(long? tenantId, DateTime utcNow, CancellationToken cancellationToken = default) { - var locks = await context.InventoryLockRecords - .Where(x => x.TenantId == tenantId && x.Status == InventoryLockStatus.Locked && x.ExpiresAt != null && x.ExpiresAt <= utcNow) - .ToListAsync(cancellationToken); + var query = context.InventoryLockRecords + .Where(x => x.Status == InventoryLockStatus.Locked && x.ExpiresAt != null && x.ExpiresAt <= utcNow); + + // 1. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + var locks = await query.ToListAsync(cancellationToken); return locks; } /// - public async Task> GetBatchesForConsumeAsync(long tenantId, long storeId, long productSkuId, InventoryBatchConsumeStrategy strategy, CancellationToken cancellationToken = default) + public async Task> GetBatchesForConsumeAsync(long? tenantId, long storeId, long productSkuId, InventoryBatchConsumeStrategy strategy, CancellationToken cancellationToken = default) { var query = context.InventoryBatches - .Where(x => x.TenantId == tenantId && x.StoreId == storeId && x.ProductSkuId == productSkuId); + .Where(x => x.StoreId == storeId && x.ProductSkuId == productSkuId); + + // 1. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } query = strategy == InventoryBatchConsumeStrategy.Fefo ? query.OrderBy(x => x.ExpireDate ?? DateTime.MaxValue).ThenBy(x => x.BatchNumber) @@ -104,11 +145,19 @@ public sealed class EfInventoryRepository(TakeoutAdminDbContext context) : IInve } /// - public async Task> GetBatchesAsync(long tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default) + public async Task> GetBatchesAsync(long? tenantId, long storeId, long productSkuId, CancellationToken cancellationToken = default) { - var batches = await context.InventoryBatches + var query = context.InventoryBatches .AsNoTracking() - .Where(x => x.TenantId == tenantId && x.StoreId == storeId && x.ProductSkuId == productSkuId) + .Where(x => x.StoreId == storeId && x.ProductSkuId == productSkuId); + + // 1. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + var batches = await query .OrderBy(x => x.ExpireDate ?? DateTime.MaxValue) .ThenBy(x => x.BatchNumber) .ToListAsync(cancellationToken); @@ -117,11 +166,18 @@ public sealed class EfInventoryRepository(TakeoutAdminDbContext context) : IInve } /// - public Task GetBatchForUpdateAsync(long tenantId, long storeId, long productSkuId, string batchNumber, CancellationToken cancellationToken = default) + public Task GetBatchForUpdateAsync(long? tenantId, long storeId, long productSkuId, string batchNumber, CancellationToken cancellationToken = default) { - return context.InventoryBatches - .Where(x => x.TenantId == tenantId && x.StoreId == storeId && x.ProductSkuId == productSkuId && x.BatchNumber == batchNumber) - .FirstOrDefaultAsync(cancellationToken); + var query = context.InventoryBatches + .Where(x => x.StoreId == storeId && x.ProductSkuId == productSkuId && x.BatchNumber == batchNumber); + + // 1. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + return query.FirstOrDefaultAsync(cancellationToken); } /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementReadRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementReadRepository.cs deleted file mode 100644 index af44e78..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementReadRepository.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Infrastructure.App.Persistence; - -namespace TakeoutSaaS.Infrastructure.App.Repositories; - -/// -/// EF 公告已读仓储。 -/// -public sealed class EfTenantAnnouncementReadRepository(TakeoutAdminDbContext context) : ITenantAnnouncementReadRepository -{ - /// - public Task> GetByAnnouncementAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default) - { - return context.TenantAnnouncementReads.AsNoTracking() - .Where(x => x.TenantId == tenantId && x.AnnouncementId == announcementId) - .OrderBy(x => x.ReadAt) - .ToListAsync(cancellationToken) - .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); - } - - /// - public Task> GetByAnnouncementAsync(long tenantId, IEnumerable announcementIds, long? userId, CancellationToken cancellationToken = default) - { - var ids = announcementIds.Distinct().ToArray(); - if (ids.Length == 0) - { - return Task.FromResult>(Array.Empty()); - } - - var query = context.TenantAnnouncementReads.AsNoTracking() - .Where(x => x.TenantId == tenantId && ids.Contains(x.AnnouncementId)); - - if (userId.HasValue) - { - query = query.Where(x => x.UserId == userId.Value); - } - else - { - query = query.Where(x => x.UserId == null); - } - - return query - .OrderByDescending(x => x.ReadAt) - .ToListAsync(cancellationToken) - .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); - } - - /// - public Task FindAsync(long tenantId, long announcementId, long? userId, CancellationToken cancellationToken = default) - { - return context.TenantAnnouncementReads - .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.AnnouncementId == announcementId && x.UserId == userId, cancellationToken); - } - - /// - public Task AddAsync(TenantAnnouncementRead record, CancellationToken cancellationToken = default) - { - return context.TenantAnnouncementReads.AddAsync(record, cancellationToken).AsTask(); - } - - /// - public Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - return context.SaveChangesAsync(cancellationToken); - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementRepository.cs index 0480f7f..98d59ff 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementRepository.cs @@ -86,63 +86,6 @@ public sealed class EfTenantAnnouncementRepository(TakeoutAdminDbContext context return await query.ToListAsync(cancellationToken); } - /// - public Task FindByIdInScopeAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default) - { - var tenantIds = new[] { tenantId, 0L }; - return context.TenantAnnouncements.AsNoTracking() - .IgnoreQueryFilters() - .FirstOrDefaultAsync(x => tenantIds.Contains(x.TenantId) && x.Id == announcementId, cancellationToken); - } - - /// - public async Task> SearchUnreadAsync( - long tenantId, - long? userId, - AnnouncementStatus? status, - bool? isActive, - DateTime? effectiveAt, - CancellationToken cancellationToken = default) - { - var tenantIds = new[] { tenantId, 0L }; - var announcementQuery = context.TenantAnnouncements.AsNoTracking() - .IgnoreQueryFilters() - .Where(x => tenantIds.Contains(x.TenantId)); - - if (status.HasValue) - { - announcementQuery = announcementQuery.Where(x => x.Status == status.Value); - } - - if (isActive.HasValue) - { - announcementQuery = isActive.Value - ? announcementQuery.Where(x => x.Status == AnnouncementStatus.Published) - : announcementQuery.Where(x => x.Status != AnnouncementStatus.Published); - } - - if (effectiveAt.HasValue) - { - var at = effectiveAt.Value; - announcementQuery = announcementQuery.Where(x => x.EffectiveFrom <= at && (x.EffectiveTo == null || x.EffectiveTo >= at)); - } - - var readQuery = context.TenantAnnouncementReads.AsNoTracking() - .IgnoreQueryFilters() - .Where(x => x.TenantId == tenantId); - - readQuery = userId.HasValue - ? readQuery.Where(x => x.UserId == null || x.UserId == userId.Value) - : readQuery.Where(x => x.UserId == null); - - var query = from announcement in announcementQuery - join read in readQuery on announcement.Id equals read.AnnouncementId into readGroup - where !readGroup.Any() - select announcement; - - return await query.ToListAsync(cancellationToken); - } - /// public Task FindByIdAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default) { diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs index 177a576..02b3275 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Configuration; using TakeoutSaaS.Infrastructure.Common.Options; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; @@ -11,7 +10,7 @@ namespace TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; /// EF Core 设计时 DbContext 工厂基类,统一读取 appsettings 中的数据库配置。 /// internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDbContextFactory - where TContext : TenantAwareDbContext + where TContext : DbContext { private readonly string _dataSourceName; private readonly string? _connectionStringEnvVar; @@ -52,7 +51,6 @@ internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDb // 2. 创建上下文 return CreateContext( optionsBuilder.Options, - new DesignTimeTenantProvider(), new DesignTimeCurrentUserAccessor()); } @@ -60,12 +58,10 @@ internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDb /// 由子类实现的上下文工厂方法。 /// /// 上下文选项。 - /// 租户提供器。 /// 当前用户访问器。 /// DbContext 实例。 protected abstract TContext CreateContext( DbContextOptions options, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor); private string ResolveConnectionString() @@ -118,9 +114,7 @@ internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDb { 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.AdminApi") }.Where(dir => !string.IsNullOrWhiteSpace(dir)); foreach (var dir in candidateDirs) @@ -155,15 +149,6 @@ internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDb File.Exists(Path.Combine(directory, "appsettings.json")) || Directory.GetFiles(directory, "appsettings.*.json").Length > 0; - private sealed class DesignTimeTenantProvider : ITenantProvider - { - /// - /// 设计时返回默认租户 ID。 - /// - /// 默认租户 ID。 - public long GetCurrentTenantId() => 0; - } - private sealed class DesignTimeCurrentUserAccessor : ICurrentUserAccessor { /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs deleted file mode 100644 index 920292e..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System.Reflection; -using TakeoutSaaS.Shared.Abstractions.Entities; -using TakeoutSaaS.Shared.Abstractions.Ids; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Infrastructure.Common.Persistence; - -/// -/// 多租户感知 DbContext:自动应用租户过滤并填充租户字段。 -/// -public abstract class TenantAwareDbContext( - DbContextOptions options, - ITenantProvider tenantProvider, - ICurrentUserAccessor? currentUserAccessor = null, - IIdGenerator? idGenerator = null) : AppDbContext(options, currentUserAccessor, idGenerator) -{ - /// - /// 当前请求租户 ID。 - /// - protected long CurrentTenantId => tenantProvider.GetCurrentTenantId(); - - /// - /// 保存前填充租户元数据并执行基础处理。 - /// - protected override void OnBeforeSaving() - { - ApplyTenantMetadata(); - base.OnBeforeSaving(); - } - - /// - /// 应用租户过滤器到所有实现 的实体。 - /// - /// 模型构建器。 - protected void ApplyTenantQueryFilters(ModelBuilder modelBuilder) - { - foreach (var entityType in modelBuilder.Model.GetEntityTypes()) - { - if (!typeof(IMultiTenantEntity).IsAssignableFrom(entityType.ClrType)) - { - continue; - } - - var methodInfo = typeof(TenantAwareDbContext) - .GetMethod(nameof(SetTenantFilter), BindingFlags.Instance | BindingFlags.NonPublic)! - .MakeGenericMethod(entityType.ClrType); - - methodInfo.Invoke(this, new object[] { modelBuilder }); - } - } - - /// - /// 为具体实体设置租户过滤器。 - /// - /// 实体类型。 - /// 模型构建器。 - private void SetTenantFilter(ModelBuilder modelBuilder) - where TEntity : class, IMultiTenantEntity - { - modelBuilder.Entity().HasQueryFilter(entity => entity.TenantId == CurrentTenantId); - } - - /// - /// 为新增实体填充租户 ID。 - /// - private void ApplyTenantMetadata() - { - var tenantId = CurrentTenantId; - - foreach (var entry in ChangeTracker.Entries()) - { - if (entry.State == EntityState.Added && entry.Entity.TenantId == 0 && tenantId != 0) - { - entry.Entity.TenantId = tenantId; - } - } - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs index 0339ac7..4526454 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs @@ -6,7 +6,6 @@ using TakeoutSaaS.Domain.SystemParameters.Entities; using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence; @@ -15,10 +14,9 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence; /// public sealed class DictionaryDbContext( DbContextOptions options, - ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, IIdGenerator? idGenerator = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) + : AppDbContext(options, currentUserAccessor, idGenerator) { /// /// 字典分组集合。 @@ -71,7 +69,6 @@ public sealed class DictionaryDbContext( ConfigureImportLog(modelBuilder.Entity()); ConfigureCacheInvalidationLog(modelBuilder.Entity()); ConfigureSystemParameter(modelBuilder.Entity()); - ApplyTenantQueryFilters(modelBuilder); } /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs index 774c649..cc01688 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs @@ -2,7 +2,6 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence; @@ -24,12 +23,10 @@ internal sealed class DictionaryDesignTimeDbContextFactory /// 创建设计时的 DictionaryDbContext。 /// /// 上下文配置。 - /// 租户提供器。 /// 当前用户访问器。 /// DictionaryDbContext 实例。 protected override DictionaryDbContext CreateContext( DbContextOptions options, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor) - => new(options, tenantProvider, currentUserAccessor); + => new(options, currentUserAccessor); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs index 217f916..73d1e01 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs @@ -24,14 +24,12 @@ public static class ServiceCollectionExtensions /// /// 服务集合。 /// 配置源。 - /// 是否启用小程序相关依赖(如微信登录)。 /// 是否启用后台账号初始化。 /// 服务集合。 /// 配置缺失时抛出。 public static IServiceCollection AddIdentityInfrastructure( this IServiceCollection services, IConfiguration configuration, - bool enableMiniFeatures = false, bool enableAdminSeed = false) { services.AddDatabaseInfrastructure(configuration); @@ -79,20 +77,6 @@ public static class ServiceCollectionExtensions services.AddOptions() .Bind(configuration.GetSection("Identity:AdminPasswordReset")); - if (enableMiniFeatures) - { - services.AddOptions() - .Bind(configuration.GetSection("Identity:WeChatMini")) - .ValidateDataAnnotations() - .ValidateOnStart(); - - services.AddHttpClient(client => - { - client.BaseAddress = new Uri("https://api.weixin.qq.com/"); - client.Timeout = TimeSpan.FromSeconds(10); - }); - } - if (enableAdminSeed) { services.AddOptions() diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/WeChatMiniOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/WeChatMiniOptions.cs deleted file mode 100644 index 0dee3be..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/WeChatMiniOptions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace TakeoutSaaS.Infrastructure.Identity.Options; - -/// -/// 微信小程序配置选项。 -/// -public sealed class WeChatMiniOptions -{ - /// - /// 微信小程序 AppId。 - /// - [Required] - public string AppId { get; set; } = string.Empty; - - /// - /// 微信小程序 AppSecret。 - /// - [Required] - public string Secret { get; set; } = string.Empty; -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs index ef0681f..a611c8f 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs @@ -5,8 +5,8 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Npgsql; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Infrastructure.Identity.Options; -using TakeoutSaaS.Shared.Abstractions.Tenancy; using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser; using DomainPermission = TakeoutSaaS.Domain.Identity.Entities.Permission; using DomainRole = TakeoutSaaS.Domain.Identity.Entities.Role; @@ -34,7 +34,6 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger var context = scope.ServiceProvider.GetRequiredService(); var options = scope.ServiceProvider.GetRequiredService>().Value; var passwordHasher = scope.ServiceProvider.GetRequiredService>(); - var tenantContextAccessor = scope.ServiceProvider.GetRequiredService(); // 2. 校验功能开关 if (!options.Enabled) @@ -58,10 +57,14 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger // 6. 逐个账号处理 foreach (var userOptions in options.Users) { - // 6.1 进入租户作用域 - using var tenantScope = EnterTenantScope(tenantContextAccessor, userOptions.TenantId); - // 6.2 查询账号并收集配置 - var user = await context.IdentityUsers.FirstOrDefaultAsync(x => x.Account == userOptions.Account, cancellationToken); + // 6.1 解析 Portal 与租户标识(TenantId=0 视为平台管理端) + var portal = userOptions.TenantId <= 0 ? PortalType.Admin : PortalType.Tenant; + var tenantId = portal == PortalType.Admin ? (long?)null : userOptions.TenantId; + + // 6.2 (空行后) 查询账号并收集配置 + var user = await context.IdentityUsers.FirstOrDefaultAsync( + x => x.Portal == portal && x.TenantId == tenantId && x.Account == userOptions.Account, + cancellationToken); var roles = NormalizeValues(userOptions.Roles); var permissions = NormalizeValues(userOptions.Permissions); @@ -71,9 +74,10 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger user = new DomainIdentityUser { Id = 0, + Portal = portal, Account = userOptions.Account, DisplayName = userOptions.DisplayName, - TenantId = userOptions.TenantId, + TenantId = tenantId, MerchantId = userOptions.MerchantId, Avatar = null }; @@ -84,8 +88,9 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger else { // 6.4 更新既有账号 + user.Portal = portal; user.DisplayName = userOptions.DisplayName; - user.TenantId = userOptions.TenantId; + user.TenantId = tenantId; user.MerchantId = userOptions.MerchantId; user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password); logger.LogInformation("已更新后台账号 {Account}", user.Account); @@ -93,7 +98,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger // 6.5 确保角色存在 var existingRoles = await context.Roles - .Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code)) + .Where(r => r.Portal == portal && r.TenantId == tenantId && roles.Contains(r.Code)) .ToListAsync(cancellationToken); var existingRoleCodes = existingRoles.Select(r => r.Code).ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var code in roles) @@ -105,7 +110,8 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger context.Roles.Add(new DomainRole { - TenantId = userOptions.TenantId, + Portal = portal, + TenantId = tenantId, Code = code, Name = code, Description = $"Seed role {code}" @@ -116,7 +122,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger var existingPermissions = await context.Permissions .IgnoreQueryFilters() .AsNoTracking() - .Where(p => permissions.Contains(p.Code)) + .Where(p => p.Portal == portal && permissions.Contains(p.Code)) .ToListAsync(cancellationToken); var existingPermissionCodes = existingPermissions .Select(p => p.Code) @@ -134,13 +140,13 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger // 6.8 重新加载角色/权限以获取 Id var roleEntities = await context.Roles - .Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code)) + .Where(r => r.Portal == portal && r.TenantId == tenantId && roles.Contains(r.Code)) .ToListAsync(cancellationToken); var permissionEntities = existingPermissions; // 6.9 重置用户角色 var existingUserRoles = await context.UserRoles - .Where(ur => ur.TenantId == userOptions.TenantId && ur.UserId == user.Id) + .Where(ur => ur.Portal == portal && ur.TenantId == tenantId && ur.UserId == user.Id) .ToListAsync(cancellationToken); context.UserRoles.RemoveRange(existingUserRoles); await context.SaveChangesAsync(cancellationToken); @@ -151,7 +157,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger try { var alreadyExists = await context.UserRoles.AnyAsync( - ur => ur.TenantId == userOptions.TenantId && ur.UserId == user.Id && ur.RoleId == roleId, + ur => ur.Portal == portal && ur.TenantId == tenantId && ur.UserId == user.Id && ur.RoleId == roleId, cancellationToken); if (alreadyExists) { @@ -160,7 +166,8 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger await context.UserRoles.AddAsync(new DomainUserRole { - TenantId = userOptions.TenantId, + Portal = portal, + TenantId = tenantId, UserId = user.Id, RoleId = roleId }, cancellationToken); @@ -178,7 +185,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger { var permissionIds = permissionEntities.Select(p => p.Id).Distinct().ToArray(); var existingRolePermissions = await context.RolePermissions - .Where(rp => rp.TenantId == userOptions.TenantId && roleIds.Contains(rp.RoleId)) + .Where(rp => rp.Portal == portal && rp.TenantId == tenantId && roleIds.Contains(rp.RoleId)) .ToListAsync(cancellationToken); context.RolePermissions.RemoveRange(existingRolePermissions); await context.SaveChangesAsync(cancellationToken); @@ -192,7 +199,8 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger try { var exists = await context.RolePermissions.AnyAsync( - rp => rp.TenantId == userOptions.TenantId + rp => rp.Portal == portal + && rp.TenantId == tenantId && rp.RoleId == roleId && rp.PermissionId == permissionId, cancellationToken); @@ -204,7 +212,8 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger // 6.10 绑定角色与权限 await context.RolePermissions.AddAsync(new DomainRolePermission { - TenantId = userOptions.TenantId, + Portal = portal, + TenantId = tenantId, RoleId = roleId, PermissionId = permissionId }, cancellationToken); @@ -324,17 +333,4 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger .Where(v => !string.IsNullOrWhiteSpace(v)) .Select(v => v.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase)]; - - private static IDisposable EnterTenantScope(ITenantContextAccessor accessor, long tenantId) - { - var previous = accessor.Current; - accessor.Current = new TenantContext(tenantId, null, "admin-seed"); - return new Scope(() => accessor.Current = previous); - } - - private sealed class Scope(Action disposeAction) : IDisposable - { - private readonly Action _disposeAction = disposeAction; - public void Dispose() => _disposeAction(); - } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs index e786235..5107789 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs @@ -6,19 +6,17 @@ using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// -/// 身份认证 DbContext,带多租户过滤与审计字段处理。 +/// 身份认证 DbContext。 /// public sealed class IdentityDbContext( DbContextOptions options, - ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, IIdGenerator? idGenerator = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) + : AppDbContext(options, currentUserAccessor, idGenerator) { /// /// 管理后台用户集合。 @@ -83,7 +81,6 @@ public sealed class IdentityDbContext( ConfigureMenuDefinition(modelBuilder.Entity()); modelBuilder.AddOutboxMessageEntity(); modelBuilder.AddOutboxStateEntity(); - ApplyTenantQueryFilters(modelBuilder); } /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs index 926cba6..de46937 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs @@ -2,7 +2,6 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; @@ -24,12 +23,10 @@ internal sealed class IdentityDesignTimeDbContextFactory /// 创建设计时的 IdentityDbContext。 /// /// DbContext 配置。 - /// 租户提供器。 /// 当前用户访问器。 /// IdentityDbContext 实例。 protected override IdentityDbContext CreateContext( DbContextOptions options, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor) - => new(options, tenantProvider, currentUserAccessor); + => new(options, currentUserAccessor); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs deleted file mode 100644 index 272ffc9..0000000 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Microsoft.Extensions.Options; -using System.Net.Http.Json; -using System.Text.Json.Serialization; -using TakeoutSaaS.Application.Identity.Abstractions; -using TakeoutSaaS.Infrastructure.Identity.Options; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Infrastructure.Identity.Services; - -/// -/// 微信 code2Session 实现 -/// -public sealed class WeChatAuthService(HttpClient httpClient, IOptions options) : IWeChatAuthService -{ - private readonly WeChatMiniOptions _options = options.Value; - - /// - /// 调用微信接口完成 code2Session。 - /// - /// 临时登录凭证 code。 - /// 取消标记。 - /// 微信会话信息。 - public async Task Code2SessionAsync(string code, CancellationToken cancellationToken = default) - { - // 1. 拼装请求地址 - var requestUri = $"sns/jscode2session?appid={Uri.EscapeDataString(_options.AppId)}&secret={Uri.EscapeDataString(_options.Secret)}&js_code={Uri.EscapeDataString(code)}&grant_type=authorization_code"; - using var response = await httpClient.GetAsync(requestUri, cancellationToken); - response.EnsureSuccessStatusCode(); - - // 2. 读取响应 - var payload = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); - if (payload == null) - { - throw new BusinessException(ErrorCodes.Unauthorized, "微信登录失败:响应为空"); - } - - // 3. 校验错误码 - if (payload.ErrorCode.HasValue && payload.ErrorCode.Value != 0) - { - var message = string.IsNullOrWhiteSpace(payload.ErrorMessage) - ? $"微信登录失败,错误码:{payload.ErrorCode}" - : payload.ErrorMessage; - throw new BusinessException(ErrorCodes.Unauthorized, message); - } - - // 4. 校验必要字段 - if (string.IsNullOrWhiteSpace(payload.OpenId) || string.IsNullOrWhiteSpace(payload.SessionKey)) - { - throw new BusinessException(ErrorCodes.Unauthorized, "微信登录失败:返回数据无效"); - } - - // 5. 组装会话信息 - return new WeChatSessionInfo - { - OpenId = payload.OpenId, - UnionId = payload.UnionId, - SessionKey = payload.SessionKey - }; - } - - private sealed class WeChatSessionResponse - { - [JsonPropertyName("openid")] - public string? OpenId { get; set; } - - [JsonPropertyName("unionid")] - public string? UnionId { get; set; } - - [JsonPropertyName("session_key")] - public string? SessionKey { get; set; } - - [JsonPropertyName("errcode")] - public int? ErrorCode { get; set; } - - [JsonPropertyName("errmsg")] - public string? ErrorMessage { get; set; } - } -} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs index a34444d..87909cd 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs @@ -6,7 +6,6 @@ using TakeoutSaaS.Domain.Tenants.Entities; using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Logs.Persistence; @@ -15,10 +14,9 @@ namespace TakeoutSaaS.Infrastructure.Logs.Persistence; /// public sealed class TakeoutLogsDbContext( DbContextOptions options, - ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, IIdGenerator? idGenerator = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) + : AppDbContext(options, currentUserAccessor, idGenerator) { /// /// 租户审计日志集合。 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDesignTimeDbContextFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDesignTimeDbContextFactory.cs index 5184dcf..c2f1864 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDesignTimeDbContextFactory.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDesignTimeDbContextFactory.cs @@ -2,7 +2,6 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Infrastructure.Logs.Persistence; @@ -24,12 +23,10 @@ internal sealed class TakeoutLogsDesignTimeDbContextFactory /// 创建日志库 DbContext。 /// /// 上下文选项。 - /// 租户提供器。 /// 当前用户访问器。 /// 日志库上下文实例。 protected override TakeoutLogsDbContext CreateContext( DbContextOptions options, - ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor) - => new(options, tenantProvider, currentUserAccessor); + => new(options, currentUserAccessor); } diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/Extensions/TenantServiceCollectionExtensions.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/Extensions/TenantServiceCollectionExtensions.cs deleted file mode 100644 index 72afde3..0000000 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/Extensions/TenantServiceCollectionExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Module.Tenancy.Extensions; - -/// -/// 多租户服务注册及中间件扩展。 -/// -public static class TenantServiceCollectionExtensions -{ - /// - /// 注册租户上下文、解析中间件及默认租户提供者。 - /// - public static IServiceCollection AddTenantResolution(this IServiceCollection services, IConfiguration configuration) - { - services.TryAddSingleton(); - services.TryAddScoped(); - - services.AddOptions() - .Bind(configuration.GetSection("Tenancy")) - .ValidateDataAnnotations(); - - return services; - } - - /// - /// 使用多租户解析中间件。 - /// - public static IApplicationBuilder UseTenantResolution(this IApplicationBuilder app) - => app.UseMiddleware(); -} diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj b/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj deleted file mode 100644 index fdb3727..0000000 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - net10.0 - enable - enable - - - - - - - - - - - diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantContextAccessor.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantContextAccessor.cs deleted file mode 100644 index ed3d78b..0000000 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantContextAccessor.cs +++ /dev/null @@ -1,35 +0,0 @@ -using TakeoutSaaS.Shared.Abstractions.Tenancy; -namespace TakeoutSaaS.Module.Tenancy; -/// -/// 基于 的租户上下文访问器,实现请求级别隔离。 -/// -public sealed class TenantContextAccessor : ITenantContextAccessor -{ - private static readonly AsyncLocal Holder = new(); - - // 当前请求的租户上下文访问入口 - /// - /// 获取或设置当前请求的租户上下文。 - /// - public TenantContext? Current - { - get => Holder.Value?.Context; - set - { - if (Holder.Value != null) - { - Holder.Value.Context = value; - } - else if (value != null) - { - Holder.Value = new TenantContextHolder { Context = value }; - } - } - } - - // 内部持有器用于绑定异步局部上下文 - private sealed class TenantContextHolder - { - public TenantContext? Context { get; set; } - } -} diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs deleted file mode 100644 index 9f6b1b9..0000000 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Module.Tenancy; - -/// -/// 默认租户提供者:基于租户上下文访问器暴露当前租户 ID。 -/// -/// -/// 初始化租户提供者。 -/// -/// 租户上下文访问器 -public sealed class TenantProvider(ITenantContextAccessor tenantContextAccessor) : ITenantProvider -{ - /// - public long GetCurrentTenantId() - => tenantContextAccessor.Current?.TenantId ?? 0; -} diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs deleted file mode 100644 index 46c9770..0000000 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionMiddleware.cs +++ /dev/null @@ -1,177 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Module.Tenancy; - -/// -/// 多租户解析中间件:支持 Header、域名与 Token Claim 的优先级解析。 -/// -/// -/// 初始化中间件。 -/// -public sealed class TenantResolutionMiddleware( - RequestDelegate next, - ILogger logger, - ITenantContextAccessor tenantContextAccessor, - IOptionsMonitor optionsMonitor) -{ - /// - /// 解析租户并将上下文注入请求。 - /// - public async Task InvokeAsync(HttpContext context) - { - var options = optionsMonitor.CurrentValue ?? new TenantResolutionOptions(); - if (ShouldSkip(context.Request.Path, options)) - { - await next(context); - return; - } - - var tenantContext = ResolveTenant(context, options); - tenantContextAccessor.Current = tenantContext; - context.Items[TenantConstants.HttpContextItemKey] = tenantContext; - - if (!tenantContext.IsResolved) - { - logger.LogDebug("未能解析租户:{Path}", context.Request.Path); - - if (options.ThrowIfUnresolved) - { - var response = ApiResponse.Error(ErrorCodes.BadRequest, "缺少租户标识"); - context.Response.StatusCode = StatusCodes.Status400BadRequest; - await context.Response.WriteAsJsonAsync(response, cancellationToken: context.RequestAborted); - tenantContextAccessor.Current = null; - context.Items.Remove(TenantConstants.HttpContextItemKey); - return; - } - } - - try - { - await next(context); - } - finally - { - tenantContextAccessor.Current = null; - context.Items.Remove(TenantConstants.HttpContextItemKey); - } - } - - private static bool ShouldSkip(PathString path, TenantResolutionOptions options) - { - if (!path.HasValue) - { - return false; - } - - var value = path.Value ?? string.Empty; - if (options.IgnoredPaths.Contains(value)) - { - return true; - } - - return options.IgnoredPaths.Any(ignore => - { - if (string.IsNullOrWhiteSpace(ignore)) - { - return false; - } - - var ignorePath = new PathString(ignore); - return path.StartsWithSegments(ignorePath); - }); - } - - private static TenantContext ResolveTenant(HttpContext context, TenantResolutionOptions options) - { - var request = context.Request; - - // 1. Header 中的租户 ID - if (!string.IsNullOrWhiteSpace(options.TenantIdHeaderName) && - request.Headers.TryGetValue(options.TenantIdHeaderName, out var tenantHeader) && - long.TryParse(tenantHeader.FirstOrDefault(), out var headerTenantId)) - { - return new TenantContext(headerTenantId, null, $"header:{options.TenantIdHeaderName}"); - } - - // 2. Header 中的租户编码 - if (!string.IsNullOrWhiteSpace(options.TenantCodeHeaderName) && - request.Headers.TryGetValue(options.TenantCodeHeaderName, out var codeHeader)) - { - var code = codeHeader.FirstOrDefault(); - if (TryResolveByCode(code, options, out var tenantFromCode)) - { - return new TenantContext(tenantFromCode, code, $"header:{options.TenantCodeHeaderName}"); - } - } - - // 3. Host 映射/子域名解析 - var host = request.Host.Host; - if (!string.IsNullOrWhiteSpace(host)) - { - if (options.DomainTenantMap.TryGetValue(host, out var tenantFromHost)) - { - return new TenantContext(tenantFromHost, null, $"host:{host}"); - } - - var codeFromHost = ResolveCodeFromHost(host, options.RootDomain); - if (TryResolveByCode(codeFromHost, options, out var tenantFromSubdomain)) - { - return new TenantContext(tenantFromSubdomain, codeFromHost, $"host:{host}"); - } - } - - // 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; - } - - private static bool TryResolveByCode(string? code, TenantResolutionOptions options, out long tenantId) - { - tenantId = 0; - if (string.IsNullOrWhiteSpace(code)) - { - return false; - } - - return options.CodeTenantMap.TryGetValue(code, out tenantId); - } - - private static string? ResolveCodeFromHost(string host, string? rootDomain) - { - if (string.IsNullOrWhiteSpace(rootDomain)) - { - return null; - } - - var normalizedRoot = rootDomain.TrimStart('.'); - if (!host.EndsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - var suffixLength = normalizedRoot.Length; - if (host.Length <= suffixLength) - { - return null; - } - - var withoutRoot = host[..(host.Length - suffixLength)]; - if (withoutRoot.EndsWith('.')) - { - withoutRoot = withoutRoot[..^1]; - } - - var segments = withoutRoot.Split('.', StringSplitOptions.RemoveEmptyEntries); - return segments.Length == 0 ? null : segments[0]; - } -} diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionOptions.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionOptions.cs deleted file mode 100644 index e43afa3..0000000 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantResolutionOptions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.ObjectModel; - -namespace TakeoutSaaS.Module.Tenancy; - -/// -/// 多租户解析配置项。 -/// -public sealed class TenantResolutionOptions -{ - /// - /// 通过 Header 解析租户 ID 时使用的头名称,默认 X-Tenant-Id。 - /// - public string TenantIdHeaderName { get; set; } = "X-Tenant-Id"; - - /// - /// 通过 Header 解析租户编码时使用的头名称,默认 X-Tenant-Code。 - /// - public string TenantCodeHeaderName { get; set; } = "X-Tenant-Code"; - - /// - /// 明确指定 host 与租户 ID 对应关系的映射表(精确匹配)。 - /// - public IDictionary DomainTenantMap { get; set; } - = new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// 租户编码到租户 ID 的映射表,用于 header 或子域名解析。 - /// - public IDictionary CodeTenantMap { get; set; } - = new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// 根域(不含子域),用于形如 {tenant}.rootDomain 的场景,例如 admin.takeoutsaas.com。 - /// - public string? RootDomain { get; set; } - - /// - /// 需要跳过租户解析的路径集合(如健康检查),默认仅包含 /health。 - /// - public ISet IgnoredPaths { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) { "/health" }; - - /// - /// 若为 true,当无法解析租户时立即返回 400;否则交由上层自行判定。 - /// - public bool ThrowIfUnresolved { get; set; } - - /// - /// 对外只读视图,便于审计日志输出。 - /// - public IReadOnlyDictionary DomainMappings => new ReadOnlyDictionary(DomainTenantMap); - - /// - /// 对外只读的编码映射。 - /// - public IReadOnlyDictionary CodeMappings => new ReadOnlyDictionary(CodeTenantMap); -} diff --git a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetAnnouncementByIdQueryHandlerTests.cs b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetAnnouncementByIdQueryHandlerTests.cs index 205bc50..a6cdfe8 100644 --- a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetAnnouncementByIdQueryHandlerTests.cs +++ b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetAnnouncementByIdQueryHandlerTests.cs @@ -5,118 +5,50 @@ using TakeoutSaaS.Application.App.Tenants.Queries; using TakeoutSaaS.Application.Tests.TestUtilities; using TakeoutSaaS.Domain.Tenants.Entities; using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Tests.App.Tenants.Handlers; +/// +/// 单元测试。 +/// public sealed class GetAnnouncementByIdQueryHandlerTests { [Fact] public async Task GivenAnnouncementMissing_WhenHandle_ThenReturnsNull() { - // Arrange - var tenantProvider = new Mock(); - tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(99); - + // 1. 准备 var announcementRepository = new Mock(); announcementRepository - .Setup(x => x.FindByIdInScopeAsync(99, 500, It.IsAny())) + .Setup(x => x.FindByIdAsync(99, 500, It.IsAny())) .ReturnsAsync((TenantAnnouncement?)null); - var readRepository = new Mock(); + // 2. (空行后) 执行 + var handler = new GetAnnouncementByIdQueryHandler(announcementRepository.Object); + var result = await handler.Handle(new GetAnnouncementByIdQuery { TenantId = 99, AnnouncementId = 500 }, CancellationToken.None); - var handler = new GetAnnouncementByIdQueryHandler( - announcementRepository.Object, - readRepository.Object, - tenantProvider.Object); - - // Act - var result = await handler.Handle(new GetAnnouncementByIdQuery { AnnouncementId = 500 }, CancellationToken.None); - - // Assert + // 3. (空行后) 断言 result.Should().BeNull(); } [Fact] - public async Task GivenTargetNotMatched_WhenHandle_ThenReturnsNullAndSkipsReadLookup() + public async Task GivenAnnouncementExists_WhenHandle_ThenReturnsDtoWithoutReadState() { - // Arrange - var tenantProvider = new Mock(); - tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(100); - - var currentUserAccessor = new Mock(); - currentUserAccessor.SetupGet(x => x.UserId).Returns(123); - currentUserAccessor.SetupGet(x => x.IsAuthenticated).Returns(true); - - var announcement = AnnouncementTestData.CreateAnnouncement(1, 100, 1, DateTime.UtcNow); - announcement.TargetType = "SPECIFIC_USERS"; - announcement.TargetParameters = "{\"userIds\":[999]}"; - - var announcementRepository = new Mock(); - announcementRepository - .Setup(x => x.FindByIdInScopeAsync(100, 1, It.IsAny())) - .ReturnsAsync(announcement); - - var readRepository = new Mock(); - - var handler = new GetAnnouncementByIdQueryHandler( - announcementRepository.Object, - readRepository.Object, - tenantProvider.Object, - currentUserAccessor.Object); - - // Act - var result = await handler.Handle(new GetAnnouncementByIdQuery { AnnouncementId = 1 }, CancellationToken.None); - - // Assert - result.Should().BeNull(); - readRepository.Verify(x => x.GetByAnnouncementAsync( - It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny()), Times.Never); - } - - [Fact] - public async Task GivenUserReadRecord_WhenHandle_ThenReturnsDtoWithReadState() - { - // Arrange - var tenantProvider = new Mock(); - tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(200); - - var currentUserAccessor = new Mock(); - currentUserAccessor.SetupGet(x => x.UserId).Returns(321); - currentUserAccessor.SetupGet(x => x.IsAuthenticated).Returns(true); - + // 1. 准备 var announcement = AnnouncementTestData.CreateAnnouncement(10, 200, 1, DateTime.UtcNow); - var readAt = DateTime.UtcNow.AddMinutes(-3); - var announcementRepository = new Mock(); announcementRepository - .Setup(x => x.FindByIdInScopeAsync(200, 10, It.IsAny())) + .Setup(x => x.FindByIdAsync(200, 10, It.IsAny())) .ReturnsAsync(announcement); - var readRepository = new Mock(); - readRepository - .Setup(x => x.GetByAnnouncementAsync(200, It.IsAny>(), 321, It.IsAny())) - .ReturnsAsync(new List - { - new() { AnnouncementId = 10, TenantId = 200, UserId = 321, ReadAt = readAt } - }); + // 2. (空行后) 执行 + var handler = new GetAnnouncementByIdQueryHandler(announcementRepository.Object); + var result = await handler.Handle(new GetAnnouncementByIdQuery { TenantId = 200, AnnouncementId = 10 }, CancellationToken.None); - var handler = new GetAnnouncementByIdQueryHandler( - announcementRepository.Object, - readRepository.Object, - tenantProvider.Object, - currentUserAccessor.Object); - - // Act - var result = await handler.Handle(new GetAnnouncementByIdQuery { AnnouncementId = 10 }, CancellationToken.None); - - // Assert + // 3. (空行后) 断言 result.Should().NotBeNull(); - result!.IsRead.Should().BeTrue(); - result.ReadAt.Should().BeCloseTo(readAt, TimeSpan.FromSeconds(1)); + result!.Id.Should().Be(announcement.Id); + result.TenantId.Should().Be(announcement.TenantId); + result.IsRead.Should().BeFalse(); + result.ReadAt.Should().BeNull(); } } diff --git a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandlerTests.cs b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandlerTests.cs index a58c775..7fbf0b1 100644 --- a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandlerTests.cs +++ b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandlerTests.cs @@ -4,24 +4,18 @@ using TakeoutSaaS.Application.App.Tenants.Handlers; using TakeoutSaaS.Application.App.Tenants.Queries; using TakeoutSaaS.Application.Tests.TestUtilities; using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Tests.App.Tenants.Handlers; +/// +/// 单元测试。 +/// public sealed class GetTenantsAnnouncementsQueryHandlerTests { [Fact] - public async Task GivenQuery_WhenHandle_ThenUsesTenantProviderAndOrdersAndPaginates() + public async Task GivenQuery_WhenHandle_ThenOrdersAndPaginates() { - // Arrange - var tenantProvider = new Mock(); - tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(42); - - var currentUserAccessor = new Mock(); - currentUserAccessor.SetupGet(x => x.UserId).Returns(0); - currentUserAccessor.SetupGet(x => x.IsAuthenticated).Returns(false); - + // 1. 准备 var announcements = new List { AnnouncementTestData.CreateAnnouncement(1, 42, priority: 1, effectiveFrom: DateTime.UtcNow.AddDays(-1)), @@ -30,12 +24,11 @@ public sealed class GetTenantsAnnouncementsQueryHandlerTests AnnouncementTestData.CreateAnnouncement(4, 42, priority: 0, effectiveFrom: DateTime.UtcNow) }; - // 模拟数据库端排序:按 priority DESC, effectiveFrom DESC + // 2. (空行后) 模拟数据库端排序:按 priority DESC, effectiveFrom DESC var sortedAnnouncements = announcements .OrderByDescending(x => x.Priority) .ThenByDescending(x => x.EffectiveFrom) .ToList(); - var announcementRepository = new Mock(); announcementRepository .Setup(x => x.SearchAsync( @@ -52,34 +45,19 @@ public sealed class GetTenantsAnnouncementsQueryHandlerTests It.IsAny())) .ReturnsAsync(sortedAnnouncements); - var announcementReadRepository = new Mock(); - announcementReadRepository - .Setup(x => x.GetByAnnouncementAsync( - It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(Array.Empty()); - - var handler = new GetTenantsAnnouncementsQueryHandler( - announcementRepository.Object, - announcementReadRepository.Object, - tenantProvider.Object, - currentUserAccessor.Object); - + // 3. (空行后) 执行 + var handler = new GetTenantsAnnouncementsQueryHandler(announcementRepository.Object); var query = new GetTenantsAnnouncementsQuery { - TenantId = 999, + TenantId = 42, Page = 2, PageSize = 2 }; - - // Act var result = await handler.Handle(query, CancellationToken.None); - // Assert + // 4. (空行后) 断言 announcementRepository.Verify(x => x.SearchAsync( - 42, + query.TenantId, query.Keyword, query.Status, query.AnnouncementType, @@ -100,22 +78,16 @@ public sealed class GetTenantsAnnouncementsQueryHandlerTests [Fact] public async Task GivenOnlyEffective_WhenHandle_ThenFiltersScheduledPublish() { - // Arrange - var tenantProvider = new Mock(); - tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(42); - + // 1. 准备 var announcement1 = AnnouncementTestData.CreateAnnouncement(1, 42, priority: 1, effectiveFrom: DateTime.UtcNow.AddDays(-1)); announcement1.ScheduledPublishAt = DateTime.UtcNow.AddMinutes(-10); - var announcement2 = AnnouncementTestData.CreateAnnouncement(2, 42, priority: 1, effectiveFrom: DateTime.UtcNow.AddDays(-1)); announcement2.ScheduledPublishAt = DateTime.UtcNow.AddMinutes(30); - var announcements = new List { announcement1, announcement2 }; - var announcementRepository = new Mock(); announcementRepository .Setup(x => x.SearchAsync( @@ -132,31 +104,18 @@ public sealed class GetTenantsAnnouncementsQueryHandlerTests It.IsAny())) .ReturnsAsync(announcements); - var announcementReadRepository = new Mock(); - announcementReadRepository - .Setup(x => x.GetByAnnouncementAsync( - It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(Array.Empty()); - - var handler = new GetTenantsAnnouncementsQueryHandler( - announcementRepository.Object, - announcementReadRepository.Object, - tenantProvider.Object); - + // 2. (空行后) 执行 + var handler = new GetTenantsAnnouncementsQueryHandler(announcementRepository.Object); var query = new GetTenantsAnnouncementsQuery { + TenantId = 42, OnlyEffective = true, Page = 1, PageSize = 10 }; - - // Act var result = await handler.Handle(query, CancellationToken.None); - // Assert + // 3. (空行后) 断言 result.Items.Should().ContainSingle(); result.Items[0].Id.Should().Be(1); } diff --git a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandlerTests.cs b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandlerTests.cs deleted file mode 100644 index 81f60f7..0000000 --- a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandlerTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using FluentAssertions; -using Moq; -using TakeoutSaaS.Application.App.Tenants.Handlers; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Application.Tests.TestUtilities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.Tests.App.Tenants.Handlers; - -public sealed class GetUnreadAnnouncementsQueryHandlerTests -{ - [Fact] - public async Task GivenUnreadAnnouncements_WhenHandle_ThenUsesTenantProviderAndPaginates() - { - // Arrange - var tenantProvider = new Mock(); - tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(55); - - var currentUserAccessor = new Mock(); - currentUserAccessor.SetupGet(x => x.UserId).Returns(0); - currentUserAccessor.SetupGet(x => x.IsAuthenticated).Returns(false); - - var announcements = new List - { - AnnouncementTestData.CreateAnnouncement(1, 55, priority: 1, effectiveFrom: DateTime.UtcNow.AddDays(-1), status: AnnouncementStatus.Published), - AnnouncementTestData.CreateAnnouncement(2, 55, priority: 3, effectiveFrom: DateTime.UtcNow.AddDays(-2), status: AnnouncementStatus.Published), - AnnouncementTestData.CreateAnnouncement(3, 55, priority: 2, effectiveFrom: DateTime.UtcNow, status: AnnouncementStatus.Published) - }; - - var announcementRepository = new Mock(); - announcementRepository - .Setup(x => x.SearchUnreadAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(announcements); - - var handler = new GetUnreadAnnouncementsQueryHandler( - announcementRepository.Object, - tenantProvider.Object, - currentUserAccessor.Object); - - var query = new GetUnreadAnnouncementsQuery - { - Page = 1, - PageSize = 2 - }; - - // Act - var result = await handler.Handle(query, CancellationToken.None); - - // Assert - announcementRepository.Verify(x => x.SearchUnreadAsync( - 55, - null, - AnnouncementStatus.Published, - true, - It.IsAny(), - It.IsAny()), Times.Once); - - result.Items.Select(x => x.Id).Should().Equal(2, 3); - result.TotalCount.Should().Be(3); - result.Page.Should().Be(1); - result.PageSize.Should().Be(2); - } -} diff --git a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs index 29c793a..706c9c1 100644 --- a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs +++ b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs @@ -13,12 +13,15 @@ using TakeoutSaaS.Shared.Abstractions.Exceptions; namespace TakeoutSaaS.Integration.Tests.App.Tenants; +/// +/// 公告工作流集成测试。 +/// public sealed class AnnouncementWorkflowTests { [Fact] public async Task GivenDraftAnnouncement_WhenPublish_ThenStatusIsPublishedAndActive() { - // Arrange + // 1. 准备 using var database = new SqliteTestDatabase(); using var context = database.CreateContext(tenantId: 100, userId: 11); @@ -28,18 +31,18 @@ public sealed class AnnouncementWorkflowTests context.ChangeTracker.Clear(); var repository = new EfTenantAnnouncementRepository(context); - var tenantProvider = new TestTenantProvider(100); var eventPublisher = new Mock(); - var handler = new PublishAnnouncementCommandHandler(repository, tenantProvider, eventPublisher.Object); + var handler = new PublishAnnouncementCommandHandler(repository, eventPublisher.Object); - // Act + // 2. (空行后) 执行 var result = await handler.Handle(new PublishAnnouncementCommand { + TenantId = 100, AnnouncementId = announcement.Id, RowVersion = announcement.RowVersion }, CancellationToken.None); - // Assert + // 3. (空行后) 断言 result.Should().NotBeNull(); result!.Status.Should().Be(AnnouncementStatus.Published); result.IsActive.Should().BeTrue(); @@ -53,7 +56,7 @@ public sealed class AnnouncementWorkflowTests [Fact] public async Task GivenPublishedAnnouncement_WhenRevoke_ThenStatusIsRevokedAndInactive() { - // Arrange + // 1. 准备 using var database = new SqliteTestDatabase(); using var context = database.CreateContext(tenantId: 200, userId: 11); @@ -64,18 +67,18 @@ public sealed class AnnouncementWorkflowTests context.ChangeTracker.Clear(); var repository = new EfTenantAnnouncementRepository(context); - var tenantProvider = new TestTenantProvider(200); var eventPublisher = new Mock(); - var handler = new RevokeAnnouncementCommandHandler(repository, tenantProvider, eventPublisher.Object); + var handler = new RevokeAnnouncementCommandHandler(repository, eventPublisher.Object); - // Act + // 2. (空行后) 执行 var result = await handler.Handle(new RevokeAnnouncementCommand { + TenantId = 200, AnnouncementId = announcement.Id, RowVersion = announcement.RowVersion }, CancellationToken.None); - // Assert + // 3. (空行后) 断言 result.Should().NotBeNull(); result!.Status.Should().Be(AnnouncementStatus.Revoked); result.IsActive.Should().BeFalse(); @@ -89,7 +92,7 @@ public sealed class AnnouncementWorkflowTests [Fact] public async Task GivenRevokedAnnouncement_WhenPublish_ThenRepublishAndClearRevokedAt() { - // Arrange + // 1. 准备 using var database = new SqliteTestDatabase(); using var context = database.CreateContext(tenantId: 300, userId: 11); @@ -101,18 +104,18 @@ public sealed class AnnouncementWorkflowTests context.ChangeTracker.Clear(); var repository = new EfTenantAnnouncementRepository(context); - var tenantProvider = new TestTenantProvider(300); var eventPublisher = new Mock(); - var handler = new PublishAnnouncementCommandHandler(repository, tenantProvider, eventPublisher.Object); + var handler = new PublishAnnouncementCommandHandler(repository, eventPublisher.Object); - // Act + // 2. (空行后) 执行 var result = await handler.Handle(new PublishAnnouncementCommand { + TenantId = 300, AnnouncementId = announcement.Id, RowVersion = announcement.RowVersion }, CancellationToken.None); - // Assert + // 3. (空行后) 断言 result.Should().NotBeNull(); result!.Status.Should().Be(AnnouncementStatus.Published); result.IsActive.Should().BeTrue(); diff --git a/tests/TakeoutSaaS.Integration.Tests/Fixtures/DictionarySqliteTestDatabase.cs b/tests/TakeoutSaaS.Integration.Tests/Fixtures/DictionarySqliteTestDatabase.cs index ccc9201..176c414 100644 --- a/tests/TakeoutSaaS.Integration.Tests/Fixtures/DictionarySqliteTestDatabase.cs +++ b/tests/TakeoutSaaS.Integration.Tests/Fixtures/DictionarySqliteTestDatabase.cs @@ -4,9 +4,13 @@ using TakeoutSaaS.Infrastructure.Dictionary.Persistence; namespace TakeoutSaaS.Integration.Tests.Fixtures; +/// +/// 集成测试用 SQLite 内存数据库(字典库)。 +/// public sealed class DictionarySqliteTestDatabase : IDisposable { private readonly SqliteConnection _connection; + private readonly TestIdGenerator _idGenerator = new(); private bool _initialized; public DictionarySqliteTestDatabase() @@ -24,10 +28,14 @@ public sealed class DictionarySqliteTestDatabase : IDisposable public DictionaryDbContext CreateContext(long tenantId, long userId = 0) { EnsureCreated(); + // 1. AdminApi 不使用租户上下文;tenantId 参数仅用于兼容测试调用方签名 + _ = tenantId; + + // 2. (空行后) 按需注入当前用户与 ID 生成器 return new DictionaryDbContext( Options, - new TestTenantProvider(tenantId), - userId == 0 ? null : new TestCurrentUserAccessor(userId)); + userId == 0 ? null : new TestCurrentUserAccessor(userId), + _idGenerator); } public void EnsureCreated() @@ -37,7 +45,8 @@ public sealed class DictionarySqliteTestDatabase : IDisposable return; } - using var context = new DictionaryDbContext(Options, new TestTenantProvider(1)); + // 1. 创建并初始化数据库结构 + using var context = new DictionaryDbContext(Options, idGenerator: _idGenerator); context.Database.EnsureCreated(); _initialized = true; } diff --git a/tests/TakeoutSaaS.Integration.Tests/Fixtures/SqliteTestDatabase.cs b/tests/TakeoutSaaS.Integration.Tests/Fixtures/SqliteTestDatabase.cs index 2b811ee..a66a5da 100644 --- a/tests/TakeoutSaaS.Integration.Tests/Fixtures/SqliteTestDatabase.cs +++ b/tests/TakeoutSaaS.Integration.Tests/Fixtures/SqliteTestDatabase.cs @@ -4,9 +4,13 @@ using TakeoutSaaS.Infrastructure.App.Persistence; namespace TakeoutSaaS.Integration.Tests.Fixtures; +/// +/// 集成测试用 SQLite 内存数据库(业务主库)。 +/// public sealed class SqliteTestDatabase : IDisposable { private readonly SqliteConnection _connection; + private readonly TestIdGenerator _idGenerator = new(); private bool _initialized; public SqliteTestDatabase() @@ -24,7 +28,14 @@ public sealed class SqliteTestDatabase : IDisposable public TakeoutAdminDbContext CreateContext(long tenantId, long userId = 0) { EnsureCreated(); - return new TakeoutAdminDbContext(Options, new TestTenantProvider(tenantId), new TestCurrentUserAccessor(userId)); + // 1. AdminApi 不使用租户上下文;tenantId 参数仅用于兼容测试调用方签名 + _ = tenantId; + + // 2. (空行后) 按需注入当前用户与 ID 生成器 + return new TakeoutAdminDbContext( + Options, + userId == 0 ? null : new TestCurrentUserAccessor(userId), + _idGenerator); } public void EnsureCreated() @@ -34,7 +45,8 @@ public sealed class SqliteTestDatabase : IDisposable return; } - using var context = new TakeoutAdminDbContext(Options, new TestTenantProvider(1)); + // 1. 创建并初始化数据库结构 + using var context = new TakeoutAdminDbContext(Options, idGenerator: _idGenerator); context.Database.EnsureCreated(); _initialized = true; } diff --git a/tests/TakeoutSaaS.Integration.Tests/Fixtures/TestIdGenerator.cs b/tests/TakeoutSaaS.Integration.Tests/Fixtures/TestIdGenerator.cs new file mode 100644 index 0000000..5c61f3a --- /dev/null +++ b/tests/TakeoutSaaS.Integration.Tests/Fixtures/TestIdGenerator.cs @@ -0,0 +1,20 @@ +using System.Threading; +using TakeoutSaaS.Shared.Abstractions.Ids; + +namespace TakeoutSaaS.Integration.Tests.Fixtures; + +/// +/// 集成测试用雪花 ID 生成器(递增模拟)。 +/// +public sealed class TestIdGenerator : IIdGenerator +{ + private long _current; + + /// + /// 生成下一个 ID。 + /// + /// 递增的 long ID。 + public long NextId() + => Interlocked.Increment(ref _current); +} + diff --git a/tests/TakeoutSaaS.Integration.Tests/Performance/AnnouncementQueryPerformanceTests.cs b/tests/TakeoutSaaS.Integration.Tests/Performance/AnnouncementQueryPerformanceTests.cs index bc742af..290991a 100644 --- a/tests/TakeoutSaaS.Integration.Tests/Performance/AnnouncementQueryPerformanceTests.cs +++ b/tests/TakeoutSaaS.Integration.Tests/Performance/AnnouncementQueryPerformanceTests.cs @@ -9,15 +9,17 @@ using TakeoutSaaS.Integration.Tests.Fixtures; namespace TakeoutSaaS.Integration.Tests.Performance; +/// +/// 公告查询性能相关测试。 +/// public sealed class AnnouncementQueryPerformanceTests { [Fact] public async Task GivenLargeDataset_WhenQueryingAnnouncements_ThenCompletesWithinThreshold() { - // Arrange + // 1. 准备 using var database = new SqliteTestDatabase(); using var context = database.CreateContext(tenantId: 900); - var announcements = new List(); for (var i = 0; i < 1000; i++) { @@ -45,30 +47,21 @@ public sealed class AnnouncementQueryPerformanceTests context.TenantAnnouncements.AddRange(announcements); await context.SaveChangesAsync(); context.ChangeTracker.Clear(); - var announcementRepository = new EfTenantAnnouncementRepository(context); - var readRepository = new EfTenantAnnouncementReadRepository(context); - var tenantProvider = new TestTenantProvider(900); - var handler = new GetTenantsAnnouncementsQueryHandler( - announcementRepository, - readRepository, - tenantProvider); - + var handler = new GetTenantsAnnouncementsQueryHandler(announcementRepository); var query = new GetTenantsAnnouncementsQuery { + TenantId = 900, Page = 1, PageSize = 50 }; - // Act + // 2. (空行后) 执行 var stopwatch = Stopwatch.StartNew(); var result = await handler.Handle(query, CancellationToken.None); stopwatch.Stop(); - // Assert - // 注意:由于性能优化,TotalCount 不再是精确的全局总数, - // 而是基于估算查询限制(page * size * 3)过滤后的结果数 - // 这是性能优化的权衡:牺牲精确性换取性能 + // 3. (空行后) 断言:TotalCount 为估算口径(page * size * 3)过滤后的数量 result.Items.Count.Should().Be(50); // 请求的页大小 result.TotalCount.Should().BeLessThanOrEqualTo(150); // 最多是 estimatedLimit result.TotalCount.Should().BeGreaterThan(0); // 至少有一些结果