From cc017b6495466fee3f968b3894fdea9ecd40af98 Mon Sep 17 00:00:00 2001 From: nanxun Date: Thu, 30 Apr 2026 21:08:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ConnectorService/ConnectorService.csproj | 20 ++ ConnectorService/ConnectorService.http | 6 + ConnectorService/Consumers/MessageConsumer.cs | 24 ++ ConnectorService/Dtos/MessageHubResponse.cs | 81 +++++ ConnectorService/Hubs/ChatHub.cs | 53 +++ ConnectorService/ModuleInit.cs | 19 ++ ConnectorService/Program.cs | 42 +++ .../Properties/launchSettings.json | 41 +++ .../ConversationIntegrationService.cs | 30 ++ .../IConversationIntergrationService.cs | 9 + ConnectorService/appsettings.Development.json | 8 + ConnectorService/appsettings.json | 9 + .../ContactService.Domain.csproj | 14 + ContactService.Domain/Entities/Friend.cs | 54 ++++ .../Entities/FriendRequest.cs | 73 +++++ .../Events/FriendAddedDomainEvent.cs | 7 + .../Events/FriendBlockDomainEvent.cs | 7 + .../Events/FriendRequestCreatedDomainEvent.cs | 7 + .../FriendRequestStateUpdateDomainEvent.cs | 7 + ContactService.Domain/FriendDomainService.cs | 28 ++ .../FriendRequestDomainService.cs | 22 ++ ContactService.Domain/FriendRequestStatus.cs | 22 ++ ContactService.Domain/FriendStatus.cs | 22 ++ ContactService.Domain/IFriendReposity.cs | 16 + .../IFriendRequestReposity.cs | 13 + .../ValueObjects/UserProfile.cs | 17 + .../Configs/FriendConfig.cs | 35 ++ .../Configs/FriendRequestConfig.cs | 20 ++ .../ContactDbContext.cs | 24 ++ .../ContactService.Infrastructure.csproj | 25 ++ .../FriendReposity.cs | 46 +++ .../FriendRequestReposity.cs | 37 +++ .../20260413114340_InitDb.Designer.cs | 161 +++++++++ .../Migrations/20260413114340_InitDb.cs | 84 +++++ .../ContactDbContextModelSnapshot.cs | 158 +++++++++ ContactService.Infrastructure/ModuleInit.cs | 17 + .../Application/Dtos/FriendRequestResponse.cs | 38 +++ .../Application/Dtos/FriendResonse.cs | 20 ++ .../Application/Dtos/UserInfoDto.cs | 16 + .../EventHandler/FriendAddedHandler.cs | 32 ++ .../FriendRequestStatusUpdateHandler.cs | 58 ++++ .../EventHandler/UserProfileUpdateHandler.cs | 35 ++ .../Application/Friend/FriendMapperConfig.cs | 23 ++ .../Application/Friend/FriendService.cs | 73 +++++ .../CreateFriendRequestCommand.cs | 18 ++ .../FriendRequest/FriendRequestConfig.cs | 13 + .../FriendRequestHandleCommand.cs | 25 ++ .../FriendRequest/FriendRequestService.cs | 81 +++++ .../IIdentityIntegrationService.cs | 10 + .../IdentityIntegrationService.cs | 47 +++ .../ContactService.WebApi.csproj | 26 ++ .../ContactService.WebApi.http | 7 + .../Controllers/FriendController.cs | 55 ++++ .../Controllers/FriendRequestAddRequest.cs | 24 ++ .../Controllers/FriendRequestController.cs | 48 +++ .../Controllers/FriendRequestHandleRequest.cs | 29 ++ .../DesignTimeDbContextFactory.cs | 18 ++ ContactService.WebApi/ModuleInit.cs | 24 ++ ContactService.WebApi/Program.cs | 42 +++ .../Properties/launchSettings.json | 49 +++ .../Services/ContactintegrationService.cs | 35 ++ .../appsettings.Development.json | 8 + ContactService.WebApi/appsettings.json | 9 + DomainCommons/AggregateRootEntity.cs | 24 ++ DomainCommons/BaseEntity.cs | 40 +++ DomainCommons/DomainException.cs | 10 + DomainCommons/IAggregateRoot.cs | 6 + DomainCommons/IDomainEvents.cs | 12 + DomainCommons/IEntity.cs | 7 + DomainCommons/IHasCreationTime.cs | 7 + DomainCommons/IHasDeletionTime.cs | 7 + DomainCommons/IHasModificationTime.cs | 7 + DomainCommons/IM.DomainCommons.csproj | 14 + DomainCommons/ISoftDelete.cs | 8 + FileService.Domain/Entities/UploadFile.cs | 43 +++ FileService.Domain/FileService.Domain.csproj | 13 + FileService.Domain/FileState.cs | 17 + FileService.Domain/ValueObjects/CheckSum.cs | 30 ++ .../ValueObjects/ContentType.cs | 27 ++ FileService.Domain/ValueObjects/FileName.cs | 34 ++ .../ValueObjects/StorageLocation.cs | 24 ++ GroupService.Domain/Entities/Group.cs | 116 +++++++ .../Entities/GroupInvitation.cs | 62 ++++ .../Entities/GroupJoinRequest.cs | 74 +++++ GroupService.Domain/Entities/GroupMember.cs | 53 +++ .../Enums/GroupAuthorityType.cs | 18 ++ .../Enums/GroupInvitationState.cs | 18 ++ .../Enums/GroupJoinRequestState.cs | 18 ++ GroupService.Domain/Enums/GroupMemberRole.cs | 18 ++ GroupService.Domain/Enums/GroupState.cs | 14 + .../Events/AllMembersBannedDomainEvent.cs | 7 + .../Events/GroupBlockedDomainEvent.cs | 7 + .../Events/GroupCreateDomainEvent.cs | 6 + .../GroupInvitationAcceptDomainEvent.cs | 7 + .../GroupInvitationCreateDomainEvent.cs | 7 + .../GroupJoinRequestDeclinedDomainEvent.cs | 7 + .../GroupJoinRequestPassedDomainEvent.cs | 7 + .../Events/GroupMemberJoinedDomainEvent.cs | 7 + .../Events/GroupUpdateDomainEvent.cs | 7 + .../GroupMemberDomainService.cs | 30 ++ .../GroupService.Domain.csproj | 14 + .../IReposities/IGroupInvitationReposity.cs | 10 + .../IReposities/IGroupMemberReposity.cs | 40 +++ .../IReposities/IGroupReposity.cs | 33 ++ .../IReposities/IGroupRequestReposity.cs | 11 + .../ValueObjects/GroupProfile.cs | 8 + .../ValueObjects/UserProfile.cs | 8 + .../Configs/GroupConfig.cs | 15 + .../Configs/GroupInvitationConfig.cs | 46 +++ .../Configs/GroupJoinRequestConfig.cs | 37 +++ .../Configs/GroupMemberConfig.cs | 14 + GroupService.Infrastructure/GroupDbContext.cs | 26 ++ .../GroupService.Infrastructure.csproj | 24 ++ .../20260419115121_InitGroupDb.Designer.cs | 126 ++++++++ .../Migrations/20260419115121_InitGroupDb.cs | 81 +++++ .../20260421141219_add_column.Designer.cs | 285 ++++++++++++++++ .../Migrations/20260421141219_add_column.cs | 78 +++++ ...oveGroupRequestOperatorProfile.Designer.cs | 277 ++++++++++++++++ ...03435_removeGroupRequestOperatorProfile.cs | 86 +++++ .../Migrations/GroupDbContextModelSnapshot.cs | 274 ++++++++++++++++ GroupService.Infrastructure/ModuleInit.cs | 20 ++ .../Reposities/GroupInvitationReposity.cs | 25 ++ .../Reposities/GroupJoinRequestReposity.cs | 33 ++ .../Reposities/GroupMemberReposity.cs | 50 +++ .../Reposities/GroupReposity.cs | 37 +++ .../Dtos/GroupInvitationResponse.cs | 37 +++ .../Application/Dtos/GroupMemberResponse.cs | 26 ++ .../Application/Dtos/GroupRequestResponse.cs | 38 +++ .../Application/Dtos/GroupResponse.cs | 45 +++ .../Application/Dtos/UserInfoDto.cs | 16 + .../EventHandler/GroupBlockHandler.cs | 24 ++ .../EventHandler/GroupCreateHandler.cs | 42 +++ .../GroupInvitationEventHandler.cs | 50 +++ .../EventHandler/GroupMemberJoinedHandler.cs | 25 ++ .../GroupRequestDeclinedHandler.cs | 29 ++ .../EventHandler/GroupRequestPassedHandler.cs | 46 +++ .../EventHandler/MessageCreatedHandler.cs | 32 ++ .../EventHandler/UserProfileUpdateHandler.cs | 31 ++ .../Application/Group/GroupCreateCommand.cs | 14 + .../Application/Group/GroupMapperConfig.cs | 18 ++ .../Application/Group/GroupService.cs | 43 +++ .../GroupInvitationHandleCommand.cs | 9 + .../GroupInvitationMapperConfig.cs | 21 ++ .../GroupInvitation/GroupInvitationService.cs | 101 ++++++ .../GroupMember/GroupMemberMapperConfig.cs | 14 + .../GroupMember/GroupMemberService.cs | 91 ++++++ .../GroupRequest/GroupRequestMapperConfig.cs | 22 ++ .../GroupRequest/GroupRequestService.cs | 101 ++++++ .../GroupRequest/RequestHandleCommand.cs | 9 + .../IDentityIntegrationService.cs | 46 +++ .../IIdentityIntegrationService.cs | 10 + .../Controllers/Group/GroupController.cs | 46 +++ .../Controllers/Group/GroupCreateRequest.cs | 18 ++ .../GroupInvitationController.cs | 45 +++ .../GroupInvitation/GroupInvitationRequest.cs | 24 ++ .../GroupMember/GroupMemberController.cs | 39 +++ .../GroupRequest/GroupRequestController.cs | 45 +++ .../GroupRequest/GroupRequestRequest.cs | 25 ++ .../DesignTimeDbContextFactory.cs | 18 ++ .../GroupService.WebApi.csproj | 25 ++ GroupService.WebApi/GroupService.WebApi.http | 6 + GroupService.WebApi/ModuleInit.cs | 27 ++ GroupService.WebApi/Program.cs | 38 +++ .../Properties/launchSettings.json | 49 +++ .../appsettings.Development.json | 8 + GroupService.WebApi/appsettings.json | 9 + IM.ASPNETCore/ApiControllerBase.cs | 8 + IM.ASPNETCore/ExceptionMiddleware.cs | 34 ++ IM.ASPNETCore/IM.ASPNETCore.csproj | 19 ++ IM.ASPNETCore/UnitOfWorkAttribute.cs | 24 ++ IM.ASPNETCore/UnitOfWorkFilter.cs | 65 ++++ IM.ASPNETCore/ValidatorFilter.cs | 27 ++ IM.Commons/BaseEvent.cs | 10 + IM.Commons/BaseSpecification.cs | 14 + IM.Commons/EnumHelper.cs | 21 ++ IM.Commons/EventHandlerException.cs | 13 + IM.Commons/GrpcOptions.cs | 15 + IM.Commons/IM.Commons.csproj | 16 + IM.Commons/IModuleInitializer.cs | 13 + IM.Commons/IRedisService.cs | 38 +++ IM.Commons/ISpecification.cs | 14 + .../IntegrationEvents/FriendAddedEvent.cs | 14 + .../FriendRequestStateUpdateEvent.cs | 31 ++ .../IntegrationEvents/GroupBlockEvent.cs | 20 ++ .../IntegrationEvents/GroupCreateEvent.cs | 18 ++ .../GroupInvitationAcceptEvent.cs | 9 + .../GroupInvitationCreateEvent.cs | 30 ++ .../GroupMemberJoinedEvent.cs | 22 ++ .../GroupRequestDeclinedEvent.cs | 9 + .../GroupRequestPassedEvent.cs | 36 +++ .../IntegrationEvents/MsgCreatedEvent.cs | 45 +++ .../IntegrationEvents/MsgWithdrawEvent.cs | 16 + .../UserProfileUpdateEvent.cs | 14 + IM.Commons/RedisCacheService.cs | 34 ++ IM.Commons/RedisHelper.cs | 15 + IM.Commons/ReflectionHelper.cs | 221 +++++++++++++ IM.Commons/Result.cs | 36 +++ IM.Commons/ResultCode.cs | 159 +++++++++ IM.InitCommon/AddDbContextExtensions.cs | 33 ++ IM.InitCommon/ApplicationBuilderExtension.cs | 18 ++ IM.InitCommon/ConnectionStringOptions.cs | 8 + IM.InitCommon/ConsulOption.cs | 7 + IM.InitCommon/CorsOptions.cs | 7 + .../DbContextOptionsBuilerFactory.cs | 17 + IM.InitCommon/GrpcExtension.cs | 57 ++++ IM.InitCommon/IM.InitCommon.csproj | 29 ++ IM.InitCommon/ModuleInitializerExtensions.cs | 34 ++ IM.InitCommon/RabbitMqExtension.cs | 36 +++ IM.InitCommon/RabbitMqOptions.cs | 11 + IM.InitCommon/SwaggerGenExtension.cs | 42 +++ .../WebApplicationBuilderExtensions.cs | 165 ++++++++++ IM.Jwt/IM.Jwt.csproj | 18 ++ IM.Jwt/ITokenService.cs | 18 ++ IM.Jwt/JwtOptions.cs | 11 + IM.Jwt/ModuleInit.cs | 13 + IM.Jwt/TokenService.cs | 70 ++++ IM.Jwt/WebApplicationJwtExtension.cs | 82 +++++ IM.Protocols/IM.Protocols.csproj | 25 ++ IM.Protocols/Protos/contact.proto | 23 ++ IM.Protocols/Protos/conversation.proto | 22 ++ IM.Protocols/Protos/group.proto | 12 + IM.Protocols/Protos/user.proto | 34 ++ IM_API_NEW.sln | 182 +++++++++++ Infrastructure/Efcore/BaseDbContext.cs | 37 +++ Infrastructure/Efcore/EfcoreExtension.cs | 28 ++ Infrastructure/Efcore/MediatorExtensions.cs | 30 ++ Infrastructure/GlobalUsing.cs | 2 + Infrastructure/IM.Infrastructure.csproj | 18 ++ .../Entities/Conversation.cs | 84 +++++ MessageService.Domain/Entities/Message.cs | 217 +++++++++++++ MessageService.Domain/Enums/ChatType.cs | 8 + MessageService.Domain/Enums/MessageState.cs | 14 + MessageService.Domain/Enums/MessageType.cs | 13 + .../Events/ConversationCreatedDomainEvent.cs | 7 + .../Events/MessageCreatedDomainEvent.cs | 7 + .../Events/MessageWithdrawDomainEvent.cs | 7 + .../IReposities/IConversationReposity.cs | 14 + .../IReposities/IMessageReposity.cs | 11 + .../KeyObjects/MessageContent.cs | 37 +++ .../KeyObjects/MessageContextContext.cs | 11 + .../KeyObjects/MsgTypeObj.cs | 8 + MessageService.Domain/KeyObjects/QuoteInfo.cs | 59 ++++ .../MessageService.Domain.csproj | 14 + .../Tools/StreamKeyBuilder.cs | 23 ++ .../Configs/ConversationConfig.cs | 17 + .../Configs/MessageConfig.cs | 46 +++ .../MessageDbContext.cs | 25 ++ .../MessageService.Infrastructure.csproj | 24 ++ .../20260423115234_InitMessageDb.Designer.cs | 187 +++++++++++ .../20260423115234_InitMessageDb.cs | 99 ++++++ .../MessageDbContextModelSnapshot.cs | 184 +++++++++++ MessageService.Infrastructure/ModuleInit.cs | 17 + .../Reposities/ConversationReposity.cs | 48 +++ .../Reposities/MessageReposity.cs | 55 ++++ .../Conversation/ConversationMapperConfig.cs | 14 + .../Conversation/ConversationService.cs | 43 +++ .../Application/Dtos/ConversationResponse.cs | 35 ++ .../Application/Dtos/MessageResponse.cs | 36 +++ .../EventHandlers/ConversationAddHandler.cs | 55 ++++ .../EventHandlers/MessageHandler.cs | 49 +++ .../EventHandlers/UserProfileUpdateHandler.cs | 30 ++ .../ContactIntegrationService.cs | 27 ++ .../GroupMemberIntegrationService.cs | 22 ++ .../IContactIntegrationService.cs | 7 + .../IGroupMemberIntegrationService.cs | 7 + .../Message/MessageMapperConfig.cs | 32 ++ .../Application/Message/MessageService.cs | 112 +++++++ .../Application/Message/SendMsgCommand.cs | 43 +++ .../Application/SquenceService.cs | 47 +++ .../Conversation/ConversationController.cs | 34 ++ .../Controllers/Message/MessageController.cs | 38 +++ .../Controllers/Message/MessageSendRequest.cs | 59 ++++ .../DesignTimeDbContextFactory.cs | 18 ++ .../MessageService.WebApi.csproj | 29 ++ .../MessageService.WebApi.http | 6 + MessageService.WebApi/ModuleInit.cs | 25 ++ MessageService.WebApi/Program.cs | 37 +++ .../Properties/launchSettings.json | 41 +++ .../ConversationIntegrationService.cs | 28 ++ .../appsettings.Development.json | 8 + MessageService.WebApi/appsettings.json | 9 + User.Domain/Entities/Role.cs | 13 + User.Domain/Entities/User.cs | 130 ++++++++ User.Domain/Events/UserBannedDomainEvent.cs | 6 + .../Events/UserProfileUpdateDomainEvent.cs | 7 + User.Domain/IIdRepository.cs | 115 +++++++ User.Domain/IdDomainService.cs | 26 ++ User.Domain/IdentityService.Domain.csproj | 18 ++ User.Domain/UserOnlineState.cs | 8 + User.Domain/UserState.cs | 10 + User.Domain/ValueObjects/Email.cs | 32 ++ User.Domain/ValueObjects/Phone.cs | 33 ++ User.Infrastructure/Configs/UserConfig.cs | 12 + User.Infrastructure/GlobalUsing.cs | 6 + User.Infrastructure/IdReposity.cs | 186 +++++++++++ User.Infrastructure/IdUserManager.cs | 24 ++ .../IdentityService.Infrastructure.csproj | 30 ++ .../20260410131653_InitialUserDb.Designer.cs | 303 +++++++++++++++++ .../20260410131653_InitialUserDb.cs | 265 +++++++++++++++ .../20260413090856_AddModifyTime.Designer.cs | 306 ++++++++++++++++++ .../20260413090856_AddModifyTime.cs | 28 ++ .../Migrations/UserDbContextModelSnapshot.cs | 303 +++++++++++++++++ User.Infrastructure/ModuleInit.cs | 15 + User.Infrastructure/UserDbContext.cs | 33 ++ .../Applications/Auth/AuthMappingProfile.cs | 24 ++ User.WebApi/Applications/Auth/AuthService.cs | 97 ++++++ .../Applications/Dtos/Common/UserResponse.cs | 16 + .../Applications/Dtos/LoginResponse.cs | 18 ++ .../EventHandler/UserProfileUpdateHandler.cs | 32 ++ .../User/UserResponseFindSpecification.cs | 35 ++ User.WebApi/Applications/User/UserService.cs | 54 ++++ .../Applications/User/UserUpdateCommand.cs | 20 ++ .../Controllers/Auth/AuthController.cs | 44 +++ User.WebApi/Controllers/Auth/LoginRequest.cs | 28 ++ .../Controllers/Auth/RefreshRequest.cs | 20 ++ .../Controllers/Auth/RegisterRequest.cs | 33 ++ .../Controllers/User/UserController.cs | 56 ++++ .../Controllers/User/UserUpdateRequest.cs | 19 ++ User.WebApi/DesignTimeDbContextFactory.cs | 18 ++ User.WebApi/IdentityService.WebApi.csproj | 24 ++ User.WebApi/ModuleInit.cs | 15 + User.WebApi/Program.cs | 57 ++++ User.WebApi/Properties/launchSettings.json | 49 +++ User.WebApi/Services/UserService.cs | 39 +++ User.WebApi/User.WebApi.http | 6 + User.WebApi/appsettings.Development.json | 8 + User.WebApi/appsettings.json | 9 + 327 files changed, 12860 insertions(+) create mode 100644 ConnectorService/ConnectorService.csproj create mode 100644 ConnectorService/ConnectorService.http create mode 100644 ConnectorService/Consumers/MessageConsumer.cs create mode 100644 ConnectorService/Dtos/MessageHubResponse.cs create mode 100644 ConnectorService/Hubs/ChatHub.cs create mode 100644 ConnectorService/ModuleInit.cs create mode 100644 ConnectorService/Program.cs create mode 100644 ConnectorService/Properties/launchSettings.json create mode 100644 ConnectorService/Services/ConversationIntegrationService.cs create mode 100644 ConnectorService/Services/IConversationIntergrationService.cs create mode 100644 ConnectorService/appsettings.Development.json create mode 100644 ConnectorService/appsettings.json create mode 100644 ContactService.Domain/ContactService.Domain.csproj create mode 100644 ContactService.Domain/Entities/Friend.cs create mode 100644 ContactService.Domain/Entities/FriendRequest.cs create mode 100644 ContactService.Domain/Events/FriendAddedDomainEvent.cs create mode 100644 ContactService.Domain/Events/FriendBlockDomainEvent.cs create mode 100644 ContactService.Domain/Events/FriendRequestCreatedDomainEvent.cs create mode 100644 ContactService.Domain/Events/FriendRequestStateUpdateDomainEvent.cs create mode 100644 ContactService.Domain/FriendDomainService.cs create mode 100644 ContactService.Domain/FriendRequestDomainService.cs create mode 100644 ContactService.Domain/FriendRequestStatus.cs create mode 100644 ContactService.Domain/FriendStatus.cs create mode 100644 ContactService.Domain/IFriendReposity.cs create mode 100644 ContactService.Domain/IFriendRequestReposity.cs create mode 100644 ContactService.Domain/ValueObjects/UserProfile.cs create mode 100644 ContactService.Infrastructure/Configs/FriendConfig.cs create mode 100644 ContactService.Infrastructure/Configs/FriendRequestConfig.cs create mode 100644 ContactService.Infrastructure/ContactDbContext.cs create mode 100644 ContactService.Infrastructure/ContactService.Infrastructure.csproj create mode 100644 ContactService.Infrastructure/FriendReposity.cs create mode 100644 ContactService.Infrastructure/FriendRequestReposity.cs create mode 100644 ContactService.Infrastructure/Migrations/20260413114340_InitDb.Designer.cs create mode 100644 ContactService.Infrastructure/Migrations/20260413114340_InitDb.cs create mode 100644 ContactService.Infrastructure/Migrations/ContactDbContextModelSnapshot.cs create mode 100644 ContactService.Infrastructure/ModuleInit.cs create mode 100644 ContactService.WebApi/Application/Dtos/FriendRequestResponse.cs create mode 100644 ContactService.WebApi/Application/Dtos/FriendResonse.cs create mode 100644 ContactService.WebApi/Application/Dtos/UserInfoDto.cs create mode 100644 ContactService.WebApi/Application/EventHandler/FriendAddedHandler.cs create mode 100644 ContactService.WebApi/Application/EventHandler/FriendRequestStatusUpdateHandler.cs create mode 100644 ContactService.WebApi/Application/EventHandler/UserProfileUpdateHandler.cs create mode 100644 ContactService.WebApi/Application/Friend/FriendMapperConfig.cs create mode 100644 ContactService.WebApi/Application/Friend/FriendService.cs create mode 100644 ContactService.WebApi/Application/FriendRequest/CreateFriendRequestCommand.cs create mode 100644 ContactService.WebApi/Application/FriendRequest/FriendRequestConfig.cs create mode 100644 ContactService.WebApi/Application/FriendRequest/FriendRequestHandleCommand.cs create mode 100644 ContactService.WebApi/Application/FriendRequest/FriendRequestService.cs create mode 100644 ContactService.WebApi/Application/IntegrationServices/IIdentityIntegrationService.cs create mode 100644 ContactService.WebApi/Application/IntegrationServices/IdentityIntegrationService.cs create mode 100644 ContactService.WebApi/ContactService.WebApi.csproj create mode 100644 ContactService.WebApi/ContactService.WebApi.http create mode 100644 ContactService.WebApi/Controllers/FriendController.cs create mode 100644 ContactService.WebApi/Controllers/FriendRequestAddRequest.cs create mode 100644 ContactService.WebApi/Controllers/FriendRequestController.cs create mode 100644 ContactService.WebApi/Controllers/FriendRequestHandleRequest.cs create mode 100644 ContactService.WebApi/DesignTimeDbContextFactory.cs create mode 100644 ContactService.WebApi/ModuleInit.cs create mode 100644 ContactService.WebApi/Program.cs create mode 100644 ContactService.WebApi/Properties/launchSettings.json create mode 100644 ContactService.WebApi/Services/ContactintegrationService.cs create mode 100644 ContactService.WebApi/appsettings.Development.json create mode 100644 ContactService.WebApi/appsettings.json create mode 100644 DomainCommons/AggregateRootEntity.cs create mode 100644 DomainCommons/BaseEntity.cs create mode 100644 DomainCommons/DomainException.cs create mode 100644 DomainCommons/IAggregateRoot.cs create mode 100644 DomainCommons/IDomainEvents.cs create mode 100644 DomainCommons/IEntity.cs create mode 100644 DomainCommons/IHasCreationTime.cs create mode 100644 DomainCommons/IHasDeletionTime.cs create mode 100644 DomainCommons/IHasModificationTime.cs create mode 100644 DomainCommons/IM.DomainCommons.csproj create mode 100644 DomainCommons/ISoftDelete.cs create mode 100644 FileService.Domain/Entities/UploadFile.cs create mode 100644 FileService.Domain/FileService.Domain.csproj create mode 100644 FileService.Domain/FileState.cs create mode 100644 FileService.Domain/ValueObjects/CheckSum.cs create mode 100644 FileService.Domain/ValueObjects/ContentType.cs create mode 100644 FileService.Domain/ValueObjects/FileName.cs create mode 100644 FileService.Domain/ValueObjects/StorageLocation.cs create mode 100644 GroupService.Domain/Entities/Group.cs create mode 100644 GroupService.Domain/Entities/GroupInvitation.cs create mode 100644 GroupService.Domain/Entities/GroupJoinRequest.cs create mode 100644 GroupService.Domain/Entities/GroupMember.cs create mode 100644 GroupService.Domain/Enums/GroupAuthorityType.cs create mode 100644 GroupService.Domain/Enums/GroupInvitationState.cs create mode 100644 GroupService.Domain/Enums/GroupJoinRequestState.cs create mode 100644 GroupService.Domain/Enums/GroupMemberRole.cs create mode 100644 GroupService.Domain/Enums/GroupState.cs create mode 100644 GroupService.Domain/Events/AllMembersBannedDomainEvent.cs create mode 100644 GroupService.Domain/Events/GroupBlockedDomainEvent.cs create mode 100644 GroupService.Domain/Events/GroupCreateDomainEvent.cs create mode 100644 GroupService.Domain/Events/GroupInvitationAcceptDomainEvent.cs create mode 100644 GroupService.Domain/Events/GroupInvitationCreateDomainEvent.cs create mode 100644 GroupService.Domain/Events/GroupJoinRequestDeclinedDomainEvent.cs create mode 100644 GroupService.Domain/Events/GroupJoinRequestPassedDomainEvent.cs create mode 100644 GroupService.Domain/Events/GroupMemberJoinedDomainEvent.cs create mode 100644 GroupService.Domain/Events/GroupUpdateDomainEvent.cs create mode 100644 GroupService.Domain/GroupMemberDomainService.cs create mode 100644 GroupService.Domain/GroupService.Domain.csproj create mode 100644 GroupService.Domain/IReposities/IGroupInvitationReposity.cs create mode 100644 GroupService.Domain/IReposities/IGroupMemberReposity.cs create mode 100644 GroupService.Domain/IReposities/IGroupReposity.cs create mode 100644 GroupService.Domain/IReposities/IGroupRequestReposity.cs create mode 100644 GroupService.Domain/ValueObjects/GroupProfile.cs create mode 100644 GroupService.Domain/ValueObjects/UserProfile.cs create mode 100644 GroupService.Infrastructure/Configs/GroupConfig.cs create mode 100644 GroupService.Infrastructure/Configs/GroupInvitationConfig.cs create mode 100644 GroupService.Infrastructure/Configs/GroupJoinRequestConfig.cs create mode 100644 GroupService.Infrastructure/Configs/GroupMemberConfig.cs create mode 100644 GroupService.Infrastructure/GroupDbContext.cs create mode 100644 GroupService.Infrastructure/GroupService.Infrastructure.csproj create mode 100644 GroupService.Infrastructure/Migrations/20260419115121_InitGroupDb.Designer.cs create mode 100644 GroupService.Infrastructure/Migrations/20260419115121_InitGroupDb.cs create mode 100644 GroupService.Infrastructure/Migrations/20260421141219_add_column.Designer.cs create mode 100644 GroupService.Infrastructure/Migrations/20260421141219_add_column.cs create mode 100644 GroupService.Infrastructure/Migrations/20260429103435_removeGroupRequestOperatorProfile.Designer.cs create mode 100644 GroupService.Infrastructure/Migrations/20260429103435_removeGroupRequestOperatorProfile.cs create mode 100644 GroupService.Infrastructure/Migrations/GroupDbContextModelSnapshot.cs create mode 100644 GroupService.Infrastructure/ModuleInit.cs create mode 100644 GroupService.Infrastructure/Reposities/GroupInvitationReposity.cs create mode 100644 GroupService.Infrastructure/Reposities/GroupJoinRequestReposity.cs create mode 100644 GroupService.Infrastructure/Reposities/GroupMemberReposity.cs create mode 100644 GroupService.Infrastructure/Reposities/GroupReposity.cs create mode 100644 GroupService.WebApi/Application/Dtos/GroupInvitationResponse.cs create mode 100644 GroupService.WebApi/Application/Dtos/GroupMemberResponse.cs create mode 100644 GroupService.WebApi/Application/Dtos/GroupRequestResponse.cs create mode 100644 GroupService.WebApi/Application/Dtos/GroupResponse.cs create mode 100644 GroupService.WebApi/Application/Dtos/UserInfoDto.cs create mode 100644 GroupService.WebApi/Application/EventHandler/GroupBlockHandler.cs create mode 100644 GroupService.WebApi/Application/EventHandler/GroupCreateHandler.cs create mode 100644 GroupService.WebApi/Application/EventHandler/GroupInvitationEventHandler.cs create mode 100644 GroupService.WebApi/Application/EventHandler/GroupMemberJoinedHandler.cs create mode 100644 GroupService.WebApi/Application/EventHandler/GroupRequestDeclinedHandler.cs create mode 100644 GroupService.WebApi/Application/EventHandler/GroupRequestPassedHandler.cs create mode 100644 GroupService.WebApi/Application/EventHandler/MessageCreatedHandler.cs create mode 100644 GroupService.WebApi/Application/EventHandler/UserProfileUpdateHandler.cs create mode 100644 GroupService.WebApi/Application/Group/GroupCreateCommand.cs create mode 100644 GroupService.WebApi/Application/Group/GroupMapperConfig.cs create mode 100644 GroupService.WebApi/Application/Group/GroupService.cs create mode 100644 GroupService.WebApi/Application/GroupInvitation/GroupInvitationHandleCommand.cs create mode 100644 GroupService.WebApi/Application/GroupInvitation/GroupInvitationMapperConfig.cs create mode 100644 GroupService.WebApi/Application/GroupInvitation/GroupInvitationService.cs create mode 100644 GroupService.WebApi/Application/GroupMember/GroupMemberMapperConfig.cs create mode 100644 GroupService.WebApi/Application/GroupMember/GroupMemberService.cs create mode 100644 GroupService.WebApi/Application/GroupRequest/GroupRequestMapperConfig.cs create mode 100644 GroupService.WebApi/Application/GroupRequest/GroupRequestService.cs create mode 100644 GroupService.WebApi/Application/GroupRequest/RequestHandleCommand.cs create mode 100644 GroupService.WebApi/Application/IntegrationServices/IDentityIntegrationService.cs create mode 100644 GroupService.WebApi/Application/IntegrationServices/IIdentityIntegrationService.cs create mode 100644 GroupService.WebApi/Controllers/Group/GroupController.cs create mode 100644 GroupService.WebApi/Controllers/Group/GroupCreateRequest.cs create mode 100644 GroupService.WebApi/Controllers/GroupInvitation/GroupInvitationController.cs create mode 100644 GroupService.WebApi/Controllers/GroupInvitation/GroupInvitationRequest.cs create mode 100644 GroupService.WebApi/Controllers/GroupMember/GroupMemberController.cs create mode 100644 GroupService.WebApi/Controllers/GroupRequest/GroupRequestController.cs create mode 100644 GroupService.WebApi/Controllers/GroupRequest/GroupRequestRequest.cs create mode 100644 GroupService.WebApi/DesignTimeDbContextFactory.cs create mode 100644 GroupService.WebApi/GroupService.WebApi.csproj create mode 100644 GroupService.WebApi/GroupService.WebApi.http create mode 100644 GroupService.WebApi/ModuleInit.cs create mode 100644 GroupService.WebApi/Program.cs create mode 100644 GroupService.WebApi/Properties/launchSettings.json create mode 100644 GroupService.WebApi/appsettings.Development.json create mode 100644 GroupService.WebApi/appsettings.json create mode 100644 IM.ASPNETCore/ApiControllerBase.cs create mode 100644 IM.ASPNETCore/ExceptionMiddleware.cs create mode 100644 IM.ASPNETCore/IM.ASPNETCore.csproj create mode 100644 IM.ASPNETCore/UnitOfWorkAttribute.cs create mode 100644 IM.ASPNETCore/UnitOfWorkFilter.cs create mode 100644 IM.ASPNETCore/ValidatorFilter.cs create mode 100644 IM.Commons/BaseEvent.cs create mode 100644 IM.Commons/BaseSpecification.cs create mode 100644 IM.Commons/EnumHelper.cs create mode 100644 IM.Commons/EventHandlerException.cs create mode 100644 IM.Commons/GrpcOptions.cs create mode 100644 IM.Commons/IM.Commons.csproj create mode 100644 IM.Commons/IModuleInitializer.cs create mode 100644 IM.Commons/IRedisService.cs create mode 100644 IM.Commons/ISpecification.cs create mode 100644 IM.Commons/IntegrationEvents/FriendAddedEvent.cs create mode 100644 IM.Commons/IntegrationEvents/FriendRequestStateUpdateEvent.cs create mode 100644 IM.Commons/IntegrationEvents/GroupBlockEvent.cs create mode 100644 IM.Commons/IntegrationEvents/GroupCreateEvent.cs create mode 100644 IM.Commons/IntegrationEvents/GroupInvitationAcceptEvent.cs create mode 100644 IM.Commons/IntegrationEvents/GroupInvitationCreateEvent.cs create mode 100644 IM.Commons/IntegrationEvents/GroupMemberJoinedEvent.cs create mode 100644 IM.Commons/IntegrationEvents/GroupRequestDeclinedEvent.cs create mode 100644 IM.Commons/IntegrationEvents/GroupRequestPassedEvent.cs create mode 100644 IM.Commons/IntegrationEvents/MsgCreatedEvent.cs create mode 100644 IM.Commons/IntegrationEvents/MsgWithdrawEvent.cs create mode 100644 IM.Commons/IntegrationEvents/UserProfileUpdateEvent.cs create mode 100644 IM.Commons/RedisCacheService.cs create mode 100644 IM.Commons/RedisHelper.cs create mode 100644 IM.Commons/ReflectionHelper.cs create mode 100644 IM.Commons/Result.cs create mode 100644 IM.Commons/ResultCode.cs create mode 100644 IM.InitCommon/AddDbContextExtensions.cs create mode 100644 IM.InitCommon/ApplicationBuilderExtension.cs create mode 100644 IM.InitCommon/ConnectionStringOptions.cs create mode 100644 IM.InitCommon/ConsulOption.cs create mode 100644 IM.InitCommon/CorsOptions.cs create mode 100644 IM.InitCommon/DbContextOptionsBuilerFactory.cs create mode 100644 IM.InitCommon/GrpcExtension.cs create mode 100644 IM.InitCommon/IM.InitCommon.csproj create mode 100644 IM.InitCommon/ModuleInitializerExtensions.cs create mode 100644 IM.InitCommon/RabbitMqExtension.cs create mode 100644 IM.InitCommon/RabbitMqOptions.cs create mode 100644 IM.InitCommon/SwaggerGenExtension.cs create mode 100644 IM.InitCommon/WebApplicationBuilderExtensions.cs create mode 100644 IM.Jwt/IM.Jwt.csproj create mode 100644 IM.Jwt/ITokenService.cs create mode 100644 IM.Jwt/JwtOptions.cs create mode 100644 IM.Jwt/ModuleInit.cs create mode 100644 IM.Jwt/TokenService.cs create mode 100644 IM.Jwt/WebApplicationJwtExtension.cs create mode 100644 IM.Protocols/IM.Protocols.csproj create mode 100644 IM.Protocols/Protos/contact.proto create mode 100644 IM.Protocols/Protos/conversation.proto create mode 100644 IM.Protocols/Protos/group.proto create mode 100644 IM.Protocols/Protos/user.proto create mode 100644 IM_API_NEW.sln create mode 100644 Infrastructure/Efcore/BaseDbContext.cs create mode 100644 Infrastructure/Efcore/EfcoreExtension.cs create mode 100644 Infrastructure/Efcore/MediatorExtensions.cs create mode 100644 Infrastructure/GlobalUsing.cs create mode 100644 Infrastructure/IM.Infrastructure.csproj create mode 100644 MessageService.Domain/Entities/Conversation.cs create mode 100644 MessageService.Domain/Entities/Message.cs create mode 100644 MessageService.Domain/Enums/ChatType.cs create mode 100644 MessageService.Domain/Enums/MessageState.cs create mode 100644 MessageService.Domain/Enums/MessageType.cs create mode 100644 MessageService.Domain/Events/ConversationCreatedDomainEvent.cs create mode 100644 MessageService.Domain/Events/MessageCreatedDomainEvent.cs create mode 100644 MessageService.Domain/Events/MessageWithdrawDomainEvent.cs create mode 100644 MessageService.Domain/IReposities/IConversationReposity.cs create mode 100644 MessageService.Domain/IReposities/IMessageReposity.cs create mode 100644 MessageService.Domain/KeyObjects/MessageContent.cs create mode 100644 MessageService.Domain/KeyObjects/MessageContextContext.cs create mode 100644 MessageService.Domain/KeyObjects/MsgTypeObj.cs create mode 100644 MessageService.Domain/KeyObjects/QuoteInfo.cs create mode 100644 MessageService.Domain/MessageService.Domain.csproj create mode 100644 MessageService.Domain/Tools/StreamKeyBuilder.cs create mode 100644 MessageService.Infrastructure/Configs/ConversationConfig.cs create mode 100644 MessageService.Infrastructure/Configs/MessageConfig.cs create mode 100644 MessageService.Infrastructure/MessageDbContext.cs create mode 100644 MessageService.Infrastructure/MessageService.Infrastructure.csproj create mode 100644 MessageService.Infrastructure/Migrations/20260423115234_InitMessageDb.Designer.cs create mode 100644 MessageService.Infrastructure/Migrations/20260423115234_InitMessageDb.cs create mode 100644 MessageService.Infrastructure/Migrations/MessageDbContextModelSnapshot.cs create mode 100644 MessageService.Infrastructure/ModuleInit.cs create mode 100644 MessageService.Infrastructure/Reposities/ConversationReposity.cs create mode 100644 MessageService.Infrastructure/Reposities/MessageReposity.cs create mode 100644 MessageService.WebApi/Application/Conversation/ConversationMapperConfig.cs create mode 100644 MessageService.WebApi/Application/Conversation/ConversationService.cs create mode 100644 MessageService.WebApi/Application/Dtos/ConversationResponse.cs create mode 100644 MessageService.WebApi/Application/Dtos/MessageResponse.cs create mode 100644 MessageService.WebApi/Application/EventHandlers/ConversationAddHandler.cs create mode 100644 MessageService.WebApi/Application/EventHandlers/MessageHandler.cs create mode 100644 MessageService.WebApi/Application/EventHandlers/UserProfileUpdateHandler.cs create mode 100644 MessageService.WebApi/Application/IntegrationServices/ContactIntegrationService.cs create mode 100644 MessageService.WebApi/Application/IntegrationServices/GroupMemberIntegrationService.cs create mode 100644 MessageService.WebApi/Application/IntegrationServices/IContactIntegrationService.cs create mode 100644 MessageService.WebApi/Application/IntegrationServices/IGroupMemberIntegrationService.cs create mode 100644 MessageService.WebApi/Application/Message/MessageMapperConfig.cs create mode 100644 MessageService.WebApi/Application/Message/MessageService.cs create mode 100644 MessageService.WebApi/Application/Message/SendMsgCommand.cs create mode 100644 MessageService.WebApi/Application/SquenceService.cs create mode 100644 MessageService.WebApi/Controllers/Conversation/ConversationController.cs create mode 100644 MessageService.WebApi/Controllers/Message/MessageController.cs create mode 100644 MessageService.WebApi/Controllers/Message/MessageSendRequest.cs create mode 100644 MessageService.WebApi/DesignTimeDbContextFactory.cs create mode 100644 MessageService.WebApi/MessageService.WebApi.csproj create mode 100644 MessageService.WebApi/MessageService.WebApi.http create mode 100644 MessageService.WebApi/ModuleInit.cs create mode 100644 MessageService.WebApi/Program.cs create mode 100644 MessageService.WebApi/Properties/launchSettings.json create mode 100644 MessageService.WebApi/Services/ConversationIntegrationService.cs create mode 100644 MessageService.WebApi/appsettings.Development.json create mode 100644 MessageService.WebApi/appsettings.json create mode 100644 User.Domain/Entities/Role.cs create mode 100644 User.Domain/Entities/User.cs create mode 100644 User.Domain/Events/UserBannedDomainEvent.cs create mode 100644 User.Domain/Events/UserProfileUpdateDomainEvent.cs create mode 100644 User.Domain/IIdRepository.cs create mode 100644 User.Domain/IdDomainService.cs create mode 100644 User.Domain/IdentityService.Domain.csproj create mode 100644 User.Domain/UserOnlineState.cs create mode 100644 User.Domain/UserState.cs create mode 100644 User.Domain/ValueObjects/Email.cs create mode 100644 User.Domain/ValueObjects/Phone.cs create mode 100644 User.Infrastructure/Configs/UserConfig.cs create mode 100644 User.Infrastructure/GlobalUsing.cs create mode 100644 User.Infrastructure/IdReposity.cs create mode 100644 User.Infrastructure/IdUserManager.cs create mode 100644 User.Infrastructure/IdentityService.Infrastructure.csproj create mode 100644 User.Infrastructure/Migrations/20260410131653_InitialUserDb.Designer.cs create mode 100644 User.Infrastructure/Migrations/20260410131653_InitialUserDb.cs create mode 100644 User.Infrastructure/Migrations/20260413090856_AddModifyTime.Designer.cs create mode 100644 User.Infrastructure/Migrations/20260413090856_AddModifyTime.cs create mode 100644 User.Infrastructure/Migrations/UserDbContextModelSnapshot.cs create mode 100644 User.Infrastructure/ModuleInit.cs create mode 100644 User.Infrastructure/UserDbContext.cs create mode 100644 User.WebApi/Applications/Auth/AuthMappingProfile.cs create mode 100644 User.WebApi/Applications/Auth/AuthService.cs create mode 100644 User.WebApi/Applications/Dtos/Common/UserResponse.cs create mode 100644 User.WebApi/Applications/Dtos/LoginResponse.cs create mode 100644 User.WebApi/Applications/EventHandler/UserProfileUpdateHandler.cs create mode 100644 User.WebApi/Applications/User/UserResponseFindSpecification.cs create mode 100644 User.WebApi/Applications/User/UserService.cs create mode 100644 User.WebApi/Applications/User/UserUpdateCommand.cs create mode 100644 User.WebApi/Controllers/Auth/AuthController.cs create mode 100644 User.WebApi/Controllers/Auth/LoginRequest.cs create mode 100644 User.WebApi/Controllers/Auth/RefreshRequest.cs create mode 100644 User.WebApi/Controllers/Auth/RegisterRequest.cs create mode 100644 User.WebApi/Controllers/User/UserController.cs create mode 100644 User.WebApi/Controllers/User/UserUpdateRequest.cs create mode 100644 User.WebApi/DesignTimeDbContextFactory.cs create mode 100644 User.WebApi/IdentityService.WebApi.csproj create mode 100644 User.WebApi/ModuleInit.cs create mode 100644 User.WebApi/Program.cs create mode 100644 User.WebApi/Properties/launchSettings.json create mode 100644 User.WebApi/Services/UserService.cs create mode 100644 User.WebApi/User.WebApi.http create mode 100644 User.WebApi/appsettings.Development.json create mode 100644 User.WebApi/appsettings.json diff --git a/ConnectorService/ConnectorService.csproj b/ConnectorService/ConnectorService.csproj new file mode 100644 index 0000000..7ae4266 --- /dev/null +++ b/ConnectorService/ConnectorService.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/ConnectorService/ConnectorService.http b/ConnectorService/ConnectorService.http new file mode 100644 index 0000000..cff0eb9 --- /dev/null +++ b/ConnectorService/ConnectorService.http @@ -0,0 +1,6 @@ +@ConnectorService_HostAddress = http://localhost:5100 + +GET {{ConnectorService_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/ConnectorService/Consumers/MessageConsumer.cs b/ConnectorService/Consumers/MessageConsumer.cs new file mode 100644 index 0000000..b5cb79b --- /dev/null +++ b/ConnectorService/Consumers/MessageConsumer.cs @@ -0,0 +1,24 @@ +using ConnectorService.Dtos; +using ConnectorService.Hubs; +using IM.Commons.IntegrationEvents; +using MassTransit; +using Microsoft.AspNetCore.SignalR; + +namespace ConnectorService.Consumers +{ + public class MessageConsumer : IConsumer + { + private readonly IHubContext hub; + + public MessageConsumer(IHubContext hub) + { + this.hub = hub; + } + + public async Task Consume(ConsumeContext context) + { + var @event = context.Message; + await hub.Clients.Group(@event.StreamKey).SendAsync("ReceiveNewMessage", @event.ToHubResponse()); + } + } +} diff --git a/ConnectorService/Dtos/MessageHubResponse.cs b/ConnectorService/Dtos/MessageHubResponse.cs new file mode 100644 index 0000000..92c1e61 --- /dev/null +++ b/ConnectorService/Dtos/MessageHubResponse.cs @@ -0,0 +1,81 @@ +using IM.Commons.IntegrationEvents; + +namespace ConnectorService.Dtos +{ + public record MessageHubResponse + { + public Guid Id { get; init; } + + /// + /// 客户端去重/回执使用的本地 ID + /// + public Guid ClientId { get; init; } + + public string ChatType { get; init; } = string.Empty; + public string MsgType { get; init; } = string.Empty; + public Guid SenderId { get; init; } + public Guid TargetId { get; init; } + public string State { get; init; } = string.Empty; + public string StreamKey { get; init; } = string.Empty; + public long SequenceId { get; init; } + + /// + /// 服务器推送到达时间 (毫秒级时间戳,强烈建议加上) + /// + public long PushTimestamp { get; init; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + public HubMsgContentDto Content { get; init; } = null!; + } + + // 嵌套的内容对象,允许 Ext 和 Quote 为 null 以缩减 JSON 体积 + public record HubMsgContentDto( + string Fallback, + object Body, + Dictionary? Ext, + HubQuoteInfoDto? Quote + ); + + public record HubQuoteInfoDto( + Guid MessageId, + Guid SenderId, + string SenderName, + string MessageType, + string Preview + ); + public static class MessageEventMapper + { + /// + /// 将内部集成事件转换为对外推送的 DTO + /// + public static MessageHubResponse ToHubResponse(this MsgCreatedEvent @event) + { + if (@event == null) throw new ArgumentNullException(nameof(@event)); + + return new MessageHubResponse + { + Id = @event.Id, + ClientId = @event.ClientId, + ChatType = @event.ChatType, + MsgType = @event.MsgType, + SenderId = @event.SenderId, + TargetId = @event.TargetId, + State = @event.State, + StreamKey = @event.StreamKey, + SequenceId = @event.SequenceId, + // 嵌套映射 + Content = @event.Content != null ? new HubMsgContentDto( + @event.Content.Fallback, + @event.Content.Body, + @event.Content.Ext, + @event.Content.Quote != null ? new HubQuoteInfoDto( + @event.Content.Quote.MessageId, + @event.Content.Quote.SenderId, + @event.Content.Quote.SenderName, + @event.Content.Quote.MessageType, + @event.Content.Quote.Preview + ) : null + ) : null! + }; + } + } +} diff --git a/ConnectorService/Hubs/ChatHub.cs b/ConnectorService/Hubs/ChatHub.cs new file mode 100644 index 0000000..0d9d83e --- /dev/null +++ b/ConnectorService/Hubs/ChatHub.cs @@ -0,0 +1,53 @@ +using ConnectorService.Services; +using IM.Commons; +using Microsoft.AspNetCore.SignalR; +using StackExchange.Redis; +using System.Security.Claims; + +namespace ConnectorService.Hubs +{ + public class ChatHub : Hub + { + private readonly IConversationIntergrationService conService; + private readonly StackExchange.Redis.IDatabase redis; + + public ChatHub(IConversationIntergrationService conService, IConnectionMultiplexer multiplexer) + { + this.conService = conService; + this.redis = multiplexer.GetDatabase(); + } + + public async override Task OnConnectedAsync() + { + if (!Context.User.Identity.IsAuthenticated) + { + Context.Abort(); + return; + } + + var userId = Context.User.FindFirstValue(ClaimTypes.NameIdentifier); + + var res = await conService.GetUserStreamKeysAsync(Guid.Parse(userId)); + foreach (var streamkey in res) + { + await Groups.AddToGroupAsync(Context.ConnectionId, streamkey); + } + + await redis.SetAddAsync(RedisHelper.GetConnectionIdKey(userId), Context.ConnectionId); + + + await base.OnConnectedAsync(); + } + + public async override Task OnDisconnectedAsync(Exception? exception) + { + if (Context.User.Identity.IsAuthenticated) + { + var userId = Context.User.FindFirstValue(ClaimTypes.NameIdentifier); + + await redis.SetRemoveAsync(RedisHelper.GetConnectionIdKey(userId), Context.ConnectionId); + } + await base.OnDisconnectedAsync(exception); + } + } +} diff --git a/ConnectorService/ModuleInit.cs b/ConnectorService/ModuleInit.cs new file mode 100644 index 0000000..11b68e2 --- /dev/null +++ b/ConnectorService/ModuleInit.cs @@ -0,0 +1,19 @@ +using IM.Commons; +using IM.Protocols.Grpc.Conversation; +using Microsoft.Extensions.Options; + +namespace ConnectorService +{ + public class ModuleInit : IModuleInitializer + { + public void Initialize(IServiceCollection services) + { + services.AddRedisCache(); + services.AddGrpcClient((sp ,o) => + { + var options = sp.GetRequiredService>(); + o.Address = new Uri(options.CurrentValue.MessageServiceUrl); + }); + } + } +} diff --git a/ConnectorService/Program.cs b/ConnectorService/Program.cs new file mode 100644 index 0000000..e2d1b92 --- /dev/null +++ b/ConnectorService/Program.cs @@ -0,0 +1,42 @@ + +using ConnectorService.Hubs; +using IM.InitCommon; + +namespace ConnectorService +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + + builder.ConfigureDbConfiguration(); + + builder.Services.AddSignalR(); + + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + builder.ConfigExtraServices(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseAppDefault(); + + + app.MapHub("/chat"); + + app.Run(); + } + } +} diff --git a/ConnectorService/Properties/launchSettings.json b/ConnectorService/Properties/launchSettings.json new file mode 100644 index 0000000..3afd3a3 --- /dev/null +++ b/ConnectorService/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:46392", + "sslPort": 44313 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5100", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7115;http://localhost:5100", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ConnectorService/Services/ConversationIntegrationService.cs b/ConnectorService/Services/ConversationIntegrationService.cs new file mode 100644 index 0000000..0cd472b --- /dev/null +++ b/ConnectorService/Services/ConversationIntegrationService.cs @@ -0,0 +1,30 @@ +using IM.Commons; +using IM.Protocols.Grpc.Conversation; + +namespace ConnectorService.Services +{ + public class ConversationIntegrationService : IConversationIntergrationService + { + private readonly ConversationInternal.ConversationInternalClient client; + public async Task> GetUserStreamKeysAsync(Guid userId) + { + var req = new GetUserStreamKeysRequest() + { + UserId = userId.ToString() + }; + var res = await client.GetUserStreamKeysAsync(req); + if(res == null) + { + return []; + } + + var list = new List(); + foreach(var item in res.StreamKeys) + { + list.Add(item); + } + + return list; + } + } +} diff --git a/ConnectorService/Services/IConversationIntergrationService.cs b/ConnectorService/Services/IConversationIntergrationService.cs new file mode 100644 index 0000000..fb9a0c3 --- /dev/null +++ b/ConnectorService/Services/IConversationIntergrationService.cs @@ -0,0 +1,9 @@ +using IM.Commons; + +namespace ConnectorService.Services +{ + public interface IConversationIntergrationService + { + Task> GetUserStreamKeysAsync(Guid userId); + } +} diff --git a/ConnectorService/appsettings.Development.json b/ConnectorService/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/ConnectorService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ConnectorService/appsettings.json b/ConnectorService/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/ConnectorService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/ContactService.Domain/ContactService.Domain.csproj b/ContactService.Domain/ContactService.Domain.csproj new file mode 100644 index 0000000..2c335d3 --- /dev/null +++ b/ContactService.Domain/ContactService.Domain.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/ContactService.Domain/Entities/Friend.cs b/ContactService.Domain/Entities/Friend.cs new file mode 100644 index 0000000..f8b58b8 --- /dev/null +++ b/ContactService.Domain/Entities/Friend.cs @@ -0,0 +1,54 @@ +using ContactService.Domain.Events; +using ContactService.Domain.ValueObjects; +using IM.DomainCommons; + +namespace ContactService.Domain.Entities +{ + public class Friend : AggregateRootEntity + { + public UserProfile Owner { get; private set; } + public UserProfile Target { get; private set; } + /// + /// 好友备注名 + /// + public string? RemarkName { get; private set; } + public FriendStatus Status { get; private set; } + + private Friend() { } + + public Friend(UserProfile owner, UserProfile target, string? remarkName) + { + Owner = owner; + Target = target; + RemarkName = remarkName; + Status = FriendStatus.Added; + + AddDomainEvent(new FriendAddedDomainEvent(this)); + } + + public void setRemarkName(string? remarkName) + { + if (remarkName.Length > 20) + { + throw new DomainException("备注名过长"); + } + RemarkName = remarkName ?? RemarkName; + } + + public void Block() + { + Status = FriendStatus.Blocked; + AddDomainEvent(new FriendBlockDomainEvent(this)); + } + + public void UpdateUserInfo(UserProfile profile) + { + if (profile.Id != Target.Id) + { + return; + } + + Target = profile; + } + } +} diff --git a/ContactService.Domain/Entities/FriendRequest.cs b/ContactService.Domain/Entities/FriendRequest.cs new file mode 100644 index 0000000..7506e48 --- /dev/null +++ b/ContactService.Domain/Entities/FriendRequest.cs @@ -0,0 +1,73 @@ +using ContactService.Domain.Events; +using IM.DomainCommons; + +namespace ContactService.Domain.Entities +{ + public class FriendRequest : AggregateRootEntity + { + /// + /// 申请人 + /// + public Guid OwnerId { get; private set; } + + /// + /// 被申请人 + /// + public Guid TargetId { get; private set; } + + + /// + /// 申请附言 + /// + public string Description { get; private set; } = "申请添加好友"; + + /// + /// 申请状态(0:待通过,1:拒绝,2:同意,3:拉黑) + /// + public FriendRequestStatus State { get; private set; } = FriendRequestStatus.Pending; + + /// + /// 备注 + /// + public string? RemarkName { get; private set; } + + private FriendRequest() { } + + public FriendRequest(Guid ownerId, Guid targetId, string? description, string? remarkName) + { + OwnerId = ownerId; + TargetId = targetId; + Description = description ?? Description; + RemarkName = remarkName; + AddDomainEvent(new FriendRequestCreatedDomainEvent(this)); + } + public void Accept(string remarkName) + { + if (State != FriendRequestStatus.Pending) + { + throw new DomainException("只能处理待处理的好友请求"); + } + State = FriendRequestStatus.Passed; + AddDomainEvent(new FriendRequestStateUpdateDomainEvent(this,remarkName)); + } + + public void Reject() + { + if (State != FriendRequestStatus.Pending) + { + throw new DomainException("只能处理待处理的好友请求"); + } + State = FriendRequestStatus.Declined; + AddDomainEvent(new FriendRequestStateUpdateDomainEvent(this)); + } + public void Block() + { + if (State != FriendRequestStatus.Pending) + { + throw new DomainException("只能处理待处理的好友请求"); + } + State = FriendRequestStatus.Blocked; + AddDomainEvent(new FriendRequestStateUpdateDomainEvent(this)); + } + } +} diff --git a/ContactService.Domain/Events/FriendAddedDomainEvent.cs b/ContactService.Domain/Events/FriendAddedDomainEvent.cs new file mode 100644 index 0000000..e9334fd --- /dev/null +++ b/ContactService.Domain/Events/FriendAddedDomainEvent.cs @@ -0,0 +1,7 @@ +using ContactService.Domain.Entities; +using MediatR; + +namespace ContactService.Domain.Events +{ + public record FriendAddedDomainEvent(Friend Friend) : INotification; +} diff --git a/ContactService.Domain/Events/FriendBlockDomainEvent.cs b/ContactService.Domain/Events/FriendBlockDomainEvent.cs new file mode 100644 index 0000000..9f8a06e --- /dev/null +++ b/ContactService.Domain/Events/FriendBlockDomainEvent.cs @@ -0,0 +1,7 @@ +using ContactService.Domain.Entities; +using MediatR; + +namespace ContactService.Domain.Events +{ + public record FriendBlockDomainEvent(Friend Friend) : INotification; +} diff --git a/ContactService.Domain/Events/FriendRequestCreatedDomainEvent.cs b/ContactService.Domain/Events/FriendRequestCreatedDomainEvent.cs new file mode 100644 index 0000000..4772fca --- /dev/null +++ b/ContactService.Domain/Events/FriendRequestCreatedDomainEvent.cs @@ -0,0 +1,7 @@ +using ContactService.Domain.Entities; +using MediatR; + +namespace ContactService.Domain.Events +{ + public record FriendRequestCreatedDomainEvent(FriendRequest Request) : INotification; +} diff --git a/ContactService.Domain/Events/FriendRequestStateUpdateDomainEvent.cs b/ContactService.Domain/Events/FriendRequestStateUpdateDomainEvent.cs new file mode 100644 index 0000000..71610a7 --- /dev/null +++ b/ContactService.Domain/Events/FriendRequestStateUpdateDomainEvent.cs @@ -0,0 +1,7 @@ +using ContactService.Domain.Entities; +using MediatR; + +namespace ContactService.Domain.Events +{ + public record FriendRequestStateUpdateDomainEvent(FriendRequest Request,string? AcceptRemarkName = default) : INotification; +} diff --git a/ContactService.Domain/FriendDomainService.cs b/ContactService.Domain/FriendDomainService.cs new file mode 100644 index 0000000..9adbcc9 --- /dev/null +++ b/ContactService.Domain/FriendDomainService.cs @@ -0,0 +1,28 @@ +using ContactService.Domain.Entities; +using ContactService.Domain.ValueObjects; +using IM.Commons; + +namespace ContactService.Domain +{ + public class FriendDomainService + { + private readonly IFriendReposity reposity; + + public FriendDomainService(IFriendReposity reposity) + { + this.reposity = reposity; + } + + public async Task> CreateAsync(UserProfile owner, UserProfile target, string? remarkName) + { + var isExist = await reposity.CheckOwnerIdAndTargetIdAsync(owner.Id, target.Id); + if (isExist) + { + return Result.Fail(ResultCode.ALREADY_FRIENDS); + } + var friend = new Friend(owner, target, remarkName); + var res = await reposity.CreateAsync(friend); + return Result.Success(res); + } + } +} diff --git a/ContactService.Domain/FriendRequestDomainService.cs b/ContactService.Domain/FriendRequestDomainService.cs new file mode 100644 index 0000000..c8184c1 --- /dev/null +++ b/ContactService.Domain/FriendRequestDomainService.cs @@ -0,0 +1,22 @@ +using ContactService.Domain.Entities; +using IM.Commons; + +namespace ContactService.Domain +{ + public class FriendRequestDomainService + { + private readonly IFriendRequestReposity reposity; + + public FriendRequestDomainService(IFriendRequestReposity reposity) + { + this.reposity = reposity; + } + + public async Task> CreateAsync(Guid ownerId, Guid targetId, string? description, string? remarkName) + { + var friendRequest = new FriendRequest(ownerId, targetId, description, remarkName); + await reposity.CreateAsync(friendRequest); + return Result.Success(friendRequest); + } + } +} diff --git a/ContactService.Domain/FriendRequestStatus.cs b/ContactService.Domain/FriendRequestStatus.cs new file mode 100644 index 0000000..1f7eba7 --- /dev/null +++ b/ContactService.Domain/FriendRequestStatus.cs @@ -0,0 +1,22 @@ +namespace ContactService.Domain +{ + public enum FriendRequestStatus + { + /// + /// 待处理 + /// + Pending = 0, + /// + /// 已通过 + /// + Passed = 2, + /// + /// 已拒绝 + /// + Declined = 1, + /// + /// 拉黑 + /// + Blocked = 3 + } +} diff --git a/ContactService.Domain/FriendStatus.cs b/ContactService.Domain/FriendStatus.cs new file mode 100644 index 0000000..be19b10 --- /dev/null +++ b/ContactService.Domain/FriendStatus.cs @@ -0,0 +1,22 @@ +namespace ContactService.Domain +{ + public enum FriendStatus + { + /// + /// 待处理 + /// + Pending = 0, + /// + /// 已添加 + /// + Added = 1, + /// + /// 已拒绝 + /// + Declined = 2, + /// + /// 已拉黑 + /// + Blocked = 3 + } +} diff --git a/ContactService.Domain/IFriendReposity.cs b/ContactService.Domain/IFriendReposity.cs new file mode 100644 index 0000000..61e9595 --- /dev/null +++ b/ContactService.Domain/IFriendReposity.cs @@ -0,0 +1,16 @@ +using ContactService.Domain.Entities; + +namespace ContactService.Domain +{ + public interface IFriendReposity + { + Task FindByIdAsync(Guid id); + Task FindByOwnerAndTargetAsync(Guid ownerId, Guid targetId); + Task> FindByTargetAsync(Guid targetId); + Task> FindByOwnerAsync(Guid ownerId); + + Task CreateAsync(Friend friend); + + Task CheckOwnerIdAndTargetIdAsync(Guid ownerId, Guid targetId); + } +} diff --git a/ContactService.Domain/IFriendRequestReposity.cs b/ContactService.Domain/IFriendRequestReposity.cs new file mode 100644 index 0000000..8845d4a --- /dev/null +++ b/ContactService.Domain/IFriendRequestReposity.cs @@ -0,0 +1,13 @@ +using ContactService.Domain.Entities; + +namespace ContactService.Domain +{ + public interface IFriendRequestReposity + { + Task CreateAsync(FriendRequest friendRequest); + Task FindByIdAsync(Guid id); + Task> FindByOwnerIdAsync(Guid ownerId); + Task> FindByTargetIdAsync(Guid targetId); + + } +} diff --git a/ContactService.Domain/ValueObjects/UserProfile.cs b/ContactService.Domain/ValueObjects/UserProfile.cs new file mode 100644 index 0000000..680b53d --- /dev/null +++ b/ContactService.Domain/ValueObjects/UserProfile.cs @@ -0,0 +1,17 @@ +namespace ContactService.Domain.ValueObjects +{ + public class UserProfile + { + public Guid Id { get; private set; } + public string NickName { get; private set; } + public string? Avatar { get; private set; } + public UserProfile() { } + + public UserProfile(Guid id, string nickName, string? avatar) + { + Id = id; + NickName = nickName; + Avatar = avatar; + } + }; +} diff --git a/ContactService.Infrastructure/Configs/FriendConfig.cs b/ContactService.Infrastructure/Configs/FriendConfig.cs new file mode 100644 index 0000000..bb4972e --- /dev/null +++ b/ContactService.Infrastructure/Configs/FriendConfig.cs @@ -0,0 +1,35 @@ +using ContactService.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ContactService.Infrastructure.Configs +{ + public class FriendConfig : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("friends"); + builder.HasKey(x => x.Id); + builder.OwnsOne(x => x.Owner, owner => + { + owner.WithOwner(); // 🔥 关键:明确归属 + + owner.Property(p => p.Id).HasColumnName("OwnerId").IsRequired(); + owner.Property(p => p.NickName).HasColumnName("OwnerNickName"); + owner.Property(p => p.Avatar).HasColumnName("OwnerAvatarUrl"); + }); + + builder.OwnsOne(x => x.Target, target => + { + target.WithOwner(); // 🔥 关键 + + target.Property(p => p.Id).HasColumnName("TargetId").IsRequired(); + target.Property(p => p.NickName).HasColumnName("TargetNickName"); + target.Property(p => p.Avatar).HasColumnName("TargetAvatarUrl"); + }); + + + + } + } +} diff --git a/ContactService.Infrastructure/Configs/FriendRequestConfig.cs b/ContactService.Infrastructure/Configs/FriendRequestConfig.cs new file mode 100644 index 0000000..d4e052b --- /dev/null +++ b/ContactService.Infrastructure/Configs/FriendRequestConfig.cs @@ -0,0 +1,20 @@ +using ContactService.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ContactService.Infrastructure.Configs +{ + public class FriendRequestConfig : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("friend_requests"); + + builder.HasKey(x => x.Id); + + builder.HasIndex(x => new { x.OwnerId, x.TargetId }); + + + } + } +} diff --git a/ContactService.Infrastructure/ContactDbContext.cs b/ContactService.Infrastructure/ContactDbContext.cs new file mode 100644 index 0000000..efb2479 --- /dev/null +++ b/ContactService.Infrastructure/ContactDbContext.cs @@ -0,0 +1,24 @@ +using ContactService.Domain.Entities; +using IM.Infrastructure.Efcore; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace ContactService.Infrastructure +{ + public class ContactDbContext : BaseDbContext + { + public DbSet Friends { get; private set; } + public DbSet FriendRequests { get; private set; } + public ContactDbContext(DbContextOptions options, IMediator mediator) : base(options, mediator) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); + + modelBuilder.EnableSoftDeletionGlobalFilter(); + } + } +} diff --git a/ContactService.Infrastructure/ContactService.Infrastructure.csproj b/ContactService.Infrastructure/ContactService.Infrastructure.csproj new file mode 100644 index 0000000..e443aa7 --- /dev/null +++ b/ContactService.Infrastructure/ContactService.Infrastructure.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/ContactService.Infrastructure/FriendReposity.cs b/ContactService.Infrastructure/FriendReposity.cs new file mode 100644 index 0000000..8605747 --- /dev/null +++ b/ContactService.Infrastructure/FriendReposity.cs @@ -0,0 +1,46 @@ +using ContactService.Domain; +using ContactService.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace ContactService.Infrastructure +{ + public class FriendReposity : IFriendReposity + { + private readonly ContactDbContext db; + + public FriendReposity(ContactDbContext db) + { + this.db = db; + } + + public async Task CheckOwnerIdAndTargetIdAsync(Guid ownerId, Guid targetId) + { + var exist = await db.Friends.AnyAsync(x => x.Owner.Id == ownerId && x.Target.Id == targetId); + return exist; + } + + public async Task CreateAsync(Friend friend) + { + db.Add(friend); + return friend; + } + + public Task FindByIdAsync(Guid id) + { + return db.Friends.FirstOrDefaultAsync(x => x.Id == id); + } + + public Task FindByOwnerAndTargetAsync(Guid ownerId, Guid targetId) + { + return db.Friends.FirstOrDefaultAsync(x => x.Owner.Id == ownerId && x.Target.Id == targetId); + } + public async Task> FindByTargetAsync(Guid targetId) + { + return await db.Friends.Where(x => x.Target.Id == targetId).ToListAsync(); + } + public async Task> FindByOwnerAsync(Guid ownerId) + { + return await db.Friends.Where(x => x.Owner.Id == ownerId).ToListAsync(); + } + } +} diff --git a/ContactService.Infrastructure/FriendRequestReposity.cs b/ContactService.Infrastructure/FriendRequestReposity.cs new file mode 100644 index 0000000..989670d --- /dev/null +++ b/ContactService.Infrastructure/FriendRequestReposity.cs @@ -0,0 +1,37 @@ +using ContactService.Domain; +using ContactService.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace ContactService.Infrastructure +{ + public class FriendRequestReposity : IFriendRequestReposity + { + private readonly ContactDbContext db; + + public FriendRequestReposity(ContactDbContext db) + { + this.db = db; + } + + public async Task CreateAsync(FriendRequest friendRequest) + { + db.Add(friendRequest); + return true; + } + + public Task FindByIdAsync(Guid id) + { + return db.FriendRequests.FirstOrDefaultAsync(x => x.Id == id); + } + + public async Task> FindByOwnerIdAsync(Guid ownerId) + { + return await db.FriendRequests.Where(x => x.OwnerId == ownerId).ToListAsync(); + } + + public async Task> FindByTargetIdAsync(Guid targetId) + { + return await db.FriendRequests.Where(x => x.TargetId == targetId).ToListAsync(); + } + } +} diff --git a/ContactService.Infrastructure/Migrations/20260413114340_InitDb.Designer.cs b/ContactService.Infrastructure/Migrations/20260413114340_InitDb.Designer.cs new file mode 100644 index 0000000..25d48dc --- /dev/null +++ b/ContactService.Infrastructure/Migrations/20260413114340_InitDb.Designer.cs @@ -0,0 +1,161 @@ +// +using System; +using ContactService.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ContactService.Infrastructure.Migrations +{ + [DbContext(typeof(ContactDbContext))] + [Migration("20260413114340_InitDb")] + partial class InitDb + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("ContactService.Domain.Entities.Friend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("RemarkName") + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("friends", (string)null); + }); + + modelBuilder.Entity("ContactService.Domain.Entities.FriendRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("OwnerId") + .HasColumnType("char(36)"); + + b.Property("RemarkName") + .HasColumnType("longtext"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("TargetId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId", "TargetId"); + + b.ToTable("friend_requests", (string)null); + }); + + modelBuilder.Entity("ContactService.Domain.Entities.Friend", b => + { + b.OwnsOne("ContactService.Domain.ValueObjects.UserProfile", "Owner", b1 => + { + b1.Property("FriendId") + .HasColumnType("char(36)"); + + b1.Property("Avatar") + .HasColumnType("longtext") + .HasColumnName("OwnerAvatarUrl"); + + b1.Property("Id") + .HasColumnType("char(36)") + .HasColumnName("OwnerId"); + + b1.Property("NickName") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("OwnerNickName"); + + b1.HasKey("FriendId"); + + b1.ToTable("friends"); + + b1.WithOwner() + .HasForeignKey("FriendId"); + }); + + b.OwnsOne("ContactService.Domain.ValueObjects.UserProfile", "Target", b1 => + { + b1.Property("FriendId") + .HasColumnType("char(36)"); + + b1.Property("Avatar") + .HasColumnType("longtext") + .HasColumnName("TargetAvatarUrl"); + + b1.Property("Id") + .HasColumnType("char(36)") + .HasColumnName("TargetId"); + + b1.Property("NickName") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("TargetNickName"); + + b1.HasKey("FriendId"); + + b1.ToTable("friends"); + + b1.WithOwner() + .HasForeignKey("FriendId"); + }); + + b.Navigation("Owner") + .IsRequired(); + + b.Navigation("Target") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ContactService.Infrastructure/Migrations/20260413114340_InitDb.cs b/ContactService.Infrastructure/Migrations/20260413114340_InitDb.cs new file mode 100644 index 0000000..2ed78db --- /dev/null +++ b/ContactService.Infrastructure/Migrations/20260413114340_InitDb.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ContactService.Infrastructure.Migrations +{ + /// + public partial class InitDb : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "friend_requests", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OwnerId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + TargetId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Description = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + State = table.Column(type: "int", nullable: false), + RemarkName = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + IsDeleted = table.Column(type: "tinyint(1)", nullable: false), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + Deletion = table.Column(type: "datetime(6)", nullable: true), + ModificationTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_friend_requests", x => x.Id); + }) + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "friends", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OwnerId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OwnerNickName = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + OwnerAvatarUrl = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + TargetId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + TargetNickName = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + TargetAvatarUrl = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + RemarkName = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + Status = table.Column(type: "int", nullable: false), + IsDeleted = table.Column(type: "tinyint(1)", nullable: false), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + Deletion = table.Column(type: "datetime(6)", nullable: true), + ModificationTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_friends", x => x.Id); + }) + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_friend_requests_OwnerId_TargetId", + table: "friend_requests", + columns: new[] { "OwnerId", "TargetId" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "friend_requests"); + + migrationBuilder.DropTable( + name: "friends"); + } + } +} diff --git a/ContactService.Infrastructure/Migrations/ContactDbContextModelSnapshot.cs b/ContactService.Infrastructure/Migrations/ContactDbContextModelSnapshot.cs new file mode 100644 index 0000000..ce604d5 --- /dev/null +++ b/ContactService.Infrastructure/Migrations/ContactDbContextModelSnapshot.cs @@ -0,0 +1,158 @@ +// +using System; +using ContactService.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ContactService.Infrastructure.Migrations +{ + [DbContext(typeof(ContactDbContext))] + partial class ContactDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("ContactService.Domain.Entities.Friend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("RemarkName") + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("friends", (string)null); + }); + + modelBuilder.Entity("ContactService.Domain.Entities.FriendRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("OwnerId") + .HasColumnType("char(36)"); + + b.Property("RemarkName") + .HasColumnType("longtext"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("TargetId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId", "TargetId"); + + b.ToTable("friend_requests", (string)null); + }); + + modelBuilder.Entity("ContactService.Domain.Entities.Friend", b => + { + b.OwnsOne("ContactService.Domain.ValueObjects.UserProfile", "Owner", b1 => + { + b1.Property("FriendId") + .HasColumnType("char(36)"); + + b1.Property("Avatar") + .HasColumnType("longtext") + .HasColumnName("OwnerAvatarUrl"); + + b1.Property("Id") + .HasColumnType("char(36)") + .HasColumnName("OwnerId"); + + b1.Property("NickName") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("OwnerNickName"); + + b1.HasKey("FriendId"); + + b1.ToTable("friends"); + + b1.WithOwner() + .HasForeignKey("FriendId"); + }); + + b.OwnsOne("ContactService.Domain.ValueObjects.UserProfile", "Target", b1 => + { + b1.Property("FriendId") + .HasColumnType("char(36)"); + + b1.Property("Avatar") + .HasColumnType("longtext") + .HasColumnName("TargetAvatarUrl"); + + b1.Property("Id") + .HasColumnType("char(36)") + .HasColumnName("TargetId"); + + b1.Property("NickName") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("TargetNickName"); + + b1.HasKey("FriendId"); + + b1.ToTable("friends"); + + b1.WithOwner() + .HasForeignKey("FriendId"); + }); + + b.Navigation("Owner") + .IsRequired(); + + b.Navigation("Target") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ContactService.Infrastructure/ModuleInit.cs b/ContactService.Infrastructure/ModuleInit.cs new file mode 100644 index 0000000..e38598e --- /dev/null +++ b/ContactService.Infrastructure/ModuleInit.cs @@ -0,0 +1,17 @@ +using ContactService.Domain; +using IM.Commons; +using Microsoft.Extensions.DependencyInjection; + +namespace ContactService.Infrastructure +{ + public class ModuleInit : IModuleInitializer + { + public void Initialize(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + } +} diff --git a/ContactService.WebApi/Application/Dtos/FriendRequestResponse.cs b/ContactService.WebApi/Application/Dtos/FriendRequestResponse.cs new file mode 100644 index 0000000..196ff67 --- /dev/null +++ b/ContactService.WebApi/Application/Dtos/FriendRequestResponse.cs @@ -0,0 +1,38 @@ +using ContactService.Domain; + +namespace ContactService.WebApi.Application.Dtos +{ + public class FriendRequestResponse + { + public Guid Id { get; private set; } + /// + /// 申请人 + /// + public Guid OwnerId { get; private set; } + + /// + /// 被申请人 + /// + public Guid TargetId { get; private set; } + + + /// + /// 申请附言 + /// + public string Description { get; private set; } + + /// + /// 申请状态(0:待通过,1:拒绝,2:同意,3:拉黑) + /// + public FriendRequestStatus State { get; private set; } + + /// + /// 备注 + /// + public string? RemarkName { get; private set; } + public DateTimeOffset CreationTime { get; private set; } + public DateTimeOffset? Deletion { get; private set; } + + public DateTimeOffset? ModificationTime { get; private set; } + } +} diff --git a/ContactService.WebApi/Application/Dtos/FriendResonse.cs b/ContactService.WebApi/Application/Dtos/FriendResonse.cs new file mode 100644 index 0000000..6c2a62f --- /dev/null +++ b/ContactService.WebApi/Application/Dtos/FriendResonse.cs @@ -0,0 +1,20 @@ +using ContactService.Domain; + +namespace ContactService.WebApi.Application.Dtos +{ + public class FriendResonse + { + public Guid Id { get; private set; } + public Guid TargetId { get; private set; } + public string? Avatar { get; private set; } + public string NickName { get; private set; } + /// + /// 好友备注名 + /// + public string? RemarkName { get; private set; } + + public DateTime CreateTime { get; private set; } + public DateTime? UpdateTime { get; private set; } + public FriendStatus Status { get; private set; } + } +} diff --git a/ContactService.WebApi/Application/Dtos/UserInfoDto.cs b/ContactService.WebApi/Application/Dtos/UserInfoDto.cs new file mode 100644 index 0000000..30e7972 --- /dev/null +++ b/ContactService.WebApi/Application/Dtos/UserInfoDto.cs @@ -0,0 +1,16 @@ +namespace ContactService.WebApi.Application.Dtos +{ + public class UserInfoDto + { + public Guid Id { get; set; } + public string UserName { get; set; } + public string NickName { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + public string Region { get; set; } + public string Description { get; set; } + public string? Avatar { get; set; } + public DateTimeOffset CreationTime { get; set; } + public DateTimeOffset? Deletion { get; set; } + } +} diff --git a/ContactService.WebApi/Application/EventHandler/FriendAddedHandler.cs b/ContactService.WebApi/Application/EventHandler/FriendAddedHandler.cs new file mode 100644 index 0000000..699f841 --- /dev/null +++ b/ContactService.WebApi/Application/EventHandler/FriendAddedHandler.cs @@ -0,0 +1,32 @@ +using ContactService.Domain.Events; +using IM.Commons.IntegrationEvents; +using MassTransit; +using MediatR; + +namespace ContactService.WebApi.Application.EventHandler +{ + public class FriendAddedHandler : INotificationHandler + { + private readonly IPublishEndpoint endpoint; + + public FriendAddedHandler(IPublishEndpoint endpoint) + { + this.endpoint = endpoint; + } + + public async Task Handle(FriendAddedDomainEvent notification, CancellationToken cancellationToken) + { + await endpoint.Publish(new FriendAddedEvent + { + OwnerAvatar = notification.Friend.Owner.Avatar, + OwnerId = notification.Friend.Owner.Id, + OwnerNickName = notification.Friend.Owner.NickName, + TargetAvatar = notification.Friend.Target.Avatar, + TargetId = notification.Friend.Target.Id, + TargetNickName = notification.Friend.Target.NickName, + RemarkName = notification.Friend.RemarkName, + Status = notification.Friend.Status.ToString(), + }, cancellationToken); + } + } +} diff --git a/ContactService.WebApi/Application/EventHandler/FriendRequestStatusUpdateHandler.cs b/ContactService.WebApi/Application/EventHandler/FriendRequestStatusUpdateHandler.cs new file mode 100644 index 0000000..12a5cd8 --- /dev/null +++ b/ContactService.WebApi/Application/EventHandler/FriendRequestStatusUpdateHandler.cs @@ -0,0 +1,58 @@ +using ContactService.Domain; +using ContactService.Domain.Events; +using ContactService.Domain.ValueObjects; +using ContactService.Infrastructure; +using ContactService.WebApi.Application.IntegrationServices; +using IM.Commons.IntegrationEvents; +using MassTransit; +using MediatR; + +namespace ContactService.WebApi.Application.EventHandler +{ + public class FriendRequestStatusUpdateHandler : INotificationHandler + { + private readonly FriendDomainService friendService; + private readonly ContactDbContext contactDb; + private readonly IPublishEndpoint endpoint; + private readonly IIdentityIntegrationService idService; + + public FriendRequestStatusUpdateHandler(FriendDomainService friendService, ContactDbContext contactDb, IPublishEndpoint endpoint, IIdentityIntegrationService idService) + { + this.friendService = friendService; + this.contactDb = contactDb; + this.endpoint = endpoint; + this.idService = idService; + } + + public async Task Handle(FriendRequestStateUpdateDomainEvent notification, CancellationToken cancellationToken) + { + var @event = notification.Request; + if (@event.State == FriendRequestStatus.Passed) + { + var ownerInfo = await idService.FindUserByIdAsync(@event.OwnerId); + var targetInfo = await idService.FindUserByIdAsync(@event.TargetId); + + if (!ownerInfo.Succeeded || !targetInfo.Succeeded) + { + return; + } + + var ownerProfile = new UserProfile(ownerInfo.Data.Id, ownerInfo.Data.NickName, ownerInfo.Data.Avatar); + var targetProfile = new UserProfile(targetInfo.Data.Id, targetInfo.Data.NickName, targetInfo.Data.Avatar); + + await friendService.CreateAsync(ownerProfile, targetProfile, @event.RemarkName); + await friendService.CreateAsync(targetProfile, ownerProfile, notification.AcceptRemarkName); + await contactDb.SaveChangesAsync(cancellationToken); + } + + await endpoint.Publish(new FriendRequestStateUpdateEvent + { + CorrelationId = @event.TargetId, + Description = @event.Description, + OwnerId = @event.OwnerId, + RemarkName = @event.RemarkName, + State = @event.State.ToString() + }); + } + } +} diff --git a/ContactService.WebApi/Application/EventHandler/UserProfileUpdateHandler.cs b/ContactService.WebApi/Application/EventHandler/UserProfileUpdateHandler.cs new file mode 100644 index 0000000..2da73a4 --- /dev/null +++ b/ContactService.WebApi/Application/EventHandler/UserProfileUpdateHandler.cs @@ -0,0 +1,35 @@ +using ContactService.Domain; +using ContactService.Infrastructure; +using IM.Commons.IntegrationEvents; +using MassTransit; + +namespace ContactService.WebApi.Application.EventHandler +{ + public class UserProfileUpdateHandler : IConsumer + { + private readonly ContactDbContext contactDb; + private readonly IFriendReposity reposity; + + public UserProfileUpdateHandler(ContactDbContext contactDb, IFriendReposity reposity) + { + this.contactDb = contactDb; + this.reposity = reposity; + } + + public async Task Consume(ConsumeContext context) + { + var @event = context.Message; + var friends = await reposity.FindByTargetAsync(@event.UserId); + + foreach (var friend in friends) + { + friend.UpdateUserInfo( + new Domain.ValueObjects.UserProfile( + @event.UserId, @event.Avatar, @event.NickName)); + } + + await contactDb.SaveChangesAsync(); + + } + } +} diff --git a/ContactService.WebApi/Application/Friend/FriendMapperConfig.cs b/ContactService.WebApi/Application/Friend/FriendMapperConfig.cs new file mode 100644 index 0000000..ec1105c --- /dev/null +++ b/ContactService.WebApi/Application/Friend/FriendMapperConfig.cs @@ -0,0 +1,23 @@ +using AutoMapper; +using ContactService.WebApi.Application.Dtos; +using Google.Protobuf.WellKnownTypes; + +namespace ContactService.WebApi.Application.Friend +{ + public class FriendMapperConfig : Profile + { + public FriendMapperConfig() + { + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.Avatar, opt => opt.MapFrom(src => src.Target.Avatar)) + .ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Status)) + .ForMember(dest => dest.UpdateTime, opt => opt.MapFrom(src => src.ModificationTime.Value.DateTime)) + .ForMember(dest => dest.CreateTime, opt => opt.MapFrom(src => src.CreationTime.DateTime)) + .ForMember(dest => dest.NickName, opt => opt.MapFrom(src => src.Target.NickName)) + .ForMember(dest => dest.RemarkName, opt => opt.MapFrom(src => src.RemarkName)) + .ForMember(dest => dest.TargetId, opt => opt.MapFrom(src => src.Target.Id)) + ; + } + } +} diff --git a/ContactService.WebApi/Application/Friend/FriendService.cs b/ContactService.WebApi/Application/Friend/FriendService.cs new file mode 100644 index 0000000..7647e10 --- /dev/null +++ b/ContactService.WebApi/Application/Friend/FriendService.cs @@ -0,0 +1,73 @@ +using AutoMapper; +using ContactService.Domain; +using ContactService.WebApi.Application.Dtos; +using ContactService.WebApi.Application.IntegrationServices; +using IM.Commons; + +namespace ContactService.WebApi.Application.Friend +{ + public class FriendService + { + private readonly IFriendReposity reposity; + private readonly FriendDomainService service; + private readonly IIdentityIntegrationService idService; + private readonly IMapper mapper; + + public FriendService(IFriendReposity reposity, FriendDomainService service + , IIdentityIntegrationService idService, IMapper mapper + ) + { + this.reposity = reposity; + this.service = service; + this.idService = idService; + this.mapper = mapper; + } + + public async Task>> GetFriendsByOwnerIdAsync(Guid ownerId) + { + IEnumerable friend = await reposity.FindByOwnerAsync(ownerId); + return Result>.Success(mapper.Map>(friend.ToList())); + } + + public async Task> DeleteFriendAsync(Guid userId, Guid friendId) + { + var friend = await reposity.FindByIdAsync(friendId); + if (friend is null) + { + return Result.Fail(ResultCode.FRIEND_RELATION_NOT_FOUND); + } + + if (friend.Owner.Id != userId) + { + return Result.Fail(ResultCode.FRIEND_RELATION_NOT_FOUND); + } + + friend.SoftDelete(); + + return Result.Success(); + } + + public async Task> BlockFriendAsync(Guid userId, Guid friendId) + { + var friend = await reposity.FindByIdAsync(friendId); + if (friend is null) + { + return Result.Fail(ResultCode.FRIEND_RELATION_NOT_FOUND); + } + + if (friend.Owner.Id != userId) + { + return Result.Fail(ResultCode.PERMISSION_DENIED); + } + + friend.Block(); + return Result.Success(); + } + + public async Task> CheckFriendAsync(Guid ownerId, Guid targetId) + { + var exist = await reposity.CheckOwnerIdAndTargetIdAsync(ownerId, targetId); + return Result.Success(exist); + } + } +} diff --git a/ContactService.WebApi/Application/FriendRequest/CreateFriendRequestCommand.cs b/ContactService.WebApi/Application/FriendRequest/CreateFriendRequestCommand.cs new file mode 100644 index 0000000..08c50a4 --- /dev/null +++ b/ContactService.WebApi/Application/FriendRequest/CreateFriendRequestCommand.cs @@ -0,0 +1,18 @@ +namespace ContactService.WebApi.Application.FriendRequest +{ + public record CreateFriendRequestCommand + { + public Guid ownerId { get; private set; } + public Guid targetId { get; private set; } + public string? description { get; private set; } + public string? remarkName { get; private set; } + + public CreateFriendRequestCommand(Guid ownerId, Guid targetId, string? description, string? remarkName) + { + this.ownerId = ownerId; + this.targetId = targetId; + this.description = description; + this.remarkName = remarkName; + } + } +} diff --git a/ContactService.WebApi/Application/FriendRequest/FriendRequestConfig.cs b/ContactService.WebApi/Application/FriendRequest/FriendRequestConfig.cs new file mode 100644 index 0000000..3a27ebf --- /dev/null +++ b/ContactService.WebApi/Application/FriendRequest/FriendRequestConfig.cs @@ -0,0 +1,13 @@ +using AutoMapper; +using ContactService.WebApi.Application.Dtos; + +namespace ContactService.WebApi.Application.FriendRequest +{ + public class FriendRequestConfig : Profile + { + public FriendRequestConfig() + { + CreateMap(); + } + } +} diff --git a/ContactService.WebApi/Application/FriendRequest/FriendRequestHandleCommand.cs b/ContactService.WebApi/Application/FriendRequest/FriendRequestHandleCommand.cs new file mode 100644 index 0000000..93308bc --- /dev/null +++ b/ContactService.WebApi/Application/FriendRequest/FriendRequestHandleCommand.cs @@ -0,0 +1,25 @@ +namespace ContactService.WebApi.Application.FriendRequest +{ + public class FriendRequestHandleCommand + { + public Guid UserId { get; private set; } + public Guid RequestId { get; private set; } + public FriendRequestAction Action { get; private set; } + public string? RemarkName { get; set; } + + public FriendRequestHandleCommand(Guid userId, Guid requestId, FriendRequestAction action, string? remarkName) + { + UserId = userId; + RequestId = requestId; + Action = action; + RemarkName = remarkName; + } + } + + public enum FriendRequestAction + { + Accpet = 0, + Reject = 1, + Block = 2 + } +} diff --git a/ContactService.WebApi/Application/FriendRequest/FriendRequestService.cs b/ContactService.WebApi/Application/FriendRequest/FriendRequestService.cs new file mode 100644 index 0000000..3c7cc5d --- /dev/null +++ b/ContactService.WebApi/Application/FriendRequest/FriendRequestService.cs @@ -0,0 +1,81 @@ +using AutoMapper; +using ContactService.Domain; +using ContactService.WebApi.Application.Dtos; +using IM.Commons; + +namespace ContactService.WebApi.Application.FriendRequest +{ + public class FriendRequestService + { + private readonly IFriendRequestReposity reposity; + private readonly FriendRequestDomainService service; + private readonly IMapper mapper; + + public FriendRequestService(IFriendRequestReposity reposity, FriendRequestDomainService service, IMapper mapper) + { + this.reposity = reposity; + this.service = service; + this.mapper = mapper; + } + + public async Task> CreateAsync(CreateFriendRequestCommand command) + { + var request = await service.CreateAsync(command.ownerId, command.targetId, command.description, command.remarkName); + if (!request.Succeeded) + { + return Result.Fail(request); + } + return Result.Success(mapper.Map(request.Data)); + } + + public async Task> UpdateStatusAsync(FriendRequestHandleCommand command) + { + var request = await reposity.FindByIdAsync(command.RequestId); + if (request == null) + { + return Result.Fail(ResultCode.FRIEND_REQUEST_NOT_FOUND); + } + + if (request.TargetId != command.UserId) + { + return Result.Fail(ResultCode.PERMISSION_DENIED); + } + + switch (command.Action) + { + case FriendRequestAction.Accpet: + request.Accept(command.RemarkName); + break; + case FriendRequestAction.Block: + request.Block(); + break; + case FriendRequestAction.Reject: + request.Reject(); + break; + default: + return Result.Fail(ResultCode.PARAMETER_ERROR); + } + return Result.Success(mapper.Map(request)); + } + + public async Task>> GetByOwnerIdAsync(Guid ownerId) + { + var requests = await reposity.FindByOwnerIdAsync(ownerId); + return Result>.Success(mapper.Map>(requests)); + + } + + public async Task>> GetByTargetIdAsync(Guid targetId) + { + var requests = await reposity.FindByTargetIdAsync(targetId); + return Result>.Success(mapper.Map>(requests)); + } + + public async Task>> GetByTargetIdOrOwnerIdAsync(Guid id) + { + var requests = await reposity.FindByTargetIdAsync(id); + var requests2 = await reposity.FindByOwnerIdAsync(id); + return Result>.Success(mapper.Map>(requests.Concat(requests2))); + } + } +} diff --git a/ContactService.WebApi/Application/IntegrationServices/IIdentityIntegrationService.cs b/ContactService.WebApi/Application/IntegrationServices/IIdentityIntegrationService.cs new file mode 100644 index 0000000..4c89f3e --- /dev/null +++ b/ContactService.WebApi/Application/IntegrationServices/IIdentityIntegrationService.cs @@ -0,0 +1,10 @@ +using ContactService.WebApi.Application.Dtos; +using IM.Commons; + +namespace ContactService.WebApi.Application.IntegrationServices +{ + public interface IIdentityIntegrationService + { + Task> FindUserByIdAsync(Guid id); + } +} diff --git a/ContactService.WebApi/Application/IntegrationServices/IdentityIntegrationService.cs b/ContactService.WebApi/Application/IntegrationServices/IdentityIntegrationService.cs new file mode 100644 index 0000000..ccf2d4b --- /dev/null +++ b/ContactService.WebApi/Application/IntegrationServices/IdentityIntegrationService.cs @@ -0,0 +1,47 @@ +using ContactService.WebApi.Application.Dtos; +using Grpc.Core; +using IM.Commons; +using IM.Protocols.Grpc.User; + +namespace ContactService.WebApi.Application.IntegrationServices +{ + public class IdentityIntegrationService : IIdentityIntegrationService + { + private readonly UserInternal.UserInternalClient client; + + public IdentityIntegrationService(UserInternal.UserInternalClient client) + { + this.client = client; + } + + public async Task> FindUserByIdAsync(Guid id) + { + var req = new GetUserInfoRequest() + { + UserId = id.ToString() + }; + try + { + var res = await client.GetUserInfoAsyncAsync(req); + return Result.Success(new UserInfoDto + { + Avatar = res.Avatar, + CreationTime = res.CreationTime.ToDateTimeOffset(), + Deletion = res.Deletion.ToDateTimeOffset(), + Description = res.Description, + Email = res.Email, + Id = Guid.Parse(res.Id), + NickName = res.NickName, + Phone = res.Phone, + Region = res.Region, + UserName = res.UserName + }); + }catch(RpcException e) + { + return Result.Fail(ResultCode.USER_NOT_FOUND); + } + + + } + } +} diff --git a/ContactService.WebApi/ContactService.WebApi.csproj b/ContactService.WebApi/ContactService.WebApi.csproj new file mode 100644 index 0000000..e8b537f --- /dev/null +++ b/ContactService.WebApi/ContactService.WebApi.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/ContactService.WebApi/ContactService.WebApi.http b/ContactService.WebApi/ContactService.WebApi.http new file mode 100644 index 0000000..31e3cc1 --- /dev/null +++ b/ContactService.WebApi/ContactService.WebApi.http @@ -0,0 +1,7 @@ +@ContactService.WebApi_HostAddress = http://localhost:5294 + +GET {{ContactService.WebApi_HostAddress}}/weatherforecast/ +Accept: application/json +### + +GET {{ContactService.WebApi_HostAddress}} diff --git a/ContactService.WebApi/Controllers/FriendController.cs b/ContactService.WebApi/Controllers/FriendController.cs new file mode 100644 index 0000000..0778926 --- /dev/null +++ b/ContactService.WebApi/Controllers/FriendController.cs @@ -0,0 +1,55 @@ +using ContactService.Infrastructure; +using ContactService.WebApi.Application.Dtos; +using ContactService.WebApi.Application.Friend; +using IM.ASPNETCore; +using IM.Commons; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace ContactService.WebApi.Controllers +{ + [Authorize] + [Route("api/[controller]/[action]")] + [ApiController] + public class FriendController : ControllerBase + { + private readonly FriendService service; + + public FriendController(FriendService service) + { + this.service = service; + } + + [HttpGet] + [ProducesDefaultResponseType(typeof(Result))] + public async Task List() + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var friends = await service.GetFriendsByOwnerIdAsync(Guid.Parse(userId)); + return Ok(friends); + } + + [HttpPost] + [UnitOfWork(typeof(ContactDbContext))] + public async Task Delete([FromQuery] Guid friendId) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.DeleteFriendAsync(Guid.Parse(userId), friendId)); + } + + [HttpPost] + [UnitOfWork(typeof(ContactDbContext))] + public async Task Block([FromQuery] Guid friendId) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.BlockFriendAsync(Guid.Parse(userId), friendId)); + } + + [HttpGet] + public async Task CheckFriend(Guid userId, Guid targetId) + { + return Ok(await service.CheckFriendAsync(userId, targetId)); + } + } +} diff --git a/ContactService.WebApi/Controllers/FriendRequestAddRequest.cs b/ContactService.WebApi/Controllers/FriendRequestAddRequest.cs new file mode 100644 index 0000000..eb88cd1 --- /dev/null +++ b/ContactService.WebApi/Controllers/FriendRequestAddRequest.cs @@ -0,0 +1,24 @@ +using FluentValidation; + +namespace ContactService.WebApi.Controllers +{ + public class FriendRequestAddRequest + { + public Guid TargetId { get; set; } + public string? Description { get; set; } + public string? RemarkName { get; set; } + + } + + public class FriendRequestAddRequestValidator : AbstractValidator + { + public FriendRequestAddRequestValidator() + { + RuleFor(r => r.TargetId) + .NotEmpty() + .NotNull() + ; + + } + } +} diff --git a/ContactService.WebApi/Controllers/FriendRequestController.cs b/ContactService.WebApi/Controllers/FriendRequestController.cs new file mode 100644 index 0000000..75de55e --- /dev/null +++ b/ContactService.WebApi/Controllers/FriendRequestController.cs @@ -0,0 +1,48 @@ +using ContactService.Infrastructure; +using ContactService.WebApi.Application.Dtos; +using ContactService.WebApi.Application.FriendRequest; +using IM.ASPNETCore; +using IM.Commons; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace ContactService.WebApi.Controllers +{ + [Authorize] + [Route("api/[controller]/[action]")] + [ApiController] + public class FriendRequestController : ControllerBase + { + private readonly FriendRequestService service; + + public FriendRequestController(FriendRequestService service) + { + this.service = service; + } + + [HttpPost] + [UnitOfWork(typeof(ContactDbContext))] + [ProducesDefaultResponseType(typeof(Result))] + public async Task Add(FriendRequestAddRequest request) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.CreateAsync(new CreateFriendRequestCommand(Guid.Parse(userId), request.TargetId, request.Description, request.RemarkName))); + } + + [HttpPost] + [UnitOfWork(typeof(ContactDbContext))] + public async Task Handle(FriendRequestHandleRequest request) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.UpdateStatusAsync(new FriendRequestHandleCommand(Guid.Parse(userId), request.RequestId, request.Action,request.RemarkName))); + } + + [HttpGet] + public async Task List() + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.GetByTargetIdOrOwnerIdAsync(Guid.Parse(userId))); + } + } +} diff --git a/ContactService.WebApi/Controllers/FriendRequestHandleRequest.cs b/ContactService.WebApi/Controllers/FriendRequestHandleRequest.cs new file mode 100644 index 0000000..20befac --- /dev/null +++ b/ContactService.WebApi/Controllers/FriendRequestHandleRequest.cs @@ -0,0 +1,29 @@ +using ContactService.WebApi.Application.FriendRequest; +using FluentValidation; + +namespace ContactService.WebApi.Controllers +{ + public class FriendRequestHandleRequest + { + public Guid RequestId { get; set; } + public FriendRequestAction Action { get; set; } + public string? RemarkName { get; set; } + } + + public class FriendRequestHandleRequestValidator : AbstractValidator + { + public FriendRequestHandleRequestValidator() + { + RuleFor(r => r.RequestId) + .NotEmpty() + .NotNull(); + + When(w => w.Action == FriendRequestAction.Accpet, () => + { + RuleFor(r => r.RemarkName) + .NotEmpty() + .NotNull(); + }); + } + } +} diff --git a/ContactService.WebApi/DesignTimeDbContextFactory.cs b/ContactService.WebApi/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..b09ae98 --- /dev/null +++ b/ContactService.WebApi/DesignTimeDbContextFactory.cs @@ -0,0 +1,18 @@ +using ContactService.Infrastructure; +using IM.InitCommon; +using Microsoft.EntityFrameworkCore.Design; + +namespace ContactService.WebApi +{ + public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory + { + public ContactDbContext CreateDbContext(string[] args) + { + // 1. 复用你写好的配置工厂,提取连接字符串 + var optionsBuilder = DbContextOptionsBuilderFactory.Create(); + + // 2. 🌟 关键补刀:把假的 Mediator 传进去,满足构造函数的要求! + return new ContactDbContext(optionsBuilder.Options, null); + } + } +} diff --git a/ContactService.WebApi/ModuleInit.cs b/ContactService.WebApi/ModuleInit.cs new file mode 100644 index 0000000..e624113 --- /dev/null +++ b/ContactService.WebApi/ModuleInit.cs @@ -0,0 +1,24 @@ +using ContactService.WebApi.Application.Friend; +using ContactService.WebApi.Application.FriendRequest; +using ContactService.WebApi.Application.IntegrationServices; +using IM.Commons; +using IM.Protocols.Grpc.User; +using Microsoft.Extensions.Options; + +namespace ContactService.WebApi +{ + public class ModuleInit : IModuleInitializer + { + public void Initialize(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddGrpcClient((sp, o) => + { + var options = sp.GetRequiredService>(); + o.Address = new Uri(options.CurrentValue.IdentityServiceUrl); + }); + } + } +} diff --git a/ContactService.WebApi/Program.cs b/ContactService.WebApi/Program.cs new file mode 100644 index 0000000..d2abc9e --- /dev/null +++ b/ContactService.WebApi/Program.cs @@ -0,0 +1,42 @@ + +using IM.InitCommon; + +namespace ContactService.WebApi +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + + builder.ConfigureDbConfiguration(); + + //builder.Services.AddControllers(); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + builder.ConfigExtraServices(); + + builder.Services.AddAllGrpcServer(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseAppDefault(); + + + app.MapControllers(); + app.MapAllGrpcServer(); + app.Run(); + } + } +} diff --git a/ContactService.WebApi/Properties/launchSettings.json b/ContactService.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..3544c49 --- /dev/null +++ b/ContactService.WebApi/Properties/launchSettings.json @@ -0,0 +1,49 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5294" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7242;http://localhost:5294" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:46536", + "sslPort": 44317 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iissettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:53560/", + "sslPort": 44389 + } + } +} \ No newline at end of file diff --git a/ContactService.WebApi/Services/ContactintegrationService.cs b/ContactService.WebApi/Services/ContactintegrationService.cs new file mode 100644 index 0000000..be5f421 --- /dev/null +++ b/ContactService.WebApi/Services/ContactintegrationService.cs @@ -0,0 +1,35 @@ +using ContactService.WebApi.Application.Friend; +using Grpc.Core; +using IM.Protocols.Grpc.Contact; + +namespace ContactService.WebApi.Services +{ + public class ContactintegrationService: ContactInternal.ContactInternalBase + { + private readonly Application.Friend.FriendService friendService; + + public ContactintegrationService(FriendService friendService) + { + this.friendService = friendService; + } + + public override async Task CheckFriendship(CheckFriendshipRequest request, ServerCallContext context) + { + var response = new CheckFriendshipResponse(); + var res = await friendService.CheckFriendAsync( + Guid.Parse(request.OwnerId), + Guid.Parse(request.TargetId) + ); + + if(res.Succeeded && res.Data) + { + response.Checked = true; + } + else + { + response.Checked = false; + } + return response; + } + } +} diff --git a/ContactService.WebApi/appsettings.Development.json b/ContactService.WebApi/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/ContactService.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ContactService.WebApi/appsettings.json b/ContactService.WebApi/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/ContactService.WebApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/DomainCommons/AggregateRootEntity.cs b/DomainCommons/AggregateRootEntity.cs new file mode 100644 index 0000000..31fa2e7 --- /dev/null +++ b/DomainCommons/AggregateRootEntity.cs @@ -0,0 +1,24 @@ +namespace IM.DomainCommons +{ + public class AggregateRootEntity : BaseEntity, IAggregateRoot, ISoftDelete, IHasCreationTime, IHasDeletionTime, IHasModificationTime + { + public bool IsDeleted { get; private set; } + + public DateTimeOffset CreationTime { get; private set; } = DateTime.Now; + + public DateTimeOffset? Deletion { get; private set; } + + public DateTimeOffset? ModificationTime { get; set; } + + public void SoftDelete() + { + Deletion = DateTime.Now; + IsDeleted = true; + } + + public void NotifyModified() + { + ModificationTime = DateTime.Now; + } + } +} diff --git a/DomainCommons/BaseEntity.cs b/DomainCommons/BaseEntity.cs new file mode 100644 index 0000000..b1e60ef --- /dev/null +++ b/DomainCommons/BaseEntity.cs @@ -0,0 +1,40 @@ +using MassTransit; +using MediatR; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IM.DomainCommons +{ + public class BaseEntity : IEntity, IDomainEvents + { + /// + /// 这里使用连续guid,防止数据库性能问题 + /// + public Guid Id { get; private set; } = NewId.NextGuid(); + + [NotMapped] + public List domainEvents = []; + + public void AddDomainEvent(INotification eventItem) + { + domainEvents.Add(eventItem); + } + + public void AddDomainEventIfAbsent(INotification eventItem) + { + if (!domainEvents.Contains(eventItem)) + { + domainEvents.Add(eventItem); + } + } + + public void ClearDomainEvents() + { + domainEvents.Clear(); + } + + public IEnumerable GetDomainEvents() + { + return domainEvents; + } + } +} diff --git a/DomainCommons/DomainException.cs b/DomainCommons/DomainException.cs new file mode 100644 index 0000000..3ce5673 --- /dev/null +++ b/DomainCommons/DomainException.cs @@ -0,0 +1,10 @@ +namespace IM.DomainCommons +{ + public class DomainException : Exception + { + public DomainException(string message) : base(message) + { + + } + } +} diff --git a/DomainCommons/IAggregateRoot.cs b/DomainCommons/IAggregateRoot.cs new file mode 100644 index 0000000..998fa19 --- /dev/null +++ b/DomainCommons/IAggregateRoot.cs @@ -0,0 +1,6 @@ +namespace IM.DomainCommons +{ + public interface IAggregateRoot + { + } +} diff --git a/DomainCommons/IDomainEvents.cs b/DomainCommons/IDomainEvents.cs new file mode 100644 index 0000000..f124c24 --- /dev/null +++ b/DomainCommons/IDomainEvents.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace IM.DomainCommons +{ + public interface IDomainEvents + { + IEnumerable GetDomainEvents(); + void AddDomainEvent(INotification eventItem); + void AddDomainEventIfAbsent(INotification eventItem); + void ClearDomainEvents(); + } +} diff --git a/DomainCommons/IEntity.cs b/DomainCommons/IEntity.cs new file mode 100644 index 0000000..5660185 --- /dev/null +++ b/DomainCommons/IEntity.cs @@ -0,0 +1,7 @@ +namespace IM.DomainCommons +{ + public interface IEntity + { + Guid Id { get; } + } +} diff --git a/DomainCommons/IHasCreationTime.cs b/DomainCommons/IHasCreationTime.cs new file mode 100644 index 0000000..fcba727 --- /dev/null +++ b/DomainCommons/IHasCreationTime.cs @@ -0,0 +1,7 @@ +namespace IM.DomainCommons +{ + public interface IHasCreationTime + { + DateTimeOffset CreationTime { get; } + } +} diff --git a/DomainCommons/IHasDeletionTime.cs b/DomainCommons/IHasDeletionTime.cs new file mode 100644 index 0000000..88de6fd --- /dev/null +++ b/DomainCommons/IHasDeletionTime.cs @@ -0,0 +1,7 @@ +namespace IM.DomainCommons +{ + public interface IHasDeletionTime + { + DateTimeOffset? Deletion { get; } + } +} diff --git a/DomainCommons/IHasModificationTime.cs b/DomainCommons/IHasModificationTime.cs new file mode 100644 index 0000000..785a086 --- /dev/null +++ b/DomainCommons/IHasModificationTime.cs @@ -0,0 +1,7 @@ +namespace IM.DomainCommons +{ + public interface IHasModificationTime + { + DateTimeOffset? ModificationTime { get; } + } +} diff --git a/DomainCommons/IM.DomainCommons.csproj b/DomainCommons/IM.DomainCommons.csproj new file mode 100644 index 0000000..efef302 --- /dev/null +++ b/DomainCommons/IM.DomainCommons.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/DomainCommons/ISoftDelete.cs b/DomainCommons/ISoftDelete.cs new file mode 100644 index 0000000..6ae2122 --- /dev/null +++ b/DomainCommons/ISoftDelete.cs @@ -0,0 +1,8 @@ +namespace IM.DomainCommons +{ + public interface ISoftDelete + { + bool IsDeleted { get; } + void SoftDelete(); + } +} diff --git a/FileService.Domain/Entities/UploadFile.cs b/FileService.Domain/Entities/UploadFile.cs new file mode 100644 index 0000000..d66b242 --- /dev/null +++ b/FileService.Domain/Entities/UploadFile.cs @@ -0,0 +1,43 @@ +using FileService.Domain.ValueObjects; +using IM.DomainCommons; + +namespace FileService.Domain.Entities +{ + public class UploadFile:AggregateRootEntity + { + public Guid OwnerId { get; private set; } + public FileName FileName { get; private set; } + public long FileSize { get;private set; } + public ContentType ContentType { get;private set; } + public FileState State { get; private set; } + public StorageLocation? StorageLocation { get; private set; } + public CheckSum CheckSum { get; private set; } + + private UploadFile() { } + public UploadFile(Guid ownerId, string fileName, string contentType) + { + OwnerId = ownerId; + FileName = fileName; + ContentType = contentType; + State = FileState.Created; + } + + public void StartUpload() + { + State = FileState.Uploading; + } + + public void CompleteUpload(StorageLocation storage, CheckSum checkSum) + { + StorageLocation = storage; + CheckSum = checkSum; + State = FileState.Uploaded; + } + + public void Fail() + { + + } + + } +} diff --git a/FileService.Domain/FileService.Domain.csproj b/FileService.Domain/FileService.Domain.csproj new file mode 100644 index 0000000..8b3eb85 --- /dev/null +++ b/FileService.Domain/FileService.Domain.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/FileService.Domain/FileState.cs b/FileService.Domain/FileState.cs new file mode 100644 index 0000000..2cb2aed --- /dev/null +++ b/FileService.Domain/FileState.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FileService.Domain +{ + public enum FileState + { + Created = 0, + Uploading = 1, + Uploaded = 2, + Failed = 3, + Deleted = 4 + } +} diff --git a/FileService.Domain/ValueObjects/CheckSum.cs b/FileService.Domain/ValueObjects/CheckSum.cs new file mode 100644 index 0000000..8d391ce --- /dev/null +++ b/FileService.Domain/ValueObjects/CheckSum.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FileService.Domain.ValueObjects +{ + public class CheckSum + { + public string Algorithm { get; } + public string Value { get; } + + public CheckSum(string algorithm, string value) + { + Algorithm = algorithm; + Value = value; + } + + public override string ToString() + { + return Value; + } + + public static implicit operator CheckSum((string algorithm, string value) value) + { + return new CheckSum(value.algorithm, value.value); + } + } +} diff --git a/FileService.Domain/ValueObjects/ContentType.cs b/FileService.Domain/ValueObjects/ContentType.cs new file mode 100644 index 0000000..56c9ea9 --- /dev/null +++ b/FileService.Domain/ValueObjects/ContentType.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FileService.Domain.ValueObjects +{ + public class ContentType + { + public string Value { get; } + public ContentType(string contentType) + { + Value = contentType; + } + + public override string ToString() + { + return Value; + } + + public static implicit operator ContentType(string contentType) + { + return new ContentType(contentType); + } + } +} diff --git a/FileService.Domain/ValueObjects/FileName.cs b/FileService.Domain/ValueObjects/FileName.cs new file mode 100644 index 0000000..cd086de --- /dev/null +++ b/FileService.Domain/ValueObjects/FileName.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FileService.Domain.ValueObjects +{ + public class FileName + { + public string Value { get; } + + public FileName(string value) + { + if(value.Length > 20) + { + throw new ArgumentException("文件名超出长度"); + } + + Value = value; + } + + public override string ToString() + { + return Value; + } + + public static implicit operator FileName(string value) + { + return new FileName(value); + } + + } +} diff --git a/FileService.Domain/ValueObjects/StorageLocation.cs b/FileService.Domain/ValueObjects/StorageLocation.cs new file mode 100644 index 0000000..b776c8b --- /dev/null +++ b/FileService.Domain/ValueObjects/StorageLocation.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FileService.Domain.ValueObjects +{ + public class StorageLocation + { + public string StorageProvider { get; private set; } + public string Bucket { get; private set; } + public string ObjectKey { get; private set; } + public string? Region { get; private set; } + + public StorageLocation(string storageProvider, string bucket, string objectKey, string? region = null) + { + this.StorageProvider = storageProvider; + this.Bucket = bucket; + this.ObjectKey = objectKey; + this.Region = region; + } + } +} diff --git a/GroupService.Domain/Entities/Group.cs b/GroupService.Domain/Entities/Group.cs new file mode 100644 index 0000000..37c3cdb --- /dev/null +++ b/GroupService.Domain/Entities/Group.cs @@ -0,0 +1,116 @@ +using GroupService.Domain.Enums; +using GroupService.Domain.Events; +using IM.DomainCommons; + +namespace GroupService.Domain.Entities +{ + public class Group : AggregateRootEntity + { + /// + /// 群聊名称 + /// + public string Name { get; private set; } = "新建群聊"; + + /// + /// 群主 + /// + public Guid GroupMaster { get; private set; } + + /// + /// 群权限 + /// (0:需管理员同意,1:任意人可加群,2:不允许任何人加入) + /// + public GroupAuthorityType Authority { get; private set; } = GroupAuthorityType.REQUIRE_CONSENT; + + /// + /// 全员禁言(false允许发言,true全员禁言) + /// + public bool AllMembersBanned { get; private set; } = false; + + /// + /// 群聊状态 + /// (1:正常,2:封禁) + /// + public GroupState Status { get; private set; } = GroupState.Normal; + + /// + /// 群公告 + /// + public string Announcement { get; private set; } = string.Empty; + + /// + /// 群头像 + /// + public string? Avatar { get; private set; } = "https://ts1.tc.mm.bing.net/th/id/OIP-C.XiB9NHwI57WPmotCbXuwawAAAA?rs=1&pid=ImgDetMain&o=7&rm=3"; + public long MaxSequenceId { get; private set; } = 0; + public string LastMessage { get; private set; } = string.Empty; + public string LastSenderName { get; private set; } = string.Empty; + + private Group() { } + + public Group(Guid groupMaster, string name = "新建群聊") + { + Name = name; + GroupMaster = groupMaster; + AddDomainEvent(new GroupCreateDomainEvent(this)); + } + + public void setAllMembersBanned(bool isBanned) + { + if (isBanned == AllMembersBanned) return; + + AllMembersBanned = isBanned; + + AddDomainEvent(new AllMembersBannedDomainEvent(this)); + } + + public void Ban() + { + Status = GroupState.Blocked; + ModificationTime = DateTime.Now; + AddDomainEvent(new GroupBlockedDomainEvent(this)); + } + + public void Update(string? name, GroupAuthorityType? groupAuthority, string? announcement, string? avatar) + { + bool isChanged = false; + if (name != null) + { + Name = name; + isChanged = true; + } + + if (groupAuthority != null) + { + Authority = groupAuthority.Value; + isChanged = true; + } + + if (announcement != null) + { + Announcement = announcement; + isChanged = true; + } + if (avatar != null) + { + Avatar = avatar; + isChanged = true; + } + + if (isChanged) + { + ModificationTime = DateTime.Now; + AddDomainEvent(new GroupUpdateDomainEvent(this)); + } + } + + public void UpdateLastMsg(long maxSequenceId, string lastMsg, string lastSenderName) + { + MaxSequenceId = maxSequenceId; + LastMessage = lastMsg; + LastSenderName = lastSenderName; + } + + + } +} diff --git a/GroupService.Domain/Entities/GroupInvitation.cs b/GroupService.Domain/Entities/GroupInvitation.cs new file mode 100644 index 0000000..dd4d8be --- /dev/null +++ b/GroupService.Domain/Entities/GroupInvitation.cs @@ -0,0 +1,62 @@ +using GroupService.Domain.Enums; +using GroupService.Domain.Events; +using GroupService.Domain.ValueObjects; +using IM.DomainCommons; + +namespace GroupService.Domain.Entities +{ + public class GroupInvitation : AggregateRootEntity + { + /// + /// 群聊编号 + /// + public Guid GroupId { get; private set; } + public GroupProfile GroupProfile { get; private set; } + + /// + /// 被邀请用户 + /// + public Guid UserId { get; private set; } + public UserProfile UserProfile { get; private set; } + + /// + /// 邀请用户 + /// + public Guid OperatorId { get; private set; } + public UserProfile OperatorProfile { get; private set; } + + /// + /// 当前状态(0:待被邀请人同意 + /// 1:被邀请人已同意) + /// + public GroupInvitationState State { get; private set; } = GroupInvitationState.Pending; + + + private GroupInvitation() { } + public GroupInvitation(Guid groupId, GroupProfile groupProfile, Guid userId, UserProfile userProfile, Guid operatorId, UserProfile operatorProfile) + { + GroupId = groupId; + GroupProfile = groupProfile; + UserId = userId; + UserProfile = userProfile; + OperatorId = operatorId; + OperatorProfile = operatorProfile; + AddDomainEvent(new GroupInvitationCreateDomainEvent(this)); + } + + public void Accept() + { + if (State != GroupInvitationState.Pending) return; + State = GroupInvitationState.Passed; + ModificationTime = DateTime.Now; + AddDomainEvent(new GroupInvitationAcceptDomainEvent(this)); + } + + public void Reject() + { + if (State != GroupInvitationState.Pending) return; + State = GroupInvitationState.Reject; + ModificationTime = DateTime.Now; + } + } +} diff --git a/GroupService.Domain/Entities/GroupJoinRequest.cs b/GroupService.Domain/Entities/GroupJoinRequest.cs new file mode 100644 index 0000000..3b5083f --- /dev/null +++ b/GroupService.Domain/Entities/GroupJoinRequest.cs @@ -0,0 +1,74 @@ +using GroupService.Domain.Enums; +using GroupService.Domain.Events; +using GroupService.Domain.ValueObjects; +using IM.DomainCommons; + +namespace GroupService.Domain.Entities +{ + public class GroupJoinRequest : AggregateRootEntity + { + /// + /// 群聊编号 + /// + /// + public Guid GroupId { get; private set; } + public GroupProfile GroupProfile { get; private set; } + + /// + /// 申请人 + /// + public Guid UserId { get; private set; } + public UserProfile UserProfile { get; private set; } + + public Guid? OperatorId { get; private set; } + public string? OperatorName { get; private set; } + public string? OperatorAvatar { get; private set; } + + /// + /// 申请状态(0:待管理员同意,1:已拒绝,2:已同意) + /// + public GroupJoinRequestState State { get; private set; } = GroupJoinRequestState.Pending; + + /// + /// 入群附言 + /// + public string Description { get; private set; } = "申请入群"; + + private GroupJoinRequest() { } + + public GroupJoinRequest(Guid userId, UserProfile user, Guid groupId, GroupProfile group, string? desc) + { + UserId = userId; + GroupId = groupId; + GroupProfile = group; + UserProfile = user; + if (desc is not null && desc.Length > 20) + { + throw new DomainException("入群描述不可超过20字符"); + } + Description = desc ?? Description; + } + + public void Approve(Guid id, string nickName, string avatar) + { + if (State != GroupJoinRequestState.Pending) return; + State = GroupJoinRequestState.Passed; + OperatorAvatar = avatar; + OperatorId = id; + OperatorName = nickName; + ModificationTime = DateTime.Now; + AddDomainEvent(new GroupJoinRequestPassedDomainEvent(this)); + } + + public void Decline(Guid id, string nickName, string avatar) + { + if (State != GroupJoinRequestState.Pending) return; + State = GroupJoinRequestState.Declined; + OperatorAvatar = avatar; + OperatorId = id; + OperatorName = nickName; + ModificationTime = DateTime.Now; + AddDomainEvent(new GroupJoinRequestDeclinedDomainEvent(this)); + } + } +} diff --git a/GroupService.Domain/Entities/GroupMember.cs b/GroupService.Domain/Entities/GroupMember.cs new file mode 100644 index 0000000..158c730 --- /dev/null +++ b/GroupService.Domain/Entities/GroupMember.cs @@ -0,0 +1,53 @@ +using GroupService.Domain.Enums; +using GroupService.Domain.Events; +using IM.DomainCommons; + +namespace GroupService.Domain.Entities +{ + public class GroupMember : AggregateRootEntity + { + /// + /// 用户编号 + /// + public Guid UserId { get; private set; } + public string GroupNickName { get; private set; } + public string? Avatar { get; private set; } + + /// + /// 群聊编号 + /// + public Guid GroupId { get; private set; } + + /// + /// 成员角色(0:普通成员,1:管理员,2:群主) + /// + public GroupMemberRole Role { get; private set; } + + private GroupMember() { } + + public GroupMember(Guid userId, Guid groupId, string groupNickname, string? avatar, GroupMemberRole role = GroupMemberRole.Normal) + { + UserId = userId; + GroupId = groupId; + Role = role; + GroupNickName = groupNickname; + Avatar = avatar; + AddDomainEvent(new GroupMemberJoinedDomainEvent(this)); + } + + public void UpdateRole(GroupMemberRole role) + { + if (role == Role) return; + Role = role; + } + + public void UpdateAvatar(string avatar) + { + Avatar = avatar; + } + public void UpdateGroupNickname(string nickname) + { + GroupNickName = nickname; + } + } +} diff --git a/GroupService.Domain/Enums/GroupAuthorityType.cs b/GroupService.Domain/Enums/GroupAuthorityType.cs new file mode 100644 index 0000000..5098f26 --- /dev/null +++ b/GroupService.Domain/Enums/GroupAuthorityType.cs @@ -0,0 +1,18 @@ +namespace GroupService.Domain.Enums +{ + public enum GroupAuthorityType + { + /// + /// 需管理员同意 + /// + REQUIRE_CONSENT = 0, + /// + /// 任何人可加入 + /// + ANYONE_CAN_JOIN = 1, + /// + /// 不允许加入 + /// + NOT_ALLOWED_TO_JOIN = 2 + } +} diff --git a/GroupService.Domain/Enums/GroupInvitationState.cs b/GroupService.Domain/Enums/GroupInvitationState.cs new file mode 100644 index 0000000..49957d0 --- /dev/null +++ b/GroupService.Domain/Enums/GroupInvitationState.cs @@ -0,0 +1,18 @@ +namespace GroupService.Domain.Enums +{ + public enum GroupInvitationState + { + /// + /// 待处理 + /// + Pending = 0, + /// + /// 已同意 + /// + Passed = 1, + /// + /// 拒绝 + /// + Reject = 2 + } +} diff --git a/GroupService.Domain/Enums/GroupJoinRequestState.cs b/GroupService.Domain/Enums/GroupJoinRequestState.cs new file mode 100644 index 0000000..3885b89 --- /dev/null +++ b/GroupService.Domain/Enums/GroupJoinRequestState.cs @@ -0,0 +1,18 @@ +namespace GroupService.Domain.Enums +{ + public enum GroupJoinRequestState + { + /// + /// 待管理员处理 + /// + Pending = 0, + /// + /// 已拒绝 + /// + Declined = 1, + /// + /// 已同意 + /// + Passed = 2 + } +} diff --git a/GroupService.Domain/Enums/GroupMemberRole.cs b/GroupService.Domain/Enums/GroupMemberRole.cs new file mode 100644 index 0000000..e06cb0d --- /dev/null +++ b/GroupService.Domain/Enums/GroupMemberRole.cs @@ -0,0 +1,18 @@ +namespace GroupService.Domain.Enums +{ + public enum GroupMemberRole + { + /// + /// 普通成员 + /// + Normal = 0, + /// + /// 管理员 + /// + Administrator = 1, + /// + /// 群主 + /// + Master = 2 + } +} diff --git a/GroupService.Domain/Enums/GroupState.cs b/GroupService.Domain/Enums/GroupState.cs new file mode 100644 index 0000000..8818043 --- /dev/null +++ b/GroupService.Domain/Enums/GroupState.cs @@ -0,0 +1,14 @@ +namespace GroupService.Domain.Enums +{ + public enum GroupState + { + /// + /// 正常 + /// + Normal = 1, + /// + /// 封禁 + /// + Blocked = 2 + } +} diff --git a/GroupService.Domain/Events/AllMembersBannedDomainEvent.cs b/GroupService.Domain/Events/AllMembersBannedDomainEvent.cs new file mode 100644 index 0000000..49071ff --- /dev/null +++ b/GroupService.Domain/Events/AllMembersBannedDomainEvent.cs @@ -0,0 +1,7 @@ +using GroupService.Domain.Entities; +using MediatR; + +namespace GroupService.Domain.Events +{ + public record AllMembersBannedDomainEvent(Group Group) : INotification; +} diff --git a/GroupService.Domain/Events/GroupBlockedDomainEvent.cs b/GroupService.Domain/Events/GroupBlockedDomainEvent.cs new file mode 100644 index 0000000..a2a115c --- /dev/null +++ b/GroupService.Domain/Events/GroupBlockedDomainEvent.cs @@ -0,0 +1,7 @@ +using GroupService.Domain.Entities; +using MediatR; + +namespace GroupService.Domain.Events +{ + public record GroupBlockedDomainEvent(Group Group) : INotification; +} diff --git a/GroupService.Domain/Events/GroupCreateDomainEvent.cs b/GroupService.Domain/Events/GroupCreateDomainEvent.cs new file mode 100644 index 0000000..0f905e0 --- /dev/null +++ b/GroupService.Domain/Events/GroupCreateDomainEvent.cs @@ -0,0 +1,6 @@ +using MediatR; + +namespace GroupService.Domain.Events +{ + public record GroupCreateDomainEvent(Domain.Entities.Group Group) : INotification; +} diff --git a/GroupService.Domain/Events/GroupInvitationAcceptDomainEvent.cs b/GroupService.Domain/Events/GroupInvitationAcceptDomainEvent.cs new file mode 100644 index 0000000..d5e2ea0 --- /dev/null +++ b/GroupService.Domain/Events/GroupInvitationAcceptDomainEvent.cs @@ -0,0 +1,7 @@ +using GroupService.Domain.Entities; +using MediatR; + +namespace GroupService.Domain.Events +{ + public record GroupInvitationAcceptDomainEvent(GroupInvitation Invitation) : INotification; +} diff --git a/GroupService.Domain/Events/GroupInvitationCreateDomainEvent.cs b/GroupService.Domain/Events/GroupInvitationCreateDomainEvent.cs new file mode 100644 index 0000000..2fee848 --- /dev/null +++ b/GroupService.Domain/Events/GroupInvitationCreateDomainEvent.cs @@ -0,0 +1,7 @@ +using GroupService.Domain.Entities; +using MediatR; + +namespace GroupService.Domain.Events +{ + public record GroupInvitationCreateDomainEvent(GroupInvitation Invitation) : INotification; +} diff --git a/GroupService.Domain/Events/GroupJoinRequestDeclinedDomainEvent.cs b/GroupService.Domain/Events/GroupJoinRequestDeclinedDomainEvent.cs new file mode 100644 index 0000000..301a7a1 --- /dev/null +++ b/GroupService.Domain/Events/GroupJoinRequestDeclinedDomainEvent.cs @@ -0,0 +1,7 @@ +using GroupService.Domain.Entities; +using MediatR; + +namespace GroupService.Domain.Events +{ + public record GroupJoinRequestDeclinedDomainEvent(GroupJoinRequest Request) : INotification; +} diff --git a/GroupService.Domain/Events/GroupJoinRequestPassedDomainEvent.cs b/GroupService.Domain/Events/GroupJoinRequestPassedDomainEvent.cs new file mode 100644 index 0000000..ef27cb4 --- /dev/null +++ b/GroupService.Domain/Events/GroupJoinRequestPassedDomainEvent.cs @@ -0,0 +1,7 @@ +using GroupService.Domain.Entities; +using MediatR; + +namespace GroupService.Domain.Events +{ + public record GroupJoinRequestPassedDomainEvent(GroupJoinRequest Request) : INotification; +} diff --git a/GroupService.Domain/Events/GroupMemberJoinedDomainEvent.cs b/GroupService.Domain/Events/GroupMemberJoinedDomainEvent.cs new file mode 100644 index 0000000..f1b3221 --- /dev/null +++ b/GroupService.Domain/Events/GroupMemberJoinedDomainEvent.cs @@ -0,0 +1,7 @@ +using GroupService.Domain.Entities; +using MediatR; + +namespace GroupService.Domain.Events +{ + public record GroupMemberJoinedDomainEvent(GroupMember Member) : INotification; +} diff --git a/GroupService.Domain/Events/GroupUpdateDomainEvent.cs b/GroupService.Domain/Events/GroupUpdateDomainEvent.cs new file mode 100644 index 0000000..a8f0c0d --- /dev/null +++ b/GroupService.Domain/Events/GroupUpdateDomainEvent.cs @@ -0,0 +1,7 @@ +using GroupService.Domain.Entities; +using MediatR; + +namespace GroupService.Domain.Events +{ + public record GroupUpdateDomainEvent(Group Group) : INotification; +} diff --git a/GroupService.Domain/GroupMemberDomainService.cs b/GroupService.Domain/GroupMemberDomainService.cs new file mode 100644 index 0000000..b59ec4c --- /dev/null +++ b/GroupService.Domain/GroupMemberDomainService.cs @@ -0,0 +1,30 @@ +using GroupService.Domain.Entities; +using GroupService.Domain.Enums; +using GroupService.Domain.IReposities; +using IM.Commons; + +namespace GroupService.Domain +{ + public class GroupMemberDomainService + { + private readonly IGroupMemberReposity reposity; + + public GroupMemberDomainService(IGroupMemberReposity reposity) + { + this.reposity = reposity; + } + + public async Task> CreateAsync( + Guid userId, Guid groupId, string nickName, string? avatar, GroupMemberRole role = GroupMemberRole.Normal) + { + var exist = await reposity.CheckMemberExistAsync(groupId, userId); + if (exist) + { + return Result.Fail(ResultCode.ALREADY_IN_GROUP); + } + var groupMember = new GroupMember(userId, groupId, nickName, avatar, role); + groupMember = reposity.Create(groupMember); + return Result.Success(groupMember); + } + } +} diff --git a/GroupService.Domain/GroupService.Domain.csproj b/GroupService.Domain/GroupService.Domain.csproj new file mode 100644 index 0000000..2c335d3 --- /dev/null +++ b/GroupService.Domain/GroupService.Domain.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/GroupService.Domain/IReposities/IGroupInvitationReposity.cs b/GroupService.Domain/IReposities/IGroupInvitationReposity.cs new file mode 100644 index 0000000..0fafca8 --- /dev/null +++ b/GroupService.Domain/IReposities/IGroupInvitationReposity.cs @@ -0,0 +1,10 @@ +using GroupService.Domain.Entities; + +namespace GroupService.Domain.IReposities +{ + public interface IGroupInvitationReposity + { + void Create(GroupInvitation invitation); + Task FindByIdAsync(Guid id); + } +} diff --git a/GroupService.Domain/IReposities/IGroupMemberReposity.cs b/GroupService.Domain/IReposities/IGroupMemberReposity.cs new file mode 100644 index 0000000..5d4bb2c --- /dev/null +++ b/GroupService.Domain/IReposities/IGroupMemberReposity.cs @@ -0,0 +1,40 @@ +using GroupService.Domain.Entities; + +namespace GroupService.Domain.IReposities +{ + public interface IGroupMemberReposity + { + /// + /// 通过群ID查询群成员 + /// + /// + /// + Task> FindByGroupIdAsync(Guid groupId); + /// + /// 通过用户ID查询 + /// + /// + /// + Task> FindByUserIdAsync(Guid userId); + /// + /// 通过群ID和用户ID查询 + /// + /// + /// + /// + Task FindOneByGroupIdAndUserIdAsync(Guid groupId, Guid userId); + /// + /// 新增群成员 + /// + /// + /// + GroupMember Create(GroupMember member); + /// + /// 检查成员是否存在 + /// + /// + /// 存在返回true,否则返回false + Task CheckMemberExistAsync(Guid groupId, Guid userId); + Task FindByIdAsync(Guid id); + } +} diff --git a/GroupService.Domain/IReposities/IGroupReposity.cs b/GroupService.Domain/IReposities/IGroupReposity.cs new file mode 100644 index 0000000..7bce2f6 --- /dev/null +++ b/GroupService.Domain/IReposities/IGroupReposity.cs @@ -0,0 +1,33 @@ +using GroupService.Domain.Entities; + +namespace GroupService.Domain.IReposities +{ + public interface IGroupReposity + { + /// + /// 通过群ID查询 + /// + /// + /// + Task FindByIdAsync(Guid id); + /// + /// 通过群名查询 + /// + /// + /// + Task> FindByNameAsync(string name); + /// + /// 通过群主ID查询 + /// + /// + /// + Task> FindByMasterIdAsync(Guid userId); + /// + /// 创建群聊 + /// + /// + /// + Group Create(Group group); + + } +} diff --git a/GroupService.Domain/IReposities/IGroupRequestReposity.cs b/GroupService.Domain/IReposities/IGroupRequestReposity.cs new file mode 100644 index 0000000..6738301 --- /dev/null +++ b/GroupService.Domain/IReposities/IGroupRequestReposity.cs @@ -0,0 +1,11 @@ +using GroupService.Domain.Entities; + +namespace GroupService.Domain.IReposities +{ + public interface IGroupRequestReposity + { + void Create(GroupJoinRequest request); + Task FindByIdAsync(Guid id); + Task> FindByGroupIdAsync(Guid groupId); + } +} diff --git a/GroupService.Domain/ValueObjects/GroupProfile.cs b/GroupService.Domain/ValueObjects/GroupProfile.cs new file mode 100644 index 0000000..563c8cd --- /dev/null +++ b/GroupService.Domain/ValueObjects/GroupProfile.cs @@ -0,0 +1,8 @@ +namespace GroupService.Domain.ValueObjects +{ + public class GroupProfile + { + public string GroupName { get; set; } + public string Avatar { get; set; } + } +} diff --git a/GroupService.Domain/ValueObjects/UserProfile.cs b/GroupService.Domain/ValueObjects/UserProfile.cs new file mode 100644 index 0000000..59d8ffe --- /dev/null +++ b/GroupService.Domain/ValueObjects/UserProfile.cs @@ -0,0 +1,8 @@ +namespace GroupService.Domain.ValueObjects +{ + public class UserProfile + { + public string NickName { get; set; } + public string? Avatar { get; set; } + } +} diff --git a/GroupService.Infrastructure/Configs/GroupConfig.cs b/GroupService.Infrastructure/Configs/GroupConfig.cs new file mode 100644 index 0000000..93dfe99 --- /dev/null +++ b/GroupService.Infrastructure/Configs/GroupConfig.cs @@ -0,0 +1,15 @@ +using GroupService.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace GroupService.Infrastructure.Configs +{ + public class GroupConfig : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("groups"); + + } + } +} diff --git a/GroupService.Infrastructure/Configs/GroupInvitationConfig.cs b/GroupService.Infrastructure/Configs/GroupInvitationConfig.cs new file mode 100644 index 0000000..e727eed --- /dev/null +++ b/GroupService.Infrastructure/Configs/GroupInvitationConfig.cs @@ -0,0 +1,46 @@ +using GroupService.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace GroupService.Infrastructure.Configs +{ + public class GroupInvitationConfig : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("group_invitations"); + builder.HasKey(x => x.Id); + builder.HasKey(x => new { x.GroupId, x.UserId }); + + builder.ComplexProperty(x => x.UserProfile, u => + { + u.Property(p => p.Avatar) + .HasMaxLength(100); + + u.Property(p => p.NickName) + .IsRequired() + .HasMaxLength(20); + }); + builder.ComplexProperty(x => x.OperatorProfile, u => + { + u.Property(p => p.Avatar) + .HasMaxLength(100); + + u.Property(p => p.NickName) + .IsRequired() + .HasMaxLength(20); + }); + + builder.ComplexProperty(x => x.GroupProfile, u => + { + u.Property(p => p.Avatar) + .IsRequired() + .HasMaxLength(100); + + u.Property(p => p.GroupName) + .IsRequired() + .HasMaxLength(20); + }); + } + } +} diff --git a/GroupService.Infrastructure/Configs/GroupJoinRequestConfig.cs b/GroupService.Infrastructure/Configs/GroupJoinRequestConfig.cs new file mode 100644 index 0000000..2e6fc4e --- /dev/null +++ b/GroupService.Infrastructure/Configs/GroupJoinRequestConfig.cs @@ -0,0 +1,37 @@ +using GroupService.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace GroupService.Infrastructure.Configs +{ + public class GroupJoinRequestConfig : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("group_join_requests"); + builder.HasKey(x => x.Id); + builder.HasKey(x => new { x.GroupId, x.UserId }); + + builder.ComplexProperty(x => x.UserProfile, u => + { + u.Property(p => p.Avatar) + .HasMaxLength(100); + + u.Property(p => p.NickName) + .IsRequired() + .HasMaxLength(20); + }); + + builder.ComplexProperty(x => x.GroupProfile, u => + { + u.Property(p => p.Avatar) + .IsRequired() + .HasMaxLength(100); + + u.Property(p => p.GroupName) + .IsRequired() + .HasMaxLength(20); + }); + } + } +} diff --git a/GroupService.Infrastructure/Configs/GroupMemberConfig.cs b/GroupService.Infrastructure/Configs/GroupMemberConfig.cs new file mode 100644 index 0000000..0c21f6b --- /dev/null +++ b/GroupService.Infrastructure/Configs/GroupMemberConfig.cs @@ -0,0 +1,14 @@ +using GroupService.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace GroupService.Infrastructure.Configs +{ + public class GroupMemberConfig : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("group_members"); + } + } +} diff --git a/GroupService.Infrastructure/GroupDbContext.cs b/GroupService.Infrastructure/GroupDbContext.cs new file mode 100644 index 0000000..14a51d9 --- /dev/null +++ b/GroupService.Infrastructure/GroupDbContext.cs @@ -0,0 +1,26 @@ +using GroupService.Domain.Entities; +using IM.Infrastructure.Efcore; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace GroupService.Infrastructure +{ + public class GroupDbContext : BaseDbContext + { + public DbSet Groups { get; private set; } + public DbSet GroupMembers { get; private set; } + public DbSet GroupJoinRequests { get; private set; } + public DbSet GroupInvitations { get; private set; } + public GroupDbContext(DbContextOptions options, IMediator mediator) : base(options, mediator) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); + + modelBuilder.EnableSoftDeletionGlobalFilter(); + } + } +} diff --git a/GroupService.Infrastructure/GroupService.Infrastructure.csproj b/GroupService.Infrastructure/GroupService.Infrastructure.csproj new file mode 100644 index 0000000..1091053 --- /dev/null +++ b/GroupService.Infrastructure/GroupService.Infrastructure.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/GroupService.Infrastructure/Migrations/20260419115121_InitGroupDb.Designer.cs b/GroupService.Infrastructure/Migrations/20260419115121_InitGroupDb.Designer.cs new file mode 100644 index 0000000..6e03845 --- /dev/null +++ b/GroupService.Infrastructure/Migrations/20260419115121_InitGroupDb.Designer.cs @@ -0,0 +1,126 @@ +// +using System; +using GroupService.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GroupService.Infrastructure.Migrations +{ + [DbContext(typeof(GroupDbContext))] + [Migration("20260419115121_InitGroupDb")] + partial class InitGroupDb + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("GroupService.Domain.Entities.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllMembersBanned") + .HasColumnType("tinyint(1)"); + + b.Property("Announcement") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Authority") + .HasColumnType("int"); + + b.Property("Avatar") + .HasColumnType("longtext"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("GroupMaster") + .HasColumnType("char(36)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LastMessage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastSenderName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MaxSequenceId") + .HasColumnType("bigint"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("GroupService.Domain.Entities.GroupMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Avatar") + .HasColumnType("longtext"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("GroupNickName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("group_members", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GroupService.Infrastructure/Migrations/20260419115121_InitGroupDb.cs b/GroupService.Infrastructure/Migrations/20260419115121_InitGroupDb.cs new file mode 100644 index 0000000..fd88f79 --- /dev/null +++ b/GroupService.Infrastructure/Migrations/20260419115121_InitGroupDb.cs @@ -0,0 +1,81 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GroupService.Infrastructure.Migrations +{ + /// + public partial class InitGroupDb : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "group_members", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + GroupNickName = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + Avatar = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + GroupId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Role = table.Column(type: "int", nullable: false), + IsDeleted = table.Column(type: "tinyint(1)", nullable: false), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + Deletion = table.Column(type: "datetime(6)", nullable: true), + ModificationTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_group_members", x => x.Id); + }) + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "groups", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Name = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + GroupMaster = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Authority = table.Column(type: "int", nullable: false), + AllMembersBanned = table.Column(type: "tinyint(1)", nullable: false), + Status = table.Column(type: "int", nullable: false), + Announcement = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + Avatar = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + MaxSequenceId = table.Column(type: "bigint", nullable: false), + LastMessage = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + LastSenderName = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + IsDeleted = table.Column(type: "tinyint(1)", nullable: false), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + Deletion = table.Column(type: "datetime(6)", nullable: true), + ModificationTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_groups", x => x.Id); + }) + .Annotation("MySql:Charset", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "group_members"); + + migrationBuilder.DropTable( + name: "groups"); + } + } +} diff --git a/GroupService.Infrastructure/Migrations/20260421141219_add_column.Designer.cs b/GroupService.Infrastructure/Migrations/20260421141219_add_column.Designer.cs new file mode 100644 index 0000000..b9dcf21 --- /dev/null +++ b/GroupService.Infrastructure/Migrations/20260421141219_add_column.Designer.cs @@ -0,0 +1,285 @@ +// +using System; +using System.Collections.Generic; +using GroupService.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GroupService.Infrastructure.Migrations +{ + [DbContext(typeof(GroupDbContext))] + [Migration("20260421141219_add_column")] + partial class add_column + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("GroupService.Domain.Entities.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllMembersBanned") + .HasColumnType("tinyint(1)"); + + b.Property("Announcement") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Authority") + .HasColumnType("int"); + + b.Property("Avatar") + .HasColumnType("longtext"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("GroupMaster") + .HasColumnType("char(36)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LastMessage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastSenderName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MaxSequenceId") + .HasColumnType("bigint"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("GroupService.Domain.Entities.GroupInvitation", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("OperatorId") + .HasColumnType("char(36)"); + + b.Property("State") + .HasColumnType("int"); + + b.ComplexProperty>("GroupProfile", "GroupService.Domain.Entities.GroupInvitation.GroupProfile#GroupProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("GroupName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.ComplexProperty>("OperatorProfile", "GroupService.Domain.Entities.GroupInvitation.OperatorProfile#UserProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("NickName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.ComplexProperty>("UserProfile", "GroupService.Domain.Entities.GroupInvitation.UserProfile#UserProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("NickName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.HasKey("GroupId", "UserId"); + + b.ToTable("group_invitations", (string)null); + }); + + modelBuilder.Entity("GroupService.Domain.Entities.GroupJoinRequest", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("OperatorId") + .HasColumnType("char(36)"); + + b.Property("State") + .HasColumnType("int"); + + b.ComplexProperty>("GroupProfile", "GroupService.Domain.Entities.GroupJoinRequest.GroupProfile#GroupProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("GroupName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.ComplexProperty>("OperatorProfile", "GroupService.Domain.Entities.GroupJoinRequest.OperatorProfile#UserProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("NickName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.ComplexProperty>("UserProfile", "GroupService.Domain.Entities.GroupJoinRequest.UserProfile#UserProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("NickName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.HasKey("GroupId", "UserId"); + + b.ToTable("group_join_requests", (string)null); + }); + + modelBuilder.Entity("GroupService.Domain.Entities.GroupMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Avatar") + .HasColumnType("longtext"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("GroupNickName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("group_members", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GroupService.Infrastructure/Migrations/20260421141219_add_column.cs b/GroupService.Infrastructure/Migrations/20260421141219_add_column.cs new file mode 100644 index 0000000..b95ed09 --- /dev/null +++ b/GroupService.Infrastructure/Migrations/20260421141219_add_column.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GroupService.Infrastructure.Migrations +{ + /// + public partial class add_column : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "group_invitations", + columns: table => new + { + GroupId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OperatorId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + State = table.Column(type: "int", nullable: false), + GroupProfile_Avatar = table.Column(type: "varchar(100)", maxLength: 100, nullable: false), + GroupProfile_GroupName = table.Column(type: "varchar(20)", maxLength: 20, nullable: false), + OperatorProfile_Avatar = table.Column(type: "varchar(100)", maxLength: 100, nullable: true), + OperatorProfile_NickName = table.Column(type: "varchar(20)", maxLength: 20, nullable: false), + UserProfile_Avatar = table.Column(type: "varchar(100)", maxLength: 100, nullable: true), + UserProfile_NickName = table.Column(type: "varchar(20)", maxLength: 20, nullable: false), + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + IsDeleted = table.Column(type: "tinyint(1)", nullable: false), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + Deletion = table.Column(type: "datetime(6)", nullable: true), + ModificationTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_group_invitations", x => new { x.GroupId, x.UserId }); + }) + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "group_join_requests", + columns: table => new + { + GroupId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OperatorId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + State = table.Column(type: "int", nullable: false), + Description = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + GroupProfile_Avatar = table.Column(type: "varchar(100)", maxLength: 100, nullable: false), + GroupProfile_GroupName = table.Column(type: "varchar(20)", maxLength: 20, nullable: false), + OperatorProfile_Avatar = table.Column(type: "varchar(100)", maxLength: 100, nullable: true), + OperatorProfile_NickName = table.Column(type: "varchar(20)", maxLength: 20, nullable: false), + UserProfile_Avatar = table.Column(type: "varchar(100)", maxLength: 100, nullable: true), + UserProfile_NickName = table.Column(type: "varchar(20)", maxLength: 20, nullable: false), + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + IsDeleted = table.Column(type: "tinyint(1)", nullable: false), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + Deletion = table.Column(type: "datetime(6)", nullable: true), + ModificationTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_group_join_requests", x => new { x.GroupId, x.UserId }); + }) + .Annotation("MySql:Charset", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "group_invitations"); + + migrationBuilder.DropTable( + name: "group_join_requests"); + } + } +} diff --git a/GroupService.Infrastructure/Migrations/20260429103435_removeGroupRequestOperatorProfile.Designer.cs b/GroupService.Infrastructure/Migrations/20260429103435_removeGroupRequestOperatorProfile.Designer.cs new file mode 100644 index 0000000..e445c4c --- /dev/null +++ b/GroupService.Infrastructure/Migrations/20260429103435_removeGroupRequestOperatorProfile.Designer.cs @@ -0,0 +1,277 @@ +// +using System; +using System.Collections.Generic; +using GroupService.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GroupService.Infrastructure.Migrations +{ + [DbContext(typeof(GroupDbContext))] + [Migration("20260429103435_removeGroupRequestOperatorProfile")] + partial class removeGroupRequestOperatorProfile + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("GroupService.Domain.Entities.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllMembersBanned") + .HasColumnType("tinyint(1)"); + + b.Property("Announcement") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Authority") + .HasColumnType("int"); + + b.Property("Avatar") + .HasColumnType("longtext"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("GroupMaster") + .HasColumnType("char(36)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LastMessage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastSenderName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MaxSequenceId") + .HasColumnType("bigint"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("GroupService.Domain.Entities.GroupInvitation", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("OperatorId") + .HasColumnType("char(36)"); + + b.Property("State") + .HasColumnType("int"); + + b.ComplexProperty>("GroupProfile", "GroupService.Domain.Entities.GroupInvitation.GroupProfile#GroupProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("GroupName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.ComplexProperty>("OperatorProfile", "GroupService.Domain.Entities.GroupInvitation.OperatorProfile#UserProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("NickName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.ComplexProperty>("UserProfile", "GroupService.Domain.Entities.GroupInvitation.UserProfile#UserProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("NickName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.HasKey("GroupId", "UserId"); + + b.ToTable("group_invitations", (string)null); + }); + + modelBuilder.Entity("GroupService.Domain.Entities.GroupJoinRequest", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("OperatorAvatar") + .HasColumnType("longtext"); + + b.Property("OperatorId") + .HasColumnType("char(36)"); + + b.Property("OperatorName") + .HasColumnType("longtext"); + + b.Property("State") + .HasColumnType("int"); + + b.ComplexProperty>("GroupProfile", "GroupService.Domain.Entities.GroupJoinRequest.GroupProfile#GroupProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("GroupName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.ComplexProperty>("UserProfile", "GroupService.Domain.Entities.GroupJoinRequest.UserProfile#UserProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("NickName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.HasKey("GroupId", "UserId"); + + b.ToTable("group_join_requests", (string)null); + }); + + modelBuilder.Entity("GroupService.Domain.Entities.GroupMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Avatar") + .HasColumnType("longtext"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("GroupNickName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("group_members", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GroupService.Infrastructure/Migrations/20260429103435_removeGroupRequestOperatorProfile.cs b/GroupService.Infrastructure/Migrations/20260429103435_removeGroupRequestOperatorProfile.cs new file mode 100644 index 0000000..37b7760 --- /dev/null +++ b/GroupService.Infrastructure/Migrations/20260429103435_removeGroupRequestOperatorProfile.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GroupService.Infrastructure.Migrations +{ + /// + public partial class removeGroupRequestOperatorProfile : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "OperatorProfile_Avatar", + table: "group_join_requests"); + + migrationBuilder.DropColumn( + name: "OperatorProfile_NickName", + table: "group_join_requests"); + + migrationBuilder.AlterColumn( + name: "OperatorId", + table: "group_join_requests", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)") + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "OperatorAvatar", + table: "group_join_requests", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "OperatorName", + table: "group_join_requests", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "OperatorAvatar", + table: "group_join_requests"); + + migrationBuilder.DropColumn( + name: "OperatorName", + table: "group_join_requests"); + + migrationBuilder.AlterColumn( + name: "OperatorId", + table: "group_join_requests", + type: "char(36)", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)", + oldNullable: true) + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "OperatorProfile_Avatar", + table: "group_join_requests", + type: "varchar(100)", + maxLength: 100, + nullable: true); + + migrationBuilder.AddColumn( + name: "OperatorProfile_NickName", + table: "group_join_requests", + type: "varchar(20)", + maxLength: 20, + nullable: false, + defaultValue: ""); + } + } +} diff --git a/GroupService.Infrastructure/Migrations/GroupDbContextModelSnapshot.cs b/GroupService.Infrastructure/Migrations/GroupDbContextModelSnapshot.cs new file mode 100644 index 0000000..592ecec --- /dev/null +++ b/GroupService.Infrastructure/Migrations/GroupDbContextModelSnapshot.cs @@ -0,0 +1,274 @@ +// +using System; +using System.Collections.Generic; +using GroupService.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GroupService.Infrastructure.Migrations +{ + [DbContext(typeof(GroupDbContext))] + partial class GroupDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("GroupService.Domain.Entities.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllMembersBanned") + .HasColumnType("tinyint(1)"); + + b.Property("Announcement") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Authority") + .HasColumnType("int"); + + b.Property("Avatar") + .HasColumnType("longtext"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("GroupMaster") + .HasColumnType("char(36)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LastMessage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastSenderName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MaxSequenceId") + .HasColumnType("bigint"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("groups", (string)null); + }); + + modelBuilder.Entity("GroupService.Domain.Entities.GroupInvitation", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("OperatorId") + .HasColumnType("char(36)"); + + b.Property("State") + .HasColumnType("int"); + + b.ComplexProperty>("GroupProfile", "GroupService.Domain.Entities.GroupInvitation.GroupProfile#GroupProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("GroupName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.ComplexProperty>("OperatorProfile", "GroupService.Domain.Entities.GroupInvitation.OperatorProfile#UserProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("NickName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.ComplexProperty>("UserProfile", "GroupService.Domain.Entities.GroupInvitation.UserProfile#UserProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("NickName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.HasKey("GroupId", "UserId"); + + b.ToTable("group_invitations", (string)null); + }); + + modelBuilder.Entity("GroupService.Domain.Entities.GroupJoinRequest", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("OperatorAvatar") + .HasColumnType("longtext"); + + b.Property("OperatorId") + .HasColumnType("char(36)"); + + b.Property("OperatorName") + .HasColumnType("longtext"); + + b.Property("State") + .HasColumnType("int"); + + b.ComplexProperty>("GroupProfile", "GroupService.Domain.Entities.GroupJoinRequest.GroupProfile#GroupProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("GroupName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.ComplexProperty>("UserProfile", "GroupService.Domain.Entities.GroupJoinRequest.UserProfile#UserProfile", b1 => + { + b1.IsRequired(); + + b1.Property("Avatar") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("NickName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + }); + + b.HasKey("GroupId", "UserId"); + + b.ToTable("group_join_requests", (string)null); + }); + + modelBuilder.Entity("GroupService.Domain.Entities.GroupMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Avatar") + .HasColumnType("longtext"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("GroupNickName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("group_members", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GroupService.Infrastructure/ModuleInit.cs b/GroupService.Infrastructure/ModuleInit.cs new file mode 100644 index 0000000..e7fa941 --- /dev/null +++ b/GroupService.Infrastructure/ModuleInit.cs @@ -0,0 +1,20 @@ +using GroupService.Domain; +using GroupService.Domain.IReposities; +using GroupService.Infrastructure.Reposities; +using IM.Commons; +using Microsoft.Extensions.DependencyInjection; + +namespace GroupService.Infrastructure +{ + public class ModuleInit : IModuleInitializer + { + public void Initialize(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + } +} diff --git a/GroupService.Infrastructure/Reposities/GroupInvitationReposity.cs b/GroupService.Infrastructure/Reposities/GroupInvitationReposity.cs new file mode 100644 index 0000000..a764a7e --- /dev/null +++ b/GroupService.Infrastructure/Reposities/GroupInvitationReposity.cs @@ -0,0 +1,25 @@ +using GroupService.Domain.Entities; +using GroupService.Domain.IReposities; +using Microsoft.EntityFrameworkCore; + +namespace GroupService.Infrastructure.Reposities +{ + public class GroupInvitationReposity : IGroupInvitationReposity + { + private readonly GroupDbContext db; + + public GroupInvitationReposity(GroupDbContext db) + { + this.db = db; + } + + public void Create(GroupInvitation invitation) + { + db.GroupInvitations.Add(invitation); + } + public async Task FindByIdAsync(Guid id) + { + return await db.GroupInvitations.FirstOrDefaultAsync(x => x.Id == id); + } + } +} diff --git a/GroupService.Infrastructure/Reposities/GroupJoinRequestReposity.cs b/GroupService.Infrastructure/Reposities/GroupJoinRequestReposity.cs new file mode 100644 index 0000000..c7d6dfa --- /dev/null +++ b/GroupService.Infrastructure/Reposities/GroupJoinRequestReposity.cs @@ -0,0 +1,33 @@ +using GroupService.Domain.Entities; +using GroupService.Domain.IReposities; +using Microsoft.EntityFrameworkCore; + +namespace GroupService.Infrastructure.Reposities +{ + public class GroupJoinRequestReposity : IGroupRequestReposity + { + private readonly GroupDbContext db; + + public GroupJoinRequestReposity(GroupDbContext db) + { + this.db = db; + } + + public void Create(GroupJoinRequest request) + { + db.GroupJoinRequests.Add(request); + } + + public async Task> FindByGroupIdAsync(Guid groupId) + { + return await db.GroupJoinRequests.Where(x => x.GroupId == groupId) + .OrderByDescending(o => o.CreationTime) + .ToListAsync(); + } + + public async Task FindByIdAsync(Guid id) + { + return await db.GroupJoinRequests.FirstOrDefaultAsync(x => x.Id == id); + } + } +} diff --git a/GroupService.Infrastructure/Reposities/GroupMemberReposity.cs b/GroupService.Infrastructure/Reposities/GroupMemberReposity.cs new file mode 100644 index 0000000..ba6f58e --- /dev/null +++ b/GroupService.Infrastructure/Reposities/GroupMemberReposity.cs @@ -0,0 +1,50 @@ +using GroupService.Domain.Entities; +using GroupService.Domain.IReposities; +using Microsoft.EntityFrameworkCore; + +namespace GroupService.Infrastructure.Reposities +{ + public class GroupMemberReposity : IGroupMemberReposity + { + private readonly GroupDbContext db; + + public GroupMemberReposity(GroupDbContext db) + { + this.db = db; + } + + public async Task CheckMemberExistAsync(Guid groupId, Guid userId) + { + return await db.GroupMembers.AnyAsync(x => x.UserId == userId && + x.GroupId == groupId + ); + } + + public GroupMember Create(GroupMember member) + { + db.GroupMembers.Add(member); + return member; + } + + public async Task> FindByGroupIdAsync(Guid groupId) + { + return await db.GroupMembers.Where(x => x.GroupId == groupId).ToListAsync(); + } + public async Task FindOneByGroupIdAndUserIdAsync(Guid groupId, Guid userId) + { + return await db.GroupMembers.FirstOrDefaultAsync(x => x.UserId == userId + && x.GroupId == groupId + ); + } + + public async Task> FindByUserIdAsync(Guid userId) + { + return await db.GroupMembers.Where(x => x.UserId == userId).ToListAsync(); + } + + public async Task FindByIdAsync(Guid id) + { + return await db.GroupMembers.FirstOrDefaultAsync(x => x.Id == id); + } + } +} diff --git a/GroupService.Infrastructure/Reposities/GroupReposity.cs b/GroupService.Infrastructure/Reposities/GroupReposity.cs new file mode 100644 index 0000000..981b962 --- /dev/null +++ b/GroupService.Infrastructure/Reposities/GroupReposity.cs @@ -0,0 +1,37 @@ +using GroupService.Domain.Entities; +using GroupService.Domain.IReposities; +using Microsoft.EntityFrameworkCore; + +namespace GroupService.Infrastructure.Reposities +{ + public class GroupReposity : IGroupReposity + { + private readonly GroupDbContext db; + + public GroupReposity(GroupDbContext db) + { + this.db = db; + } + + public Group Create(Group group) + { + db.Groups.Add(group); + return group; + } + + public async Task FindByIdAsync(Guid id) + { + return await db.Groups.FirstOrDefaultAsync(x => x.Id == id); + } + + public async Task> FindByMasterIdAsync(Guid userId) + { + return await db.Groups.Where(x => x.GroupMaster == userId).ToListAsync(); + } + + public async Task> FindByNameAsync(string name) + { + return await db.Groups.Where(x => x.Name == name).ToListAsync(); + } + } +} diff --git a/GroupService.WebApi/Application/Dtos/GroupInvitationResponse.cs b/GroupService.WebApi/Application/Dtos/GroupInvitationResponse.cs new file mode 100644 index 0000000..bbe65c5 --- /dev/null +++ b/GroupService.WebApi/Application/Dtos/GroupInvitationResponse.cs @@ -0,0 +1,37 @@ +using GroupService.Domain.Enums; + +namespace GroupService.WebApi.Application.Dtos +{ + public class GroupInvitationResponse + { + public Guid Id { get; private set; } + /// + /// 群聊编号 + /// + public Guid GroupId { get; private set; } + public string GroupAvatar { get; private set; } + public string GroupName { get; private set; } + + /// + /// 被邀请用户 + /// + public Guid UserId { get; private set; } + public string? UserAvatar { get; private set; } + public string UserNickName { get; private set; } + + /// + /// 邀请用户 + /// + public Guid OperatorId { get; private set; } + public string OperatorName { get; private set; } + public string? OperatorAvatar { get; private set; } + + /// + /// 当前状态(0:待被邀请人同意 + /// 1:被邀请人已同意) + /// + public GroupInvitationState State { get; private set; } + public DateTimeOffset Created { get; private set; } + public DateTimeOffset Updated { get; private set; } + } +} diff --git a/GroupService.WebApi/Application/Dtos/GroupMemberResponse.cs b/GroupService.WebApi/Application/Dtos/GroupMemberResponse.cs new file mode 100644 index 0000000..dbd8d15 --- /dev/null +++ b/GroupService.WebApi/Application/Dtos/GroupMemberResponse.cs @@ -0,0 +1,26 @@ +using GroupService.Domain.Enums; + +namespace GroupService.WebApi.Application.Dtos +{ + public class GroupMemberResponse + { + public Guid Id { get; private set; } + /// + /// 用户编号 + /// + public Guid UserId { get; private set; } + public string GroupNickName { get; private set; } + public string? Avatar { get; private set; } + + /// + /// 群聊编号 + /// + public Guid GroupId { get; private set; } + + /// + /// 成员角色(0:普通成员,1:管理员,2:群主) + /// + public GroupMemberRole Role { get; private set; } + public DateTimeOffset Created { get; private set; } + } +} diff --git a/GroupService.WebApi/Application/Dtos/GroupRequestResponse.cs b/GroupService.WebApi/Application/Dtos/GroupRequestResponse.cs new file mode 100644 index 0000000..9b50973 --- /dev/null +++ b/GroupService.WebApi/Application/Dtos/GroupRequestResponse.cs @@ -0,0 +1,38 @@ +using GroupService.Domain.Enums; + +namespace GroupService.WebApi.Application.Dtos +{ + public class GroupRequestResponse + { + public Guid Id { get; private set; } + /// + /// 群聊编号 + /// + /// + public Guid GroupId { get; private set; } + public string GroupAvatar { get; private set; } + public string GroupName { get; private set; } + + /// + /// 申请人 + /// + public Guid UserId { get; private set; } + public string? UserAvatar { get; private set; } + public string UserNickName { get; private set; } + public Guid OperatorId { get; private set; } + public string OperatorName { get; private set; } + public string? OperatorAvatar { get; private set; } + + /// + /// 申请状态(0:待管理员同意,1:已拒绝,2:已同意) + /// + public GroupJoinRequestState State { get; private set; } + + /// + /// 入群附言 + /// + public string Description { get; private set; } + public DateTimeOffset Created { get; private set; } + public DateTimeOffset Updated { get; private set; } + } +} diff --git a/GroupService.WebApi/Application/Dtos/GroupResponse.cs b/GroupService.WebApi/Application/Dtos/GroupResponse.cs new file mode 100644 index 0000000..95ec0db --- /dev/null +++ b/GroupService.WebApi/Application/Dtos/GroupResponse.cs @@ -0,0 +1,45 @@ +using GroupService.Domain.Enums; + +namespace GroupService.WebApi.Application.Dtos +{ + public class GroupResponse + { + public Guid Id { get; private set; } + public string Name { get; private set; } + /// + /// 群主 + /// + public Guid GroupMaster { get; private set; } + + /// + /// 群权限 + /// (0:需管理员同意,1:任意人可加群,2:不允许任何人加入) + /// + public GroupAuthorityType Authority { get; private set; } + /// + /// 全员禁言(false允许发言,true全员禁言) + /// + public bool AllMembersBanned { get; private set; } + /// + /// 群聊状态 + /// (1:正常,2:封禁) + /// + public GroupState Status { get; private set; } + + /// + /// 群公告 + /// + public string Announcement { get; private set; } + + /// + /// 群头像 + /// + public string? Avatar { get; private set; } + public long MaxSequenceId { get; private set; } + public string LastMessage { get; private set; } + public string LastSenderName { get; private set; } + public DateTimeOffset Created { get; private set; } + public DateTimeOffset Updated { get; private set; } + } + +} diff --git a/GroupService.WebApi/Application/Dtos/UserInfoDto.cs b/GroupService.WebApi/Application/Dtos/UserInfoDto.cs new file mode 100644 index 0000000..651a938 --- /dev/null +++ b/GroupService.WebApi/Application/Dtos/UserInfoDto.cs @@ -0,0 +1,16 @@ +namespace GroupService.WebApi.Application.Dtos +{ + public class UserInfoDto + { + public Guid Id { get; set; } + public string UserName { get; set; } + public string NickName { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + public string Region { get; set; } + public string Description { get; set; } + public string? Avatar { get; set; } + public DateTimeOffset CreationTime { get; set; } + public DateTimeOffset? Deletion { get; set; } + } +} diff --git a/GroupService.WebApi/Application/EventHandler/GroupBlockHandler.cs b/GroupService.WebApi/Application/EventHandler/GroupBlockHandler.cs new file mode 100644 index 0000000..f5f0c73 --- /dev/null +++ b/GroupService.WebApi/Application/EventHandler/GroupBlockHandler.cs @@ -0,0 +1,24 @@ +using GroupService.Domain.Events; +using IM.Commons.IntegrationEvents; +using MassTransit; +using MediatR; + +namespace GroupService.WebApi.Application.EventHandler +{ + public class GroupBlockHandler : INotificationHandler + { + private readonly IPublishEndpoint endpoint; + + public GroupBlockHandler(IPublishEndpoint endpoint) + { + this.endpoint = endpoint; + } + + public async Task Handle(GroupBlockedDomainEvent notification, CancellationToken cancellationToken) + { + var groupInfo = notification.Group; + await endpoint.Publish(new GroupBlockEvent(groupInfo.Id, groupInfo.Name, + groupInfo.GroupMaster, groupInfo.Status.ToString(), groupInfo.Avatar)); + } + } +} diff --git a/GroupService.WebApi/Application/EventHandler/GroupCreateHandler.cs b/GroupService.WebApi/Application/EventHandler/GroupCreateHandler.cs new file mode 100644 index 0000000..aed9645 --- /dev/null +++ b/GroupService.WebApi/Application/EventHandler/GroupCreateHandler.cs @@ -0,0 +1,42 @@ +using GroupService.Domain.Events; +using GroupService.Domain.IReposities; +using GroupService.Infrastructure; +using GroupService.WebApi.Application.GroupMember; +using GroupService.WebApi.Application.IntegrationServices; +using IM.Commons.IntegrationEvents; +using MassTransit; +using MediatR; + +namespace GroupService.WebApi.Application.EventHandler +{ + public class GroupCreateHandler : INotificationHandler + { + private readonly IPublishEndpoint endpoint; + private readonly IGroupMemberReposity reposity; + private readonly IIdentityIntegrationService service; + private readonly GroupDbContext groupDb; + + public GroupCreateHandler(IPublishEndpoint endpoint, IGroupMemberReposity reposity, IIdentityIntegrationService service, GroupDbContext groupDb) + { + this.endpoint = endpoint; + this.reposity = reposity; + this.service = service; + this.groupDb = groupDb; + } + + public async Task Handle(GroupCreateDomainEvent notification, CancellationToken cancellationToken) + { + var group = notification.Group; + + var userInfo = await service.FindUserByIdAsync(group.GroupMaster); + string nickName = "未知昵称"; + if (userInfo.Succeeded) + { + nickName = userInfo.Data.NickName; + } + reposity.Create(new Domain.Entities.GroupMember(group.GroupMaster, group.Id, userInfo.Data.NickName, userInfo.Data.Avatar, Domain.Enums.GroupMemberRole.Master)); + await groupDb.SaveChangesAsync(); + await endpoint.Publish(new GroupCreateEvent(group.Id, group.Name, group.GroupMaster, group.Avatar)); + } + } +} diff --git a/GroupService.WebApi/Application/EventHandler/GroupInvitationEventHandler.cs b/GroupService.WebApi/Application/EventHandler/GroupInvitationEventHandler.cs new file mode 100644 index 0000000..3ed63f7 --- /dev/null +++ b/GroupService.WebApi/Application/EventHandler/GroupInvitationEventHandler.cs @@ -0,0 +1,50 @@ +using GroupService.Domain.Events; +using GroupService.WebApi.Application.GroupRequest; +using IM.Commons; +using IM.Commons.IntegrationEvents; +using MassTransit; +using MediatR; + +namespace GroupService.WebApi.Application.EventHandler +{ + public class GroupInvitationEventHandler : + INotificationHandler + , INotificationHandler + { + private readonly IPublishEndpoint endpoint; + private readonly GroupRequestService requestService; + + public GroupInvitationEventHandler(IPublishEndpoint endpoint, GroupRequestService requestService) + { + this.endpoint = endpoint; + this.requestService = requestService; + } + + public async Task Handle(GroupInvitationAcceptDomainEvent notification, CancellationToken cancellationToken) + { + var invitation = notification.Invitation; + var res = await requestService.CreateAsync(invitation.GroupId, invitation.UserId, + $"邀请入群" + ); + + await endpoint.Publish(new GroupInvitationAcceptEvent(invitation.Id, invitation.UserId, invitation.UserProfile.NickName, invitation.UserProfile.Avatar + , invitation.GroupId, invitation.GroupProfile.GroupName, invitation.GroupProfile.Avatar, invitation.OperatorId + , invitation.OperatorProfile.NickName, invitation.OperatorProfile.Avatar + ), cancellationToken); + + if (!res.Succeeded) + { + + throw new EventHandlerException(res.Message); + } + } + + public async Task Handle(GroupInvitationCreateDomainEvent notification, CancellationToken cancellationToken) + { + var invitation = notification.Invitation; + await endpoint.Publish(new GroupInvitationCreateEvent(invitation.Id, invitation.UserId, invitation.UserProfile.NickName, invitation.UserProfile.Avatar + , invitation.GroupId, invitation.GroupProfile.GroupName, invitation.GroupProfile.Avatar, invitation.OperatorId + , invitation.OperatorProfile.NickName, invitation.OperatorProfile.Avatar), cancellationToken); + } + } +} diff --git a/GroupService.WebApi/Application/EventHandler/GroupMemberJoinedHandler.cs b/GroupService.WebApi/Application/EventHandler/GroupMemberJoinedHandler.cs new file mode 100644 index 0000000..03a7fd5 --- /dev/null +++ b/GroupService.WebApi/Application/EventHandler/GroupMemberJoinedHandler.cs @@ -0,0 +1,25 @@ +using GroupService.Domain.Events; +using IM.Commons.IntegrationEvents; +using MassTransit; +using MediatR; + +namespace GroupService.WebApi.Application.EventHandler +{ + public class GroupMemberJoinedHandler : INotificationHandler + { + private readonly IPublishEndpoint endpoint; + + public GroupMemberJoinedHandler(IPublishEndpoint endpoint) + { + this.endpoint = endpoint; + } + + public async Task Handle(GroupMemberJoinedDomainEvent notification, CancellationToken cancellationToken) + { + var member = notification.Member; + await endpoint.Publish(new GroupMemberJoinedEvent(member.Id, + member.UserId, member.GroupId, member.GroupNickName, member.Avatar, + member.Role.ToString()), cancellationToken); + } + } +} diff --git a/GroupService.WebApi/Application/EventHandler/GroupRequestDeclinedHandler.cs b/GroupService.WebApi/Application/EventHandler/GroupRequestDeclinedHandler.cs new file mode 100644 index 0000000..8bce96d --- /dev/null +++ b/GroupService.WebApi/Application/EventHandler/GroupRequestDeclinedHandler.cs @@ -0,0 +1,29 @@ +using GroupService.Domain.Events; +using IM.Commons.IntegrationEvents; +using MassTransit; +using MediatR; + +namespace GroupService.WebApi.Application.EventHandler +{ + public class GroupRequestDeclinedHandler : INotificationHandler + { + private readonly IPublishEndpoint endpoint; + + public GroupRequestDeclinedHandler(IPublishEndpoint endpoint) + { + this.endpoint = endpoint; + } + + public async Task Handle(GroupJoinRequestDeclinedDomainEvent notification, CancellationToken cancellationToken) + { + var request = notification.Request; + await endpoint.Publish(new GroupRequestDeclinedEvent( + request.Id, request.GroupId, request.GroupProfile.GroupName, + request.GroupProfile.Avatar, request.UserId, + request.UserProfile.NickName, request.UserProfile.Avatar, + request.OperatorId.Value,request.OperatorName, + request.OperatorAvatar, request.Description, + request.CreationTime, request.ModificationTime.Value)); + } + } +} diff --git a/GroupService.WebApi/Application/EventHandler/GroupRequestPassedHandler.cs b/GroupService.WebApi/Application/EventHandler/GroupRequestPassedHandler.cs new file mode 100644 index 0000000..7f59e0c --- /dev/null +++ b/GroupService.WebApi/Application/EventHandler/GroupRequestPassedHandler.cs @@ -0,0 +1,46 @@ +using GroupService.Domain.Events; +using GroupService.WebApi.Application.GroupMember; +using IM.Commons; +using IM.Commons.IntegrationEvents; +using MassTransit; +using MediatR; + +namespace GroupService.WebApi.Application.EventHandler +{ + public class GroupRequestPassedHandler : INotificationHandler + { + private readonly IPublishEndpoint endpoint; + private readonly Application.GroupMember.GroupMemberService memberService; + private readonly ILogger logger; + + public GroupRequestPassedHandler(IPublishEndpoint endpoint, + GroupMemberService memberService, ILogger logger) + { + this.endpoint = endpoint; + this.memberService = memberService; + this.logger = logger; + } + + public async Task Handle(GroupJoinRequestPassedDomainEvent notification, CancellationToken cancellationToken) + { + var request = notification.Request; + + var memberCreateRes = await memberService.CreateAsync(request.GroupId, request.UserId); + + await endpoint.Publish(new GroupRequestPassedEvent(request.Id, request.GroupId, request.GroupProfile.GroupName, + request.GroupProfile.Avatar, request.UserId, + request.UserProfile.NickName, request.UserProfile.Avatar, + request.OperatorId.Value, request.OperatorName, + request.OperatorAvatar, request.Description, + request.CreationTime, request.ModificationTime.Value)); + + if (!memberCreateRes.Succeeded) + { + logger.LogError(memberCreateRes.Message); + throw new EventHandlerException(memberCreateRes.Message); + } + + + } + } +} diff --git a/GroupService.WebApi/Application/EventHandler/MessageCreatedHandler.cs b/GroupService.WebApi/Application/EventHandler/MessageCreatedHandler.cs new file mode 100644 index 0000000..90a7c24 --- /dev/null +++ b/GroupService.WebApi/Application/EventHandler/MessageCreatedHandler.cs @@ -0,0 +1,32 @@ +using GroupService.Domain.IReposities; +using GroupService.Infrastructure; +using IM.Commons.IntegrationEvents; +using MassTransit; + +namespace GroupService.WebApi.Application.EventHandler +{ + public class MessageCreatedHandler : IConsumer + { + private readonly IGroupReposity reposity; + private readonly GroupDbContext groupDb; + + public MessageCreatedHandler(IGroupReposity reposity, GroupDbContext groupDb) + { + this.reposity = reposity; + this.groupDb = groupDb; + } + + public async Task Consume(ConsumeContext context) + { + var @event = context.Message; + if(@event.MsgType == "GROUP") + { + var group = await reposity.FindByIdAsync(@event.TargetId); + if (group is null) return; + group.UpdateLastMsg(@event.SequenceId, @event.Content.Fallback, "未知用户"); + groupDb.Groups.Update(group); + await groupDb.SaveChangesAsync(); + } + } + } +} diff --git a/GroupService.WebApi/Application/EventHandler/UserProfileUpdateHandler.cs b/GroupService.WebApi/Application/EventHandler/UserProfileUpdateHandler.cs new file mode 100644 index 0000000..820cae2 --- /dev/null +++ b/GroupService.WebApi/Application/EventHandler/UserProfileUpdateHandler.cs @@ -0,0 +1,31 @@ +using GroupService.Domain.IReposities; +using GroupService.Infrastructure; +using IM.Commons.IntegrationEvents; +using MassTransit; + +namespace GroupService.WebApi.Application.EventHandler +{ + public class UserProfileUpdateHandler : IConsumer + { + private readonly GroupDbContext db; + private readonly IGroupMemberReposity memberReposity; + + public UserProfileUpdateHandler(GroupDbContext db, IGroupMemberReposity memberReposity) + { + this.db = db; + this.memberReposity = memberReposity; + } + + public async Task Consume(ConsumeContext context) + { + var @event = context.Message; + var members = await memberReposity.FindByUserIdAsync(@event.UserId); + foreach (var member in members) + { + member.UpdateAvatar(@event.Avatar); + } + + await db.SaveChangesAsync(); + } + } +} diff --git a/GroupService.WebApi/Application/Group/GroupCreateCommand.cs b/GroupService.WebApi/Application/Group/GroupCreateCommand.cs new file mode 100644 index 0000000..df9d0c8 --- /dev/null +++ b/GroupService.WebApi/Application/Group/GroupCreateCommand.cs @@ -0,0 +1,14 @@ +namespace GroupService.WebApi.Application.Group +{ + public class GroupCreateCommand + { + public Guid GroupMasterId { get; private set; } + public string? Name { get; private set; } = "新建群聊"; + + public GroupCreateCommand(Guid groupMasterId, string? name) + { + GroupMasterId = groupMasterId; + Name = name; + } + } +} diff --git a/GroupService.WebApi/Application/Group/GroupMapperConfig.cs b/GroupService.WebApi/Application/Group/GroupMapperConfig.cs new file mode 100644 index 0000000..ab67964 --- /dev/null +++ b/GroupService.WebApi/Application/Group/GroupMapperConfig.cs @@ -0,0 +1,18 @@ +using AutoMapper; +using GroupService.WebApi.Application.Dtos; + +namespace GroupService.WebApi.Application.Group +{ + public class GroupMapperConfig : Profile + { + public GroupMapperConfig() + { + CreateMap() + .ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.CreationTime)) + .ForMember(dest => dest.Updated, opt => opt.MapFrom(src => src.ModificationTime)) + ; + + + } + } +} diff --git a/GroupService.WebApi/Application/Group/GroupService.cs b/GroupService.WebApi/Application/Group/GroupService.cs new file mode 100644 index 0000000..3ff9d2e --- /dev/null +++ b/GroupService.WebApi/Application/Group/GroupService.cs @@ -0,0 +1,43 @@ +using AutoMapper; +using GroupService.Domain.IReposities; +using GroupService.WebApi.Application.Dtos; +using IM.Commons; + +namespace GroupService.WebApi.Application.Group +{ + public class GroupService + { + private readonly IGroupReposity reposity; + private readonly IMapper mapper; + + public GroupService(IGroupReposity reposity, IMapper mapper) + { + this.reposity = reposity; + this.mapper = mapper; + } + + public async Task> CreateAsync(GroupCreateCommand command) + { + var group = new Domain.Entities.Group(command.GroupMasterId, command.Name); + reposity.Create(group); + return Result.Success(mapper.Map(group)); + } + + public async Task>> GetAllAsync(Guid userId) + { + var groups = await reposity.FindByMasterIdAsync(userId); + return Result>.Success(mapper.Map>(groups)); + } + + public async Task> GetByIdAsync(Guid groupId) + { + var group = await reposity.FindByIdAsync(groupId); + if (group is null) + { + return Result.Fail(ResultCode.GROUP_NOT_FOUND); + } + + return Result.Success(mapper.Map(group)); + } + } +} diff --git a/GroupService.WebApi/Application/GroupInvitation/GroupInvitationHandleCommand.cs b/GroupService.WebApi/Application/GroupInvitation/GroupInvitationHandleCommand.cs new file mode 100644 index 0000000..e8e882a --- /dev/null +++ b/GroupService.WebApi/Application/GroupInvitation/GroupInvitationHandleCommand.cs @@ -0,0 +1,9 @@ +namespace GroupService.WebApi.Application.GroupInvitation +{ + public record GroupInvitationHandleCommand(Guid InvitationId, Guid UserId, GroupInvitationAction Action); + public enum GroupInvitationAction + { + Accept = 0, + Reject = 1 + } +} diff --git a/GroupService.WebApi/Application/GroupInvitation/GroupInvitationMapperConfig.cs b/GroupService.WebApi/Application/GroupInvitation/GroupInvitationMapperConfig.cs new file mode 100644 index 0000000..9842e82 --- /dev/null +++ b/GroupService.WebApi/Application/GroupInvitation/GroupInvitationMapperConfig.cs @@ -0,0 +1,21 @@ +using AutoMapper; +using GroupService.WebApi.Application.Dtos; + +namespace GroupService.WebApi.Application.GroupInvitation +{ + public class GroupInvitationMapperConfig : Profile + { + public GroupInvitationMapperConfig() + { + CreateMap() + .ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.CreationTime)) + .ForMember(dest => dest.Updated, opt => opt.MapFrom(src => src.ModificationTime)) + .ForMember(dest => dest.GroupAvatar, opt => opt.MapFrom(src => src.GroupProfile.Avatar)) + .ForMember(dest => dest.GroupName, opt => opt.MapFrom(src => src.GroupProfile.GroupName)) + .ForMember(dest => dest.UserNickName, opt => opt.MapFrom(src => src.UserProfile.NickName)) + .ForMember(dest => dest.UserAvatar, opt => opt.MapFrom(src => src.UserProfile.Avatar)) + .ForMember(dest => dest.OperatorAvatar, opt => opt.MapFrom(src => src.OperatorProfile.Avatar)) + .ForMember(dest => dest.OperatorName, opt => opt.MapFrom(src => src.OperatorProfile.NickName)); + } + } +} diff --git a/GroupService.WebApi/Application/GroupInvitation/GroupInvitationService.cs b/GroupService.WebApi/Application/GroupInvitation/GroupInvitationService.cs new file mode 100644 index 0000000..1a287cf --- /dev/null +++ b/GroupService.WebApi/Application/GroupInvitation/GroupInvitationService.cs @@ -0,0 +1,101 @@ +using AutoMapper; +using GroupService.Domain.IReposities; +using GroupService.Domain.ValueObjects; +using GroupService.WebApi.Application.Dtos; +using GroupService.WebApi.Application.IntegrationServices; +using IM.Commons; + +namespace GroupService.WebApi.Application.GroupInvitation +{ + public class GroupInvitationService + { + private readonly IGroupInvitationReposity reposity; + private readonly IGroupMemberReposity memberReposity; + private readonly IIdentityIntegrationService idService; + private readonly IGroupReposity groupReposity; + private readonly IMapper mapper; + + public GroupInvitationService(IGroupInvitationReposity reposity, IGroupMemberReposity memberReposity, IIdentityIntegrationService idService, IGroupReposity groupReposity, IMapper mapper) + { + this.reposity = reposity; + this.memberReposity = memberReposity; + this.idService = idService; + this.groupReposity = groupReposity; + this.mapper = mapper; + } + + public async Task> CreateAsync(Guid operatorId, Guid userId, Guid groupId) + { + var userRes = await idService.FindUserByIdAsync(userId); + if (!userRes.Succeeded) + { + return Result.Fail(userRes); + } + var operatorInfo = await idService.FindUserByIdAsync(operatorId); + + var member = await memberReposity.FindOneByGroupIdAndUserIdAsync(groupId, operatorId); + if (member == null) + { + return Result.Fail(ResultCode.PERMISSION_DENIED); + } + + var group = await groupReposity.FindByIdAsync(groupId); + + var userProfile = new UserProfile() + { + Avatar = userRes.Data.Avatar, + NickName = userRes.Data.NickName + }; + + var operatorProfile = new UserProfile() + { + Avatar = operatorInfo.Data.Avatar, + NickName = operatorInfo.Data.NickName + }; + + var groupProfile = new GroupProfile() + { + Avatar = group.Avatar, + GroupName = group.Name + }; + + var invitation = new Domain.Entities.GroupInvitation(groupId, groupProfile, userId, userProfile + , operatorId, operatorProfile); + reposity.Create(invitation); + + return Result.Success(mapper.Map(invitation)); + } + + public async Task> HandleAsync(GroupInvitationHandleCommand command) + { + var invitation = await reposity.FindByIdAsync(command.InvitationId); + if (invitation is null || invitation.UserId != command.UserId) + { + return Result.Fail(ResultCode.GROUP_INVITE_EXPIRED); + } + + if (command.Action == GroupInvitationAction.Accept) + { + invitation.Accept(); + } + else if (command.Action == GroupInvitationAction.Reject) + { + invitation.Reject(); + } + + return Result.Success(); + } + + public async Task> GetByIdAsync(Guid id, Guid userId) + { + var invitation = await reposity.FindByIdAsync(id); + + if (invitation is null || (invitation.UserId != userId && invitation.OperatorId != userId)) + { + return Result.Fail(ResultCode.GROUP_INVITE_EXPIRED); + } + + return Result.Success(mapper.Map(invitation)); + } + } +} diff --git a/GroupService.WebApi/Application/GroupMember/GroupMemberMapperConfig.cs b/GroupService.WebApi/Application/GroupMember/GroupMemberMapperConfig.cs new file mode 100644 index 0000000..9bcbefc --- /dev/null +++ b/GroupService.WebApi/Application/GroupMember/GroupMemberMapperConfig.cs @@ -0,0 +1,14 @@ +using AutoMapper; +using GroupService.WebApi.Application.Dtos; + +namespace GroupService.WebApi.Application.GroupMember +{ + public class GroupMemberMapperConfig : Profile + { + public GroupMemberMapperConfig() + { + CreateMap() + .ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.CreationTime)); + } + } +} diff --git a/GroupService.WebApi/Application/GroupMember/GroupMemberService.cs b/GroupService.WebApi/Application/GroupMember/GroupMemberService.cs new file mode 100644 index 0000000..1a3b02e --- /dev/null +++ b/GroupService.WebApi/Application/GroupMember/GroupMemberService.cs @@ -0,0 +1,91 @@ +using AutoMapper; +using GroupService.Domain; +using GroupService.Domain.IReposities; +using GroupService.WebApi.Application.Dtos; +using GroupService.WebApi.Application.IntegrationServices; +using IM.Commons; + +namespace GroupService.WebApi.Application.GroupMember +{ + public class GroupMemberService + { + private readonly IGroupMemberReposity reposity; + private readonly IGroupReposity groupReposity; + private readonly GroupMemberDomainService service; + private readonly IIdentityIntegrationService idService; + private IMapper mapper; + + public GroupMemberService(IGroupMemberReposity reposity, IGroupReposity groupReposity, GroupMemberDomainService service, IIdentityIntegrationService idService, IMapper mapper) + { + this.reposity = reposity; + this.groupReposity = groupReposity; + this.service = service; + this.idService = idService; + this.mapper = mapper; + } + + public async Task>> GetByGroupIdAsync(Guid groupId) + { + var group = await groupReposity.FindByIdAsync(groupId); + if (group is null) + { + return Result>.Fail(ResultCode.GROUP_NOT_FOUND); + } + var members = await reposity.FindByGroupIdAsync(groupId); + + return Result>.Success(mapper.Map>(members.ToList())); + } + + public async Task> CreateAsync(Guid groupId, Guid userId) + { + var group = await groupReposity.FindByIdAsync(groupId); + + if (group is null) + { + return Result.Fail(ResultCode.GROUP_NOT_FOUND); + } + + var userRes = await idService.FindUserByIdAsync(userId); + if (!userRes.Succeeded) + { + return Result.Fail(userRes); + } + + var memberRes = await service.CreateAsync(userId, groupId, userRes.Data.NickName, userRes.Data.Avatar); + + if (!memberRes.Succeeded) + { + return Result.Fail(userRes); + } + + return Result.Success(mapper.Map(memberRes.Data)); + + } + + public async Task> CheckMemberAsync(Guid groupId, Guid userId) + { + var exist = await reposity.CheckMemberExistAsync(groupId, userId); + + return Result.Success(exist); + } + + public async Task> DeleteAsync(Guid memberId, Guid operatorId) + { + var member = await reposity.FindByIdAsync(memberId); + if (member is null) + { + return Result.Fail(ResultCode.GROUP_MEMBER_NOT_FOUNT); + } + + var operatorMember = await reposity.FindOneByGroupIdAndUserIdAsync(member.GroupId, operatorId); + if (operatorMember is null || operatorMember.Role == Domain.Enums.GroupMemberRole.Normal) + { + return Result.Fail(ResultCode.PERMISSION_DENIED); + } + + member.SoftDelete(); + + return Result.Success(); + } + } +} diff --git a/GroupService.WebApi/Application/GroupRequest/GroupRequestMapperConfig.cs b/GroupService.WebApi/Application/GroupRequest/GroupRequestMapperConfig.cs new file mode 100644 index 0000000..784cf4d --- /dev/null +++ b/GroupService.WebApi/Application/GroupRequest/GroupRequestMapperConfig.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using GroupService.Domain.Entities; +using GroupService.WebApi.Application.Dtos; + +namespace GroupService.WebApi.Application.GroupRequest +{ + public class GroupRequestMapperConfig : Profile + { + public GroupRequestMapperConfig() + { + CreateMap() + .ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.CreationTime)) + .ForMember(dest => dest.Updated, opt => opt.MapFrom(src => src.ModificationTime)) + .ForMember(dest => dest.GroupAvatar, opt => opt.MapFrom(src => src.GroupProfile.Avatar)) + .ForMember(dest => dest.GroupName, opt => opt.MapFrom(src => src.GroupProfile.GroupName)) + .ForMember(dest => dest.UserNickName, opt => opt.MapFrom(src => src.UserProfile.NickName)) + .ForMember(dest => dest.UserAvatar, opt => opt.MapFrom(src => src.UserProfile.Avatar)) + .ForMember(dest => dest.OperatorAvatar, opt => opt.MapFrom(src => src.OperatorAvatar)) + .ForMember(dest => dest.OperatorName, opt => opt.MapFrom(src => src.OperatorName)); + } + } +} diff --git a/GroupService.WebApi/Application/GroupRequest/GroupRequestService.cs b/GroupService.WebApi/Application/GroupRequest/GroupRequestService.cs new file mode 100644 index 0000000..cf3c42e --- /dev/null +++ b/GroupService.WebApi/Application/GroupRequest/GroupRequestService.cs @@ -0,0 +1,101 @@ +using AutoMapper; +using GroupService.Domain.Entities; +using GroupService.Domain.IReposities; +using GroupService.Domain.ValueObjects; +using GroupService.WebApi.Application.Dtos; +using GroupService.WebApi.Application.IntegrationServices; +using IM.Commons; + +namespace GroupService.WebApi.Application.GroupRequest +{ + public class GroupRequestService + { + private readonly IGroupRequestReposity reposity; + private readonly IGroupReposity groupReposity; + private readonly IMapper mapper; + private readonly IGroupMemberReposity memberReposity; + private readonly IIdentityIntegrationService idService; + + public GroupRequestService(IGroupRequestReposity reposity, IGroupReposity groupReposity, IMapper mapper, IGroupMemberReposity memberReposity, IIdentityIntegrationService idService) + { + this.reposity = reposity; + this.groupReposity = groupReposity; + this.mapper = mapper; + this.memberReposity = memberReposity; + this.idService = idService; + } + + public async Task> CreateAsync(Guid groupId, Guid userId, string? desc) + { + var group = await groupReposity.FindByIdAsync(groupId); + if (group == null) + { + return Result.Fail(ResultCode.GROUP_NOT_FOUND); + } + + var user = await idService.FindUserByIdAsync(userId); + + var groupProfile = new GroupProfile() + { + Avatar = group.Avatar, + GroupName = group.Name + }; + + var userProfile = new UserProfile() + { + Avatar = user.Data.Avatar, + NickName = user.Data.NickName + }; + + + var request = new GroupJoinRequest(userId,userProfile, groupId, groupProfile, desc); + reposity.Create(request); + + if (group.Authority == Domain.Enums.GroupAuthorityType.ANYONE_CAN_JOIN) + { + request.Approve(user.Data.Id, user.Data.NickName, user.Data.Avatar); + } + + return Result.Success(mapper.Map(request)); + } + + public async Task> HandleAsync(RequestHandleCommand command) + { + var request = await reposity.FindByIdAsync(command.RequestId); + if (request is null) + { + return Result.Fail(ResultCode.GROUP_REQUEST_NOT_FOUND); + } + + var member = await memberReposity.FindOneByGroupIdAndUserIdAsync(request.GroupId, command.UserId); + + if (member is null || member.Role == Domain.Enums.GroupMemberRole.Normal) + { + return Result.Fail(ResultCode.PERMISSION_DENIED); + } + + if (command.Action == RequestHandleAction.Accept) + { + request.Approve(member.UserId, member.GroupNickName, member.Avatar); + } + else if (command.Action == RequestHandleAction.Reject) + { + request.Decline(member.UserId, member.GroupNickName, member.Avatar); + } + return Result.Success(); + + } + + public async Task> GetByIdAsync(Guid id, Guid userId) + { + var request = await reposity.FindByIdAsync(id); + if (request is null || (request.UserId != userId && request.OperatorId != userId)) + { + return Result.Fail(ResultCode.GROUP_REQUEST_NOT_FOUND); + } + + return Result.Success(mapper.Map(request)); + } + + } +} diff --git a/GroupService.WebApi/Application/GroupRequest/RequestHandleCommand.cs b/GroupService.WebApi/Application/GroupRequest/RequestHandleCommand.cs new file mode 100644 index 0000000..327ce2c --- /dev/null +++ b/GroupService.WebApi/Application/GroupRequest/RequestHandleCommand.cs @@ -0,0 +1,9 @@ +namespace GroupService.WebApi.Application.GroupRequest +{ + public record RequestHandleCommand(Guid RequestId, Guid UserId, RequestHandleAction Action); + public enum RequestHandleAction + { + Accept = 0, + Reject = 1 + } +} diff --git a/GroupService.WebApi/Application/IntegrationServices/IDentityIntegrationService.cs b/GroupService.WebApi/Application/IntegrationServices/IDentityIntegrationService.cs new file mode 100644 index 0000000..31331d7 --- /dev/null +++ b/GroupService.WebApi/Application/IntegrationServices/IDentityIntegrationService.cs @@ -0,0 +1,46 @@ +using GroupService.WebApi.Application.Dtos; +using Grpc.Core; +using IM.Commons; +using IM.Protocols.Grpc.User; + +namespace GroupService.WebApi.Application.IntegrationServices +{ + public class IDentityIntegrationService : IIdentityIntegrationService + { + private readonly UserInternal.UserInternalClient client; + + public IDentityIntegrationService(UserInternal.UserInternalClient client) + { + this.client = client; + } + + public async Task> FindUserByIdAsync(Guid id) + { + try + { + var response = await client.GetUserInfoAsyncAsync(new GetUserInfoRequest() + { + UserId = id.ToString() + }); + return Result.Success(new UserInfoDto() + { + Avatar = response.Avatar, + CreationTime = response.CreationTime.ToDateTimeOffset(), + Deletion = response.Deletion.ToDateTimeOffset(), + Description = response.Description, + Email = response.Email, + Id = Guid.Parse(response.Id), + NickName = response.NickName, + Phone = response.Phone, + Region = response.Region, + UserName = response.UserName + + }); + }catch(RpcException e) + { + return Result.Fail(ResultCode.USER_NOT_FOUND); + } + + } + } +} diff --git a/GroupService.WebApi/Application/IntegrationServices/IIdentityIntegrationService.cs b/GroupService.WebApi/Application/IntegrationServices/IIdentityIntegrationService.cs new file mode 100644 index 0000000..c283d33 --- /dev/null +++ b/GroupService.WebApi/Application/IntegrationServices/IIdentityIntegrationService.cs @@ -0,0 +1,10 @@ +using GroupService.WebApi.Application.Dtos; +using IM.Commons; + +namespace GroupService.WebApi.Application.IntegrationServices +{ + public interface IIdentityIntegrationService + { + Task> FindUserByIdAsync(Guid id); + } +} diff --git a/GroupService.WebApi/Controllers/Group/GroupController.cs b/GroupService.WebApi/Controllers/Group/GroupController.cs new file mode 100644 index 0000000..1187bb2 --- /dev/null +++ b/GroupService.WebApi/Controllers/Group/GroupController.cs @@ -0,0 +1,46 @@ +using GroupService.Infrastructure; +using GroupService.WebApi.Application.Dtos; +using GroupService.WebApi.Application.Group; +using IM.ASPNETCore; +using IM.Commons; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace GroupService.WebApi.Controllers.Group +{ + [Route("api/[controller]/[action]")] + [Authorize] + [ApiController] + public class GroupController : ControllerBase + { + private readonly Application.Group.GroupService service; + + public GroupController(Application.Group.GroupService service) + { + this.service = service; + } + + [HttpGet] + [ProducesDefaultResponseType(typeof(Result>))] + public async Task GetAll() + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.GetAllAsync(Guid.Parse(userId))); + } + [HttpGet("~/api/[controller]/{userId}")] + [ProducesDefaultResponseType(typeof(Result))] + public async Task GetOne([FromRoute] Guid userId) + { + return Ok(await service.GetByIdAsync(userId)); + } + + [HttpPost] + [UnitOfWork(typeof(GroupDbContext))] + public async Task Create(GroupCreateRequest request) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.CreateAsync(new GroupCreateCommand(Guid.Parse(userId), request.Name))); + } + } +} diff --git a/GroupService.WebApi/Controllers/Group/GroupCreateRequest.cs b/GroupService.WebApi/Controllers/Group/GroupCreateRequest.cs new file mode 100644 index 0000000..1c40f7e --- /dev/null +++ b/GroupService.WebApi/Controllers/Group/GroupCreateRequest.cs @@ -0,0 +1,18 @@ +using FluentValidation; + +namespace GroupService.WebApi.Controllers.Group +{ + public class GroupCreateRequest + { + public string? Name { get; set; } + } + + public class GroupCreateRequestValidator : AbstractValidator + { + public GroupCreateRequestValidator() + { + RuleFor(r => r.Name) + .MaximumLength(20); + } + } +} diff --git a/GroupService.WebApi/Controllers/GroupInvitation/GroupInvitationController.cs b/GroupService.WebApi/Controllers/GroupInvitation/GroupInvitationController.cs new file mode 100644 index 0000000..a84cc43 --- /dev/null +++ b/GroupService.WebApi/Controllers/GroupInvitation/GroupInvitationController.cs @@ -0,0 +1,45 @@ +using GroupService.Infrastructure; +using GroupService.WebApi.Application.GroupInvitation; +using IM.ASPNETCore; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace GroupService.WebApi.Controllers.GroupInvitation +{ + [Authorize] + [Route("api/[controller]/[action]")] + [ApiController] + public class GroupInvitationController : ControllerBase + { + private readonly GroupInvitationService service; + + public GroupInvitationController(GroupInvitationService service) + { + this.service = service; + } + + [HttpPost] + [UnitOfWork(typeof(GroupDbContext))] + public async Task Send([FromBody] GroupInvitationRequest request) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.CreateAsync(Guid.Parse(userId), request.UserId, request.GroupId)); + } + + [HttpGet] + public async Task Get([FromQuery] Guid invitationId) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.GetByIdAsync(invitationId, Guid.Parse(userId))); + } + + [HttpPost] + [UnitOfWork(typeof(GroupDbContext))] + public async Task Handle([FromQuery] Guid invitationId, [FromQuery] GroupInvitationAction action) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.HandleAsync(new GroupInvitationHandleCommand(invitationId, Guid.Parse(userId), action))); + } + } +} diff --git a/GroupService.WebApi/Controllers/GroupInvitation/GroupInvitationRequest.cs b/GroupService.WebApi/Controllers/GroupInvitation/GroupInvitationRequest.cs new file mode 100644 index 0000000..ff7efda --- /dev/null +++ b/GroupService.WebApi/Controllers/GroupInvitation/GroupInvitationRequest.cs @@ -0,0 +1,24 @@ +using FluentValidation; + +namespace GroupService.WebApi.Controllers.GroupInvitation +{ + public class GroupInvitationRequest + { + public Guid GroupId { get; set; } + public Guid UserId { get; set; } + } + + public class GroupInvitationRequestValidator : AbstractValidator + { + public GroupInvitationRequestValidator() + { + RuleFor(r => r.UserId) + .NotNull() + .NotEmpty(); + + RuleFor(r => r.GroupId) + .NotEmpty() + .NotNull(); + } + } +} diff --git a/GroupService.WebApi/Controllers/GroupMember/GroupMemberController.cs b/GroupService.WebApi/Controllers/GroupMember/GroupMemberController.cs new file mode 100644 index 0000000..1962706 --- /dev/null +++ b/GroupService.WebApi/Controllers/GroupMember/GroupMemberController.cs @@ -0,0 +1,39 @@ +using GroupService.WebApi.Application.GroupMember; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace GroupService.WebApi.Controllers.GroupMember +{ + [Route("api/[controller]/[action]")] + [ApiController] + public class GroupMemberController : ControllerBase + { + private readonly GroupMemberService service; + + public GroupMemberController(GroupMemberService service) + { + this.service = service; + } + + [HttpGet] + public async Task CheckMember(Guid userId, Guid groupId) + { + return Ok(await service.CheckMemberAsync(groupId, userId)); + } + + [HttpGet] + public async Task List(Guid groupId) + { + return Ok(await service.GetByGroupIdAsync(groupId)); + } + + [HttpPost] + public async Task Delete([FromRoute] Guid memberId) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.DeleteAsync(memberId, Guid.Parse(userId))); + } + + + } +} diff --git a/GroupService.WebApi/Controllers/GroupRequest/GroupRequestController.cs b/GroupService.WebApi/Controllers/GroupRequest/GroupRequestController.cs new file mode 100644 index 0000000..93ffef2 --- /dev/null +++ b/GroupService.WebApi/Controllers/GroupRequest/GroupRequestController.cs @@ -0,0 +1,45 @@ +using GroupService.Infrastructure; +using GroupService.WebApi.Application.GroupRequest; +using IM.ASPNETCore; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace GroupService.WebApi.Controllers.GroupRequest +{ + [Authorize] + [Route("api/[controller]/[action]")] + [ApiController] + public class GroupRequestController : ControllerBase + { + private readonly GroupRequestService service; + + public GroupRequestController(GroupRequestService service) + { + this.service = service; + } + + [HttpPost] + [UnitOfWork(typeof(GroupDbContext))] + public async Task Send([FromBody] GroupRequestRequest request) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.CreateAsync(request.GroupId, Guid.Parse(userId), request.Desc)); + } + + [HttpPost] + [UnitOfWork(typeof(GroupDbContext))] + public async Task Handle([FromQuery] Guid requestId, [FromQuery] RequestHandleAction action) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.HandleAsync(new RequestHandleCommand(requestId, Guid.Parse(userId), action))); + } + + [HttpGet] + public async Task Find([FromQuery] Guid id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.GetByIdAsync(id, Guid.Parse(userId))); + } + } +} diff --git a/GroupService.WebApi/Controllers/GroupRequest/GroupRequestRequest.cs b/GroupService.WebApi/Controllers/GroupRequest/GroupRequestRequest.cs new file mode 100644 index 0000000..afd4629 --- /dev/null +++ b/GroupService.WebApi/Controllers/GroupRequest/GroupRequestRequest.cs @@ -0,0 +1,25 @@ +using FluentValidation; + +namespace GroupService.WebApi.Controllers.GroupRequest +{ + public class GroupRequestRequest + { + public Guid GroupId { get; set; } + public string? Desc { get; set; } + } + + public class GroupRequestRequestValidator : AbstractValidator + { + public GroupRequestRequestValidator() + { + RuleFor(r => r.GroupId) + .NotEmpty() + .NotNull(); + + RuleFor(r => r.Desc) + .MaximumLength(20) + .WithMessage("入群描述不可超过20字符") + ; + } + } +} diff --git a/GroupService.WebApi/DesignTimeDbContextFactory.cs b/GroupService.WebApi/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..7c5fed0 --- /dev/null +++ b/GroupService.WebApi/DesignTimeDbContextFactory.cs @@ -0,0 +1,18 @@ +using GroupService.Infrastructure; +using IM.InitCommon; +using Microsoft.EntityFrameworkCore.Design; + +namespace ContactService.WebApi +{ + public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory + { + public GroupDbContext CreateDbContext(string[] args) + { + // 1. 复用你写好的配置工厂,提取连接字符串 + var optionsBuilder = DbContextOptionsBuilderFactory.Create(); + + // 2. 🌟 关键补刀:把假的 Mediator 传进去,满足构造函数的要求! + return new GroupDbContext(optionsBuilder.Options, null); + } + } +} diff --git a/GroupService.WebApi/GroupService.WebApi.csproj b/GroupService.WebApi/GroupService.WebApi.csproj new file mode 100644 index 0000000..a8bccb8 --- /dev/null +++ b/GroupService.WebApi/GroupService.WebApi.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/GroupService.WebApi/GroupService.WebApi.http b/GroupService.WebApi/GroupService.WebApi.http new file mode 100644 index 0000000..906a211 --- /dev/null +++ b/GroupService.WebApi/GroupService.WebApi.http @@ -0,0 +1,6 @@ +@GroupService.WebApi_HostAddress = http://localhost:5070 + +GET {{GroupService.WebApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/GroupService.WebApi/ModuleInit.cs b/GroupService.WebApi/ModuleInit.cs new file mode 100644 index 0000000..7637ae2 --- /dev/null +++ b/GroupService.WebApi/ModuleInit.cs @@ -0,0 +1,27 @@ +using GroupService.WebApi.Application.GroupInvitation; +using GroupService.WebApi.Application.GroupMember; +using GroupService.WebApi.Application.GroupRequest; +using GroupService.WebApi.Application.IntegrationServices; +using IM.Commons; +using IM.Protocols.Grpc.User; +using Microsoft.Extensions.Options; + +namespace GroupService.WebApi +{ + public class ModuleInit : IModuleInitializer + { + public void Initialize(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddGrpcClient((sp, o) => + { + var options = sp.GetRequiredService>(); + o.Address = new Uri(options.CurrentValue.IdentityServiceUrl); + }); + } + } +} diff --git a/GroupService.WebApi/Program.cs b/GroupService.WebApi/Program.cs new file mode 100644 index 0000000..05a7d19 --- /dev/null +++ b/GroupService.WebApi/Program.cs @@ -0,0 +1,38 @@ + +using IM.InitCommon; + +namespace GroupService.WebApi +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + builder.ConfigureDbConfiguration(); + + builder.Services.AddControllers(); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + builder.ConfigExtraServices(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseAppDefault(); + + + app.MapControllers(); + + app.Run(); + } + } +} diff --git a/GroupService.WebApi/Properties/launchSettings.json b/GroupService.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..791534c --- /dev/null +++ b/GroupService.WebApi/Properties/launchSettings.json @@ -0,0 +1,49 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5070" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7205;http://localhost:5070" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:24564", + "sslPort": 44370 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iissettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:53562/", + "sslPort": 44369 + } + } +} \ No newline at end of file diff --git a/GroupService.WebApi/appsettings.Development.json b/GroupService.WebApi/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/GroupService.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/GroupService.WebApi/appsettings.json b/GroupService.WebApi/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/GroupService.WebApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/IM.ASPNETCore/ApiControllerBase.cs b/IM.ASPNETCore/ApiControllerBase.cs new file mode 100644 index 0000000..ba955b3 --- /dev/null +++ b/IM.ASPNETCore/ApiControllerBase.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Mvc; + +namespace IM.ASPNETCore +{ + public class ApiControllerBase : ControllerBase + { + } +} diff --git a/IM.ASPNETCore/ExceptionMiddleware.cs b/IM.ASPNETCore/ExceptionMiddleware.cs new file mode 100644 index 0000000..3151725 --- /dev/null +++ b/IM.ASPNETCore/ExceptionMiddleware.cs @@ -0,0 +1,34 @@ +using IM.Commons; +using IM.DomainCommons; +using Microsoft.AspNetCore.Http; + +namespace IM.ASPNETCore +{ + public class ExceptionMiddleware + { + private readonly RequestDelegate _next; + public ExceptionMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (DomainException ex) + { + await DomainExceptionHandlerAsync(context, ex); + } + } + + public Task DomainExceptionHandlerAsync(HttpContext context, DomainException ex) + { + context.Response.ContentType = "application/json"; + var result = Result.Fail(ResultCode.PARAMETER_ERROR, ex.Message); // 包装成你的 Result + return context.Response.WriteAsJsonAsync(result); + } + } +} diff --git a/IM.ASPNETCore/IM.ASPNETCore.csproj b/IM.ASPNETCore/IM.ASPNETCore.csproj new file mode 100644 index 0000000..3f48768 --- /dev/null +++ b/IM.ASPNETCore/IM.ASPNETCore.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/IM.ASPNETCore/UnitOfWorkAttribute.cs b/IM.ASPNETCore/UnitOfWorkAttribute.cs new file mode 100644 index 0000000..a0e54ed --- /dev/null +++ b/IM.ASPNETCore/UnitOfWorkAttribute.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; + +namespace IM.ASPNETCore +{ + [AttributeUsage(AttributeTargets.Class + | AttributeTargets.Method, AllowMultiple = false, Inherited = true + )] + public class UnitOfWorkAttribute : Attribute + { + public Type[] DbContextTypes { get; private set; } + + public UnitOfWorkAttribute(params Type[] dbContextTypes) + { + this.DbContextTypes = dbContextTypes; + foreach (var type in dbContextTypes) + { + if (!typeof(DbContext).IsAssignableFrom(type)) + { + throw new ArgumentException($"{type} must inherit from DbContext"); + } + } + } + } +} diff --git a/IM.ASPNETCore/UnitOfWorkFilter.cs b/IM.ASPNETCore/UnitOfWorkFilter.cs new file mode 100644 index 0000000..c58f2a1 --- /dev/null +++ b/IM.ASPNETCore/UnitOfWorkFilter.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; +using System.Transactions; + +namespace IM.ASPNETCore +{ + public class UnitOfWorkFilter : IAsyncActionFilter + { + private static UnitOfWorkAttribute? GetUoWAttr(ActionDescriptor actionDesc) + { + var caDesc = actionDesc as ControllerActionDescriptor; + if (caDesc == null) + { + return null; + } + //try to get UnitOfWorkAttribute from controller, + //if there is no UnitOfWorkAttribute on controller, + //try to get UnitOfWorkAttribute from action + var uowAttr = caDesc.ControllerTypeInfo + .GetCustomAttribute(); + if (uowAttr != null) + { + return uowAttr; + } + else + { + return caDesc.MethodInfo + .GetCustomAttribute(); + } + } + public async Task OnActionExecutionAsync(ActionExecutingContext context, + ActionExecutionDelegate next) + { + var uowAttr = GetUoWAttr(context.ActionDescriptor); + if (uowAttr == null) + { + await next(); + return; + } + using TransactionScope txScope = new(TransactionScopeAsyncFlowOption.Enabled); + List dbCtxs = new List(); + foreach (var dbCtxType in uowAttr.DbContextTypes) + { + //用HttpContext的RequestServices + //确保获取的是和请求相关的Scope实例 + var sp = context.HttpContext.RequestServices; + DbContext dbCtx = (DbContext)sp.GetRequiredService(dbCtxType); + dbCtxs.Add(dbCtx); + } + var result = await next(); + if (result.Exception == null) + { + foreach (var dbCtx in dbCtxs) + { + await dbCtx.SaveChangesAsync(); + } + txScope.Complete(); + } + } + } +} diff --git a/IM.ASPNETCore/ValidatorFilter.cs b/IM.ASPNETCore/ValidatorFilter.cs new file mode 100644 index 0000000..ea9953e --- /dev/null +++ b/IM.ASPNETCore/ValidatorFilter.cs @@ -0,0 +1,27 @@ +using IM.Commons; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace IM.ASPNETCore +{ + public class ValidatorFilter : IActionFilter + { + public void OnActionExecuted(ActionExecutedContext context) + { + + } + + public void OnActionExecuting(ActionExecutingContext context) + { + if (context.ModelState.IsValid) return; + + var errors = context.ModelState.Where(x => x.Value.Errors.Count() > 0).ToList(); + + var errMsg = errors.FirstOrDefault().Value?.Errors.First().ErrorMessage + ?? ResultCode.PARAMETER_ERROR.GetDescription(); + var result = Result.Fail(ResultCode.PARAMETER_ERROR, errMsg); + + context.Result = new OkObjectResult(result); + } + } +} diff --git a/IM.Commons/BaseEvent.cs b/IM.Commons/BaseEvent.cs new file mode 100644 index 0000000..071fe1b --- /dev/null +++ b/IM.Commons/BaseEvent.cs @@ -0,0 +1,10 @@ +namespace IM.Commons +{ + public class BaseEvent + { + // --- 标准元数据 --- + public Guid EventId { get; init; } = Guid.NewGuid(); + public DateTime OccurredOn { get; init; } = DateTime.Now; + public Guid CorrelationId { get; init; } + } +} diff --git a/IM.Commons/BaseSpecification.cs b/IM.Commons/BaseSpecification.cs new file mode 100644 index 0000000..81e326b --- /dev/null +++ b/IM.Commons/BaseSpecification.cs @@ -0,0 +1,14 @@ +using System.Linq.Expressions; + +namespace IM.Commons +{ + public abstract class BaseSpecification : ISpecification + { + public Expression> Criteria { get; private set; } + + public List>> Includes { get; private set; } + + + public Expression> Select { get; private set; } + } +} diff --git a/IM.Commons/EnumHelper.cs b/IM.Commons/EnumHelper.cs new file mode 100644 index 0000000..76fcd5e --- /dev/null +++ b/IM.Commons/EnumHelper.cs @@ -0,0 +1,21 @@ +using System.ComponentModel; +using System.Reflection; + +namespace IM.Commons +{ + public static class EnumHelper + { + /// + /// 获取枚举的 Description 描述信息 + /// + public static string GetDescription(this Enum value) + { + FieldInfo? field = value.GetType().GetField(value.ToString()); + if (field == null) return value.ToString(); + + DescriptionAttribute? attribute = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute; + + return attribute == null ? value.ToString() : attribute.Description; + } + } +} diff --git a/IM.Commons/EventHandlerException.cs b/IM.Commons/EventHandlerException.cs new file mode 100644 index 0000000..99df5f6 --- /dev/null +++ b/IM.Commons/EventHandlerException.cs @@ -0,0 +1,13 @@ +namespace IM.Commons +{ + public class EventHandlerException : Exception + { + public EventHandlerException() + { + } + + public EventHandlerException(string? message) : base(message) + { + } + } +} diff --git a/IM.Commons/GrpcOptions.cs b/IM.Commons/GrpcOptions.cs new file mode 100644 index 0000000..31c18ee --- /dev/null +++ b/IM.Commons/GrpcOptions.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IM.Commons +{ + public class GrpcOptions + { + public string MessageServiceUrl { get; set; } + public string IdentityServiceUrl { get; set; } + public string ContactServiceUrl { get; set; } + } +} diff --git a/IM.Commons/IM.Commons.csproj b/IM.Commons/IM.Commons.csproj new file mode 100644 index 0000000..e0af041 --- /dev/null +++ b/IM.Commons/IM.Commons.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/IM.Commons/IModuleInitializer.cs b/IM.Commons/IModuleInitializer.cs new file mode 100644 index 0000000..e24626a --- /dev/null +++ b/IM.Commons/IModuleInitializer.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace IM.Commons +{ + /// + /// 所有项目中的实现了IModuleInitializer接口都会被调用,请在Initialize中编写注册本模块需要的服务。 + /// 一个项目中可以放多个实现了IModuleInitializer的类。不过为了集中管理,还是建议一个项目中只放一个实现了IModuleInitializer的类 + /// + public interface IModuleInitializer + { + public void Initialize(IServiceCollection services); + } +} diff --git a/IM.Commons/IRedisService.cs b/IM.Commons/IRedisService.cs new file mode 100644 index 0000000..94e250f --- /dev/null +++ b/IM.Commons/IRedisService.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace IM.Commons +{ + public interface IRedisService + { + /// + /// 设置缓存 + /// + /// 缓存对象类型 + /// 缓存索引值 + /// 要缓存的对象 + /// 过期时间 + /// + Task SetAsync(string key, T value, TimeSpan? expiration = null); + /// + /// 获取缓存 + /// + /// + /// 缓存索引 + /// + Task GetAsync(string key); + /// + /// 删除缓存 + /// + /// 缓存索引 + /// + Task RemoveAsync(string key); + } + + public static class RedisServiceExtension + { + public static IServiceCollection AddRedisCache(this IServiceCollection services) + { + return services.AddScoped(); + } + } +} diff --git a/IM.Commons/ISpecification.cs b/IM.Commons/ISpecification.cs new file mode 100644 index 0000000..98e2070 --- /dev/null +++ b/IM.Commons/ISpecification.cs @@ -0,0 +1,14 @@ +using System.Linq.Expressions; + +namespace IM.Commons +{ + public interface ISpecification : ISpecification + { + public Expression> Select { get; } + } + public interface ISpecification + { + Expression> Criteria { get; } + List>> Includes { get; } + } +} diff --git a/IM.Commons/IntegrationEvents/FriendAddedEvent.cs b/IM.Commons/IntegrationEvents/FriendAddedEvent.cs new file mode 100644 index 0000000..9134335 --- /dev/null +++ b/IM.Commons/IntegrationEvents/FriendAddedEvent.cs @@ -0,0 +1,14 @@ +namespace IM.Commons.IntegrationEvents +{ + public class FriendAddedEvent : BaseEvent + { + public Guid OwnerId { get; set; } + public string OwnerNickName { get; set; } + public string? OwnerAvatar { get; set; } + public Guid TargetId { get; set; } + public string TargetNickName { get; set; } + public string? TargetAvatar { get; set; } + public string? RemarkName { get; set; } + public string Status { get; set; } + } +} diff --git a/IM.Commons/IntegrationEvents/FriendRequestStateUpdateEvent.cs b/IM.Commons/IntegrationEvents/FriendRequestStateUpdateEvent.cs new file mode 100644 index 0000000..d9755d3 --- /dev/null +++ b/IM.Commons/IntegrationEvents/FriendRequestStateUpdateEvent.cs @@ -0,0 +1,31 @@ +namespace IM.Commons.IntegrationEvents +{ + public class FriendRequestStateUpdateEvent : BaseEvent + { + /// + /// 申请人 + /// + public Guid OwnerId { get; set; } + + /// + /// 被申请人 + /// + public Guid TargetId { get; set; } + + + /// + /// 申请附言 + /// + public string Description { get; set; } + + /// + /// 申请状态(0:待通过,1:拒绝,2:同意,3:拉黑) + /// + public string State { get; set; } + + /// + /// 备注 + /// + public string? RemarkName { get; set; } + } +} diff --git a/IM.Commons/IntegrationEvents/GroupBlockEvent.cs b/IM.Commons/IntegrationEvents/GroupBlockEvent.cs new file mode 100644 index 0000000..c0511a8 --- /dev/null +++ b/IM.Commons/IntegrationEvents/GroupBlockEvent.cs @@ -0,0 +1,20 @@ +namespace IM.Commons.IntegrationEvents +{ + public class GroupBlockEvent : BaseEvent + { + public Guid GroupId { get; private set; } + public string GroupName { get; private set; } + public Guid GroupMaster { get; private set; } + public string Status { get; private set; } + public string? Avatar { get; private set; } + + public GroupBlockEvent(Guid groupId, string groupName, Guid groupMaster, string status, string? avatar) + { + GroupId = groupId; + GroupName = groupName; + GroupMaster = groupMaster; + Status = status; + Avatar = avatar; + } + } +} diff --git a/IM.Commons/IntegrationEvents/GroupCreateEvent.cs b/IM.Commons/IntegrationEvents/GroupCreateEvent.cs new file mode 100644 index 0000000..e313452 --- /dev/null +++ b/IM.Commons/IntegrationEvents/GroupCreateEvent.cs @@ -0,0 +1,18 @@ +namespace IM.Commons.IntegrationEvents +{ + public class GroupCreateEvent + { + public Guid GroupId { get; private set; } + public string GroupName { get; private set; } + public Guid GroupMaster { get; private set; } + public string? Avatar { get; private set; } + + public GroupCreateEvent(Guid groupId, string groupName, Guid groupMaster, string? avatar) + { + GroupId = groupId; + GroupName = groupName; + GroupMaster = groupMaster; + Avatar = avatar; + } + } +} diff --git a/IM.Commons/IntegrationEvents/GroupInvitationAcceptEvent.cs b/IM.Commons/IntegrationEvents/GroupInvitationAcceptEvent.cs new file mode 100644 index 0000000..dfc877a --- /dev/null +++ b/IM.Commons/IntegrationEvents/GroupInvitationAcceptEvent.cs @@ -0,0 +1,9 @@ +namespace IM.Commons.IntegrationEvents +{ + public class GroupInvitationAcceptEvent : GroupInvitationCreateEvent + { + public GroupInvitationAcceptEvent(Guid invitationId, Guid userId, string userNickName, string? userAvatar, Guid groupId, string groupName, string groupAvatar, Guid operatorId, string operatorName, string operatorAvatar) : base(invitationId, userId, userNickName, userAvatar, groupId, groupName, groupAvatar, operatorId, operatorName, operatorAvatar) + { + } + } +} diff --git a/IM.Commons/IntegrationEvents/GroupInvitationCreateEvent.cs b/IM.Commons/IntegrationEvents/GroupInvitationCreateEvent.cs new file mode 100644 index 0000000..4ff35cc --- /dev/null +++ b/IM.Commons/IntegrationEvents/GroupInvitationCreateEvent.cs @@ -0,0 +1,30 @@ +namespace IM.Commons.IntegrationEvents +{ + public class GroupInvitationCreateEvent : BaseEvent + { + public Guid InvitationId { get; private set; } + public Guid UserId { get; private set; } + public string UserNickName { get; private set; } + public string? UserAvatar { get; private set; } + public Guid GroupId { get; private set; } + public string GroupName { get; private set; } + public string GroupAvatar { get; private set; } + public Guid OperatorId { get; private set; } + public string OperatorName { get; private set; } + public string OperatorAvatar { get; private set; } + + public GroupInvitationCreateEvent(Guid invitationId, Guid userId, string userNickName, string? userAvatar, Guid groupId, string groupName, string groupAvatar, Guid operatorId, string operatorName, string operatorAvatar) + { + InvitationId = invitationId; + UserId = userId; + UserNickName = userNickName; + UserAvatar = userAvatar; + GroupId = groupId; + GroupName = groupName; + GroupAvatar = groupAvatar; + OperatorId = operatorId; + OperatorName = operatorName; + OperatorAvatar = operatorAvatar; + } + } +} diff --git a/IM.Commons/IntegrationEvents/GroupMemberJoinedEvent.cs b/IM.Commons/IntegrationEvents/GroupMemberJoinedEvent.cs new file mode 100644 index 0000000..083bec6 --- /dev/null +++ b/IM.Commons/IntegrationEvents/GroupMemberJoinedEvent.cs @@ -0,0 +1,22 @@ +namespace IM.Commons.IntegrationEvents +{ + public class GroupMemberJoinedEvent + { + public Guid Id { get; private set; } + public Guid UserId { get; private set; } + public Guid GroupId { get; private set; } + public string GroupNickName { get; private set; } + public string? Avatar { get; private set; } + public string Role { get; private set; } + + public GroupMemberJoinedEvent(Guid id, Guid userId, Guid groupId, string groupNickName, string? avatar, string role) + { + Id = id; + UserId = userId; + GroupId = groupId; + GroupNickName = groupNickName; + Avatar = avatar; + Role = role; + } + } +} diff --git a/IM.Commons/IntegrationEvents/GroupRequestDeclinedEvent.cs b/IM.Commons/IntegrationEvents/GroupRequestDeclinedEvent.cs new file mode 100644 index 0000000..e14282b --- /dev/null +++ b/IM.Commons/IntegrationEvents/GroupRequestDeclinedEvent.cs @@ -0,0 +1,9 @@ +namespace IM.Commons.IntegrationEvents +{ + public class GroupRequestDeclinedEvent : GroupRequestPassedEvent + { + public GroupRequestDeclinedEvent(Guid requestId, Guid groupId, string groupName, string groupAvatar, Guid userId, string userNickName, string? userAvatar, Guid operatorId, string operatorName, string operatorAvatar, string description, DateTimeOffset created, DateTimeOffset updated) : base(requestId, groupId, groupName, groupAvatar, userId, userNickName, userAvatar, operatorId, operatorName, operatorAvatar, description, created, updated) + { + } + } +} diff --git a/IM.Commons/IntegrationEvents/GroupRequestPassedEvent.cs b/IM.Commons/IntegrationEvents/GroupRequestPassedEvent.cs new file mode 100644 index 0000000..d46cfcf --- /dev/null +++ b/IM.Commons/IntegrationEvents/GroupRequestPassedEvent.cs @@ -0,0 +1,36 @@ +namespace IM.Commons.IntegrationEvents +{ + public class GroupRequestPassedEvent + { + public Guid RequestId { get; private set; } + public Guid GroupId { get; private set; } + public string GroupName { get; private set; } + public string GroupAvatar { get; private set; } + public Guid UserId { get; private set; } + public string UserNickName { get; private set; } + public string? UserAvatar { get; private set; } + public Guid OperatorId { get; private set; } + public string OperatorName { get; private set; } + public string OperatorAvatar { get; private set; } + public string Description { get; private set; } + public DateTimeOffset Created { get; private set; } + public DateTimeOffset Updated { get; private set; } + + public GroupRequestPassedEvent(Guid requestId, Guid groupId, string groupName, string groupAvatar, Guid userId, string userNickName, string? userAvatar, Guid operatorId, string operatorName, string operatorAvatar, string description, DateTimeOffset created, DateTimeOffset updated) + { + RequestId = requestId; + GroupId = groupId; + GroupName = groupName; + GroupAvatar = groupAvatar; + UserId = userId; + UserNickName = userNickName; + UserAvatar = userAvatar; + OperatorId = operatorId; + OperatorName = operatorName; + OperatorAvatar = operatorAvatar; + Description = description; + Created = created; + Updated = updated; + } + } +} diff --git a/IM.Commons/IntegrationEvents/MsgCreatedEvent.cs b/IM.Commons/IntegrationEvents/MsgCreatedEvent.cs new file mode 100644 index 0000000..85c5836 --- /dev/null +++ b/IM.Commons/IntegrationEvents/MsgCreatedEvent.cs @@ -0,0 +1,45 @@ +namespace IM.Commons.IntegrationEvents +{ + public class MsgCreatedEvent + { + public Guid Id { get; set; } + public Guid ClientId { get; set; } + public string ChatType { get; set; } + + /// + /// 消息类型 + /// (0:文本,1:图片,2:语音,3:视频,4:文件,5:语音聊天,6:视频聊天) + /// + public string MsgType { get; set; } + /// + /// 发送者 + /// + public Guid SenderId { get; set; } + + /// + /// 接收者(私聊为用户ID,群聊为群聊ID) + /// + public Guid TargetId { get; set; } + + /// + /// 消息状态(0:已发送,1:已撤回) + /// + public string State { get; set; } + + /// + /// 消息推送唯一标识符 + /// + public string StreamKey { get; set; } + + /// + /// 消息排序标识 + /// + + public long SequenceId { get; set; } + public MsgContent Content { get; set; } + + } + + public record MsgContent(string Fallback, object Body, Dictionary Ext, QuoteInfoDto Quote); + public record QuoteInfoDto(Guid MessageId, Guid SenderId, string SenderName, string MessageType, string Preview); +} diff --git a/IM.Commons/IntegrationEvents/MsgWithdrawEvent.cs b/IM.Commons/IntegrationEvents/MsgWithdrawEvent.cs new file mode 100644 index 0000000..5671212 --- /dev/null +++ b/IM.Commons/IntegrationEvents/MsgWithdrawEvent.cs @@ -0,0 +1,16 @@ +namespace IM.Commons.IntegrationEvents +{ + public class MsgWithdrawEvent + { + public Guid MsgId { get; private set; } + public string State { get; private set; } + public string StreamKey { get; private set; } + + public MsgWithdrawEvent(Guid msgId, string state, string streamKey) + { + MsgId = msgId; + State = state; + StreamKey = streamKey; + } + } +} diff --git a/IM.Commons/IntegrationEvents/UserProfileUpdateEvent.cs b/IM.Commons/IntegrationEvents/UserProfileUpdateEvent.cs new file mode 100644 index 0000000..85c4102 --- /dev/null +++ b/IM.Commons/IntegrationEvents/UserProfileUpdateEvent.cs @@ -0,0 +1,14 @@ +namespace IM.Commons.IntegrationEvents +{ + public class UserProfileUpdateEvent : BaseEvent + { + public Guid UserId { get; set; } + public string NickName { get; set; } + public string? Avatar { get; set; } + public string Description { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + public string Status { get; set; } + + } +} diff --git a/IM.Commons/RedisCacheService.cs b/IM.Commons/RedisCacheService.cs new file mode 100644 index 0000000..e405964 --- /dev/null +++ b/IM.Commons/RedisCacheService.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; + +namespace IM.Commons +{ + public class RedisCacheService : IRedisService + { + private readonly IDistributedCache _cache; + public RedisCacheService(IDistributedCache cache) + { + _cache = cache; + } + + public async Task GetAsync(string key) + { + var valueBytes = await _cache.GetAsync(key); + if (valueBytes is null || valueBytes.Length == 0) return default; + return JsonSerializer.Deserialize(valueBytes); + } + + public async Task RemoveAsync(string key) => await _cache.RemoveAsync(key); + + public async Task SetAsync(string key, T value, TimeSpan? expiration = null) + { + var options = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromHours(1) + }; + var valueBytes = JsonSerializer.SerializeToUtf8Bytes(value); + await _cache.SetAsync(key, valueBytes, options); + } + + } +} diff --git a/IM.Commons/RedisHelper.cs b/IM.Commons/RedisHelper.cs new file mode 100644 index 0000000..af438c1 --- /dev/null +++ b/IM.Commons/RedisHelper.cs @@ -0,0 +1,15 @@ +namespace IM.Commons +{ + public static class RedisHelper + { + public static string GetRefreshTokenKey(string token) => $"sys:refreshtoken:{token}"; + public static string GetUserinfoKey(string userId) => $"user:uinfo:{userId}"; + public static string GetUserinfoKeyByUsername(string username) => $"user:uinfobyid:{username}"; + public static string GetSequenceIdKey(string streamKey) => $"chat:seq:{streamKey}"; + public static string GetSequenceIdLockKey(string streamKey) => $"lock:seq:{streamKey}"; + public static string GetConnectionIdKey(string userId) => $"signalr:user:con:{userId}"; + + public static string GetUploadPartKey(Guid taskId) => $"upload:task:{taskId}:parts"; + public static string MergeStatus(Guid taskId) => $"upload:task:{taskId}:merge"; + } +} diff --git a/IM.Commons/ReflectionHelper.cs b/IM.Commons/ReflectionHelper.cs new file mode 100644 index 0000000..9141400 --- /dev/null +++ b/IM.Commons/ReflectionHelper.cs @@ -0,0 +1,221 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +namespace IM.Commons +{ + public static class ReflectionHelper + { + + /// + /// 据产品名称获取程序集 + /// + /// + /// + public static IEnumerable GetAssembliesByProductName(string productName) + { + var asms = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var asm in asms) + { + var asmCompanyAttr = asm.GetCustomAttribute(); + if (asmCompanyAttr != null && asmCompanyAttr.Product == productName) + { + yield return asm; + } + } + } + //是否是微软等的官方Assembly + private static bool IsSystemAssembly(Assembly asm) + { + var asmCompanyAttr = asm.GetCustomAttribute(); + if (asmCompanyAttr == null) + { + return false; + } + else + { + string companyName = asmCompanyAttr.Company; + return companyName.Contains("Microsoft"); + } + } + + private static bool IsSystemAssembly(string asmPath) + { + var moduleDef = AsmResolver.DotNet.ModuleDefinition.FromFile(asmPath); + var assembly = moduleDef.Assembly; + if (assembly == null) + { + return false; + } + var asmCompanyAttr = assembly.CustomAttributes.FirstOrDefault(c => c.Constructor?.DeclaringType?.FullName == typeof(AssemblyCompanyAttribute).FullName); + if (asmCompanyAttr == null) + { + return false; + } + var companyName = ((AsmResolver.Utf8String?)asmCompanyAttr.Signature?.FixedArguments[0]?.Element)?.Value; + if (companyName == null) + { + return false; + } + return companyName.Contains("Microsoft"); + } + + /// + /// 判断file这个文件是否是程序集 + /// + /// + /// + private static bool IsManagedAssembly(string file) + { + using var fs = File.OpenRead(file); + using PEReader peReader = new PEReader(fs); + return peReader.HasMetadata && peReader.GetMetadataReader().IsAssembly; + } + + private static Assembly? TryLoadAssembly(string asmPath) + { + AssemblyName asmName = AssemblyName.GetAssemblyName(asmPath); + Assembly? asm = null; + try + { + asm = Assembly.Load(asmName); + } + catch (BadImageFormatException ex) + { + Debug.WriteLine(ex); + } + catch (FileLoadException ex) + { + Debug.WriteLine(ex); + } + + if (asm == null) + { + try + { + asm = Assembly.LoadFile(asmPath); + } + catch (BadImageFormatException ex) + { + Debug.WriteLine(ex); + } + catch (FileLoadException ex) + { + Debug.WriteLine(ex); + } + } + return asm; + } + + /// + /// loop through all assemblies + /// + /// + public static IEnumerable GetAllReferencedAssemblies(bool skipSystemAssemblies = true) + { + Assembly? rootAssembly = Assembly.GetEntryAssembly(); + if (rootAssembly == null) + { + rootAssembly = Assembly.GetCallingAssembly(); + } + var returnAssemblies = new HashSet(new AssemblyEquality()); + var loadedAssemblies = new HashSet(); + var assembliesToCheck = new Queue(); + assembliesToCheck.Enqueue(rootAssembly); + if (skipSystemAssemblies && IsSystemAssembly(rootAssembly) != false) + { + if (IsValid(rootAssembly)) + { + returnAssemblies.Add(rootAssembly); + } + } + while (assembliesToCheck.Any()) + { + var assemblyToCheck = assembliesToCheck.Dequeue(); + foreach (var reference in assemblyToCheck.GetReferencedAssemblies()) + { + if (!loadedAssemblies.Contains(reference.FullName)) + { + var assembly = Assembly.Load(reference); + if (skipSystemAssemblies && IsSystemAssembly(assembly)) + { + continue; + } + assembliesToCheck.Enqueue(assembly); + loadedAssemblies.Add(reference.FullName); + if (IsValid(assembly)) + { + returnAssemblies.Add(assembly); + } + } + } + } + var asmsInBaseDir = Directory.EnumerateFiles(AppContext.BaseDirectory, + "*.dll", new EnumerationOptions { RecurseSubdirectories = true }); + foreach (var asmPath in asmsInBaseDir) + { + if (!IsManagedAssembly(asmPath)) + { + continue; + } + AssemblyName asmName = AssemblyName.GetAssemblyName(asmPath); + //如果程序集已经加载过了就不再加载 + if (returnAssemblies.Any(x => AssemblyName.ReferenceMatchesDefinition(x.GetName(), asmName))) + { + continue; + } + if (skipSystemAssemblies && IsSystemAssembly(asmPath)) + { + continue; + } + Assembly? asm = TryLoadAssembly(asmPath); + if (asm == null) + { + continue; + } + //Assembly asm = Assembly.Load(asmName); + if (!IsValid(asm)) + { + continue; + } + if (skipSystemAssemblies && IsSystemAssembly(asm)) + { + continue; + } + returnAssemblies.Add(asm); + } + return returnAssemblies.ToArray(); + } + + private static bool IsValid(Assembly asm) + { + try + { + asm.GetTypes(); + asm.DefinedTypes.ToList(); + return true; + } + catch (ReflectionTypeLoadException) + { + return false; + } + } + + class AssemblyEquality : EqualityComparer + { + public override bool Equals(Assembly? x, Assembly? y) + { + if (x == null && y == null) return true; + if (x == null || y == null) return false; + return AssemblyName.ReferenceMatchesDefinition(x.GetName(), y.GetName()); + } + + public override int GetHashCode([DisallowNull] Assembly obj) + { + return obj.GetName().FullName.GetHashCode(); + } + } + } +} diff --git a/IM.Commons/Result.cs b/IM.Commons/Result.cs new file mode 100644 index 0000000..30695b9 --- /dev/null +++ b/IM.Commons/Result.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace IM.Commons +{ + public class Result(int code, string message, T? data = default) + { + public int Code { get; private set; } = code; + public string Message { get; private set; } = message; + public T? Data { get; private set; } = data; + [JsonIgnore] + public bool Succeeded + { + get => Code == (int)ResultCode.SUCCESS; + } + + public static Result Success(T? data = default) => new((int)ResultCode.SUCCESS, ResultCode.SUCCESS.GetDescription(), data); + + public static Result Fail(ResultCode code) => new((int)code, code.GetDescription()); + public static Result Fail(ResultCode code, string errorMsg) => new((int)code, errorMsg); + public static Result Fail(Result result) => new(result.Code, result.Message); + } + + public class Result + { + public static Result Success(T? data = default) => new((int)ResultCode.SUCCESS, ResultCode.SUCCESS.GetDescription(), data); + + public static Result Success() => new((int)ResultCode.SUCCESS, ResultCode.SUCCESS.GetDescription(), null); + + public static Result Fail(ResultCode code) => new((int)code, code.GetDescription()); + public static Result Fail(ResultCode code) => new((int)code, code.GetDescription()); + public static Result Fail(ResultCode code, string errorMsg) => new((int)code, errorMsg); + public static Result Fail(Result result) => new(result.Code, result.Message); + public static Result Fail(Result result) => new(result.Code, result.Message); + + } +} diff --git a/IM.Commons/ResultCode.cs b/IM.Commons/ResultCode.cs new file mode 100644 index 0000000..9a59822 --- /dev/null +++ b/IM.Commons/ResultCode.cs @@ -0,0 +1,159 @@ +using System.ComponentModel; + +namespace IM.Commons +{ + public enum ResultCode + { + // 3.1 成功类 + /// 成功响应 + [Description("成功")] + SUCCESS = 0, + + // 3.2 系统级错误(1000 ~ 1999) + /// 未知异常 + [Description("系统错误")] + SYSTEM_ERROR = 1000, + /// 服务器维护中或宕机 + [Description("服务不可用")] + SERVICE_UNAVAILABLE = 1001, + /// 后端超时 + [Description("请求超时")] + REQUEST_TIMEOUT = 1002, + /// 缺少或参数不合法 + [Description("参数错误")] + PARAMETER_ERROR = 1003, + /// 数据库读写失败 + [Description("数据库错误")] + DATABASE_ERROR = 1004, + /// 无权限访问 + [Description("权限不足")] + PERMISSION_DENIED = 1005, + /// Token 无效/过期 + [Description("认证失败")] + AUTH_FAILED = 1006, + + // 3.3 用户相关错误(2000 ~ 2099) + /// 查询不到用户 + [Description("用户不存在")] + USER_NOT_FOUND = 2000, + /// 注册时用户已存在 + [Description("用户已存在")] + USER_ALREADY_EXISTS = 2001, + /// 登录密码错误 + [Description("密码错误")] + PASSWORD_ERROR = 2002, + /// 被管理员封禁 + [Description("用户被禁用")] + USER_DISABLED = 2003, + /// 需重新登录 + [Description("登录过期")] + LOGIN_EXPIRED = 2004, + + // 3.4 好友相关错误(2100 ~ 2199) + /// 重复申请 + [Description("好友申请已存在")] + FRIEND_REQUEST_EXISTS = 2100, + /// 不是好友 + [Description("好友关系不存在")] + FRIEND_RELATION_NOT_FOUND = 2101, + /// 重复添加 + [Description("已经是好友")] + ALREADY_FRIENDS = 2102, + /// 被对方拒绝 + [Description("好友请求被拒绝")] + FRIEND_REQUEST_REJECTED = 2103, + /// 被对方拉黑 + [Description("无法申请加好友")] + CANNOT_ADD_FRIEND = 2104, + /// 好友请求不存在 + [Description("好友请求不存在")] + FRIEND_REQUEST_NOT_FOUND = 2105, + /// 处理好友请求操作无效 + [Description("处理好友请求操作无效")] + INVALID_ACTION = 2106, + /// 注册错误 + [Description("注册错误")] + REGISTER_ERROR = 2107, + + // 3.5 群聊相关错误(2200 ~ 2299) + /// 查询不到群 + [Description("群不存在")] + GROUP_NOT_FOUND = 2200, + /// 不能重复加入 + [Description("已在群中")] + ALREADY_IN_GROUP = 2201, + /// 超出限制 + [Description("群成员已满")] + GROUP_FULL = 2202, + /// 需要邀请/验证 + [Description("无加群权限")] + NO_GROUP_PERMISSION = 2203, + /// 邀请链接过期 + [Description("群邀请已过期")] + GROUP_INVITE_EXPIRED = 2204, + /// 群聊请求不存在 + [Description("群聊请求不存在")] + GROUP_REQUEST_NOT_FOUND = 2205, + /// 群聊成员成员不存在 + [Description("群聊成员不存在")] + GROUP_MEMBER_NOT_FOUNT = 2206, + + // 3.6 消息相关错误(2300 ~ 2399) + /// 发送时异常 + [Description("消息发送失败")] + MESSAGE_SEND_FAILED = 2300, + /// 查询不到消息 + [Description("消息不存在")] + MESSAGE_NOT_FOUND = 2301, + /// 超过时间限制 + [Description("消息撤回失败")] + MESSAGE_RECALL_FAILED = 2302, + /// message_type 不合法 + [Description("不支持的消息类型")] + UNSUPPORTED_MESSAGE_TYPE = 2303, + + // 3.7 文件相关错误(2400 ~ 2499) + /// 存储服务错误 + [Description("文件上传失败")] + FILE_UPLOAD_FAILED = 2400, + /// 下载时未找到 + [Description("文件不存在")] + FILE_NOT_FOUND = 2401, + /// 超过配置限制 + [Description("文件大小超限")] + FILE_TOO_LARGE = 2402, + /// 格式不允许 + [Description("文件类型不支持")] + FILE_TYPE_NOT_SUPPORTED = 2403, + + // 3.8 管理后台相关错误(3000 ~ 3099) + /// 账号错误 + [Description("管理员不存在")] + ADMIN_NOT_FOUND = 3000, + /// 后台登录失败 + [Description("密码错误")] + ADMIN_PASSWORD_ERROR = 3001, + /// 角色未找到 + [Description("角色不存在")] + ROLE_NOT_FOUND = 3002, + /// 无操作权限 + [Description("权限不足")] + ADMIN_PERMISSION_DENIED = 3003, + /// 后台日志写入失败 + [Description("操作记录失败")] + OPERATION_LOG_FAILED = 3004, + + // 3.9 会话相关错误(3100 ~ 3199) + /// 发送时异常 + [Description("会话不存在")] + CONVERSATION_NOT_FOUND = 3100, + + // 3.10 分片相关错误(3200 ~ 3299) + /// 分片不存在异常 + [Description("分片不存在")] + CHUNK_NOT_FOUND = 3201, + /// 分片合并异常 + [Description("分片合并失败")] + CHUNK_COMBINE_FAIL = 3202 + } +} diff --git a/IM.InitCommon/AddDbContextExtensions.cs b/IM.InitCommon/AddDbContextExtensions.cs new file mode 100644 index 0000000..7fc16f4 --- /dev/null +++ b/IM.InitCommon/AddDbContextExtensions.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace IM.InitCommon +{ + public static class AddDbContextExtensions + { + public static IServiceCollection AddAllDbContexts(this IServiceCollection services, Action action, IEnumerable assemblies) + { + //AddDbContextPool不支持DbContext注入其他对象,而且使用不当有内存暴涨的问题,因此不用AddDbContextPool + Type[] types = new Type[] { typeof(IServiceCollection), typeof(Action), typeof(ServiceLifetime), typeof(ServiceLifetime) }; + var methodAddDbContext = typeof(EntityFrameworkServiceCollectionExtensions) + .GetMethod(nameof(EntityFrameworkServiceCollectionExtensions.AddDbContext), 1, types); + foreach (var asmToLoad in assemblies) + { + Type[] typesInAsm = asmToLoad.GetTypes(); + //Register DbContext + //GetTypes() include public/protected ones + //GetExportedTypes only include public ones + //so that XXDbContext in Agrregation can be internal to keep insulated + foreach (var dbCtxType in typesInAsm + .Where(t => !t.IsAbstract && typeof(DbContext).IsAssignableFrom(t))) + { + //similar to serviceCollection.AddDbContextPool(opt=>new DbContextOptionsBuilder(dbCtxOpt)); + var methodGenericAddDbContext = methodAddDbContext.MakeGenericMethod(dbCtxType); + methodGenericAddDbContext.Invoke(null, new object[] { services, action, ServiceLifetime.Scoped, ServiceLifetime.Scoped }); + } + } + return services; + } + } +} diff --git a/IM.InitCommon/ApplicationBuilderExtension.cs b/IM.InitCommon/ApplicationBuilderExtension.cs new file mode 100644 index 0000000..2956b5b --- /dev/null +++ b/IM.InitCommon/ApplicationBuilderExtension.cs @@ -0,0 +1,18 @@ +using IM.ASPNETCore; +using Microsoft.AspNetCore.Builder; + +namespace IM.InitCommon +{ + public static class ApplicationBuilderExtension + { + public static IApplicationBuilder UseAppDefault(this IApplicationBuilder app) + { + app.UseCors(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseMiddleware(); + app.UseForwardedHeaders(); + return app; + } + } +} diff --git a/IM.InitCommon/ConnectionStringOptions.cs b/IM.InitCommon/ConnectionStringOptions.cs new file mode 100644 index 0000000..356178d --- /dev/null +++ b/IM.InitCommon/ConnectionStringOptions.cs @@ -0,0 +1,8 @@ +namespace IM.InitCommon +{ + public class ConnectionStringOptions + { + public string DefaultConnection { get; init; } + public string Redis { get; init; } + } +} diff --git a/IM.InitCommon/ConsulOption.cs b/IM.InitCommon/ConsulOption.cs new file mode 100644 index 0000000..48ab98c --- /dev/null +++ b/IM.InitCommon/ConsulOption.cs @@ -0,0 +1,7 @@ +namespace IM.InitCommon +{ + public class ConsulOption + { + public string Url { get; private set; } = "http://192.168.5.100:8500"; + } +} diff --git a/IM.InitCommon/CorsOptions.cs b/IM.InitCommon/CorsOptions.cs new file mode 100644 index 0000000..e447697 --- /dev/null +++ b/IM.InitCommon/CorsOptions.cs @@ -0,0 +1,7 @@ +namespace IM.InitCommon +{ + public class CorsOptions + { + public string[] Origins { get; set; } + } +} diff --git a/IM.InitCommon/DbContextOptionsBuilerFactory.cs b/IM.InitCommon/DbContextOptionsBuilerFactory.cs new file mode 100644 index 0000000..5585ff3 --- /dev/null +++ b/IM.InitCommon/DbContextOptionsBuilerFactory.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; + +namespace IM.InitCommon +{ + public static class DbContextOptionsBuilderFactory + { + public static DbContextOptionsBuilder Create() + where TDbContext : DbContext + { + var connStr = Environment.GetEnvironmentVariable("DefaultDB_ConnStr"); + var optionsBuilder = new DbContextOptionsBuilder(); + //optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=YouzackVNextDB;User ID=sa;Password=dLLikhQWy5TBz1uM;"); + optionsBuilder.UseMySql(connStr, ServerVersion.AutoDetect(connStr)); + return optionsBuilder; + } + } +} diff --git a/IM.InitCommon/GrpcExtension.cs b/IM.InitCommon/GrpcExtension.cs new file mode 100644 index 0000000..705f48e --- /dev/null +++ b/IM.InitCommon/GrpcExtension.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace IM.InitCommon +{ + public static class GrpcExtension + { + public static IServiceCollection AddAllGrpcServer(this IServiceCollection services) + { + services.AddGrpc(options => + { + // 开启详细错误(开发环境很有用,生产环境可结合配置读取) + options.EnableDetailedErrors = true; + + // 限制最大接收和发送的消息大小 (例如 10MB,防止大包攻击) + options.MaxReceiveMessageSize = 10 * 1024 * 1024; + options.MaxSendMessageSize = 10 * 1024 * 1024; + + // TODO: 未来你可以在这里添加全局异常拦截器 (Interceptor) + // options.Interceptors.Add(); + }); + + return services; + } + + public static IEndpointRouteBuilder MapAllGrpcServer(this IEndpointRouteBuilder endpoints) + { + // 获取调用此方法的程序集(即具体的微服务项目,如 MessageService) + var assembly = Assembly.GetCallingAssembly(); + + // 获取 MapGrpcService 的方法反射信息 + var mapGrpcServiceMethod = typeof(GrpcEndpointRouteBuilderExtensions) + .GetMethods(BindingFlags.Static | BindingFlags.Public) + .First(m => m.Name == "MapGrpcService" && m.GetGenericArguments().Length == 1); + + // 查找当前项目中所有继承了 gRPC 生成的 Base 类的具体实现类 + // gRPC 生成的基类通常以 "Base" 结尾,例如 ConversationInternalBase + var grpcTypes = assembly.GetTypes() + .Where(t => t.IsClass + && !t.IsAbstract + && t.BaseType != null + && t.BaseType.Name.EndsWith("Base")) + .ToList(); + + // 循环并动态调用 MapGrpcService + foreach (var type in grpcTypes) + { + var genericMethod = mapGrpcServiceMethod.MakeGenericMethod(type); + genericMethod.Invoke(null, new object[] { endpoints }); + } + + return endpoints; + } + } +} diff --git a/IM.InitCommon/IM.InitCommon.csproj b/IM.InitCommon/IM.InitCommon.csproj new file mode 100644 index 0000000..f8e4c7a --- /dev/null +++ b/IM.InitCommon/IM.InitCommon.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IM.InitCommon/ModuleInitializerExtensions.cs b/IM.InitCommon/ModuleInitializerExtensions.cs new file mode 100644 index 0000000..5bea727 --- /dev/null +++ b/IM.InitCommon/ModuleInitializerExtensions.cs @@ -0,0 +1,34 @@ +using IM.Commons; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace IM.InitCommon +{ + public static class ModuleInitializerExtensions + { + /// + /// 每个项目中都可以自己写一些实现了IModuleInitializer接口的类,在其中注册自己需要的服务,这样避免所有内容到入口项目中注册 + /// + /// + /// + public static IServiceCollection RunModuleInitializers(this IServiceCollection services, + IEnumerable assemblies) + { + foreach (var asm in assemblies) + { + Type[] types = asm.GetTypes(); + var moduleInitializerTypes = types.Where(t => !t.IsAbstract && typeof(IModuleInitializer).IsAssignableFrom(t)); + foreach (var implType in moduleInitializerTypes) + { + var initializer = (IModuleInitializer?)Activator.CreateInstance(implType); + if (initializer == null) + { + throw new ApplicationException($"Cannot create ${implType}"); + } + initializer.Initialize(services); + } + } + return services; + } + } +} diff --git a/IM.InitCommon/RabbitMqExtension.cs b/IM.InitCommon/RabbitMqExtension.cs new file mode 100644 index 0000000..8f628b8 --- /dev/null +++ b/IM.InitCommon/RabbitMqExtension.cs @@ -0,0 +1,36 @@ +using MassTransit; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace IM.InitCommon +{ + public static class RabbitMqExtension + { + public static IServiceCollection AddRabbitMq(this IServiceCollection services, RabbitMqOptions options, IEnumerable assemblies) + { + + var safeAssemblies = assemblies + .Where(a => a.FullName != null && + !a.FullName.StartsWith("MassTransit", StringComparison.OrdinalIgnoreCase) && + !a.FullName.StartsWith("System", StringComparison.OrdinalIgnoreCase) && + !a.FullName.StartsWith("Microsoft", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + return services.AddMassTransit(x => + { + x.AddConsumers(safeAssemblies); + + x.UsingRabbitMq((context, cfg) => + { + cfg.Host(options.Host, (ushort)options.Port, "/", c => + { + c.Username(options.Username); + c.Password(options.Password); + }); + + cfg.ConfigureEndpoints(context); + }); + }); + } + } +} diff --git a/IM.InitCommon/RabbitMqOptions.cs b/IM.InitCommon/RabbitMqOptions.cs new file mode 100644 index 0000000..57ea8cb --- /dev/null +++ b/IM.InitCommon/RabbitMqOptions.cs @@ -0,0 +1,11 @@ +namespace IM.InitCommon +{ + public class RabbitMqOptions + { + public string Host { get; set; } + public int Port { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string QuequeName { get; set; } + } +} diff --git a/IM.InitCommon/SwaggerGenExtension.cs b/IM.InitCommon/SwaggerGenExtension.cs new file mode 100644 index 0000000..60f77a5 --- /dev/null +++ b/IM.InitCommon/SwaggerGenExtension.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; + +namespace IM.InitCommon +{ + public static class SwaggerGenExtension + { + public static IServiceCollection AddSwaggerGenOpt(this IServiceCollection services) + { + return services.AddSwaggerGen(options => + { + // 1. 定义安全定义 (Security Definition) + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Name = "Authorization", + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "请输入 JWT Token,不需要输入 'Bearer ' 前缀,系统会自动添加。" + }); + + // 2. 开启全局安全要求 (Security Requirement) + // 这样 Swagger UI 所有的接口都会出现锁头图标 + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] {} + } + }); + }); + } + } +} diff --git a/IM.InitCommon/WebApplicationBuilderExtensions.cs b/IM.InitCommon/WebApplicationBuilderExtensions.cs new file mode 100644 index 0000000..0db0335 --- /dev/null +++ b/IM.InitCommon/WebApplicationBuilderExtensions.cs @@ -0,0 +1,165 @@ +using FluentValidation; +using FluentValidation.AspNetCore; +using IM.ASPNETCore; +using IM.Commons; +using IM.Jwt; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using RedLockNet; +using RedLockNet.SERedis; +using RedLockNet.SERedis.Configuration; +using StackExchange.Redis; +using Swashbuckle.AspNetCore.SwaggerGen; +using Winton.Extensions.Configuration.Consul; + +namespace IM.InitCommon +{ + public static class WebApplicationBuilderExtensions + { + public static void ConfigureDbConfiguration(this WebApplicationBuilder builder) + { + builder.Host.ConfigureAppConfiguration((hostCtx, configbuilder) => + { + var env = hostCtx.HostingEnvironment; + string serviceName = env.ApplicationName.Replace(".WebApi", ""); + + ConsulOption consulOption = hostCtx.Configuration.GetSection("ConsulOptions").Get() ?? new ConsulOption(); + string consulKey = $"IM/{env.EnvironmentName}/appsettings.json"; + string serviceConsulKey = $"IM/{env.EnvironmentName}/{serviceName}/appsettings.json"; + configbuilder.AddConsul( + consulKey, + options => + { + options.ConsulConfigurationOptions = cco => + { + cco.Address = new Uri(consulOption.Url); + }; + //options.Optional = true; // 如果本地开发没开 Consul,不报错,继续运行 + options.ReloadOnChange = true; // 开启热更新!Consul 里改了,程序立马生效 + //options.OnLoadException = exceptionContext => { exceptionContext.Ignore = true; }; + } + ); + + configbuilder.AddConsul(serviceConsulKey, options => + { + options.ConsulConfigurationOptions = cco => { cco.Address = new Uri(consulOption.Url); }; + options.Optional = true; + options.ReloadOnChange = true; + }); + }); + } + + public static void ConfigExtraServices(this WebApplicationBuilder builder) + { + var services = builder.Services; + var configuration = builder.Configuration; + + + var assemblies = ReflectionHelper.GetAllReferencedAssemblies(); + + services.RunModuleInitializers(assemblies); + + services.AddAutoMapper(cfg => { }, assemblies); + + //数据库配置 + var conOpt = configuration.GetSection("ConnectionStrings").Get(); + builder.Services.Configure(builder.Configuration.GetSection("GrpcConfigs")); + + services.AddMediatR(ctx => + { + ctx.RegisterServicesFromAssemblies([.. assemblies]); + }); + + var rabbitmqOpt = configuration.GetSection("RabbitMQOptions").Get(); + + services.AddRabbitMq(rabbitmqOpt, assemblies); + + services.AddAllDbContexts(options => + { + options.UseMySql(conOpt.DefaultConnection, ServerVersion.AutoDetect(conOpt.DefaultConnection)); + }, assemblies); + + services.Configure(c => + { + + }); + + services.Configure(options => + { + // 禁用默认的自动 400 响应 + options.SuppressModelStateInvalidFilter = true; + }); + + + + JwtOptions jwtOptions = configuration.GetSection("Jwt").Get(); + + services.AddJwt(jwtOptions); + + services.Configure(m => + { + m.Filters.Add(); + m.Filters.Add(); + }); + + services.Configure(configuration.GetSection("Jwt")); + + //模型校验 + services.AddValidatorsFromAssemblies(assemblies); + + services.AddFluentValidationAutoValidation(); + + + //配置跨域 + services.AddCors(options => + { + //更好的在Program.cs中用绑定方式读取配置的方法:https://github.com/dotnet/aspnetcore/issues/21491 + //不过比较麻烦。 + var corsOpt = configuration.GetSection("Cors").Get(); + string[] urls = corsOpt.Origins; + options.AddDefaultPolicy(builder => builder.WithOrigins(urls) + .AllowAnyMethod().AllowAnyHeader().AllowCredentials()); + } + ); + + + //redis + IConnectionMultiplexer redisCon = ConnectionMultiplexer.Connect(conOpt.Redis); + + services.AddStackExchangeRedisCache(options => + { + options.ConnectionMultiplexerFactory = () => Task.FromResult(redisCon); + }); + services.AddSingleton(sp => + { + var connection = sp.GetRequiredService(); + // 这里可以配置多个 Redis 节点提高安全性,单机运行传一个即可 + return RedLockFactory.Create(new List { new RedLockMultiplexer(redisCon) }); + }); + + + services.AddSingleton(typeof(IConnectionMultiplexer), redisCon); + + + services.Configure(f => + { + f.ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.All; + }); + + services.AddSwaggerGenOpt(); + + services.AddControllers() + .AddJsonOptions(options => + { + // 将枚举转换为字符串的转换器 + options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); + }); + + + + } + } +} diff --git a/IM.Jwt/IM.Jwt.csproj b/IM.Jwt/IM.Jwt.csproj new file mode 100644 index 0000000..90dd680 --- /dev/null +++ b/IM.Jwt/IM.Jwt.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/IM.Jwt/ITokenService.cs b/IM.Jwt/ITokenService.cs new file mode 100644 index 0000000..6bb1826 --- /dev/null +++ b/IM.Jwt/ITokenService.cs @@ -0,0 +1,18 @@ +using System.Security.Claims; + +namespace IM.Jwt +{ + public interface ITokenService + { + /// + /// 获取令牌 + /// + /// 令牌负载 + /// + /// + string GetToken(IEnumerable claims, JwtOptions options); + Task CreateRefreshTokenAsync(Guid userId, CancellationToken cancellationToken = default); + Task RevokeRefreshTokenAsync(string refreshToken); + Task<(bool ok, Guid userId)> ValidateRefreshTokenAsync(string token, CancellationToken cancellation = default); + } +} diff --git a/IM.Jwt/JwtOptions.cs b/IM.Jwt/JwtOptions.cs new file mode 100644 index 0000000..13dad07 --- /dev/null +++ b/IM.Jwt/JwtOptions.cs @@ -0,0 +1,11 @@ +namespace IM.Jwt +{ + public class JwtOptions + { + public string Key { get; init; } + public string Issuer { get; init; } + public string Audience { get; init; } + public int AccessTokenMinutes { get; init; } + public int RefreshTokenDays { get; init; } + } +} diff --git a/IM.Jwt/ModuleInit.cs b/IM.Jwt/ModuleInit.cs new file mode 100644 index 0000000..8e314f8 --- /dev/null +++ b/IM.Jwt/ModuleInit.cs @@ -0,0 +1,13 @@ +using IM.Commons; +using Microsoft.Extensions.DependencyInjection; + +namespace IM.Jwt +{ + public class ModuleInit : IModuleInitializer + { + public void Initialize(IServiceCollection services) + { + services.AddScoped(); + } + } +} diff --git a/IM.Jwt/TokenService.cs b/IM.Jwt/TokenService.cs new file mode 100644 index 0000000..f1fb98c --- /dev/null +++ b/IM.Jwt/TokenService.cs @@ -0,0 +1,70 @@ +using IM.Commons; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using StackExchange.Redis; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; + +namespace IM.Jwt +{ + public class TokenService : ITokenService + { + private readonly IDatabase _redis; + private readonly IOptions _options; + public TokenService(IConnectionMultiplexer multiplexer, IOptions options) + { + _redis = multiplexer.GetDatabase(); + _options = options; + } + + private static string GenerateTokenStr() + { + var bytes = RandomNumberGenerator.GetBytes(32); + return Convert.ToBase64String(bytes); + } + public async Task CreateRefreshTokenAsync(Guid userId, CancellationToken cancellationToken = default) + { + string token = GenerateTokenStr(); + var payload = new { UserId = userId, CreateAt = DateTime.Now }; + string json = JsonConvert.SerializeObject(payload); + //token写入redis + await _redis.StringSetAsync(RedisHelper.GetRefreshTokenKey(token), json, TimeSpan.FromDays(_options.Value.RefreshTokenDays)); + return token; + } + + public string GetToken(IEnumerable claims, JwtOptions options) + { + TimeSpan ExpiryDuration = TimeSpan.FromMinutes(options.AccessTokenMinutes); + var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(options.Key)); + var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature); + var tokenDescriptor = new JwtSecurityToken(options.Issuer, options.Audience, claims, + expires: DateTime.Now.Add(ExpiryDuration), signingCredentials: credentials); + return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); + } + + public async Task RevokeRefreshTokenAsync(string refreshToken) + { + await _redis.KeyDeleteAsync(RedisHelper.GetRefreshTokenKey(refreshToken)); + } + + public async Task<(bool ok, Guid userId)> ValidateRefreshTokenAsync(string token, CancellationToken cancellation = default) + { + var json = await _redis.StringGetAsync(RedisHelper.GetRefreshTokenKey(token)); + if (json.IsNullOrEmpty) return (false, Guid.Empty); + try + { + using var doc = JsonDocument.Parse(json.ToString()); + var userId = doc.RootElement.GetProperty("UserId").GetGuid(); + return (true, userId); + } + catch + { + return (false, Guid.Empty); + } + } + } +} diff --git a/IM.Jwt/WebApplicationJwtExtension.cs b/IM.Jwt/WebApplicationJwtExtension.cs new file mode 100644 index 0000000..15d8a4f --- /dev/null +++ b/IM.Jwt/WebApplicationJwtExtension.cs @@ -0,0 +1,82 @@ +using IM.Commons; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace IM.Jwt +{ + public static class WebApplicationJwtExtension + { + public static IServiceCollection AddJwt(this IServiceCollection services, JwtOptions jwtOptions) + { + services.AddAuthentication(options => + { + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = jwtOptions.Issuer, + + ValidateAudience = true, + ValidAudience = jwtOptions.Audience, + + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero, + + // 验证签名秘钥(防止 Token 被篡改) + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Key)), + }; + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + var path = context.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hub")) // 假设你的 SignalR 路径是 /hub + { + context.Token = accessToken; + } + return Task.CompletedTask; + }, + OnAuthenticationFailed = context => + { + // 在这里打断点,查看 context.Exception + // 常见的有:SecurityTokenExpiredException (过期) + // 或 SecurityTokenInvalidSignatureException (密钥不对) + Console.WriteLine("验证失败原因: " + context.Exception.Message); + return Task.CompletedTask; + }, + OnChallenge = async context => + { + context.HandleResponse(); + + context.Response.ContentType = "application/json"; + context.Response.StatusCode = StatusCodes.Status200OK; + + var result = Result.Fail(ResultCode.AUTH_FAILED); + await context.Response.WriteAsJsonAsync(result); + }, + + OnForbidden = async context => + { + + context.Response.ContentType = "application/json"; + context.Response.StatusCode = StatusCodes.Status200OK; + var result = Result.Fail(ResultCode.PERMISSION_DENIED); + await context.Response.WriteAsJsonAsync(result); + } + }; + }); + + return services; + } + } +} diff --git a/IM.Protocols/IM.Protocols.csproj b/IM.Protocols/IM.Protocols.csproj new file mode 100644 index 0000000..097728a --- /dev/null +++ b/IM.Protocols/IM.Protocols.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/IM.Protocols/Protos/contact.proto b/IM.Protocols/Protos/contact.proto new file mode 100644 index 0000000..41c9847 --- /dev/null +++ b/IM.Protocols/Protos/contact.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +// ָɵ C# ռ +option csharp_namespace = "IM.Protocols.Grpc.Contact"; + +package contact; + +// +service ContactInternal { + //ѹϵ + rpc CheckFriendship (CheckFriendshipRequest) returns (CheckFriendshipResponse); +} + +// +message CheckFriendshipRequest { + string owner_id = 1; + string target_id = 2; +} + +// Ӧ +message CheckFriendshipResponse { + bool checked = 1; +} \ No newline at end of file diff --git a/IM.Protocols/Protos/conversation.proto b/IM.Protocols/Protos/conversation.proto new file mode 100644 index 0000000..0900333 --- /dev/null +++ b/IM.Protocols/Protos/conversation.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +// ָɵ C# ռ +option csharp_namespace = "IM.Protocols.Grpc.Conversation"; + +package conversation; + +// +service ConversationInternal { + // ȡûԿб + rpc GetUserStreamKeys (GetUserStreamKeysRequest) returns (UserStreamKeysResponse); +} + +// +message GetUserStreamKeysRequest { + string user_id = 1; +} + +// Ӧ +message UserStreamKeysResponse { + repeated string stream_keys = 1; +} \ No newline at end of file diff --git a/IM.Protocols/Protos/group.proto b/IM.Protocols/Protos/group.proto new file mode 100644 index 0000000..9a3d388 --- /dev/null +++ b/IM.Protocols/Protos/group.proto @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IM.Protocols.Protos +{ + internal class group + { + } +} diff --git a/IM.Protocols/Protos/user.proto b/IM.Protocols/Protos/user.proto new file mode 100644 index 0000000..c32f583 --- /dev/null +++ b/IM.Protocols/Protos/user.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +// ʱ֧ +import "google/protobuf/timestamp.proto"; + +// ָɵ C# ռ +option csharp_namespace = "IM.Protocols.Grpc.User"; + +package User; + +service UserInternal { + // ȡûϢ + rpc GetUserInfoAsync (GetUserInfoRequest) returns (UserResponse); +} + +// Guid proto ͨ string ʽ +message GetUserInfoRequest { + string userId = 1; +} + +message UserResponse { + string id = 1; // Guid ӳΪ string + string userName = 2; + string nickName = 3; + optional string email = 4; // ʹ optional Ӧ string? + optional string phone = 5; + string region = 6; + string description = 7; + optional string avatar = 8; + + // ʹùٷʱ + google.protobuf.Timestamp creationTime = 9; + optional google.protobuf.Timestamp deletion = 10; +} \ No newline at end of file diff --git a/IM_API_NEW.sln b/IM_API_NEW.sln new file mode 100644 index 0000000..b576d4d --- /dev/null +++ b/IM_API_NEW.sln @@ -0,0 +1,182 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36930.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Commons", "Commons", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IM.DomainCommons", "DomainCommons\IM.DomainCommons.csproj", "{A08384EA-AB27-4CE5-A84D-094FCDC36A42}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IM.Infrastructure", "Infrastructure\IM.Infrastructure.csproj", "{DD477B8B-4F7A-4CE3-AE47-000C1243501D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IdentityService", "IdentityService", "{32C5A534-1FDC-4480-8D44-88411C19B1AF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityService.Infrastructure", "User.Infrastructure\IdentityService.Infrastructure.csproj", "{C9A6D34A-29A3-44F5-B0BC-11A734B1B0AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityService.WebApi", "User.WebApi\IdentityService.WebApi.csproj", "{148C0E23-8225-4790-A920-6C5DE6C8FF50}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IM.InitCommon", "IM.InitCommon\IM.InitCommon.csproj", "{B245AB7B-841A-469E-950D-B08E4C1C8094}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IM.ASPNETCore", "IM.ASPNETCore\IM.ASPNETCore.csproj", "{E89E5F35-4D54-4FE7-9A47-63752B355DA9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityService.Domain", "User.Domain\IdentityService.Domain.csproj", "{6795A287-3488-B0A3-B242-C19526B6A88D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IM.Jwt", "IM.Jwt\IM.Jwt.csproj", "{096064BE-F09C-40CA-AB54-A78AFE5C88BC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ContactService", "ContactService", "{9032F7F6-E74A-4AFF-AB43-44E180728FEB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ContactService.Domain", "ContactService.Domain\ContactService.Domain.csproj", "{2085AC3B-BDF9-4F02-B80A-217685A99CEC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ContactService.Infrastructure", "ContactService.Infrastructure\ContactService.Infrastructure.csproj", "{11CB06A3-4906-4E66-BDF6-04D9EDB002CD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ContactService.WebApi", "ContactService.WebApi\ContactService.WebApi.csproj", "{130FE785-7DCA-4609-9E9C-5257198FE36A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GroupService", "GroupService", "{4C54CB80-67B7-48BD-8289-588DE06E5886}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GroupService.Infrastructure", "GroupService.Infrastructure\GroupService.Infrastructure.csproj", "{EB435E96-1088-49DF-AF28-74098BFEA14D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GroupService.Domain", "GroupService.Domain\GroupService.Domain.csproj", "{9D905D4C-8E0B-41D1-AFCA-E0EC110AFDF7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GroupService.WebApi", "GroupService.WebApi\GroupService.WebApi.csproj", "{80C73C60-EC7D-4FC0-84FF-0D9510F4183A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MessageService", "MessageService", "{C6A9E6D0-F123-44B0-BC4B-97F9D008B2AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessageService.Domain", "MessageService.Domain\MessageService.Domain.csproj", "{E5017B8A-060E-4C1B-BF2E-B5EBE3D106D6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessageService.Infrastructure", "MessageService.Infrastructure\MessageService.Infrastructure.csproj", "{AD01D163-BAD7-4C2B-8E0D-BE2B16FCAD57}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessageService.WebApi", "MessageService.WebApi\MessageService.WebApi.csproj", "{2154F7AE-A269-4D06-8CA2-B7A5C33FC498}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ConnectorService", "ConnectorService", "{229DA5B7-3CE3-440F-B2B7-89D62B5FC23C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConnectorService", "ConnectorService\ConnectorService.csproj", "{432D7A3C-EA35-4AEE-91BF-8F93DC4DA565}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IM.Protocols", "IM.Protocols\IM.Protocols.csproj", "{693CCDD3-FA2D-4A3A-82EA-C460569FAF16}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FileService", "FileService", "{136DC96D-82FC-4F77-91A7-B7D91298A323}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileService.Domain", "FileService.Domain\FileService.Domain.csproj", "{DE97FF4D-ADE0-4D1E-9ECD-CF9CD6C3685D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IM.Commons", "IM.Commons\IM.Commons.csproj", "{4D16531A-25B1-E469-8A71-F3ABFB52FAC8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A08384EA-AB27-4CE5-A84D-094FCDC36A42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A08384EA-AB27-4CE5-A84D-094FCDC36A42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A08384EA-AB27-4CE5-A84D-094FCDC36A42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A08384EA-AB27-4CE5-A84D-094FCDC36A42}.Release|Any CPU.Build.0 = Release|Any CPU + {DD477B8B-4F7A-4CE3-AE47-000C1243501D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD477B8B-4F7A-4CE3-AE47-000C1243501D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD477B8B-4F7A-4CE3-AE47-000C1243501D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD477B8B-4F7A-4CE3-AE47-000C1243501D}.Release|Any CPU.Build.0 = Release|Any CPU + {C9A6D34A-29A3-44F5-B0BC-11A734B1B0AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9A6D34A-29A3-44F5-B0BC-11A734B1B0AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9A6D34A-29A3-44F5-B0BC-11A734B1B0AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9A6D34A-29A3-44F5-B0BC-11A734B1B0AB}.Release|Any CPU.Build.0 = Release|Any CPU + {148C0E23-8225-4790-A920-6C5DE6C8FF50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {148C0E23-8225-4790-A920-6C5DE6C8FF50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {148C0E23-8225-4790-A920-6C5DE6C8FF50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {148C0E23-8225-4790-A920-6C5DE6C8FF50}.Release|Any CPU.Build.0 = Release|Any CPU + {B245AB7B-841A-469E-950D-B08E4C1C8094}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B245AB7B-841A-469E-950D-B08E4C1C8094}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B245AB7B-841A-469E-950D-B08E4C1C8094}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B245AB7B-841A-469E-950D-B08E4C1C8094}.Release|Any CPU.Build.0 = Release|Any CPU + {E89E5F35-4D54-4FE7-9A47-63752B355DA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E89E5F35-4D54-4FE7-9A47-63752B355DA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E89E5F35-4D54-4FE7-9A47-63752B355DA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E89E5F35-4D54-4FE7-9A47-63752B355DA9}.Release|Any CPU.Build.0 = Release|Any CPU + {6795A287-3488-B0A3-B242-C19526B6A88D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6795A287-3488-B0A3-B242-C19526B6A88D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6795A287-3488-B0A3-B242-C19526B6A88D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6795A287-3488-B0A3-B242-C19526B6A88D}.Release|Any CPU.Build.0 = Release|Any CPU + {096064BE-F09C-40CA-AB54-A78AFE5C88BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {096064BE-F09C-40CA-AB54-A78AFE5C88BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {096064BE-F09C-40CA-AB54-A78AFE5C88BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {096064BE-F09C-40CA-AB54-A78AFE5C88BC}.Release|Any CPU.Build.0 = Release|Any CPU + {2085AC3B-BDF9-4F02-B80A-217685A99CEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2085AC3B-BDF9-4F02-B80A-217685A99CEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2085AC3B-BDF9-4F02-B80A-217685A99CEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2085AC3B-BDF9-4F02-B80A-217685A99CEC}.Release|Any CPU.Build.0 = Release|Any CPU + {11CB06A3-4906-4E66-BDF6-04D9EDB002CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11CB06A3-4906-4E66-BDF6-04D9EDB002CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11CB06A3-4906-4E66-BDF6-04D9EDB002CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11CB06A3-4906-4E66-BDF6-04D9EDB002CD}.Release|Any CPU.Build.0 = Release|Any CPU + {130FE785-7DCA-4609-9E9C-5257198FE36A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {130FE785-7DCA-4609-9E9C-5257198FE36A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {130FE785-7DCA-4609-9E9C-5257198FE36A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {130FE785-7DCA-4609-9E9C-5257198FE36A}.Release|Any CPU.Build.0 = Release|Any CPU + {EB435E96-1088-49DF-AF28-74098BFEA14D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB435E96-1088-49DF-AF28-74098BFEA14D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB435E96-1088-49DF-AF28-74098BFEA14D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB435E96-1088-49DF-AF28-74098BFEA14D}.Release|Any CPU.Build.0 = Release|Any CPU + {9D905D4C-8E0B-41D1-AFCA-E0EC110AFDF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D905D4C-8E0B-41D1-AFCA-E0EC110AFDF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D905D4C-8E0B-41D1-AFCA-E0EC110AFDF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D905D4C-8E0B-41D1-AFCA-E0EC110AFDF7}.Release|Any CPU.Build.0 = Release|Any CPU + {80C73C60-EC7D-4FC0-84FF-0D9510F4183A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80C73C60-EC7D-4FC0-84FF-0D9510F4183A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80C73C60-EC7D-4FC0-84FF-0D9510F4183A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80C73C60-EC7D-4FC0-84FF-0D9510F4183A}.Release|Any CPU.Build.0 = Release|Any CPU + {E5017B8A-060E-4C1B-BF2E-B5EBE3D106D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5017B8A-060E-4C1B-BF2E-B5EBE3D106D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5017B8A-060E-4C1B-BF2E-B5EBE3D106D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5017B8A-060E-4C1B-BF2E-B5EBE3D106D6}.Release|Any CPU.Build.0 = Release|Any CPU + {AD01D163-BAD7-4C2B-8E0D-BE2B16FCAD57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD01D163-BAD7-4C2B-8E0D-BE2B16FCAD57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD01D163-BAD7-4C2B-8E0D-BE2B16FCAD57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD01D163-BAD7-4C2B-8E0D-BE2B16FCAD57}.Release|Any CPU.Build.0 = Release|Any CPU + {2154F7AE-A269-4D06-8CA2-B7A5C33FC498}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2154F7AE-A269-4D06-8CA2-B7A5C33FC498}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2154F7AE-A269-4D06-8CA2-B7A5C33FC498}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2154F7AE-A269-4D06-8CA2-B7A5C33FC498}.Release|Any CPU.Build.0 = Release|Any CPU + {432D7A3C-EA35-4AEE-91BF-8F93DC4DA565}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {432D7A3C-EA35-4AEE-91BF-8F93DC4DA565}.Debug|Any CPU.Build.0 = Debug|Any CPU + {432D7A3C-EA35-4AEE-91BF-8F93DC4DA565}.Release|Any CPU.ActiveCfg = Release|Any CPU + {432D7A3C-EA35-4AEE-91BF-8F93DC4DA565}.Release|Any CPU.Build.0 = Release|Any CPU + {693CCDD3-FA2D-4A3A-82EA-C460569FAF16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {693CCDD3-FA2D-4A3A-82EA-C460569FAF16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {693CCDD3-FA2D-4A3A-82EA-C460569FAF16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {693CCDD3-FA2D-4A3A-82EA-C460569FAF16}.Release|Any CPU.Build.0 = Release|Any CPU + {DE97FF4D-ADE0-4D1E-9ECD-CF9CD6C3685D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE97FF4D-ADE0-4D1E-9ECD-CF9CD6C3685D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE97FF4D-ADE0-4D1E-9ECD-CF9CD6C3685D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE97FF4D-ADE0-4D1E-9ECD-CF9CD6C3685D}.Release|Any CPU.Build.0 = Release|Any CPU + {4D16531A-25B1-E469-8A71-F3ABFB52FAC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D16531A-25B1-E469-8A71-F3ABFB52FAC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D16531A-25B1-E469-8A71-F3ABFB52FAC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D16531A-25B1-E469-8A71-F3ABFB52FAC8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A08384EA-AB27-4CE5-A84D-094FCDC36A42} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {DD477B8B-4F7A-4CE3-AE47-000C1243501D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {C9A6D34A-29A3-44F5-B0BC-11A734B1B0AB} = {32C5A534-1FDC-4480-8D44-88411C19B1AF} + {148C0E23-8225-4790-A920-6C5DE6C8FF50} = {32C5A534-1FDC-4480-8D44-88411C19B1AF} + {B245AB7B-841A-469E-950D-B08E4C1C8094} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {E89E5F35-4D54-4FE7-9A47-63752B355DA9} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {6795A287-3488-B0A3-B242-C19526B6A88D} = {32C5A534-1FDC-4480-8D44-88411C19B1AF} + {096064BE-F09C-40CA-AB54-A78AFE5C88BC} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {2085AC3B-BDF9-4F02-B80A-217685A99CEC} = {9032F7F6-E74A-4AFF-AB43-44E180728FEB} + {11CB06A3-4906-4E66-BDF6-04D9EDB002CD} = {9032F7F6-E74A-4AFF-AB43-44E180728FEB} + {130FE785-7DCA-4609-9E9C-5257198FE36A} = {9032F7F6-E74A-4AFF-AB43-44E180728FEB} + {EB435E96-1088-49DF-AF28-74098BFEA14D} = {4C54CB80-67B7-48BD-8289-588DE06E5886} + {9D905D4C-8E0B-41D1-AFCA-E0EC110AFDF7} = {4C54CB80-67B7-48BD-8289-588DE06E5886} + {80C73C60-EC7D-4FC0-84FF-0D9510F4183A} = {4C54CB80-67B7-48BD-8289-588DE06E5886} + {E5017B8A-060E-4C1B-BF2E-B5EBE3D106D6} = {C6A9E6D0-F123-44B0-BC4B-97F9D008B2AB} + {AD01D163-BAD7-4C2B-8E0D-BE2B16FCAD57} = {C6A9E6D0-F123-44B0-BC4B-97F9D008B2AB} + {2154F7AE-A269-4D06-8CA2-B7A5C33FC498} = {C6A9E6D0-F123-44B0-BC4B-97F9D008B2AB} + {432D7A3C-EA35-4AEE-91BF-8F93DC4DA565} = {229DA5B7-3CE3-440F-B2B7-89D62B5FC23C} + {693CCDD3-FA2D-4A3A-82EA-C460569FAF16} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {DE97FF4D-ADE0-4D1E-9ECD-CF9CD6C3685D} = {136DC96D-82FC-4F77-91A7-B7D91298A323} + {4D16531A-25B1-E469-8A71-F3ABFB52FAC8} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {67902CF1-7322-4BD9-B1BD-10391EC03A00} + EndGlobalSection +EndGlobal diff --git a/Infrastructure/Efcore/BaseDbContext.cs b/Infrastructure/Efcore/BaseDbContext.cs new file mode 100644 index 0000000..7b24eaf --- /dev/null +++ b/Infrastructure/Efcore/BaseDbContext.cs @@ -0,0 +1,37 @@ +using IM.DomainCommons; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace IM.Infrastructure.Efcore +{ + public class BaseDbContext : DbContext + { + private IMediator? mediator; + + public BaseDbContext(DbContextOptions options, IMediator mediator) + : base(options) + { + this.mediator = mediator; + } + + public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) + { + if (mediator != null) + { + await mediator.DispatchDomainEventsAsync(this); + } + + var softDeletedEntities = ChangeTracker + .Entries() + .Where(x => x.State == EntityState.Modified && x.Entity.IsDeleted) + .ToList(); + + + var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + + softDeletedEntities.ForEach(e => e.State = EntityState.Detached); + + return result; + } + } +} diff --git a/Infrastructure/Efcore/EfcoreExtension.cs b/Infrastructure/Efcore/EfcoreExtension.cs new file mode 100644 index 0000000..aeabcbd --- /dev/null +++ b/Infrastructure/Efcore/EfcoreExtension.cs @@ -0,0 +1,28 @@ +using IM.DomainCommons; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace IM.Infrastructure.Efcore +{ + public static class EfcoreExtension + { + public static void EnableSoftDeletionGlobalFilter(this ModelBuilder modelBuilder) + { + var entityTypesHasSoftDeletion = modelBuilder.Model.GetEntityTypes() + .Where(e => e.ClrType.IsAssignableTo(typeof(ISoftDelete))); + + foreach (var entityType in entityTypesHasSoftDeletion) + { + var isDeletedProperty = entityType.FindProperty(nameof(ISoftDelete.IsDeleted)); + var parameter = Expression.Parameter(entityType.ClrType, "p"); + var filter = Expression.Lambda(Expression.Not(Expression.Property(parameter, isDeletedProperty.PropertyInfo)), parameter); + entityType.SetQueryFilter(filter); + } + } + + public static IQueryable Query(this DbContext context) where T : class + { + return context.Set().AsNoTracking(); + } + } +} diff --git a/Infrastructure/Efcore/MediatorExtensions.cs b/Infrastructure/Efcore/MediatorExtensions.cs new file mode 100644 index 0000000..e5cd10a --- /dev/null +++ b/Infrastructure/Efcore/MediatorExtensions.cs @@ -0,0 +1,30 @@ +using IM.DomainCommons; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace IM.Infrastructure.Efcore +{ + public static class MediatorExtensions + { + public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext db) + { + var domainEntities = db.ChangeTracker + .Entries() + .Where(x => x.Entity.GetDomainEvents().Any()); + + var domainEvents = domainEntities + .SelectMany(s => s.Entity.GetDomainEvents()) + .ToList(); + + domainEntities.ToList().ForEach(e => + { + e.Entity.ClearDomainEvents(); + }); + + foreach (var domainEvent in domainEvents) + { + await mediator.Publish(domainEvent); + } + } + } +} diff --git a/Infrastructure/GlobalUsing.cs b/Infrastructure/GlobalUsing.cs new file mode 100644 index 0000000..32570f1 --- /dev/null +++ b/Infrastructure/GlobalUsing.cs @@ -0,0 +1,2 @@ +global using System.Linq; +global using System.Threading.Tasks; diff --git a/Infrastructure/IM.Infrastructure.csproj b/Infrastructure/IM.Infrastructure.csproj new file mode 100644 index 0000000..8aaabd7 --- /dev/null +++ b/Infrastructure/IM.Infrastructure.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/MessageService.Domain/Entities/Conversation.cs b/MessageService.Domain/Entities/Conversation.cs new file mode 100644 index 0000000..3a577f9 --- /dev/null +++ b/MessageService.Domain/Entities/Conversation.cs @@ -0,0 +1,84 @@ +using IM.DomainCommons; +using MessageService.Domain.Enums; +using MessageService.Domain.Events; +using MessageService.Domain.Tools; + +namespace MessageService.Domain.Entities +{ + public class Conversation : AggregateRootEntity + { + /// + /// 用户 + /// + public Guid UserId { get; private set; } + + /// + /// 对方ID(群聊为群聊ID,单聊为单聊ID) + /// + public Guid TargetId { get; private set; } + public string TargetAvatar { get; private set; } + public string TargetName { get; private set; } + + /// + /// 最后一条未读消息ID + /// + public long? LastReadSequenceId { get; private set; } + + /// + /// 未读消息数 + /// + public int UnreadCount { get; private set; } + + public ChatType ChatType { get; private set; } + + /// + /// 消息推送唯一标识符 + /// + public string StreamKey { get; private set; } + + /// + /// 最后一条最新消息 + /// + public string LastMessage { get; private set; } + private Conversation() { } + + public Conversation(Guid userId, Guid targetId, string targetAvatar, string targetName, long? lastReadSequenceId, int unreadCount, ChatType chatType, string lastMessage) + { + UserId = userId; + TargetId = targetId; + TargetAvatar = targetAvatar; + TargetName = targetName; + LastReadSequenceId = lastReadSequenceId; + UnreadCount = unreadCount; + ChatType = chatType; + LastMessage = lastMessage; + StreamKey = ChatType == ChatType.GROUP ? StreamKeyBuilder.Group(targetId) : StreamKeyBuilder.Private(userId, targetId); + AddDomainEvent(new ConversationCreatedDomainEvent(this)); + } + + public void Update(long? LastReadSequenceId = default, int? unreadCount = default, string? lastMsg = default) + { + if (LastReadSequenceId != null) + { + LastReadSequenceId = LastReadSequenceId.Value; + } + if (unreadCount != null) + { + UnreadCount += unreadCount.Value; + } + + if (lastMsg != null) + { + LastMessage = lastMsg; + } + } + + public void UpdateProfile(string name, string avatar) + { + TargetAvatar = avatar; + TargetName = name; + } + + + } +} diff --git a/MessageService.Domain/Entities/Message.cs b/MessageService.Domain/Entities/Message.cs new file mode 100644 index 0000000..e0fade0 --- /dev/null +++ b/MessageService.Domain/Entities/Message.cs @@ -0,0 +1,217 @@ +using IM.Commons.IntegrationEvents; +using IM.DomainCommons; +using MessageService.Domain.Enums; +using MessageService.Domain.Events; +using MessageService.Domain.KeyObjects; +using MessageService.Domain.Tools; +using System.Text.Json; + +namespace MessageService.Domain.Entities +{ + public class Message : AggregateRootEntity + { + /// + /// 聊天类型 + /// (0:私聊,1:群聊) + /// + public ChatType ChatType { get; private set; } + + /// + /// 消息类型 + /// (0:文本,1:图片,2:语音,3:视频,4:文件,5:语音聊天,6:视频聊天) + /// + public MessageType MsgType { get; private set; } + public Guid ClientMsgId { get; private set; } + + /// + /// 消息内容 + /// + public MessageContent Content { get; private set; } = null!; + + /// + /// 发送者 + /// + public Guid SenderId { get; private set; } + + /// + /// 接收者(私聊为用户ID,群聊为群聊ID) + /// + public Guid TargetId { get; private set; } + + /// + /// 消息状态(0:已发送,1:已撤回) + /// + public MessageState State { get; private set; } = MessageState.Sent; + + /// + /// 消息推送唯一标识符 + /// + public string StreamKey { get; private set; } + + /// + /// 消息排序标识 + /// + + public long SequenceId { get; private set; } + + private Message() { } + + private Message(MessageCreateContext ctx, long sequenceId, MessageType messageType + , MessageContent content + ) + { + ChatType = ctx.ChatType; + SenderId = ctx.SenderId; + ClientMsgId = ctx.ClientMsgId; + TargetId = ctx.TargetId; + SequenceId = sequenceId; + MsgType = messageType; + StreamKey = ChatType == ChatType.GROUP ? StreamKeyBuilder.Group(ctx.TargetId) : StreamKeyBuilder.Private(ctx.SenderId, ctx.TargetId); + Content = content; + } + /// + /// 文本消息 + /// + /// + /// 文本内容 + /// + /// + public static Message BuildTxt(MessageCreateContext ctx, string text, long sequenceId) + { + var content = new MessageContent(text, new TextBody(text)); + return new Message(ctx, sequenceId, MessageType.Text, content); + } + /// + /// 图片消息 + /// + /// + /// 相对路径 + /// 宽度 + /// 高度 + /// 预览图 + /// + /// + public static Message BuildImg(MessageCreateContext ctx, string url, + int width, int height, string thumb, long sequenceId) + { + var content = new MessageContent("[图片]", new ImageBody(url, width, height, thumb)); + return new Message(ctx, sequenceId, MessageType.Image, content); + } + /// + /// 视频消息 + /// + /// + /// + /// + /// + /// + /// + /// + public static Message BuildVideo(MessageCreateContext ctx, string url, + int width, int height, string thumb, long sequenceId) + { + var content = new MessageContent("[视频]", new VideoBody(url, width, height, thumb)); + return new Message(ctx, sequenceId, MessageType.Video, content); + } + /// + /// 语音消息 + /// + /// + /// + /// 持续时间 + /// + /// + public static Message BuildVoice(MessageCreateContext ctx, string url, + int duration, long sequenceId) + { + var content = new MessageContent("[音频]", new VoiceBody(url, duration)); + return new Message(ctx, sequenceId, MessageType.Voice, content); + } + /// + /// 文件 + /// + /// + /// 相对路径 + /// 文件名 + /// 文件大小(KB) + /// 文件格式 + /// + /// + public static Message BuildFile(MessageCreateContext ctx, string url, + string name, long size, + string format, long sequenceId) + { + var content = new MessageContent("[文件]", new FileBody(url, name, size, format)); + return new Message(ctx, sequenceId, MessageType.File, content); + } + /// + /// 添加扩展字段 + /// + /// 字段名 + /// 值 + /// + public Message WithExt(string key, string value) + { + var dic = new Dictionary(); + dic.Add(key, value); + Content.RawExt = JsonSerializer.Serialize(dic); + return this; + } + /// + /// 添加消息引用 + /// + /// + /// + public Message WithQuote(QuoteInfo quote) + { + this.Content.Quote = quote; + return this; + } + /// + /// 完成消息 + /// + public void Send() + { + AddDomainEvent(new MessageCreatedDomainEvent(this)); + } + + public void Withdraw() + { + if (State != MessageState.Sent) + { + throw new DomainException("消息不可撤回"); + } + State = MessageState.Withdrwan; + AddDomainEvent(new MessageWithdrawDomainEvent(this)); + } + public MsgCreatedEvent ToIntegrationEvent() + { + return new MsgCreatedEvent + { + Id = this.Id, + ClientId = this.ClientMsgId, // 假设你有存发送端的设备ID + ChatType = this.ChatType.ToString(), // 比如 "private" 或 "group" + MsgType = this.MsgType.ToString() ?? MessageType.Text.ToString(), // 动态获取类型名,或从 Content 里的定义拿 + SenderId = this.SenderId, + TargetId = this.TargetId, + State = State.ToString(), + StreamKey = this.StreamKey, + SequenceId = this.SequenceId, + // 核心载荷:利用计算属性触发反序列化,打包成 DTO + Content = new MsgContent( + this.Content.Fallback, + this.Content.Body, // 触发实体内部的反序列化逻辑 + this.Content.Ext, + this.Content.Quote != null ? new QuoteInfoDto( + this.Content.Quote.MessageId, + this.Content.Quote.SenderId, + this.Content.Quote.SenderName, + this.Content.Quote.MessageType.ToString(), + this.Content.Quote.Preview + ) : null + ) + }; + } + + } +} diff --git a/MessageService.Domain/Enums/ChatType.cs b/MessageService.Domain/Enums/ChatType.cs new file mode 100644 index 0000000..65c28bd --- /dev/null +++ b/MessageService.Domain/Enums/ChatType.cs @@ -0,0 +1,8 @@ +namespace MessageService.Domain.Enums +{ + public enum ChatType + { + PRIVATE = 0, + GROUP = 1 + } +} diff --git a/MessageService.Domain/Enums/MessageState.cs b/MessageService.Domain/Enums/MessageState.cs new file mode 100644 index 0000000..8a5bdff --- /dev/null +++ b/MessageService.Domain/Enums/MessageState.cs @@ -0,0 +1,14 @@ +namespace MessageService.Domain.Enums +{ + public enum MessageState + { + /// + /// 已发送 + /// + Sent = 0, + /// + /// 已撤回 + /// + Withdrwan = 1 + } +} diff --git a/MessageService.Domain/Enums/MessageType.cs b/MessageService.Domain/Enums/MessageType.cs new file mode 100644 index 0000000..812e4ed --- /dev/null +++ b/MessageService.Domain/Enums/MessageType.cs @@ -0,0 +1,13 @@ +namespace MessageService.Domain.Enums +{ + public enum MessageType + { + Text = 0, + Image = 1, + Voice = 2, + Video = 3, + File = 4, + VoiceChat = 5, + VideoChat = 6 + } +} diff --git a/MessageService.Domain/Events/ConversationCreatedDomainEvent.cs b/MessageService.Domain/Events/ConversationCreatedDomainEvent.cs new file mode 100644 index 0000000..1a89b32 --- /dev/null +++ b/MessageService.Domain/Events/ConversationCreatedDomainEvent.cs @@ -0,0 +1,7 @@ +using MediatR; +using MessageService.Domain.Entities; + +namespace MessageService.Domain.Events +{ + public record ConversationCreatedDomainEvent(Conversation Conversation) : INotification; +} diff --git a/MessageService.Domain/Events/MessageCreatedDomainEvent.cs b/MessageService.Domain/Events/MessageCreatedDomainEvent.cs new file mode 100644 index 0000000..84e4d45 --- /dev/null +++ b/MessageService.Domain/Events/MessageCreatedDomainEvent.cs @@ -0,0 +1,7 @@ +using MediatR; +using MessageService.Domain.Entities; + +namespace MessageService.Domain.Events +{ + public record MessageCreatedDomainEvent(Message Message) : INotification; +} diff --git a/MessageService.Domain/Events/MessageWithdrawDomainEvent.cs b/MessageService.Domain/Events/MessageWithdrawDomainEvent.cs new file mode 100644 index 0000000..fa8e469 --- /dev/null +++ b/MessageService.Domain/Events/MessageWithdrawDomainEvent.cs @@ -0,0 +1,7 @@ +using MediatR; +using MessageService.Domain.Entities; + +namespace MessageService.Domain.Events +{ + public record MessageWithdrawDomainEvent(Message Message) : INotification; +} diff --git a/MessageService.Domain/IReposities/IConversationReposity.cs b/MessageService.Domain/IReposities/IConversationReposity.cs new file mode 100644 index 0000000..c54848b --- /dev/null +++ b/MessageService.Domain/IReposities/IConversationReposity.cs @@ -0,0 +1,14 @@ +using MessageService.Domain.Entities; + +namespace MessageService.Domain.IReposities +{ + public interface IConversationReposity + { + Task FindByIdAsync(Guid id); + Task> FindByUserIdAsync(Guid userId); + Task> FindByTargetIdAsync(Guid targetId); + Task> FindByStreamKeyAsync(string streamKey); + void Create(Conversation conversation); + Task> FindAllStreamKeyAsync(Guid userId); + } +} diff --git a/MessageService.Domain/IReposities/IMessageReposity.cs b/MessageService.Domain/IReposities/IMessageReposity.cs new file mode 100644 index 0000000..1c923dd --- /dev/null +++ b/MessageService.Domain/IReposities/IMessageReposity.cs @@ -0,0 +1,11 @@ +using MessageService.Domain.Entities; + +namespace MessageService.Domain.IReposities +{ + public interface IMessageReposity + { + Task FindByIdAsync(Guid id); + Task<(IEnumerable messages, bool hasMore)> GetAsync(string streamKey, long? cusor, int direction, int limit); + void Create(Message message); + } +} diff --git a/MessageService.Domain/KeyObjects/MessageContent.cs b/MessageService.Domain/KeyObjects/MessageContent.cs new file mode 100644 index 0000000..fd338e4 --- /dev/null +++ b/MessageService.Domain/KeyObjects/MessageContent.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MessageService.Domain.KeyObjects +{ + public class MessageContent + { + // 兜底文本 + [JsonPropertyName("fallback")] + public string Fallback { get; init; } + + // 核心 Payload,反序列化时根据外层 MessageType 转换为具体子类 + public string RawBody { get; init; } + public string RawExt { get; set; } + [JsonPropertyName("body")] + // 业务代码使用的对象 + [NotMapped] + public object Body => string.IsNullOrEmpty(RawBody) ? null : JsonSerializer.Deserialize(RawBody); + + [NotMapped] + public Dictionary Ext => string.IsNullOrEmpty(RawExt) + ? new() + : JsonSerializer.Deserialize>(RawExt); + + // 引用信息 + [JsonPropertyName("quote")] + public QuoteInfo Quote { get; set; } = new(); + + private MessageContent() { } + public MessageContent(string fallback, object body) + { + Fallback = fallback; + RawBody = body == null ? null : JsonSerializer.Serialize(body); + } + } +} diff --git a/MessageService.Domain/KeyObjects/MessageContextContext.cs b/MessageService.Domain/KeyObjects/MessageContextContext.cs new file mode 100644 index 0000000..186aa5e --- /dev/null +++ b/MessageService.Domain/KeyObjects/MessageContextContext.cs @@ -0,0 +1,11 @@ +using MessageService.Domain.Enums; + +namespace MessageService.Domain.KeyObjects +{ + public record MessageCreateContext( + ChatType ChatType, + Guid ClientMsgId, + Guid SenderId, + Guid TargetId +); +} diff --git a/MessageService.Domain/KeyObjects/MsgTypeObj.cs b/MessageService.Domain/KeyObjects/MsgTypeObj.cs new file mode 100644 index 0000000..647a895 --- /dev/null +++ b/MessageService.Domain/KeyObjects/MsgTypeObj.cs @@ -0,0 +1,8 @@ +namespace MessageService.Domain.KeyObjects +{ + public record TextBody(string Text); + public record ImageBody(string Url, int Width, int Height, string Thumb); + public record VideoBody(string Url, int Width, int Height, string Thumb); + public record VoiceBody(string Url, int Duration); + public record FileBody(string Url, string FileName, long Size, string Format); +} diff --git a/MessageService.Domain/KeyObjects/QuoteInfo.cs b/MessageService.Domain/KeyObjects/QuoteInfo.cs new file mode 100644 index 0000000..5104069 --- /dev/null +++ b/MessageService.Domain/KeyObjects/QuoteInfo.cs @@ -0,0 +1,59 @@ +using IM.DomainCommons; +using MessageService.Domain.Enums; + +namespace MessageService.Domain.KeyObjects +{ + public class QuoteInfo + { + /// + /// 被引用消息的唯一标识 + /// + public Guid MessageId { get; init; } + + /// + /// 被引用消息的发送者 ID + /// + public Guid SenderId { get; init; } + + /// + /// 快照:发送者当时的昵称 (非常关键) + /// 避免客户端为了显示 "回复 @张三" 而去额外查询一次用户信息 + /// + public string SenderName { get; init; } + + /// + /// 被引用消息的类型 (例如:1=文本, 2=图片, 3=文件) + /// 帮助客户端决定如何渲染左侧的 Icon (比如是一段文字,还是一个小图片占位符) + /// + public MessageType MessageType { get; init; } + + /// + /// 被引用消息的内容预览 + /// 如果原消息是文本,则截取前50个字符;如果是图片,可以是 "[图片]" + /// + public string Preview { get; init; } + + /// + /// 构造函数与自校验 + /// + public QuoteInfo() { } + public QuoteInfo(Guid messageId, Guid senderId, string senderName, MessageType messageType, string preview) + { + if (messageId == Guid.Empty) + throw new DomainException("回复消息ID不可为空"); + + if (senderId == Guid.Empty) + throw new DomainException("回复发送者ID不可为空"); + + MessageId = messageId; + SenderId = senderId; + SenderName = string.IsNullOrWhiteSpace(senderName) ? "Unknown" : senderName; + MessageType = messageType; + + // 限制预览文本的长度,防止 Payload 过大(截断处理) + Preview = string.IsNullOrWhiteSpace(preview) + ? string.Empty + : preview.Length > 50 ? preview[..47] + "..." : preview; + } + } +} diff --git a/MessageService.Domain/MessageService.Domain.csproj b/MessageService.Domain/MessageService.Domain.csproj new file mode 100644 index 0000000..ded8088 --- /dev/null +++ b/MessageService.Domain/MessageService.Domain.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/MessageService.Domain/Tools/StreamKeyBuilder.cs b/MessageService.Domain/Tools/StreamKeyBuilder.cs new file mode 100644 index 0000000..bf4e9f0 --- /dev/null +++ b/MessageService.Domain/Tools/StreamKeyBuilder.cs @@ -0,0 +1,23 @@ +namespace MessageService.Domain.Tools +{ + public static class StreamKeyBuilder + { + public static string Private(Guid a, Guid b) + { + // Guid 类型使用 Guid.Empty 来判断是否为无效/初始化的 ID + if (a == Guid.Empty || b == Guid.Empty) + throw new ArgumentException("IDs cannot be empty."); + + // Guid 实现了 IComparable,使用 CompareTo 来保持生成 Key 的顺序一致性 + return $"p:{(a.CompareTo(b) < 0 ? $"{a}_{b}" : $"{b}_{a}")}"; + } + + public static string Group(Guid groupId) + { + if (groupId == Guid.Empty) + throw new ArgumentException("Group ID cannot be empty."); + + return $"g:{groupId}"; + } + } +} diff --git a/MessageService.Infrastructure/Configs/ConversationConfig.cs b/MessageService.Infrastructure/Configs/ConversationConfig.cs new file mode 100644 index 0000000..7057e2e --- /dev/null +++ b/MessageService.Infrastructure/Configs/ConversationConfig.cs @@ -0,0 +1,17 @@ +using MessageService.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace MessageService.Infrastructure.Configs +{ + public class ConversationConfig : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("conversations"); + builder.HasKey(x => x.Id); + builder.HasIndex(x => x.UserId); + + } + } +} diff --git a/MessageService.Infrastructure/Configs/MessageConfig.cs b/MessageService.Infrastructure/Configs/MessageConfig.cs new file mode 100644 index 0000000..a19d29f --- /dev/null +++ b/MessageService.Infrastructure/Configs/MessageConfig.cs @@ -0,0 +1,46 @@ +using MessageService.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace MessageService.Infrastructure.Configs +{ + public class MessageConfig : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("messages"); + builder.HasKey(x => x.Id); + builder.HasIndex(x => new { x.StreamKey, x.SequenceId }); + builder.ComplexProperty(x => x.Content, c => + { + // 1. Fallback 是简单字符串,直接映射 + c.Property(p => p.Fallback) + .HasMaxLength(255) + .IsRequired(); + + // 2. Body 是 object (JSON 载荷) + // ComplexProperty 无法直接处理 object 类型,通常将其序列化为 JSON 字符串存储 + c.Property(p => p.RawBody) + .HasColumnName("Content_Body"); + + // 3. Ext 是 Dictionary + // 同样推荐使用 HasConversion 映射为 JSON 字符串,或者在某些库中使用 JSONB 映射 + // 映射 RawExt 字符串 + c.Property(p => p.RawExt) + .HasColumnName("Content_Ext"); + + // 4. Quote 是嵌套的值对象 (QuoteInfo) + // ComplexProperty 支持嵌套定义 + c.ComplexProperty(p => p.Quote, q => + { + q.IsRequired(true); // 引用信息是可选的 + q.Property(qi => qi.MessageId).HasColumnName("Quote_MsgId"); + q.Property(qi => qi.SenderId).HasColumnName("Quote_SenderId"); + q.Property(qi => qi.SenderName).HasMaxLength(50).HasColumnName("Quote_SenderName"); + q.Property(qi => qi.MessageType).HasColumnName("Quote_MsgType"); + q.Property(qi => qi.Preview).HasMaxLength(100).HasColumnName("Quote_Preview"); + }); + }); + } + } +} diff --git a/MessageService.Infrastructure/MessageDbContext.cs b/MessageService.Infrastructure/MessageDbContext.cs new file mode 100644 index 0000000..dada164 --- /dev/null +++ b/MessageService.Infrastructure/MessageDbContext.cs @@ -0,0 +1,25 @@ +using IM.Infrastructure.Efcore; +using MediatR; +using MessageService.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace MessageService.Infrastructure +{ + public class MessageDbContext : BaseDbContext + { + + public DbSet Messages { get; private set; } + public DbSet Conversations { get; private set; } + + public MessageDbContext(DbContextOptions options, IMediator mediator) : base(options, mediator) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); + modelBuilder.EnableSoftDeletionGlobalFilter(); + } + } +} diff --git a/MessageService.Infrastructure/MessageService.Infrastructure.csproj b/MessageService.Infrastructure/MessageService.Infrastructure.csproj new file mode 100644 index 0000000..13a9b8f --- /dev/null +++ b/MessageService.Infrastructure/MessageService.Infrastructure.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/MessageService.Infrastructure/Migrations/20260423115234_InitMessageDb.Designer.cs b/MessageService.Infrastructure/Migrations/20260423115234_InitMessageDb.Designer.cs new file mode 100644 index 0000000..0331c29 --- /dev/null +++ b/MessageService.Infrastructure/Migrations/20260423115234_InitMessageDb.Designer.cs @@ -0,0 +1,187 @@ +// +using System; +using System.Collections.Generic; +using MessageService.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MessageService.Infrastructure.Migrations +{ + [DbContext(typeof(MessageDbContext))] + [Migration("20260423115234_InitMessageDb")] + partial class InitMessageDb + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("MessageService.Domain.Entities.Conversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ChatType") + .HasColumnType("int"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LastMessage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastReadSequenceId") + .HasColumnType("bigint"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("StreamKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TargetAvatar") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TargetId") + .HasColumnType("char(36)"); + + b.Property("TargetName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UnreadCount") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("conversations", (string)null); + }); + + modelBuilder.Entity("MessageService.Domain.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ChatType") + .HasColumnType("int"); + + b.Property("ClientMsgId") + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("MsgType") + .HasColumnType("int"); + + b.Property("SenderId") + .HasColumnType("char(36)"); + + b.Property("SequenceId") + .HasColumnType("bigint"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("StreamKey") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TargetId") + .HasColumnType("char(36)"); + + b.ComplexProperty>("Content", "MessageService.Domain.Entities.Message.Content#MessageContent", b1 => + { + b1.IsRequired(); + + b1.Property("Fallback") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasAnnotation("Relational:JsonPropertyName", "fallback"); + + b1.Property("RawBody") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("Content_Body"); + + b1.Property("RawExt") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("Content_Ext"); + + b1.ComplexProperty>("Quote", "MessageService.Domain.Entities.Message.Content#MessageContent.Quote#QuoteInfo", b2 => + { + b2.IsRequired(); + + b2.Property("MessageId") + .HasColumnType("char(36)") + .HasColumnName("Quote_MsgId"); + + b2.Property("MessageType") + .HasColumnType("int") + .HasColumnName("Quote_MsgType"); + + b2.Property("Preview") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("Quote_Preview"); + + b2.Property("SenderId") + .HasColumnType("char(36)") + .HasColumnName("Quote_SenderId"); + + b2.Property("SenderName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("Quote_SenderName"); + }); + }); + + b.HasKey("Id"); + + b.HasIndex("StreamKey", "SequenceId"); + + b.ToTable("messages", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MessageService.Infrastructure/Migrations/20260423115234_InitMessageDb.cs b/MessageService.Infrastructure/Migrations/20260423115234_InitMessageDb.cs new file mode 100644 index 0000000..18070b7 --- /dev/null +++ b/MessageService.Infrastructure/Migrations/20260423115234_InitMessageDb.cs @@ -0,0 +1,99 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MessageService.Infrastructure.Migrations +{ + /// + public partial class InitMessageDb : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "conversations", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + TargetId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + TargetAvatar = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + TargetName = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + LastReadSequenceId = table.Column(type: "bigint", nullable: true), + UnreadCount = table.Column(type: "int", nullable: false), + ChatType = table.Column(type: "int", nullable: false), + StreamKey = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + LastMessage = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + IsDeleted = table.Column(type: "tinyint(1)", nullable: false), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + Deletion = table.Column(type: "datetime(6)", nullable: true), + ModificationTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_conversations", x => x.Id); + }) + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "messages", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ChatType = table.Column(type: "int", nullable: false), + MsgType = table.Column(type: "int", nullable: false), + ClientMsgId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + SenderId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + TargetId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + State = table.Column(type: "int", nullable: false), + StreamKey = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + SequenceId = table.Column(type: "bigint", nullable: false), + Content_Fallback = table.Column(type: "varchar(255)", maxLength: 255, nullable: false), + Content_Body = table.Column(type: "longtext", nullable: false), + Content_Ext = table.Column(type: "longtext", nullable: false), + Quote_MsgId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Quote_MsgType = table.Column(type: "int", nullable: false), + Quote_Preview = table.Column(type: "varchar(100)", maxLength: 100, nullable: false), + Quote_SenderId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Quote_SenderName = table.Column(type: "varchar(50)", maxLength: 50, nullable: false), + IsDeleted = table.Column(type: "tinyint(1)", nullable: false), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + Deletion = table.Column(type: "datetime(6)", nullable: true), + ModificationTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_messages", x => x.Id); + }) + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_conversations_UserId", + table: "conversations", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_messages_StreamKey_SequenceId", + table: "messages", + columns: new[] { "StreamKey", "SequenceId" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "conversations"); + + migrationBuilder.DropTable( + name: "messages"); + } + } +} diff --git a/MessageService.Infrastructure/Migrations/MessageDbContextModelSnapshot.cs b/MessageService.Infrastructure/Migrations/MessageDbContextModelSnapshot.cs new file mode 100644 index 0000000..3c3daf6 --- /dev/null +++ b/MessageService.Infrastructure/Migrations/MessageDbContextModelSnapshot.cs @@ -0,0 +1,184 @@ +// +using System; +using System.Collections.Generic; +using MessageService.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MessageService.Infrastructure.Migrations +{ + [DbContext(typeof(MessageDbContext))] + partial class MessageDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("MessageService.Domain.Entities.Conversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ChatType") + .HasColumnType("int"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LastMessage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastReadSequenceId") + .HasColumnType("bigint"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("StreamKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TargetAvatar") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TargetId") + .HasColumnType("char(36)"); + + b.Property("TargetName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UnreadCount") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("conversations", (string)null); + }); + + modelBuilder.Entity("MessageService.Domain.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ChatType") + .HasColumnType("int"); + + b.Property("ClientMsgId") + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("MsgType") + .HasColumnType("int"); + + b.Property("SenderId") + .HasColumnType("char(36)"); + + b.Property("SequenceId") + .HasColumnType("bigint"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("StreamKey") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TargetId") + .HasColumnType("char(36)"); + + b.ComplexProperty>("Content", "MessageService.Domain.Entities.Message.Content#MessageContent", b1 => + { + b1.IsRequired(); + + b1.Property("Fallback") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasAnnotation("Relational:JsonPropertyName", "fallback"); + + b1.Property("RawBody") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("Content_Body"); + + b1.Property("RawExt") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("Content_Ext"); + + b1.ComplexProperty>("Quote", "MessageService.Domain.Entities.Message.Content#MessageContent.Quote#QuoteInfo", b2 => + { + b2.IsRequired(); + + b2.Property("MessageId") + .HasColumnType("char(36)") + .HasColumnName("Quote_MsgId"); + + b2.Property("MessageType") + .HasColumnType("int") + .HasColumnName("Quote_MsgType"); + + b2.Property("Preview") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("Quote_Preview"); + + b2.Property("SenderId") + .HasColumnType("char(36)") + .HasColumnName("Quote_SenderId"); + + b2.Property("SenderName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("Quote_SenderName"); + }); + }); + + b.HasKey("Id"); + + b.HasIndex("StreamKey", "SequenceId"); + + b.ToTable("messages", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MessageService.Infrastructure/ModuleInit.cs b/MessageService.Infrastructure/ModuleInit.cs new file mode 100644 index 0000000..4042c01 --- /dev/null +++ b/MessageService.Infrastructure/ModuleInit.cs @@ -0,0 +1,17 @@ +using IM.Commons; +using MessageService.Domain.IReposities; +using MessageService.Infrastructure.Reposities; +using Microsoft.Extensions.DependencyInjection; + +namespace MessageService.Infrastructure +{ + public class ModuleInit : IModuleInitializer + { + public void Initialize(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + + } + } +} diff --git a/MessageService.Infrastructure/Reposities/ConversationReposity.cs b/MessageService.Infrastructure/Reposities/ConversationReposity.cs new file mode 100644 index 0000000..657eb10 --- /dev/null +++ b/MessageService.Infrastructure/Reposities/ConversationReposity.cs @@ -0,0 +1,48 @@ +using MessageService.Domain.Entities; +using MessageService.Domain.IReposities; +using Microsoft.EntityFrameworkCore; + +namespace MessageService.Infrastructure.Reposities +{ + public class ConversationReposity : IConversationReposity + { + private readonly MessageDbContext db; + + public ConversationReposity(MessageDbContext db) + { + this.db = db; + } + + public void Create(Conversation conversation) + { + db.Conversations.Add(conversation); + } + + public async Task> FindAllStreamKeyAsync(Guid userId) + { + return await db.Conversations.Where(x => x.UserId == userId) + .Select(s => s.StreamKey) + .ToListAsync(); + } + + public async Task FindByIdAsync(Guid id) + { + return await db.Conversations.FirstOrDefaultAsync(x => x.Id == id); + } + + public async Task> FindByStreamKeyAsync(string streamKey) + { + return await db.Conversations.Where(x => x.StreamKey == streamKey).ToListAsync(); + } + + public async Task> FindByTargetIdAsync(Guid targetId) + { + return await db.Conversations.Where(x => x.TargetId == targetId).ToListAsync(); + } + + public async Task> FindByUserIdAsync(Guid userId) + { + return await db.Conversations.Where(x => x.UserId == userId).ToListAsync(); + } + } +} diff --git a/MessageService.Infrastructure/Reposities/MessageReposity.cs b/MessageService.Infrastructure/Reposities/MessageReposity.cs new file mode 100644 index 0000000..fb15cd4 --- /dev/null +++ b/MessageService.Infrastructure/Reposities/MessageReposity.cs @@ -0,0 +1,55 @@ +using MessageService.Domain.Entities; +using MessageService.Domain.IReposities; +using Microsoft.EntityFrameworkCore; + +namespace MessageService.Infrastructure.Reposities +{ + public class MessageReposity : IMessageReposity + { + private readonly MessageDbContext db; + + public MessageReposity(MessageDbContext db) + { + this.db = db; + } + + public void Create(Message message) + { + db.Messages.Add(message); + } + + public async Task FindByIdAsync(Guid id) + { + return await db.Messages.FirstOrDefaultAsync(x => x.Id == id); + } + + public async Task<(IEnumerable messages, bool hasMore)> GetAsync(string streamKey, long? cusor, int direction, int limit) + { + var query = db.Messages.Where(x => x.StreamKey == streamKey); + List messages = []; + if (direction == 0) // Before: 找比锚点小的,按倒序排 + { + if (cusor.HasValue) + query = query.Where(m => m.SequenceId < cusor.Value); + + var list = await query + .OrderByDescending(m => m.SequenceId) // 最新消息在最前 + .Take(limit + 1) + .ToListAsync(); + + messages = [.. list.OrderBy(s => s.SequenceId)]; + } + else + { + if (cusor is null) + return (messages, false); + + messages = await query.OrderBy(o => o.SequenceId) + .Take(limit + 1) + .ToListAsync(); + } + + return (messages, messages.Count > limit); + } + } +} diff --git a/MessageService.WebApi/Application/Conversation/ConversationMapperConfig.cs b/MessageService.WebApi/Application/Conversation/ConversationMapperConfig.cs new file mode 100644 index 0000000..e6233fd --- /dev/null +++ b/MessageService.WebApi/Application/Conversation/ConversationMapperConfig.cs @@ -0,0 +1,14 @@ +using AutoMapper; +using MessageService.WebApi.Application.Dtos; + +namespace MessageService.WebApi.Application.Conversation +{ + public class ConversationMapperConfig : Profile + { + public ConversationMapperConfig() + { + CreateMap() + ; + } + } +} diff --git a/MessageService.WebApi/Application/Conversation/ConversationService.cs b/MessageService.WebApi/Application/Conversation/ConversationService.cs new file mode 100644 index 0000000..d2dc3b9 --- /dev/null +++ b/MessageService.WebApi/Application/Conversation/ConversationService.cs @@ -0,0 +1,43 @@ +using AutoMapper; +using IM.Commons; +using MessageService.Domain.IReposities; +using MessageService.WebApi.Application.Dtos; + +namespace MessageService.WebApi.Application.Conversation +{ + public class ConversationService + { + private readonly IConversationReposity reposity; + private readonly IMapper mapper; + + public ConversationService(IConversationReposity reposity, IMapper mapper) + { + this.reposity = reposity; + this.mapper = mapper; + } + + public async Task>> GetByOwnerIdAsync(Guid userId) + { + var list = await reposity.FindByUserIdAsync(userId); + return Result.Success(mapper.Map>(list.ToList())); + } + + public async Task> GetByIdAsync(Guid id, Guid userId) + { + var conversation = await reposity.FindByIdAsync(id); + + if (conversation is null || conversation.UserId != userId) + { + return Result.Fail(ResultCode.CONVERSATION_NOT_FOUND); + } + + return Result.Success(mapper.Map(conversation)); + } + + public async Task>> GetStreamkeysAsync(Guid userId) + { + var list = await reposity.FindAllStreamKeyAsync(userId); + return Result.Success(list.ToList()); + } + } +} diff --git a/MessageService.WebApi/Application/Dtos/ConversationResponse.cs b/MessageService.WebApi/Application/Dtos/ConversationResponse.cs new file mode 100644 index 0000000..c903608 --- /dev/null +++ b/MessageService.WebApi/Application/Dtos/ConversationResponse.cs @@ -0,0 +1,35 @@ +using MessageService.Domain.Enums; + +namespace MessageService.WebApi.Application.Dtos +{ + public class ConversationResponse + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + + /// + /// 对方ID(群聊为群聊ID,单聊为单聊ID) + /// + public Guid TargetId { get; set; } + public string TargetAvatar { get; set; } + public string TargetName { get; set; } + + /// + /// 最后一条未读消息ID + /// + public long? LastReadSequenceId { get; set; } + + /// + /// 未读消息数 + /// + public int UnreadCount { get; set; } + + public ChatType ChatType { get; set; } + + + /// + /// 最后一条最新消息 + /// + public string LastMessage { get; set; } + } +} diff --git a/MessageService.WebApi/Application/Dtos/MessageResponse.cs b/MessageService.WebApi/Application/Dtos/MessageResponse.cs new file mode 100644 index 0000000..1146cd5 --- /dev/null +++ b/MessageService.WebApi/Application/Dtos/MessageResponse.cs @@ -0,0 +1,36 @@ +using MessageService.Domain.Enums; + +namespace MessageService.WebApi.Application.Dtos +{ + public record MessageResponse + { + public Guid Id { get; init; } + public Guid ClientMsgId { get; init; } + public ChatType ChatType { get; init; } + public MessageType MsgType { get; init; } + public Guid SenderId { get; init; } + public Guid TargetId { get; init; } + public MessageState State { get; init; } + public string StreamKey { get; init; } + public long SequenceId { get; init; } + public DateTimeOffset CreationTime { get; init; } + + // 关键:展开 Content + public MessageContentResponse Content { get; init; } + } + + public record MessageContentResponse( + string Fallback, + object Body, // 已经是反序列化后的具体对象 + Dictionary Ext, + QuoteInfoResponse? Quote + ); + + public record QuoteInfoResponse( + Guid MessageId, + Guid SenderId, + string SenderName, + MessageType MessageType, + string Preview + ); +} diff --git a/MessageService.WebApi/Application/EventHandlers/ConversationAddHandler.cs b/MessageService.WebApi/Application/EventHandlers/ConversationAddHandler.cs new file mode 100644 index 0000000..5c2ca76 --- /dev/null +++ b/MessageService.WebApi/Application/EventHandlers/ConversationAddHandler.cs @@ -0,0 +1,55 @@ +using IM.Commons.IntegrationEvents; +using MassTransit; +using MessageService.Domain.IReposities; +using MessageService.Infrastructure; + +namespace MessageService.WebApi.Application.EventHandlers +{ + public class ConversationAddHandler : IConsumer, + IConsumer + { + + private readonly IConversationReposity reposity; + private readonly MessageDbContext messageDb; + + public ConversationAddHandler(IConversationReposity reposity, MessageDbContext messageDb) + { + this.reposity = reposity; + this.messageDb = messageDb; + } + + public async Task Consume(ConsumeContext context) + { + var @event = context.Message; + reposity.Create(new Domain.Entities.Conversation( + userId: @event.UserId, + targetId: @event.GroupId, + targetAvatar: @event.Avatar, + targetName: @event.GroupNickName, + lastReadSequenceId: null, + unreadCount:0, + chatType: Domain.Enums.ChatType.GROUP, + lastMessage: string.Empty + )); + + await messageDb.SaveChangesAsync(); + } + + public async Task Consume(ConsumeContext context) + { + var @event = context.Message; + reposity.Create(new Domain.Entities.Conversation( + userId: @event.OwnerId, + targetId: @event.TargetId, + targetAvatar: @event.TargetAvatar, + targetName: @event.TargetNickName, + lastReadSequenceId: null, + unreadCount: 0, + chatType: Domain.Enums.ChatType.PRIVATE, + lastMessage: string.Empty + )); + + await messageDb.SaveChangesAsync(); + } + } +} diff --git a/MessageService.WebApi/Application/EventHandlers/MessageHandler.cs b/MessageService.WebApi/Application/EventHandlers/MessageHandler.cs new file mode 100644 index 0000000..f162d1a --- /dev/null +++ b/MessageService.WebApi/Application/EventHandlers/MessageHandler.cs @@ -0,0 +1,49 @@ +using IM.Commons.IntegrationEvents; +using MassTransit; +using MediatR; +using MessageService.Domain.Events; +using MessageService.Domain.IReposities; +using MessageService.Infrastructure; + +namespace MessageService.WebApi.Application.EventHandlers +{ + public class MessageHandler : INotificationHandler, INotificationHandler + { + private readonly IPublishEndpoint endpoint; + private readonly IConversationReposity reposity; + private readonly MessageDbContext messageDb; + + public MessageHandler(IPublishEndpoint endpoint, IConversationReposity reposity, MessageDbContext messageDb) + { + this.endpoint = endpoint; + this.reposity = reposity; + this.messageDb = messageDb; + } + + public async Task Handle(MessageCreatedDomainEvent notification, CancellationToken cancellationToken) + { + var message = notification.Message; + + if(message.ChatType == Domain.Enums.ChatType.PRIVATE) + { + var list = await reposity.FindByStreamKeyAsync(message.StreamKey); + var owner = list.First(x => x.UserId == message.SenderId); + var target = list.First(x => x.UserId == message.TargetId); + + owner.Update(message.SequenceId, 0, message.Content.Fallback); + target.Update(target.LastReadSequenceId, target.UnreadCount + 1, message.Content.Fallback); + + messageDb.Conversations.UpdateRange(owner,target); + await messageDb.SaveChangesAsync(cancellationToken); + } + + await endpoint.Publish(message.ToIntegrationEvent()); + } + + public async Task Handle(MessageWithdrawDomainEvent notification, CancellationToken cancellationToken) + { + var message = notification.Message; + await endpoint.Publish(new MsgWithdrawEvent(message.Id, message.State.ToString(), message.StreamKey), cancellationToken); + } + } +} diff --git a/MessageService.WebApi/Application/EventHandlers/UserProfileUpdateHandler.cs b/MessageService.WebApi/Application/EventHandlers/UserProfileUpdateHandler.cs new file mode 100644 index 0000000..b73db8b --- /dev/null +++ b/MessageService.WebApi/Application/EventHandlers/UserProfileUpdateHandler.cs @@ -0,0 +1,30 @@ +using IM.Commons.IntegrationEvents; +using MassTransit; +using MessageService.Domain.IReposities; +using MessageService.Infrastructure; + +namespace MessageService.WebApi.Application.EventHandlers +{ + public class UserProfileUpdateHandler : IConsumer + { + private readonly MessageDbContext db; + private readonly IConversationReposity reposity; + + public UserProfileUpdateHandler(MessageDbContext db, IConversationReposity reposity) + { + this.db = db; + this.reposity = reposity; + } + + public async Task Consume(ConsumeContext context) + { + var @event = context.Message; + var conversations = await reposity.FindByTargetIdAsync(@event.UserId); + foreach (var conversation in conversations) + { + conversation.UpdateProfile(@event.NickName, @event.Avatar); + } + await db.SaveChangesAsync(); + } + } +} diff --git a/MessageService.WebApi/Application/IntegrationServices/ContactIntegrationService.cs b/MessageService.WebApi/Application/IntegrationServices/ContactIntegrationService.cs new file mode 100644 index 0000000..ad810f7 --- /dev/null +++ b/MessageService.WebApi/Application/IntegrationServices/ContactIntegrationService.cs @@ -0,0 +1,27 @@ + +using IM.Commons; +using IM.Protocols.Grpc.Contact; + +namespace MessageService.WebApi.Application.IntegrationServices +{ + public class ContactIntegrationService : IContactIntegrationService + { + private readonly ContactInternal.ContactInternalClient client; + + public ContactIntegrationService(ContactInternal.ContactInternalClient client) + { + this.client = client; + } + + public async Task CheckContactAsync(Guid ownerId, Guid targetId) + { + var req = new CheckFriendshipRequest() + { + OwnerId = ownerId.ToString(), + TargetId = targetId.ToString(), + }; + var res = await client.CheckFriendshipAsync(req); + return res.Checked; + } + } +} diff --git a/MessageService.WebApi/Application/IntegrationServices/GroupMemberIntegrationService.cs b/MessageService.WebApi/Application/IntegrationServices/GroupMemberIntegrationService.cs new file mode 100644 index 0000000..2ff1284 --- /dev/null +++ b/MessageService.WebApi/Application/IntegrationServices/GroupMemberIntegrationService.cs @@ -0,0 +1,22 @@ + +using IM.Commons; + +namespace MessageService.WebApi.Application.IntegrationServices +{ + public class GroupMemberIntegrationService : IGroupMemberIntegrationService + { + private readonly HttpClient http; + public async Task CheckGroupMemberAsync(Guid userId, Guid groupId) + { + var result = await http.GetFromJsonAsync>( + $"api/groupmember/checkmember?userId={userId}&groupId={groupId}"); + + if (!result.Succeeded) + { + return false; + } + + return result.Data; + } + } +} diff --git a/MessageService.WebApi/Application/IntegrationServices/IContactIntegrationService.cs b/MessageService.WebApi/Application/IntegrationServices/IContactIntegrationService.cs new file mode 100644 index 0000000..98687af --- /dev/null +++ b/MessageService.WebApi/Application/IntegrationServices/IContactIntegrationService.cs @@ -0,0 +1,7 @@ +namespace MessageService.WebApi.Application.IntegrationServices +{ + public interface IContactIntegrationService + { + Task CheckContactAsync(Guid ownerId, Guid targetId); + } +} diff --git a/MessageService.WebApi/Application/IntegrationServices/IGroupMemberIntegrationService.cs b/MessageService.WebApi/Application/IntegrationServices/IGroupMemberIntegrationService.cs new file mode 100644 index 0000000..ac5075c --- /dev/null +++ b/MessageService.WebApi/Application/IntegrationServices/IGroupMemberIntegrationService.cs @@ -0,0 +1,7 @@ +namespace MessageService.WebApi.Application.IntegrationServices +{ + public interface IGroupMemberIntegrationService + { + Task CheckGroupMemberAsync(Guid userId, Guid groupId); + } +} diff --git a/MessageService.WebApi/Application/Message/MessageMapperConfig.cs b/MessageService.WebApi/Application/Message/MessageMapperConfig.cs new file mode 100644 index 0000000..eb9497e --- /dev/null +++ b/MessageService.WebApi/Application/Message/MessageMapperConfig.cs @@ -0,0 +1,32 @@ +using AutoMapper; +using MessageService.Domain.KeyObjects; +using MessageService.WebApi.Application.Dtos; + +namespace MessageService.WebApi.Application.Message +{ + public class MessageMapperConfig : Profile + { + public MessageMapperConfig() + { + // 1. 配置 QuoteInfo -> QuoteInfoResponse + CreateMap(); + + // 2. 配置 MessageContent -> MessageContentResponse + CreateMap() + // 关键点:Body 是 object,由于我们在实体里写了 Body 计算属性 + // AutoMapper 默认会识别到同名的 Body 属性并进行映射 + // 如果你想显式指定逻辑,可以取消下面这行的注释: + // .ForMember(dest => dest.Body, opt => opt.MapFrom(src => src.Body)) + .ForMember(dest => dest.Ext, opt => opt.MapFrom(src => src.Ext)) + .ForMember(dest => dest.Quote, opt => opt.MapFrom(src => + src.Quote.MessageId == Guid.Empty ? null : src.Quote)); + + // 3. 配置 Message -> MessageResponse + CreateMap() + // 映射审计字段中的创建时间 + .ForMember(dest => dest.CreationTime, opt => opt.MapFrom(src => src.CreationTime)) + // 嵌套映射 Content + .ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.Content)); + } + } +} diff --git a/MessageService.WebApi/Application/Message/MessageService.cs b/MessageService.WebApi/Application/Message/MessageService.cs new file mode 100644 index 0000000..2489fc0 --- /dev/null +++ b/MessageService.WebApi/Application/Message/MessageService.cs @@ -0,0 +1,112 @@ +using AutoMapper; +using IM.Commons; +using MessageService.Domain.Enums; +using MessageService.Domain.IReposities; +using MessageService.Domain.KeyObjects; +using MessageService.Domain.Tools; +using MessageService.WebApi.Application.Dtos; +using MessageService.WebApi.Application.IntegrationServices; + +namespace MessageService.WebApi.Application.Message +{ + public class MessageService + { + private readonly IMessageReposity reposity; + private readonly IMapper mapper; + private readonly IGroupMemberIntegrationService memberService; + private readonly IContactIntegrationService contactService; + private readonly SquenceService squenceService; + + public MessageService(IMessageReposity reposity, IMapper mapper, + IGroupMemberIntegrationService memberService, + IContactIntegrationService contactService, + SquenceService squenceService + ) + { + this.reposity = reposity; + this.mapper = mapper; + this.memberService = memberService; + this.contactService = contactService; + this.squenceService = squenceService; + } + + public async Task> SendMsgAsync(SendMsgCommand command) + { + if (command.ChatType == Domain.Enums.ChatType.PRIVATE) + { + bool passed = await contactService.CheckContactAsync(command.SenderId, command.TargetId); + if (!passed) + return Result.Fail(ResultCode.FRIEND_RELATION_NOT_FOUND); + } + else + { + bool passed = await memberService.CheckGroupMemberAsync(command.SenderId, command.TargetId); + if (!passed) + return Result.Fail(ResultCode.NO_GROUP_PERMISSION); + } + + var ctx = new MessageCreateContext(command.ChatType, command.ClientMsgId, command.SenderId, command.TargetId); + + var streamKey = command.ChatType == ChatType.GROUP ? StreamKeyBuilder.Group(ctx.TargetId) : StreamKeyBuilder.Private(ctx.SenderId, ctx.TargetId); + long sequenceId = await squenceService.GetNextSquenceIdAsync(streamKey); + + Domain.Entities.Message message = command.MsgType switch + { + MessageType.Text => Domain.Entities.Message.BuildTxt(ctx, command.Text!, sequenceId), + + MessageType.Image => Domain.Entities.Message.BuildImg(ctx, command.Url!, + command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId), + + MessageType.Video => Domain.Entities.Message.BuildVideo(ctx, command.Url!, + command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId), + + MessageType.Voice => Domain.Entities.Message.BuildVoice(ctx, command.Url!, command.Duration ?? 0, sequenceId), + + _ => null + }; + + if (message == null) + { + return Result.Fail(ResultCode.UNSUPPORTED_MESSAGE_TYPE); + } + + // 2. 处理引用逻辑(如果传了 QuoteMessageId) + QuoteInfo? quote = null; + if (command.QuoteMessageId.HasValue) + { + var originMsg = await reposity.FindByIdAsync(command.QuoteMessageId.Value); + if (originMsg != null) + { + quote = new QuoteInfo + { + MessageId = originMsg.Id, + SenderId = originMsg.SenderId, + SenderName = "未知昵称", // 这里建议从缓存或DB获取发送者昵称 + MessageType = originMsg.MsgType, + Preview = originMsg.Content.Fallback + }; + } + } + + message.WithQuote(quote); + + reposity.Create(message); + + return Result.Success(mapper.Map(message)); + + } + + public async Task> WithDrawMsgAsync(Guid msgId, Guid senderId) + { + var msg = await reposity.FindByIdAsync(msgId); + if (msg == null || msg.SenderId != senderId) + { + return Result.Fail(ResultCode.MESSAGE_NOT_FOUND); + } + + msg.Withdraw(); + + return Result.Success(); + } + } +} diff --git a/MessageService.WebApi/Application/Message/SendMsgCommand.cs b/MessageService.WebApi/Application/Message/SendMsgCommand.cs new file mode 100644 index 0000000..c37bce4 --- /dev/null +++ b/MessageService.WebApi/Application/Message/SendMsgCommand.cs @@ -0,0 +1,43 @@ +using MessageService.Domain.Enums; + +namespace MessageService.WebApi.Application.Message +{ + public record SendMsgCommand + { + // 核心区别:Command 必须包含 SenderId,这是从后端 Token 解析出来的 + public Guid SenderId { get; init; } + + public Guid TargetId { get; init; } + public ChatType ChatType { get; init; } + public MessageType MsgType { get; init; } + public Guid ClientMsgId { get; init; } + + public Guid? QuoteMessageId { get; init; } + public Dictionary? Ext { get; init; } + + // 拍扁后的参数,方便 Service 直接调用工厂方法 + public string? Text { get; init; } + public string? Url { get; init; } + public int? Width { get; init; } + public int? Height { get; init; } + public string? Thumb { get; init; } + public int? Duration { get; init; } + + public SendMsgCommand(Guid senderId, Guid targetId, ChatType chatType, MessageType msgType, Guid clientMsgId, Guid? quoteMessageId, Dictionary? ext, string? text, string? url, int? width, int? height, string? thumb, int? duration) + { + SenderId = senderId; + TargetId = targetId; + ChatType = chatType; + MsgType = msgType; + ClientMsgId = clientMsgId; + QuoteMessageId = quoteMessageId; + Ext = ext; + Text = text; + Url = url; + Width = width; + Height = height; + Thumb = thumb; + Duration = duration; + } + } +} diff --git a/MessageService.WebApi/Application/SquenceService.cs b/MessageService.WebApi/Application/SquenceService.cs new file mode 100644 index 0000000..1c1d2ed --- /dev/null +++ b/MessageService.WebApi/Application/SquenceService.cs @@ -0,0 +1,47 @@ +using IM.Commons; +using MessageService.Infrastructure; +using Microsoft.EntityFrameworkCore; +using RedLockNet; +using StackExchange.Redis; + +namespace MessageService.WebApi.Application +{ + public class SquenceService + { + private readonly IDatabase _database; + private readonly IDistributedLockFactory _lockFactory; + private readonly MessageDbContext messageDb; + public SquenceService(IConnectionMultiplexer multiplexer, + IDistributedLockFactory distributedLockFactory, + MessageDbContext messageDb + ) + { + _database = multiplexer.GetDatabase(); + _lockFactory = distributedLockFactory; + this.messageDb = messageDb; + } + public async Task GetNextSquenceIdAsync(string streamKey) + { + string key = RedisHelper.GetSequenceIdKey(streamKey); + string lockKey = RedisHelper.GetSequenceIdLockKey(streamKey); + var exists = await _database.KeyExistsAsync(key); + if (!exists) + { + using (var _lock = await _lockFactory.CreateLockAsync(lockKey, TimeSpan.FromSeconds(5))) + { + if (_lock.IsAcquired) + { + if (!await _database.KeyExistsAsync(key)) + { + var max = await messageDb.Messages + .Where(x => x.StreamKey == streamKey) + .MaxAsync(m => (long?)m.SequenceId) ?? 0; + await _database.StringSetAsync(key, max, TimeSpan.FromDays(7)); + } + } + } + } + return await _database.StringIncrementAsync(key); + } + } +} diff --git a/MessageService.WebApi/Controllers/Conversation/ConversationController.cs b/MessageService.WebApi/Controllers/Conversation/ConversationController.cs new file mode 100644 index 0000000..b364108 --- /dev/null +++ b/MessageService.WebApi/Controllers/Conversation/ConversationController.cs @@ -0,0 +1,34 @@ +using MessageService.WebApi.Application.Conversation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace MessageService.WebApi.Controllers.Conversation +{ + [Route("api/[controller]/[action]")] + [Authorize] + [ApiController] + public class ConversationController : ControllerBase + { + private readonly ConversationService service; + + public ConversationController(ConversationService service) + { + this.service = service; + } + + [HttpGet] + public async Task List() + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.GetByOwnerIdAsync(Guid.Parse(userId))); + } + [HttpGet] + public async Task Get([FromRoute] Guid id) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.GetByIdAsync(id, Guid.Parse(userId))); + } + + } +} diff --git a/MessageService.WebApi/Controllers/Message/MessageController.cs b/MessageService.WebApi/Controllers/Message/MessageController.cs new file mode 100644 index 0000000..29741f6 --- /dev/null +++ b/MessageService.WebApi/Controllers/Message/MessageController.cs @@ -0,0 +1,38 @@ +using IM.ASPNETCore; +using MessageService.Infrastructure; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace MessageService.WebApi.Controllers.Message +{ + [Route("api/[controller]/[action]")] + [Authorize] + [ApiController] + public class MessageController : ControllerBase + { + private readonly Application.Message.MessageService service; + + public MessageController(Application.Message.MessageService service) + { + this.service = service; + } + + [HttpPost] + [UnitOfWork(typeof(MessageDbContext))] + public async Task Send(MessageSendRequest request) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var command = request.ToCommand(Guid.Parse(userId)); + return Ok(await service.SendMsgAsync(command)); + } + + [HttpPost] + [UnitOfWork(typeof(MessageDbContext))] + public async Task WithDraw([FromRoute] Guid msgId) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + return Ok(await service.WithDrawMsgAsync(msgId, Guid.Parse(userId))); + } + } +} diff --git a/MessageService.WebApi/Controllers/Message/MessageSendRequest.cs b/MessageService.WebApi/Controllers/Message/MessageSendRequest.cs new file mode 100644 index 0000000..f4f6265 --- /dev/null +++ b/MessageService.WebApi/Controllers/Message/MessageSendRequest.cs @@ -0,0 +1,59 @@ +using FluentValidation; +using MessageService.Domain.Enums; +using MessageService.WebApi.Application.Message; + +namespace MessageService.WebApi.Controllers.Message +{ + public class MessageSendRequest + { + // 基础元数据 + public Guid ClientMsgId { get; init; } + public Guid TargetId { get; init; } + public ChatType ChatType { get; init; } + public MessageType MsgType { get; init; } + + // 业务属性 + public Guid? QuoteMessageId { get; init; } + public Dictionary? Ext { get; init; } + + // 载荷数据(根据 MsgType 选择性填充) + public string? Text { get; init; } + public string? Url { get; init; } + public int? Width { get; init; } + public int? Height { get; init; } + public string? Thumb { get; init; } + public int? Duration { get; init; } + + public SendMsgCommand ToCommand(Guid senderId) + { + return new SendMsgCommand( + senderId, + TargetId, + ChatType, + MsgType, + ClientMsgId, + QuoteMessageId, + Ext, + Text, + Url, + Width, + Height, + Thumb, + Duration + ); + } + } + + public class MessageSendRequestValidator : AbstractValidator + { + public MessageSendRequestValidator() + { + RuleFor(r => r.ClientMsgId) + .NotNull() + .NotEmpty(); + RuleFor(r => r.TargetId) + .NotEmpty() + .NotNull(); + } + } +} diff --git a/MessageService.WebApi/DesignTimeDbContextFactory.cs b/MessageService.WebApi/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..66fdf53 --- /dev/null +++ b/MessageService.WebApi/DesignTimeDbContextFactory.cs @@ -0,0 +1,18 @@ +using IM.InitCommon; +using MessageService.Infrastructure; +using Microsoft.EntityFrameworkCore.Design; + +namespace MessageService.WebApi +{ + public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory + { + public MessageDbContext CreateDbContext(string[] args) + { + // 1. 复用你写好的配置工厂,提取连接字符串 + var optionsBuilder = DbContextOptionsBuilderFactory.Create(); + + // 2. 🌟 关键补刀:把假的 Mediator 传进去,满足构造函数的要求! + return new MessageDbContext(optionsBuilder.Options, null); + } + } +} diff --git a/MessageService.WebApi/MessageService.WebApi.csproj b/MessageService.WebApi/MessageService.WebApi.csproj new file mode 100644 index 0000000..82f976b --- /dev/null +++ b/MessageService.WebApi/MessageService.WebApi.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/MessageService.WebApi/MessageService.WebApi.http b/MessageService.WebApi/MessageService.WebApi.http new file mode 100644 index 0000000..7bbee78 --- /dev/null +++ b/MessageService.WebApi/MessageService.WebApi.http @@ -0,0 +1,6 @@ +@MessageService.WebApi_HostAddress = http://localhost:5067 + +GET {{MessageService.WebApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/MessageService.WebApi/ModuleInit.cs b/MessageService.WebApi/ModuleInit.cs new file mode 100644 index 0000000..943ed5e --- /dev/null +++ b/MessageService.WebApi/ModuleInit.cs @@ -0,0 +1,25 @@ +using IM.Commons; +using IM.Protocols.Grpc.Contact; +using IM.Protocols.Grpc.User; +using MessageService.WebApi.Application; +using MessageService.WebApi.Application.IntegrationServices; +using Microsoft.Extensions.Options; + +namespace MessageService.WebApi +{ + public class ModuleInit : IModuleInitializer + { + public void Initialize(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddGrpcClient((sp, o) => + { + var options = sp.GetRequiredService>(); + o.Address = new Uri(options.CurrentValue.ContactServiceUrl); + }); + } + } +} diff --git a/MessageService.WebApi/Program.cs b/MessageService.WebApi/Program.cs new file mode 100644 index 0000000..74741e0 --- /dev/null +++ b/MessageService.WebApi/Program.cs @@ -0,0 +1,37 @@ + +using IM.InitCommon; + +namespace MessageService.WebApi +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + builder.ConfigureDbConfiguration(); + builder.Services.AddControllers(); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + builder.ConfigExtraServices(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseAppDefault(); + + + app.MapControllers(); + + app.Run(); + } + } +} diff --git a/MessageService.WebApi/Properties/launchSettings.json b/MessageService.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..30dc7d4 --- /dev/null +++ b/MessageService.WebApi/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5067" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7143;http://localhost:5067" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:25172", + "sslPort": 44390 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/MessageService.WebApi/Services/ConversationIntegrationService.cs b/MessageService.WebApi/Services/ConversationIntegrationService.cs new file mode 100644 index 0000000..cee23b6 --- /dev/null +++ b/MessageService.WebApi/Services/ConversationIntegrationService.cs @@ -0,0 +1,28 @@ +using Grpc.Core; +using IM.Protocols.Grpc.Conversation; +using MessageService.WebApi.Application.Conversation; + +namespace MessageService.WebApi.Services +{ + public class ConversationIntegrationService:ConversationInternal.ConversationInternalBase + { + private readonly ConversationService service; + + public ConversationIntegrationService(ConversationService service) + { + this.service = service; + } + public override async Task GetUserStreamKeys(GetUserStreamKeysRequest request, ServerCallContext context) + { + var res = await service.GetStreamkeysAsync(Guid.Parse(request.UserId)); + if (!res.Succeeded) + { + throw new RpcException(new Status(StatusCode.InvalidArgument, res.Message)); + } + + var response = new UserStreamKeysResponse(); + response.StreamKeys.AddRange(res.Data); + return response; + } + } +} diff --git a/MessageService.WebApi/appsettings.Development.json b/MessageService.WebApi/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/MessageService.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/MessageService.WebApi/appsettings.json b/MessageService.WebApi/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/MessageService.WebApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/User.Domain/Entities/Role.cs b/User.Domain/Entities/Role.cs new file mode 100644 index 0000000..670a051 --- /dev/null +++ b/User.Domain/Entities/Role.cs @@ -0,0 +1,13 @@ +using MassTransit; +using Microsoft.AspNetCore.Identity; + +namespace IdentityService.Domain.Entities +{ + public class Role : IdentityRole + { + public Role() + { + Id = NewId.NextGuid(); + } + } +} diff --git a/User.Domain/Entities/User.cs b/User.Domain/Entities/User.cs new file mode 100644 index 0000000..653a139 --- /dev/null +++ b/User.Domain/Entities/User.cs @@ -0,0 +1,130 @@ +using IdentityService.Domain.Events; +using IM.DomainCommons; +using MassTransit; +using MediatR; +using Microsoft.AspNetCore.Identity; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IdentityService.Domain.Entities +{ + public class User : IdentityUser, IHasCreationTime, IHasDeletionTime, ISoftDelete, IDomainEvents, IHasModificationTime + { + [NotMapped] + public List domainEvents = []; + + /// + /// 用户昵称 + /// + public string NickName { get; private set; } = null!; + /// + /// 用户签名 + /// + public string Description { get; private set; } = ""; + /// + /// 地区 + /// + public string Region { get; private set; } = "未知地区"; + + /// + /// 用户在线状态 + /// 0(默认):不在线 + /// 1:在线 + /// + //public UserOnlineState OnlineStatus { get; private set; } + + + /// + /// 账户状态 + /// (0:未激活,1:正常,2:封禁) + /// + public UserState Status { get; private set; } + + /// + /// 用户头像链接 + /// + public string? Avatar { get; private set; } + + public DateTimeOffset CreationTime { get; private set; } = DateTime.Now; + + public DateTimeOffset? Deletion { get; private set; } + + public bool IsDeleted { get; private set; } + + public DateTimeOffset? ModificationTime { get; private set; } + + private User() { } + public User(string username, string nickName) + { + Id = NewId.NextGuid(); + UserName = username; + NickName = nickName; + } + + public void Ban(string reason) + { + if (this.Status == UserState.Banned) return; + this.Status = UserState.Banned; + + AddDomainEvent(new UserBannedDomainEvent(this.Id, reason)); + } + + public void Update(string? nickName, string? region, string? avatar, string? desc) + { + if (nickName != null) + { + if (nickName.Trim() == string.Empty) throw new DomainException("昵称不可为空"); + if (nickName.Length > 20) throw new DomainException("昵称不可大于20"); + this.NickName = nickName; + } + + if (region != null) + { + if (region.Trim() == string.Empty) throw new DomainException("地区不可为空"); + if (region.Length > 20) throw new DomainException("地区不可大于20"); + this.Region = region; + } + + if (avatar != null) + { + + this.Avatar = avatar; + } + + if (desc != null) + { + this.Description = desc; + } + + ModificationTime = DateTime.Now; + AddDomainEvent(new UserProfileUpdateDomainEvent(this)); + } + + public void SoftDelete() + { + this.IsDeleted = true; + } + + public IEnumerable GetDomainEvents() + { + return domainEvents; + } + + public void AddDomainEvent(INotification eventItem) + { + domainEvents.Add(eventItem); + } + + public void AddDomainEventIfAbsent(INotification eventItem) + { + if (!domainEvents.Contains(eventItem)) + { + domainEvents.Add(eventItem); + } + } + + public void ClearDomainEvents() + { + domainEvents.Clear(); + } + } +} diff --git a/User.Domain/Events/UserBannedDomainEvent.cs b/User.Domain/Events/UserBannedDomainEvent.cs new file mode 100644 index 0000000..9720e55 --- /dev/null +++ b/User.Domain/Events/UserBannedDomainEvent.cs @@ -0,0 +1,6 @@ +using MediatR; + +namespace IdentityService.Domain.Events +{ + public record UserBannedDomainEvent(Guid UserId, string Reason) : INotification; +} diff --git a/User.Domain/Events/UserProfileUpdateDomainEvent.cs b/User.Domain/Events/UserProfileUpdateDomainEvent.cs new file mode 100644 index 0000000..352e603 --- /dev/null +++ b/User.Domain/Events/UserProfileUpdateDomainEvent.cs @@ -0,0 +1,7 @@ +using IdentityService.Domain.Entities; +using MediatR; + +namespace IdentityService.Domain.Events +{ + public record UserProfileUpdateDomainEvent(User User) : INotification; +} diff --git a/User.Domain/IIdRepository.cs b/User.Domain/IIdRepository.cs new file mode 100644 index 0000000..03562f3 --- /dev/null +++ b/User.Domain/IIdRepository.cs @@ -0,0 +1,115 @@ +using IdentityService.Domain.Entities; +using IM.Commons; +using Microsoft.AspNetCore.Identity; + +namespace IdentityService.Domain +{ + public interface IIdRepository + { + /// + /// 通过用户名查找 + /// + /// 用户名 + /// + Task FindByUserNameAsync(string userName); + /// + /// 通过邮箱地址查找 + /// + /// 邮箱地址 + /// + Task FindByEmailAsync(string Email); + /// + /// 通过ID查询 + /// + /// + /// + Task FindByIdAsync(Guid id); + /// + /// 通过手机号查找 + /// + /// + /// + Task FindByPhoneAsync(string phone); + /// + /// 创建用户 + /// + /// + /// + /// + Task CreateAsync(User user, string password); + /// + /// 修改密码 + /// + /// + /// 原密码 + /// + Task ChangePasswordAsync(User user, string password); + /// + /// 记录一次失败登录 + /// + /// + /// + Task AccessFailedAsync(User user); + /// + /// 获取角色 + /// + /// + /// + Task> GetRolesAsync(User user); + /// + /// 添加角色 + /// + /// + /// + /// + Task AddRoleAsync(User user, string role); + /// + /// 为登录检查用户名密码 + /// + /// + /// + /// 是否记录登录失败 + /// + Task CheckForSignInAsync(User user, string password, bool lockoutOnFailure); + + /// + /// 删除用户 + /// + /// + /// + Task RemoveAsync(User user); + /// + /// 重置密码 + /// + /// + /// + + Task<(IdentityResult, User?, string password)> ResetPasswordAsync(User user); + /// + /// 检查用户名 + /// + /// + /// + Task CheckUsernameAsync(string username); + /// + /// 检查邮箱 + /// + /// + /// + Task CheckEmailAsync(string email); + /// + /// 检查手机号 + /// + /// + /// + Task CheckPhoneAsync(string phone); + /// + /// 批量获取用户信息 + /// + /// + /// + + Task> GetUsersAsync(ISpecification specification); + Task> GetUsersAsync(ISpecification specification); + } +} diff --git a/User.Domain/IdDomainService.cs b/User.Domain/IdDomainService.cs new file mode 100644 index 0000000..9dcc69a --- /dev/null +++ b/User.Domain/IdDomainService.cs @@ -0,0 +1,26 @@ +using IdentityService.Domain.Entities; +using IM.Commons; +using Microsoft.AspNetCore.Identity; + +namespace IdentityService.Domain +{ + public class IdDomainService + { + private readonly IIdRepository idRepository; + public IdDomainService(IIdRepository idRepository) + { + this.idRepository = idRepository; + } + + public async Task> CreateUserAsync(string userName, string password, string nickName) + { + SignInResult checkUsername = await idRepository.CheckUsernameAsync(userName); + if (!checkUsername.Succeeded) + { + return Result.Fail(ResultCode.USER_ALREADY_EXISTS); + } + return Result.Success(new User(userName, nickName)); + } + + } +} diff --git a/User.Domain/IdentityService.Domain.csproj b/User.Domain/IdentityService.Domain.csproj new file mode 100644 index 0000000..338b5ff --- /dev/null +++ b/User.Domain/IdentityService.Domain.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/User.Domain/UserOnlineState.cs b/User.Domain/UserOnlineState.cs new file mode 100644 index 0000000..876d024 --- /dev/null +++ b/User.Domain/UserOnlineState.cs @@ -0,0 +1,8 @@ +namespace IdentityService.Domain +{ + public enum UserOnlineState + { + Offline = 0, + Online = 1, + } +} diff --git a/User.Domain/UserState.cs b/User.Domain/UserState.cs new file mode 100644 index 0000000..5dcc0ea --- /dev/null +++ b/User.Domain/UserState.cs @@ -0,0 +1,10 @@ +namespace IdentityService.Domain +{ + public enum UserState + { + Inactive = 0, + Normal = 1, + Banned = 2 + + } +} diff --git a/User.Domain/ValueObjects/Email.cs b/User.Domain/ValueObjects/Email.cs new file mode 100644 index 0000000..3f24f30 --- /dev/null +++ b/User.Domain/ValueObjects/Email.cs @@ -0,0 +1,32 @@ +using System.Text.RegularExpressions; + +namespace IdentityService.Domain.ValueObjects +{ + public class Email + { + public string Value { get; } + public Email(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentNullException("邮箱地址不可为空"); + } + + string pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"; + if (!Regex.IsMatch(value, pattern)) + { + throw new ArgumentException("邮箱地址格式不符合要求"); + } + this.Value = value.ToLowerInvariant(); + } + public override string ToString() + { + return Value; + } + + public static implicit operator Email(string value) + { + return new Email(value); + } + } +} diff --git a/User.Domain/ValueObjects/Phone.cs b/User.Domain/ValueObjects/Phone.cs new file mode 100644 index 0000000..89a8a9a --- /dev/null +++ b/User.Domain/ValueObjects/Phone.cs @@ -0,0 +1,33 @@ +using System.Text.RegularExpressions; + +namespace IdentityService.Domain.ValueObjects +{ + public class Phone + { + public string Value { get; } + public Phone(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentNullException("手机号不可为空"); + } + + + string pattern = "^1[3-9]\\d{9}$"; + if (!Regex.IsMatch(value, pattern)) + { + throw new ArgumentException("手机号格式不符合要求"); + } + Value = value; + } + public override string ToString() + { + return Value; + } + + public static implicit operator Phone(string value) + { + return new Phone(value); + } + } +} diff --git a/User.Infrastructure/Configs/UserConfig.cs b/User.Infrastructure/Configs/UserConfig.cs new file mode 100644 index 0000000..4fe6683 --- /dev/null +++ b/User.Infrastructure/Configs/UserConfig.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace IdentityService.Infrastructure.Configs +{ + internal class UserConfig : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("users"); + } + } +} diff --git a/User.Infrastructure/GlobalUsing.cs b/User.Infrastructure/GlobalUsing.cs new file mode 100644 index 0000000..0d3bc9c --- /dev/null +++ b/User.Infrastructure/GlobalUsing.cs @@ -0,0 +1,6 @@ +global using IM.Infrastructure.Efcore; +global using MediatR; +global using Microsoft.EntityFrameworkCore; +global using System; +global using System.Collections.Generic; +global using System.Linq; \ No newline at end of file diff --git a/User.Infrastructure/IdReposity.cs b/User.Infrastructure/IdReposity.cs new file mode 100644 index 0000000..06e1db4 --- /dev/null +++ b/User.Infrastructure/IdReposity.cs @@ -0,0 +1,186 @@ +using IdentityService.Domain; +using IdentityService.Domain.Entities; +using IM.Commons; +using Microsoft.AspNetCore.Identity; + +namespace IdentityService.Infrastructure +{ + public class IdReposity(UserDbContext userDbContext, IdUserManager userManager, RoleManager roleManager) : IIdRepository + { + private readonly UserDbContext userDbContext = userDbContext; + private readonly IdUserManager userManager = userManager; + private readonly RoleManager roleManager = roleManager; + + public async Task AccessFailedAsync(User user) + { + return await userManager.AccessFailedAsync(user); + } + + public async Task AddRoleAsync(User user, string role) + { + return await userManager.AddToRoleAsync(user, role); + } + + public async Task ChangePasswordAsync(User user, string password) + { + if (password.Length < 6) + { + IdentityError error = new IdentityError(); + error.Code = "Password Failure"; + error.Description = "密码长度不能小于6"; + return IdentityResult.Failed(error); + } + string token = await userManager.GeneratePasswordResetTokenAsync(user); + return await userManager.ResetPasswordAsync(user, token, password); + } + + public async Task CheckForSignInAsync(User user, string password, bool lockoutOnFailure) + { + if (await userManager.IsLockedOutAsync(user)) + { + return SignInResult.LockedOut; + } + bool isSuccess = await userManager.CheckPasswordAsync(user, password); + if (!isSuccess) + { + await userManager.AccessFailedAsync(user); + return SignInResult.Failed; + } + else + { + return SignInResult.Success; + } + + } + + public async Task CreateAsync(User user, string password) + { + return await userManager.CreateAsync(user, password); + } + + public async Task FindByEmailAsync(string Email) + { + return await userManager.FindByEmailAsync(Email); + } + + public async Task FindByIdAsync(Guid id) + { + return await userManager.FindByIdAsync(id.ToString()); + } + + public async Task FindByPhoneAsync(string phone) + { + return await userManager.Users.FirstOrDefaultAsync(x => x.PhoneNumber == phone); + } + + public async Task FindByUserNameAsync(string userName) + { + return await userManager.FindByNameAsync(userName); + } + + public async Task> GetRolesAsync(User user) + { + return await userManager.GetRolesAsync(user); + } + + public async Task RemoveAsync(User user) + { + var userStore = userManager.UserLoginStore; + var cancelToken = default(CancellationToken); + + var logins = await userStore.GetLoginsAsync(user, cancelToken); + + foreach (var log in logins) + { + await userStore.RemoveLoginAsync(user, log.LoginProvider, log.ProviderKey, cancelToken); + } + user.SoftDelete(); + return await userManager.UpdateAsync(user); + } + + public Task<(IdentityResult, User?, string password)> ResetPasswordAsync(User user) + { + throw new NotImplementedException(); + } + + + public async Task CheckEmailAsync(string email) + { + bool isExist = await userManager.Users.AnyAsync(x => x.Email == email); + if (isExist) + { + return SignInResult.Failed; + } + else + { + return SignInResult.Success; + } + } + + + public async Task CheckPhoneAsync(string phone) + { + bool isExist = await userManager.Users.AnyAsync(x => x.PhoneNumber == phone); + if (isExist) + { + return SignInResult.Failed; + } + else + { + return SignInResult.Success; + } + } + + public async Task CheckUsernameAsync(string username) + { + var isExist = await userManager.Users.AnyAsync(x => x.UserName == username); + if (isExist) + { + return SignInResult.Failed; + } + else + { + return SignInResult.Success; + } + } + + public async Task> GetUsersAsync(ISpecification specification) + { + var query = userManager.Users.AsNoTracking(); + + if (specification.Criteria != null) + { + query = query.Where(specification.Criteria); + } + + if (specification.Includes != null && specification.Includes.Count() > 0) + { + foreach (var include in specification.Includes) + { + query = query.Include(include); + } + } + return await query.Select(specification.Select).ToListAsync(); + + } + public async Task> GetUsersAsync(ISpecification specification) + { + var query = userManager.Users.AsNoTracking(); + + if (specification.Criteria != null) + { + query = query.Where(specification.Criteria); + } + + if (specification.Includes != null && specification.Includes.Count() > 0) + { + foreach (var include in specification.Includes) + { + query = query.Include(include); + } + } + return await query.ToListAsync(); + + } + } +} diff --git a/User.Infrastructure/IdUserManager.cs b/User.Infrastructure/IdUserManager.cs new file mode 100644 index 0000000..c43520c --- /dev/null +++ b/User.Infrastructure/IdUserManager.cs @@ -0,0 +1,24 @@ +using IdentityService.Domain.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace IdentityService.Infrastructure +{ + public class IdUserManager : UserManager + { + public IdUserManager(IUserStore store, IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + { + + } + + public IUserLoginStore UserLoginStore + { + get + { + return (IUserLoginStore)this.Store; + } + } + + } +} diff --git a/User.Infrastructure/IdentityService.Infrastructure.csproj b/User.Infrastructure/IdentityService.Infrastructure.csproj new file mode 100644 index 0000000..a941962 --- /dev/null +++ b/User.Infrastructure/IdentityService.Infrastructure.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/User.Infrastructure/Migrations/20260410131653_InitialUserDb.Designer.cs b/User.Infrastructure/Migrations/20260410131653_InitialUserDb.Designer.cs new file mode 100644 index 0000000..c4298dc --- /dev/null +++ b/User.Infrastructure/Migrations/20260410131653_InitialUserDb.Designer.cs @@ -0,0 +1,303 @@ +// +using System; +using IdentityService.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace IdentityService.Infrastructure.Migrations +{ + [DbContext(typeof(UserDbContext))] + [Migration("20260410131653_InitialUserDb")] + partial class InitialUserDb + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("IdentityService.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("IdentityService.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Avatar") + .HasColumnType("longtext"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NickName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("Region") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("IdentityService.Domain.Entities.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("IdentityService.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("IdentityService.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("IdentityService.Domain.Entities.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityService.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("IdentityService.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/User.Infrastructure/Migrations/20260410131653_InitialUserDb.cs b/User.Infrastructure/Migrations/20260410131653_InitialUserDb.cs new file mode 100644 index 0000000..47ce528 --- /dev/null +++ b/User.Infrastructure/Migrations/20260410131653_InitialUserDb.cs @@ -0,0 +1,265 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IdentityService.Infrastructure.Migrations +{ + /// + public partial class InitialUserDb : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Name = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + NormalizedName = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + ConcurrencyStamp = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }) + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + NickName = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + Description = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + Region = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + Status = table.Column(type: "int", nullable: false), + Avatar = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + Deletion = table.Column(type: "datetime(6)", nullable: true), + IsDeleted = table.Column(type: "tinyint(1)", nullable: false), + UserName = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + NormalizedUserName = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + Email = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + NormalizedEmail = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + EmailConfirmed = table.Column(type: "tinyint(1)", nullable: false), + PasswordHash = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + SecurityStamp = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + ConcurrencyStamp = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + PhoneNumber = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + PhoneNumberConfirmed = table.Column(type: "tinyint(1)", nullable: false), + TwoFactorEnabled = table.Column(type: "tinyint(1)", nullable: false), + LockoutEnd = table.Column(type: "datetime(6)", nullable: true), + LockoutEnabled = table.Column(type: "tinyint(1)", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_users", x => x.Id); + }) + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + RoleId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ClaimType = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + ClaimValue = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ClaimType = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + ClaimValue = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + ProviderKey = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + ProviderDisplayName = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4"), + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci") + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + RoleId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci") + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + LoginProvider = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + Name = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:Charset", "utf8mb4"), + Value = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:Charset", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "users", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "users", + column: "NormalizedUserName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "users"); + } + } +} diff --git a/User.Infrastructure/Migrations/20260413090856_AddModifyTime.Designer.cs b/User.Infrastructure/Migrations/20260413090856_AddModifyTime.Designer.cs new file mode 100644 index 0000000..2e28ee1 --- /dev/null +++ b/User.Infrastructure/Migrations/20260413090856_AddModifyTime.Designer.cs @@ -0,0 +1,306 @@ +// +using System; +using IdentityService.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace IdentityService.Infrastructure.Migrations +{ + [DbContext(typeof(UserDbContext))] + [Migration("20260413090856_AddModifyTime")] + partial class AddModifyTime + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("IdentityService.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("IdentityService.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Avatar") + .HasColumnType("longtext"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("NickName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("Region") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("IdentityService.Domain.Entities.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("IdentityService.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("IdentityService.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("IdentityService.Domain.Entities.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityService.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("IdentityService.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/User.Infrastructure/Migrations/20260413090856_AddModifyTime.cs b/User.Infrastructure/Migrations/20260413090856_AddModifyTime.cs new file mode 100644 index 0000000..1247670 --- /dev/null +++ b/User.Infrastructure/Migrations/20260413090856_AddModifyTime.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IdentityService.Infrastructure.Migrations +{ + /// + public partial class AddModifyTime : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ModificationTime", + table: "users", + type: "datetime(6)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ModificationTime", + table: "users"); + } + } +} diff --git a/User.Infrastructure/Migrations/UserDbContextModelSnapshot.cs b/User.Infrastructure/Migrations/UserDbContextModelSnapshot.cs new file mode 100644 index 0000000..3e3b63c --- /dev/null +++ b/User.Infrastructure/Migrations/UserDbContextModelSnapshot.cs @@ -0,0 +1,303 @@ +// +using System; +using IdentityService.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace IdentityService.Infrastructure.Migrations +{ + [DbContext(typeof(UserDbContext))] + partial class UserDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("IdentityService.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("IdentityService.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Avatar") + .HasColumnType("longtext"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Deletion") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("ModificationTime") + .HasColumnType("datetime(6)"); + + b.Property("NickName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("Region") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("IdentityService.Domain.Entities.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("IdentityService.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("IdentityService.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("IdentityService.Domain.Entities.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityService.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("IdentityService.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/User.Infrastructure/ModuleInit.cs b/User.Infrastructure/ModuleInit.cs new file mode 100644 index 0000000..0991687 --- /dev/null +++ b/User.Infrastructure/ModuleInit.cs @@ -0,0 +1,15 @@ +using IdentityService.Domain; +using IM.Commons; +using Microsoft.Extensions.DependencyInjection; + +namespace IdentityService.Infrastructure +{ + public class ModuleInit : IModuleInitializer + { + public void Initialize(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } + } +} diff --git a/User.Infrastructure/UserDbContext.cs b/User.Infrastructure/UserDbContext.cs new file mode 100644 index 0000000..2d4809a --- /dev/null +++ b/User.Infrastructure/UserDbContext.cs @@ -0,0 +1,33 @@ +using IdentityService.Domain.Entities; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; + +namespace IdentityService.Infrastructure +{ + public class UserDbContext : IdentityDbContext + { + private IMediator? mediator; + public DbSet Users { get; private set; } + + public UserDbContext(DbContextOptions options, IMediator mediator) + : base(options) + { + this.mediator = mediator; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); + modelBuilder.EnableSoftDeletionGlobalFilter(); + } + + public async override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) + { + if (mediator != null) + { + await mediator.DispatchDomainEventsAsync(this); + } + return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + } + } +} diff --git a/User.WebApi/Applications/Auth/AuthMappingProfile.cs b/User.WebApi/Applications/Auth/AuthMappingProfile.cs new file mode 100644 index 0000000..6817384 --- /dev/null +++ b/User.WebApi/Applications/Auth/AuthMappingProfile.cs @@ -0,0 +1,24 @@ +using AutoMapper; +using IdentityService.WebApi.Applications.Dtos.Common; + +namespace IdentityService.WebApi.Applications.Auth +{ + public class AuthMappingProfile : Profile + { + public AuthMappingProfile() + { + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.UserName)) + .ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email)) + .ForMember(dest => dest.Phone, opt => opt.MapFrom(src => src.PhoneNumber)) + .ForMember(dest => dest.Region, opt => opt.MapFrom(src => src.Region)) + .ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description)) + .ForMember(dest => dest.Avatar, opt => opt.MapFrom(src => src.Avatar)) + .ForMember(dest => dest.CreationTime, opt => opt.MapFrom(src => src.CreationTime)) + .ForMember(dest => dest.Deletion, opt => opt.MapFrom(src => src.Deletion)) + .ForMember(dest => dest.NickName, opt => opt.MapFrom(src => src.NickName)) + ; + } + } +} diff --git a/User.WebApi/Applications/Auth/AuthService.cs b/User.WebApi/Applications/Auth/AuthService.cs new file mode 100644 index 0000000..6f6a84f --- /dev/null +++ b/User.WebApi/Applications/Auth/AuthService.cs @@ -0,0 +1,97 @@ +using AutoMapper; +using IdentityService.Domain; +using IdentityService.WebApi.Applications.Dtos; +using IdentityService.WebApi.Applications.Dtos.Common; +using IM.Commons; +using IM.Jwt; +using Microsoft.Extensions.Options; +using System.Security.Claims; + +namespace IdentityService.WebApi.Applications.Auth +{ + public class AuthService + { + private readonly ITokenService tokenService; + private readonly IIdRepository idRepository; + private readonly IOptions jwtOptions; + private readonly IdDomainService idDomainService; + private readonly IMapper mapper; + + public AuthService(ITokenService tokenService, IIdRepository idRepository, + IOptions options, IdDomainService idDomainService, + IMapper mapper + ) + { + this.tokenService = tokenService; + this.idRepository = idRepository; + jwtOptions = options; + this.idDomainService = idDomainService; + this.mapper = mapper; + } + + public async Task> LoginAsync(string username, string password) + { + var user = await idRepository.FindByUserNameAsync(username); + + if (user is null) + { + return Result.Fail(ResultCode.USER_NOT_FOUND); + } + var idResult = await idRepository.CheckForSignInAsync(user, password, true); + if (!idResult.Succeeded) + { + return Result.Fail(ResultCode.PASSWORD_ERROR); + } + var token = await BuildTokenAsync(user); + var refreshToken = await tokenService.CreateRefreshTokenAsync(user.Id); + return Result.Success(new LoginResponse(user.Id, token, refreshToken, null)); + } + public async Task> RegisterAsync(string userName, string password, string nickName) + { + var userResult = await idDomainService.CreateUserAsync(userName, password, nickName); + if (!userResult.Succeeded) + return Result.Fail(userResult); + + var idResult = await idRepository.CreateAsync(userResult.Data!, password); + if (!idResult.Succeeded) + { + var msg = idResult.Errors.FirstOrDefault(); + return Result.Fail( + ResultCode.REGISTER_ERROR, + msg?.Description ?? ResultCode.REGISTER_ERROR.GetDescription()); + } + + return Result.Success(mapper.Map(userResult.Data)); + } + public async Task> RefreshAsync(string refreshToken) + { + var validateRes = await tokenService.ValidateRefreshTokenAsync(refreshToken); + if (!validateRes.ok) + { + return Result.Fail(ResultCode.AUTH_FAILED); + } + + var user = await idRepository.FindByIdAsync(validateRes.userId); + + if (user is null) + { + return Result.Fail(ResultCode.USER_NOT_FOUND); + } + + var token = await BuildTokenAsync(user); + + return Result.Success(new LoginResponse(user.Id, token, refreshToken, null)); + } + private async Task BuildTokenAsync(Domain.Entities.User user) + { + var roles = await idRepository.GetRolesAsync(user); + List claims = new List(); + claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())); + foreach (string role in roles) + { + claims.Add(new Claim(ClaimTypes.Role, role)); + } + return tokenService.GetToken(claims, jwtOptions.Value); + } + } +} diff --git a/User.WebApi/Applications/Dtos/Common/UserResponse.cs b/User.WebApi/Applications/Dtos/Common/UserResponse.cs new file mode 100644 index 0000000..540e29c --- /dev/null +++ b/User.WebApi/Applications/Dtos/Common/UserResponse.cs @@ -0,0 +1,16 @@ +namespace IdentityService.WebApi.Applications.Dtos.Common +{ + public class UserResponse + { + public Guid Id { get; set; } + public string UserName { get; set; } + public string NickName { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + public string Region { get; set; } + public string Description { get; set; } + public string? Avatar { get; set; } + public DateTimeOffset CreationTime { get; set; } + public DateTimeOffset? Deletion { get; set; } + } +} diff --git a/User.WebApi/Applications/Dtos/LoginResponse.cs b/User.WebApi/Applications/Dtos/LoginResponse.cs new file mode 100644 index 0000000..1a88552 --- /dev/null +++ b/User.WebApi/Applications/Dtos/LoginResponse.cs @@ -0,0 +1,18 @@ +namespace IdentityService.WebApi.Applications.Dtos +{ + public class LoginResponse + { + public Guid UserId { get; init; } + public string Token { get; init; } + public string RefreshToken { get; init; } + public DateTime? Expired { get; init; } + + public LoginResponse(Guid userId, string token, string refreshToken, DateTime? expired) + { + UserId = userId; + Token = token; + RefreshToken = refreshToken; + Expired = expired; + } + } +} diff --git a/User.WebApi/Applications/EventHandler/UserProfileUpdateHandler.cs b/User.WebApi/Applications/EventHandler/UserProfileUpdateHandler.cs new file mode 100644 index 0000000..09c0a90 --- /dev/null +++ b/User.WebApi/Applications/EventHandler/UserProfileUpdateHandler.cs @@ -0,0 +1,32 @@ +using IdentityService.Domain.Events; +using IM.Commons.IntegrationEvents; +using MassTransit; +using MediatR; + +namespace IdentityService.WebApi.Applications.EventHandler +{ + public class UserProfileUpdateHandler : INotificationHandler + { + private readonly IPublishEndpoint endpoint; + + public UserProfileUpdateHandler(IPublishEndpoint endpoint) + { + this.endpoint = endpoint; + } + + public async Task Handle(UserProfileUpdateDomainEvent notification, CancellationToken cancellationToken) + { + await endpoint.Publish(new UserProfileUpdateEvent + { + CorrelationId = notification.User.Id, + Avatar = notification.User.Avatar, + Description = notification.User.Description, + Email = notification.User.Email, + NickName = notification.User.NickName, + Phone = notification.User.PhoneNumber, + Status = notification.User.Status.ToString(), + UserId = notification.User.Id + }); + } + } +} diff --git a/User.WebApi/Applications/User/UserResponseFindSpecification.cs b/User.WebApi/Applications/User/UserResponseFindSpecification.cs new file mode 100644 index 0000000..0e52185 --- /dev/null +++ b/User.WebApi/Applications/User/UserResponseFindSpecification.cs @@ -0,0 +1,35 @@ +using AutoMapper; +using IdentityService.WebApi.Applications.Dtos.Common; +using IM.Commons; +using System.Linq.Expressions; + +namespace IdentityService.WebApi.Applications.User +{ + public class UserResponseFindSpecification : ISpecification + { + private readonly IEnumerable Ids; + public UserResponseFindSpecification(IEnumerable ids, IMapper mapper) + { + Ids = ids; + Criteria = u => Ids.Contains(u.Id); + Select = u => new UserResponse + { + Avatar = u.Avatar, + CreationTime = u.CreationTime, + Deletion = u.Deletion, + Description = u.Description, + Email = u.Email, + Id = u.Id, + Phone = u.PhoneNumber, + Region = u.Region, + UserName = u.UserName + }; + } + + public Expression> Criteria { get; } + + public List>> Includes { get; } + + public Expression> Select { get; } + } +} diff --git a/User.WebApi/Applications/User/UserService.cs b/User.WebApi/Applications/User/UserService.cs new file mode 100644 index 0000000..7655006 --- /dev/null +++ b/User.WebApi/Applications/User/UserService.cs @@ -0,0 +1,54 @@ +using AutoMapper; +using IdentityService.Domain; +using IdentityService.Infrastructure; +using IdentityService.WebApi.Applications.Dtos.Common; +using IM.Commons; + +namespace IdentityService.WebApi.Applications.User +{ + public class UserService + { + private readonly IIdRepository repository; + private readonly IMapper mapper; + private readonly UserDbContext userDb; + + public UserService(IIdRepository repository, IMapper mapper, + UserDbContext userDbContext + ) + { + this.repository = repository; + this.mapper = mapper; + userDb = userDbContext; + } + + public async Task> GetUserInfoAsync(Guid userId) + { + var user = await repository.FindByIdAsync(userId); + if (user is null) + { + return Result.Fail(ResultCode.USER_NOT_FOUND); + } + + return Result.Success(mapper.Map(user)); + } + + public async Task> UpdateAsync(UserUpdateCommand command) + { + var user = await repository.FindByIdAsync(command.UserId); + if (user is null) + { + return Result.Fail(ResultCode.USER_NOT_FOUND); + } + user.Update(command.NickName, command.Region, command.Avatar, command.Description); + + return Result.Success(mapper.Map(user)); + } + + public async Task>> GetUsersByIdsAsync(IEnumerable ids) + { + var specification = new UserResponseFindSpecification(ids, mapper); + var users = await repository.GetUsersAsync(specification); + return Result>.Success([.. users]); + } + } +} diff --git a/User.WebApi/Applications/User/UserUpdateCommand.cs b/User.WebApi/Applications/User/UserUpdateCommand.cs new file mode 100644 index 0000000..07f2b53 --- /dev/null +++ b/User.WebApi/Applications/User/UserUpdateCommand.cs @@ -0,0 +1,20 @@ +namespace IdentityService.WebApi.Applications.User +{ + public class UserUpdateCommand + { + public Guid UserId { get; private set; } + public string? NickName { get; private set; } + public string? Region { get; private set; } + public string? Avatar { get; private set; } + public string? Description { get; private set; } + + public UserUpdateCommand(Guid userId, string? nickName, string? region, string? avatar, string? description) + { + UserId = userId; + NickName = nickName; + Region = region; + Avatar = avatar; + Description = description; + } + } +} diff --git a/User.WebApi/Controllers/Auth/AuthController.cs b/User.WebApi/Controllers/Auth/AuthController.cs new file mode 100644 index 0000000..68ca0cc --- /dev/null +++ b/User.WebApi/Controllers/Auth/AuthController.cs @@ -0,0 +1,44 @@ +using IdentityService.Domain; +using IdentityService.WebApi.Applications.Auth; +using IdentityService.WebApi.Applications.Dtos; +using IdentityService.WebApi.Applications.Dtos.Common; +using IM.Commons; +using Microsoft.AspNetCore.Mvc; + +namespace IdentityService.WebApi.Controllers.Auth +{ + [Route("/api/[controller]/[action]")] + [ApiController] + public class AuthController : ControllerBase + { + private readonly AuthService authService; + private readonly IIdRepository idRepository; + + public AuthController(AuthService authService, IIdRepository idRepository) + { + this.authService = authService; + this.idRepository = idRepository; + } + + [HttpPost] + [ProducesDefaultResponseType(typeof(Result))] + public async Task Login([FromBody] LoginRequest loginRequest) + { + return Ok(await authService.LoginAsync(loginRequest.UserName, loginRequest.Password)); + } + + [HttpPost] + [ProducesDefaultResponseType(typeof(Result))] + public async Task Register([FromBody] RegisterRequest registerRequest) + { + return Ok(await authService.RegisterAsync(registerRequest.UserName, registerRequest.Password, registerRequest.NickName)); + } + + [HttpPost] + [ProducesDefaultResponseType(typeof(Result))] + public async Task Refresh([FromBody] RefreshRequest refreshRequest) + { + return Ok(await authService.RefreshAsync(refreshRequest.RefreshToken)); + } + } +} diff --git a/User.WebApi/Controllers/Auth/LoginRequest.cs b/User.WebApi/Controllers/Auth/LoginRequest.cs new file mode 100644 index 0000000..f52b633 --- /dev/null +++ b/User.WebApi/Controllers/Auth/LoginRequest.cs @@ -0,0 +1,28 @@ +using FluentValidation; + +namespace IdentityService.WebApi.Controllers.Auth +{ + public class LoginRequest + { + public string UserName { get; set; } + public string Password { get; set; } + } + + public class LoginRequestValidator : AbstractValidator + { + public LoginRequestValidator() + { + RuleFor(x => x.UserName) + .NotEmpty() + .NotNull() + .MaximumLength(20) + .MinimumLength(5); + + RuleFor(x => x.Password) + .NotEmpty() + .NotNull() + .MaximumLength(50) + ; + } + } +} diff --git a/User.WebApi/Controllers/Auth/RefreshRequest.cs b/User.WebApi/Controllers/Auth/RefreshRequest.cs new file mode 100644 index 0000000..2926cd8 --- /dev/null +++ b/User.WebApi/Controllers/Auth/RefreshRequest.cs @@ -0,0 +1,20 @@ +using FluentValidation; + +namespace IdentityService.WebApi.Controllers.Auth +{ + public class RefreshRequest + { + public string RefreshToken { get; set; } + } + + public class RefreshTokenValidator : AbstractValidator + { + public RefreshTokenValidator() + { + RuleFor(r => r.RefreshToken) + .NotEmpty() + .NotNull() + ; + } + } +} diff --git a/User.WebApi/Controllers/Auth/RegisterRequest.cs b/User.WebApi/Controllers/Auth/RegisterRequest.cs new file mode 100644 index 0000000..5007633 --- /dev/null +++ b/User.WebApi/Controllers/Auth/RegisterRequest.cs @@ -0,0 +1,33 @@ +using FluentValidation; + +namespace IdentityService.WebApi.Controllers.Auth +{ + public class RegisterRequest + { + public string UserName { get; set; } + public string Password { get; set; } + public string NickName { get; set; } + } + public class RegisterRequestValidator : AbstractValidator + { + public RegisterRequestValidator() + { + RuleFor(r => r.UserName) + .NotEmpty() + .NotNull() + .MaximumLength(20) + .MinimumLength(5); + + RuleFor(r => r.Password) + .NotEmpty() + .NotNull() + .MinimumLength(6) + .MaximumLength(50); + + RuleFor(r => r.NickName) + .NotEmpty() + .NotNull() + .MaximumLength(50); + } + } +} diff --git a/User.WebApi/Controllers/User/UserController.cs b/User.WebApi/Controllers/User/UserController.cs new file mode 100644 index 0000000..11027e4 --- /dev/null +++ b/User.WebApi/Controllers/User/UserController.cs @@ -0,0 +1,56 @@ +using IdentityService.Infrastructure; +using IdentityService.WebApi.Applications.Dtos.Common; +using IdentityService.WebApi.Applications.User; +using IM.ASPNETCore; +using IM.Commons; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace IdentityService.WebApi.Controllers.User +{ + [Authorize] + [UnitOfWork(typeof(UserDbContext))] + [Route("api/[controller]/[action]")] + [ApiController] + public class UserController : ControllerBase + { + private readonly UserService userService; + public UserController(UserService userService) + { + this.userService = userService; + } + + [HttpGet] + [ProducesDefaultResponseType(typeof(Result))] + public async Task Me() + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var res = await userService.GetUserInfoAsync(Guid.Parse(userId)); + return Ok(res); + } + + [HttpGet] + [ProducesDefaultResponseType(typeof(Result))] + public async Task Find(Guid userId) + { + return Ok(await userService.GetUserInfoAsync(userId)); + } + + [HttpPost] + [ProducesDefaultResponseType(typeof(Result))] + public async Task Update([FromBody] UserUpdateRequest request) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var res = await userService.UpdateAsync(new UserUpdateCommand(Guid.Parse(userId), request.NickName, request.Region, request.Avatar, request.Description)); + return Ok(res); + } + + [HttpPost] + [ProducesDefaultResponseType(typeof(Result>))] + public async Task GetUsersByIds([FromBody] List ids) + { + return Ok(await userService.GetUsersByIdsAsync(ids)); + } + } +} diff --git a/User.WebApi/Controllers/User/UserUpdateRequest.cs b/User.WebApi/Controllers/User/UserUpdateRequest.cs new file mode 100644 index 0000000..4ca9b33 --- /dev/null +++ b/User.WebApi/Controllers/User/UserUpdateRequest.cs @@ -0,0 +1,19 @@ +using FluentValidation; + +namespace IdentityService.WebApi.Controllers.User +{ + public class UserUpdateRequest + { + public string? NickName { get; set; } + public string? Region { get; set; } + public string? Avatar { get; set; } + public string? Description { get; set; } + } + + public class UserUpdateRequestValidator : AbstractValidator + { + public UserUpdateRequestValidator() + { + } + } +} diff --git a/User.WebApi/DesignTimeDbContextFactory.cs b/User.WebApi/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..abf6d8a --- /dev/null +++ b/User.WebApi/DesignTimeDbContextFactory.cs @@ -0,0 +1,18 @@ +using IdentityService.Infrastructure; +using IM.InitCommon; +using Microsoft.EntityFrameworkCore.Design; + +namespace IdentityService.WebApi +{ + public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory + { + public UserDbContext CreateDbContext(string[] args) + { + // 1. 复用你写好的配置工厂,提取连接字符串 + var optionsBuilder = DbContextOptionsBuilderFactory.Create(); + + // 2. 🌟 关键补刀:把假的 Mediator 传进去,满足构造函数的要求! + return new UserDbContext(optionsBuilder.Options, null); + } + } +} diff --git a/User.WebApi/IdentityService.WebApi.csproj b/User.WebApi/IdentityService.WebApi.csproj new file mode 100644 index 0000000..bf5df9c --- /dev/null +++ b/User.WebApi/IdentityService.WebApi.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/User.WebApi/ModuleInit.cs b/User.WebApi/ModuleInit.cs new file mode 100644 index 0000000..cfafc40 --- /dev/null +++ b/User.WebApi/ModuleInit.cs @@ -0,0 +1,15 @@ +using IdentityService.WebApi.Applications.Auth; +using IdentityService.WebApi.Applications.User; +using IM.Commons; + +namespace IdentityService.WebApi +{ + public class ModuleInit : IModuleInitializer + { + public void Initialize(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } + } +} diff --git a/User.WebApi/Program.cs b/User.WebApi/Program.cs new file mode 100644 index 0000000..4e6234e --- /dev/null +++ b/User.WebApi/Program.cs @@ -0,0 +1,57 @@ + +using IdentityService.Domain.Entities; +using IdentityService.Infrastructure; +using IM.InitCommon; +using Microsoft.AspNetCore.Identity; + +namespace IdentityService.WebApi +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + + builder.ConfigureDbConfiguration(); + builder.ConfigExtraServices(); + // 2. 🌟 微软 Identity 终极注册组合拳 + builder.Services.AddIdentityCore(options => + { + // 这里可以顺手配置一下密码规则,比如不需要大写字母、最小长度等 + options.Password.RequireLowercase = false; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + options.Password.RequiredLength = 6; + }) + .AddRoles() + .AddEntityFrameworkStores() // 👈 灵魂所在:自动向 DI 注入 IUserStore 等几十个底层接口! + .AddUserManager() // 👈 告诉框架:不要用你默认的 UserManager,用我自定义的 IdUserManager! + .AddRoleManager>() + .AddDefaultTokenProviders(); // 👈 顺带注册生成验证码/重置密码Token的服务 + + + builder.Services.AddAllGrpcServer(); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseAppDefault(); + + app.MapControllers(); + app.MapAllGrpcServer(); + + app.Run(); + } + } +} diff --git a/User.WebApi/Properties/launchSettings.json b/User.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..5f162e6 --- /dev/null +++ b/User.WebApi/Properties/launchSettings.json @@ -0,0 +1,49 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5176" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7210;http://localhost:5176" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:8830", + "sslPort": 44347 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iissettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:53561/", + "sslPort": 44384 + } + } +} \ No newline at end of file diff --git a/User.WebApi/Services/UserService.cs b/User.WebApi/Services/UserService.cs new file mode 100644 index 0000000..0c65e96 --- /dev/null +++ b/User.WebApi/Services/UserService.cs @@ -0,0 +1,39 @@ +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using IdentityService.WebApi.Applications.User; +using IM.Protocols.Grpc.User; + +namespace IdentityService.WebApi.Services +{ + public class UserService:UserInternal.UserInternalBase + { + private readonly Applications.User.UserService service; + + public UserService(Applications.User.UserService service) + { + this.service = service; + } + public override async Task GetUserInfoAsync(GetUserInfoRequest request, ServerCallContext context) + { + var res = await service.GetUserInfoAsync(Guid.Parse(request.UserId)); + if (!res.Succeeded) + { + throw new RpcException(new Status(StatusCode.NotFound, res.Message)); + } + + return new UserResponse() + { + Avatar = res.Data.Avatar ?? "", + CreationTime = res.Data.CreationTime.ToUniversalTime().ToTimestamp(), + Deletion = res.Data.Deletion is null ? DateTime.MinValue.ToUniversalTime().ToTimestamp() : res.Data.Deletion.Value.ToUniversalTime().ToTimestamp(), + Description = res.Data.Description, + Email = res.Data.Email ?? "", + Id = res.Data.Id.ToString(), + NickName = res.Data.NickName, + Phone = res.Data.Phone ?? "", + Region = res.Data.Region, + UserName = res.Data.UserName + }; + } + } +} diff --git a/User.WebApi/User.WebApi.http b/User.WebApi/User.WebApi.http new file mode 100644 index 0000000..394bf68 --- /dev/null +++ b/User.WebApi/User.WebApi.http @@ -0,0 +1,6 @@ +@User.WebApi_HostAddress = http://localhost:5176 + +GET {{User.WebApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/User.WebApi/appsettings.Development.json b/User.WebApi/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/User.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/User.WebApi/appsettings.json b/User.WebApi/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/User.WebApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}