diff --git a/backend/IMTest/bin/Debug/net8.0/IMTest.dll b/backend/IMTest/bin/Debug/net8.0/IMTest.dll index c856d72..4c5461c 100644 Binary files a/backend/IMTest/bin/Debug/net8.0/IMTest.dll and b/backend/IMTest/bin/Debug/net8.0/IMTest.dll differ diff --git a/backend/IMTest/bin/Debug/net8.0/IMTest.pdb b/backend/IMTest/bin/Debug/net8.0/IMTest.pdb index d7f40fa..a90a420 100644 Binary files a/backend/IMTest/bin/Debug/net8.0/IMTest.pdb and b/backend/IMTest/bin/Debug/net8.0/IMTest.pdb differ diff --git a/backend/IMTest/bin/Debug/net8.0/IM_API.dll b/backend/IMTest/bin/Debug/net8.0/IM_API.dll index 902d7d7..16a00ed 100644 Binary files a/backend/IMTest/bin/Debug/net8.0/IM_API.dll and b/backend/IMTest/bin/Debug/net8.0/IM_API.dll differ diff --git a/backend/IMTest/bin/Debug/net8.0/IM_API.exe b/backend/IMTest/bin/Debug/net8.0/IM_API.exe index b68e34c..fd67847 100644 Binary files a/backend/IMTest/bin/Debug/net8.0/IM_API.exe and b/backend/IMTest/bin/Debug/net8.0/IM_API.exe differ diff --git a/backend/IMTest/bin/Debug/net8.0/IM_API.pdb b/backend/IMTest/bin/Debug/net8.0/IM_API.pdb index c8d340a..6fb8b01 100644 Binary files a/backend/IMTest/bin/Debug/net8.0/IM_API.pdb and b/backend/IMTest/bin/Debug/net8.0/IM_API.pdb differ diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfo.cs b/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfo.cs index 821e8d2..ff595c8 100644 --- a/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfo.cs +++ b/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfo.cs @@ -14,7 +14,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("IMTest")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f42e990c45b8536af0a4b37e4e12a46c917ca02c")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+5274f0d22d4fe646d03a3bc0ea6621d299074816")] [assembly: System.Reflection.AssemblyProductAttribute("IMTest")] [assembly: System.Reflection.AssemblyTitleAttribute("IMTest")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfoInputs.cache b/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfoInputs.cache index dfa3b85..815557e 100644 --- a/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfoInputs.cache +++ b/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfoInputs.cache @@ -1 +1 @@ -0157672e490e31c525582995ff9972f067909b74fb5aeca923e211efe4aa3af8 +7390b702e3c578dad3a8fa4fa4cc93b25ccd34a9b353beca60372a7182717d73 diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.AssemblyReference.cache b/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.AssemblyReference.cache index 8398886..a6386cf 100644 Binary files a/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.AssemblyReference.cache and b/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.AssemblyReference.cache differ diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.dll b/backend/IMTest/obj/Debug/net8.0/IMTest.dll index c856d72..4c5461c 100644 Binary files a/backend/IMTest/obj/Debug/net8.0/IMTest.dll and b/backend/IMTest/obj/Debug/net8.0/IMTest.dll differ diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.pdb b/backend/IMTest/obj/Debug/net8.0/IMTest.pdb index d7f40fa..a90a420 100644 Binary files a/backend/IMTest/obj/Debug/net8.0/IMTest.pdb and b/backend/IMTest/obj/Debug/net8.0/IMTest.pdb differ diff --git a/backend/IMTest/obj/Debug/net8.0/ref/IMTest.dll b/backend/IMTest/obj/Debug/net8.0/ref/IMTest.dll index b4d29fa..5f6f5ea 100644 Binary files a/backend/IMTest/obj/Debug/net8.0/ref/IMTest.dll and b/backend/IMTest/obj/Debug/net8.0/ref/IMTest.dll differ diff --git a/backend/IMTest/obj/Debug/net8.0/refint/IMTest.dll b/backend/IMTest/obj/Debug/net8.0/refint/IMTest.dll index b4d29fa..5f6f5ea 100644 Binary files a/backend/IMTest/obj/Debug/net8.0/refint/IMTest.dll and b/backend/IMTest/obj/Debug/net8.0/refint/IMTest.dll differ diff --git a/backend/IM_API/Application/EventHandlers/ConversationEventHandler.cs b/backend/IM_API/Application/EventHandlers/ConversationEventHandler.cs new file mode 100644 index 0000000..057e5e5 --- /dev/null +++ b/backend/IM_API/Application/EventHandlers/ConversationEventHandler.cs @@ -0,0 +1,88 @@ +using AutoMapper; +using IM_API.Application.Interfaces; +using IM_API.Domain.Events; +using IM_API.Dtos; +using IM_API.Exceptions; +using IM_API.Interface.Services; +using IM_API.Models; +using IM_API.Services; +using IM_API.Tools; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; + +namespace IM_API.Application.EventHandlers +{ + public class ConversationEventHandler : IEventHandler + { + private readonly IConversationService _conversationService; + private readonly ILogger _logger; + private readonly ImContext _context; + private readonly IMapper _mapper; + public ConversationEventHandler( + IConversationService conversationService, + ILogger logger, + ImContext imContext, + IMapper mapper + ) + { + _conversationService = conversationService; + _logger = logger; + _context = imContext; + _mapper = mapper; + } + + /* + * 此方法有并发问题,当双方同时第一次发送消息时, + * 会出现同时创建的情况,其中一方会报错, + * 导致也未走到更新逻辑,会话丢失 + */ + public async Task Handle(MessageCreatedEvent @event) + { + //此处仅处理私聊会话创建 + if (@event.ChatType == ChatType.GROUP) + { + return; + } + var conversation = await _context.Conversations.FirstOrDefaultAsync( + x => x.UserId == @event.MsgSenderId && x.TargetId == @event.MsgRecipientId + ); + //如果首次发消息则创建双方会话 + if (conversation is null) + { + Conversation senderCon = _mapper.Map(@event); + Conversation ReceptCon = _mapper.Map(@event); + ReceptCon.UserId = @event.MsgRecipientId; + ReceptCon.TargetId = @event.MsgSenderId; + ReceptCon.UnreadCount += 1; + ReceptCon.LastReadMessageId = null; + _context.Conversations.AddRange(senderCon,ReceptCon); + await _context.SaveChangesAsync(); + } + else + { + Conversation senderCon = conversation; + Conversation? ReceptCon = await _context.Conversations.FirstOrDefaultAsync( + x => x.UserId == @event.MsgRecipientId && x.TargetId == @event.MsgSenderId); + if (ReceptCon is null) + { + _logger.LogError("ConversationEventHandlerError:接收者会话对象缺失!Event:{Event}", JsonConvert.SerializeObject(@event)); + throw new BaseException(CodeDefine.SYSTEM_ERROR); + } + + //更新发送者conversation + senderCon.UnreadCount = 0; + senderCon.LastReadMessageId = @event.MessageId; + senderCon.LastMessage = @event.MessageContent; + senderCon.LastMessageTime = DateTime.Now; + //更新接收者conversation + ReceptCon.UnreadCount += 1; + ReceptCon.LastMessage = @event.MessageContent; + senderCon.LastMessageTime = DateTime.Now; + _context.Conversations.UpdateRange(senderCon, ReceptCon); + await _context.SaveChangesAsync(); + + + } + } + } +} diff --git a/backend/IM_API/Application/EventHandlers/SignalREventHandler.cs b/backend/IM_API/Application/EventHandlers/SignalREventHandler.cs new file mode 100644 index 0000000..970336f --- /dev/null +++ b/backend/IM_API/Application/EventHandlers/SignalREventHandler.cs @@ -0,0 +1,23 @@ +using IM_API.Application.Interfaces; +using IM_API.Domain.Events; +using IM_API.Hubs; +using IM_API.Tools; +using Microsoft.AspNetCore.SignalR; + +namespace IM_API.Application.EventHandlers +{ + public class SignalREventHandler : IEventHandler + { + private readonly IHubContext _hub; + public SignalREventHandler(IHubContext hub) + { + _hub = hub; + } + + public async Task Handle(MessageCreatedEvent @event) + { + var streamKey = @event.StreamKey; + await _hub.Clients.Group(streamKey).SendAsync(SignalRMethodDefine.ReceiveMessage, @event); + } + } +} diff --git a/backend/IM_API/Application/Interfaces/IEventBus.cs b/backend/IM_API/Application/Interfaces/IEventBus.cs new file mode 100644 index 0000000..a2d67eb --- /dev/null +++ b/backend/IM_API/Application/Interfaces/IEventBus.cs @@ -0,0 +1,9 @@ +using IM_API.Domain.Interfaces; + +namespace IM_API.Application.Interfaces +{ + public interface IEventBus + { + Task PublishAsync(TEvent @event) where TEvent : IEvent; + } +} diff --git a/backend/IM_API/Application/Interfaces/IEventHandler.cs b/backend/IM_API/Application/Interfaces/IEventHandler.cs new file mode 100644 index 0000000..7c6eb47 --- /dev/null +++ b/backend/IM_API/Application/Interfaces/IEventHandler.cs @@ -0,0 +1,9 @@ +using IM_API.Domain.Interfaces; + +namespace IM_API.Application.Interfaces +{ + public interface IEventHandler where TEvent : IEvent + { + Task Handle(TEvent @event); + } +} diff --git a/backend/IM_API/Configs/MapperConfig.cs b/backend/IM_API/Configs/MapperConfig.cs index 6a41699..0ec907d 100644 --- a/backend/IM_API/Configs/MapperConfig.cs +++ b/backend/IM_API/Configs/MapperConfig.cs @@ -1,4 +1,5 @@ using AutoMapper; +using IM_API.Domain.Events; using IM_API.Dtos; using IM_API.Models; @@ -59,11 +60,46 @@ namespace IM_API.Configs .ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.TimeStamp)) .ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.Content)) .ForMember(dest => dest.Recipient, opt => opt.MapFrom(src => src.ReceiverId)) + .ForMember(dest => dest.StreamKey, opt => opt.Ignore() ) .ForMember(dest => dest.StateEnum, opt => opt.MapFrom(src => MessageState.Sent)) .ForMember(dest => dest.ChatType, opt => opt.Ignore()) .ForMember(dest => dest.MsgType, opt => opt.Ignore()) ; + //会话对象深拷贝 + CreateMap() + .ForMember(dest => dest.Id, opt => opt.Ignore()) + .ForMember(dest => dest.UserId, opt => opt.Ignore()) + .ForMember(dest => dest.TargetId, opt => opt.Ignore()) + .ForMember(dest => dest.ChatType, opt => opt.Ignore()) + .ForMember(dest => dest.StreamKey, opt => opt.Ignore()) + ; + + //消息对象转消息创建事件对象 + CreateMap() + .ForMember(dest => dest.MessageMsgType, opt => opt.MapFrom(src => src.MsgTypeEnum)) + .ForMember(dest => dest.ChatType, opt => opt.MapFrom(src => src.ChatTypeEnum)) + .ForMember(dest => dest.MessageContent, opt => opt.MapFrom(src => src.Content)) + .ForMember(dest => dest.State, opt => opt.MapFrom(src => src.StateEnum)) + .ForMember(dest => dest.MessageCreated, opt => opt.MapFrom(src => src.Created)) + .ForMember(dest => dest.MessageId, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.MsgRecipientId, opt => opt.MapFrom(src => src.Recipient)) + .ForMember(dest => dest.MsgSenderId, opt => opt.MapFrom(src => src.Sender)) + .ForMember(dest => dest.StreamKey, opt => opt.MapFrom(src => src.StreamKey)) + ; + + //消息发送事件转换会话对象 + CreateMap() + .ForMember(dest => dest.LastReadMessageId, opt => opt.MapFrom(src => src.MessageId)) + .ForMember(dest => dest.LastMessage, opt => opt.MapFrom(src => src.MessageContent)) + .ForMember(dest => dest.ChatType, opt => opt.MapFrom(src => (int)src.ChatType)) + .ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.MsgSenderId)) + .ForMember(dest => dest.TargetId, opt => opt.MapFrom(src => src.MsgRecipientId)) + .ForMember(dest => dest.UnreadCount, opt => opt.MapFrom(src => 0)) + .ForMember(dest => dest.StreamKey, opt => opt.MapFrom(src => src.StreamKey)) + .ForMember(dest => dest.LastMessageTime, opt => opt.MapFrom(src => DateTime.Now)) + ; + } } } diff --git a/backend/IM_API/Configs/ServiceCollectionExtensions.cs b/backend/IM_API/Configs/ServiceCollectionExtensions.cs index 20a931d..6293182 100644 --- a/backend/IM_API/Configs/ServiceCollectionExtensions.cs +++ b/backend/IM_API/Configs/ServiceCollectionExtensions.cs @@ -1,4 +1,8 @@ -using IM_API.Dtos; +using IM_API.Application.EventHandlers; +using IM_API.Application.Interfaces; +using IM_API.Domain.Events; +using IM_API.Dtos; +using IM_API.Infrastructure.EventBus; using IM_API.Interface.Services; using IM_API.Services; using IM_API.Tools; @@ -18,6 +22,9 @@ namespace IM_API.Configs services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddScoped(); + services.AddScoped, SignalREventHandler>(); + services.AddScoped, ConversationEventHandler>(); services.AddSingleton(); services.AddSingleton(); return services; diff --git a/backend/IM_API/Controllers/ConversationController.cs b/backend/IM_API/Controllers/ConversationController.cs index d04c63b..bca8f94 100644 --- a/backend/IM_API/Controllers/ConversationController.cs +++ b/backend/IM_API/Controllers/ConversationController.cs @@ -25,7 +25,7 @@ namespace IM_API.Controllers { var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier); var list = await _conversationSerivice.GetConversationsAsync(int.Parse(userIdStr)); - var res = new BaseResponse>(list); + var res = new BaseResponse>(list); return Ok(res); } [HttpPost] diff --git a/backend/IM_API/Domain/Events/DomainEvent.cs b/backend/IM_API/Domain/Events/DomainEvent.cs new file mode 100644 index 0000000..7a5f335 --- /dev/null +++ b/backend/IM_API/Domain/Events/DomainEvent.cs @@ -0,0 +1,13 @@ +using IM_API.Domain.Interfaces; + +namespace IM_API.Domain.Events +{ + public abstract record DomainEvent: IEvent + { + public Guid EventId { get; init; } = Guid.NewGuid(); + public DateTime OccurredAt { get; init; } = DateTime.UtcNow; + public long OperatorId { get; init; } + public string AggregateId { get; init; } = ""; + public abstract string EventType { get; } + } +} diff --git a/backend/IM_API/Domain/Events/MessageCreatedEvent.cs b/backend/IM_API/Domain/Events/MessageCreatedEvent.cs new file mode 100644 index 0000000..c720dd5 --- /dev/null +++ b/backend/IM_API/Domain/Events/MessageCreatedEvent.cs @@ -0,0 +1,22 @@ +using IM_API.Dtos; +using IM_API.Models; + +namespace IM_API.Domain.Events +{ + public record MessageCreatedEvent : DomainEvent + { + public override string EventType => "IM.MESSAGE.MESSAGE_CREATED"; + public ChatType ChatType { get; set; } + public MessageMsgType MessageMsgType { get; set; } + public int MessageId { get; set; } + public string MessageContent { get; set; } + public int MsgSenderId { get; set; } + public int MsgRecipientId { get; set; } + public MessageState State { get; set; } + public DateTime MessageCreated { get; set; } + public string StreamKey { get; set; } + + + + } +} diff --git a/backend/IM_API/Domain/Interfaces/IEvent.cs b/backend/IM_API/Domain/Interfaces/IEvent.cs new file mode 100644 index 0000000..d3fc5a3 --- /dev/null +++ b/backend/IM_API/Domain/Interfaces/IEvent.cs @@ -0,0 +1,6 @@ +namespace IM_API.Domain.Interfaces +{ + public interface IEvent + { + } +} diff --git a/backend/IM_API/Dtos/ConversationDto.cs b/backend/IM_API/Dtos/ConversationDto.cs new file mode 100644 index 0000000..6d7842f --- /dev/null +++ b/backend/IM_API/Dtos/ConversationDto.cs @@ -0,0 +1,51 @@ +using IM_API.Models; + +namespace IM_API.Dtos +{ + public class ConversationDto + { + public int Id { get; set; } + + /// + /// 用户 + /// + public int UserId { get; set; } + + /// + /// 对方ID(群聊为群聊ID,单聊为单聊ID) + /// + public int TargetId { get; set; } + + /// + /// 最后一条未读消息ID + /// + public int? LastReadMessageId { get; set; } + + /// + /// 未读消息数 + /// + public int UnreadCount { get; set; } + + public int ChatType { get; set; } + + + /// + /// 最后一条最新消息 + /// + public string LastMessage { get; set; } = null!; + /// + /// 最后一条消息时间 + /// + public DateTime? DateTime { get; set; } = null; + + /// + /// 对方昵称 + /// + public string TargetName { get; set; } + /// + /// 对方头像 + /// + public string? TargetAvatar { get; set; } + public virtual Message? LastReadMessage { get; set; } + } +} diff --git a/backend/IM_API/Hubs/ChatHub.cs b/backend/IM_API/Hubs/ChatHub.cs index 0448ba1..12cb1b4 100644 --- a/backend/IM_API/Hubs/ChatHub.cs +++ b/backend/IM_API/Hubs/ChatHub.cs @@ -9,19 +9,27 @@ namespace IM_API.Hubs public class ChatHub:Hub { private IMessageSevice _messageService; - public ChatHub(IMessageSevice messageService) + private readonly IConversationService _conversationService; + public ChatHub(IMessageSevice messageService, IConversationService conversationService) { _messageService = messageService; + _conversationService = conversationService; } public async override Task OnConnectedAsync() { if (!Context.User.Identity.IsAuthenticated) { - await Clients.Caller.SendAsync("ReceiveMessage",new BaseResponse(CodeDefine.AUTH_FAILED)); Context.Abort(); return; } + //将用户加入已加入聊天的会话组 + string userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier); + var keys = await _conversationService.GetUserAllStreamKeyAsync(int.Parse(userIdStr)); + foreach(var key in keys) + { + await Groups.AddToGroupAsync(Context.ConnectionId,key); + } await base.OnConnectedAsync(); } public async Task SendPrivateMessage(MessageBaseDto dto) diff --git a/backend/IM_API/Infrastructure/EventBus/InMemoryEventBus.cs b/backend/IM_API/Infrastructure/EventBus/InMemoryEventBus.cs new file mode 100644 index 0000000..45ed2b2 --- /dev/null +++ b/backend/IM_API/Infrastructure/EventBus/InMemoryEventBus.cs @@ -0,0 +1,32 @@ +using IM_API.Application.Interfaces; +using IM_API.Domain.Interfaces; + +namespace IM_API.Infrastructure.EventBus +{ + public class InMemoryEventBus : IEventBus + { + private IServiceProvider _serviceProvider; + private ILogger _logger; + public InMemoryEventBus(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + public async Task PublishAsync(TEvent @event) where TEvent : IEvent + { + var handlers = _serviceProvider.GetServices>(); + foreach (var handler in handlers) + { + try + { + await handler.Handle(@event); + } + catch(Exception e) + { + _logger.LogError("EventHandler error:"+e.Message, e); + } + } + } + } +} diff --git a/backend/IM_API/Interface/Services/IConversationService.cs b/backend/IM_API/Interface/Services/IConversationService.cs index 3f2aa5a..10fb99d 100644 --- a/backend/IM_API/Interface/Services/IConversationService.cs +++ b/backend/IM_API/Interface/Services/IConversationService.cs @@ -22,6 +22,12 @@ namespace IM_API.Interface.Services /// /// 用户id /// - Task> GetConversationsAsync(int userId); + Task> GetConversationsAsync(int userId); + /// + /// 获取指定用户的所有推送标识符 + /// + /// + /// + Task> GetUserAllStreamKeyAsync(int userId); } } diff --git a/backend/IM_API/Models/Conversation.cs b/backend/IM_API/Models/Conversation.cs index b9c5688..1d7b428 100644 --- a/backend/IM_API/Models/Conversation.cs +++ b/backend/IM_API/Models/Conversation.cs @@ -18,27 +18,33 @@ public partial class Conversation public int TargetId { get; set; } /// - /// 消息类型(同Messages.MsgType) + /// 最后一条未读消息ID /// - public int MsgType { get; set; } - - /// - /// 最后一条消息ID - /// - public int LastMessageId { get; set; } + public int? LastReadMessageId { get; set; } /// /// 未读消息数 /// public int UnreadCount { get; set; } - public string TargetName { get; set; } = null!; - - public string? TargetAvatar { get; set; } - public int ChatType { get; set; } - public virtual Message LastMessage { get; set; } = null!; + /// + /// 消息推送唯一标识符 + /// + public string StreamKey { get; set; } = null!; + + /// + /// 最后一条最新消息 + /// + public string LastMessage { get; set; } = null!; + + /// + /// 最后一条消息发送时间 + /// + public DateTime LastMessageTime { get; set; } + + public virtual Message? LastReadMessage { get; set; } public virtual User User { get; set; } = null!; } diff --git a/backend/IM_API/Models/Group.cs b/backend/IM_API/Models/Group.cs index e4f89ae..86522af 100644 --- a/backend/IM_API/Models/Group.cs +++ b/backend/IM_API/Models/Group.cs @@ -44,6 +44,11 @@ public partial class Group /// public DateTime Created { get; set; } + /// + /// 群头像 + /// + public string Avatar { get; set; } = null!; + public virtual ICollection GroupInvites { get; set; } = new List(); public virtual User GroupMasterNavigation { get; set; } = null!; diff --git a/backend/IM_API/Models/ImContext.cs b/backend/IM_API/Models/ImContext.cs index 6a5a9b3..6d796cd 100644 --- a/backend/IM_API/Models/ImContext.cs +++ b/backend/IM_API/Models/ImContext.cs @@ -49,17 +49,10 @@ public partial class ImContext : DbContext public virtual DbSet Roles { get; set; } public virtual DbSet Users { get; set; } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - if (!optionsBuilder.IsConfigured) - { - optionsBuilder.UseMySql( - "server=frp-era.com;port=26582;database=IM;user=product;password=12345678", - ServerVersion.Parse("5.7.44-mysql") - ); - } - } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263. + => optionsBuilder.UseMySql("server=frp-era.com;port=26582;database=IM;user=product;password=12345678", Microsoft.EntityFrameworkCore.ServerVersion.Parse("5.7.44-mysql")); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -117,18 +110,25 @@ public partial class ImContext : DbContext entity.HasIndex(e => e.UserId, "Userid"); - entity.HasIndex(e => e.LastMessageId, "lastMessageId"); + entity.HasIndex(e => e.LastReadMessageId, "lastMessageId"); entity.Property(e => e.Id) .HasColumnType("int(11)") .HasColumnName("ID"); - entity.Property(e => e.LastMessageId) - .HasComment("最后一条消息ID ") + entity.Property(e => e.ChatType).HasColumnType("int(11)"); + entity.Property(e => e.LastMessage) + .HasMaxLength(255) + .HasComment("最后一条最新消息"); + entity.Property(e => e.LastMessageTime) + .HasComment("最后一条消息发送时间") + .HasColumnType("datetime"); + entity.Property(e => e.LastReadMessageId) + .HasComment("最后一条未读消息ID ") .HasColumnType("int(11)") - .HasColumnName("lastMessageId"); - entity.Property(e => e.MsgType) - .HasComment("消息类型(同Messages.MsgType) ") - .HasColumnType("int(11)"); + .HasColumnName("lastReadMessageId"); + entity.Property(e => e.StreamKey) + .HasMaxLength(255) + .HasComment("消息推送唯一标识符"); entity.Property(e => e.TargetId) .HasComment("对方ID(群聊为群聊ID,单聊为单聊ID) ") .HasColumnType("int(11)"); @@ -139,9 +139,8 @@ public partial class ImContext : DbContext .HasComment("用户") .HasColumnType("int(11)"); - entity.HasOne(d => d.LastMessage).WithMany(p => p.Conversations) - .HasForeignKey(d => d.LastMessageId) - .OnDelete(DeleteBehavior.ClientSetNull) + entity.HasOne(d => d.LastReadMessage).WithMany(p => p.Conversations) + .HasForeignKey(d => d.LastReadMessageId) .HasConstraintName("conversations_ibfk_2"); entity.HasOne(d => d.User).WithMany(p => p.Conversations) @@ -337,6 +336,9 @@ public partial class ImContext : DbContext entity.Property(e => e.Auhority) .HasComment("群权限\r\n(0:需管理员同意,1:任意人可加群,2:不允许任何人加入)") .HasColumnType("tinyint(4)"); + entity.Property(e => e.Avatar) + .HasMaxLength(255) + .HasComment("群头像"); entity.Property(e => e.Created) .HasComment("群聊创建时间") .HasColumnType("datetime"); @@ -557,6 +559,9 @@ public partial class ImContext : DbContext entity.Property(e => e.State) .HasComment("消息状态(0:已发送,1:已撤回) ") .HasColumnType("tinyint(4)"); + entity.Property(e => e.StreamKey) + .HasMaxLength(255) + .HasComment("消息推送唯一标识符"); entity.HasOne(d => d.SenderNavigation).WithMany(p => p.Messages) .HasForeignKey(d => d.Sender) diff --git a/backend/IM_API/Models/ImDbContext.cs b/backend/IM_API/Models/ImDbContext.cs index eef5d0d..67a195e 100644 --- a/backend/IM_API/Models/ImDbContext.cs +++ b/backend/IM_API/Models/ImDbContext.cs @@ -110,24 +110,22 @@ public partial class ImDbContext : DbContext entity.HasIndex(e => e.UserId, "Userid"); - entity.HasIndex(e => e.LastMessageId, "lastMessageId"); + entity.HasIndex(e => e.LastReadMessageId, "lastReadMessageId"); entity.Property(e => e.Id) .HasColumnType("int(11)") .HasColumnName("ID"); entity.Property(e => e.ChatType).HasColumnType("int(11)"); - entity.Property(e => e.LastMessageId) - .HasComment("最后一条消息ID ") + entity.Property(e => e.LastReadMessageId) + .HasComment("最后一条已读消息ID ") .HasColumnType("int(11)") .HasColumnName("lastMessageId"); - entity.Property(e => e.MsgType) - .HasComment("消息类型(同Messages.MsgType) ") - .HasColumnType("int(11)"); - entity.Property(e => e.TargetAvatar).HasMaxLength(255); + entity.Property(e => e.StreamKey) + .HasMaxLength(255) + .HasComment("消息推送唯一标识符"); entity.Property(e => e.TargetId) .HasComment("对方ID(群聊为群聊ID,单聊为单聊ID) ") .HasColumnType("int(11)"); - entity.Property(e => e.TargetName).HasMaxLength(255); entity.Property(e => e.UnreadCount) .HasComment("未读消息数 ") .HasColumnType("int(11)"); @@ -135,11 +133,6 @@ public partial class ImDbContext : DbContext .HasComment("用户") .HasColumnType("int(11)"); - entity.HasOne(d => d.LastMessage).WithMany(p => p.Conversations) - .HasForeignKey(d => d.LastMessageId) - .OnDelete(DeleteBehavior.ClientSetNull) - .HasConstraintName("conversations_ibfk_2"); - entity.HasOne(d => d.User).WithMany(p => p.Conversations) .HasForeignKey(d => d.UserId) .OnDelete(DeleteBehavior.ClientSetNull) @@ -553,6 +546,9 @@ public partial class ImDbContext : DbContext entity.Property(e => e.State) .HasComment("消息状态(0:已发送,1:已撤回) ") .HasColumnType("tinyint(4)"); + entity.Property(e => e.StreamKey) + .HasMaxLength(255) + .HasComment("消息推送唯一标识符"); entity.HasOne(d => d.SenderNavigation).WithMany(p => p.Messages) .HasForeignKey(d => d.Sender) diff --git a/backend/IM_API/Models/Message.cs b/backend/IM_API/Models/Message.cs index 312f075..fcd7e9f 100644 --- a/backend/IM_API/Models/Message.cs +++ b/backend/IM_API/Models/Message.cs @@ -44,6 +44,11 @@ public partial class Message /// public DateTime Created { get; set; } + /// + /// 消息推送唯一标识符 + /// + public string StreamKey { get; set; } = null!; + public virtual ICollection Conversations { get; set; } = new List(); public virtual ICollection Files { get; set; } = new List(); diff --git a/backend/IM_API/Services/ConversationService.cs b/backend/IM_API/Services/ConversationService.cs index 56632a1..41ae75f 100644 --- a/backend/IM_API/Services/ConversationService.cs +++ b/backend/IM_API/Services/ConversationService.cs @@ -24,9 +24,48 @@ namespace IM_API.Services #endregion #region 获取用户会话列表 - public async Task> GetConversationsAsync(int userId) + public async Task> GetConversationsAsync(int userId) { - return await _context.Conversations.Where(x => x.UserId == userId) + var privateQuery = from c in _context.Conversations + join f in _context.Friends on new { c.UserId, c.TargetId} + equals new { UserId = f.UserId, TargetId = f.FriendId} + where c.UserId == userId && c.ChatType == (int)ChatType.PRIVATE + select new ConversationDto + { + Id = c.Id, + UserId = c.UserId, + TargetId = c.TargetId, + LastReadMessageId = c.LastReadMessageId, + LastReadMessage = c.LastReadMessage, + UnreadCount = c.UnreadCount, + ChatType = c.ChatType, + LastMessage = c.LastMessage, + TargetAvatar = f.Avatar, + TargetName = f.RemarkName, + DateTime = c.LastMessageTime + + }; + + var groupQuery = from c in _context.Conversations + join g in _context.Groups on c.TargetId equals g.Id + where c.UserId == userId && c.ChatType == (int)ChatType.GROUP + select new ConversationDto + { + Id = c.Id, + UserId = c.UserId, + TargetId = c.TargetId, + LastReadMessageId = c.LastReadMessageId, + LastReadMessage = c.LastReadMessage, + UnreadCount = c.UnreadCount, + ChatType = c.ChatType, + LastMessage = c.LastMessage, + TargetAvatar = g.Avatar, + TargetName = g.Name, + DateTime = c.LastMessageTime + }; + return await privateQuery + .Concat(groupQuery) + .OrderByDescending(x => x.DateTime) .ToListAsync(); } #endregion @@ -39,6 +78,15 @@ namespace IM_API.Services await _context.SaveChangesAsync(); return true; } + + #endregion + public async Task> GetUserAllStreamKeyAsync(int userId) + { + return await _context.Conversations.Where(x => x.UserId == userId) + .Select(x => x.StreamKey) + .Distinct() + .ToListAsync(); + } } } \ No newline at end of file diff --git a/backend/IM_API/Services/MessageService.cs b/backend/IM_API/Services/MessageService.cs index 223c775..2c13695 100644 --- a/backend/IM_API/Services/MessageService.cs +++ b/backend/IM_API/Services/MessageService.cs @@ -65,6 +65,7 @@ namespace IM_API.Services if (!isMember) throw new BaseException(CodeDefine.NO_GROUP_PERMISSION); var message = _mapper.Map(dto); message.Sender = senderId; + message.StreamKey = StreamKeyBuilder.Group(groupId); _context.Messages.Add(message); await _context.SaveChangesAsync(); return true; @@ -78,6 +79,8 @@ namespace IM_API.Services if (!isExist) throw new BaseException(CodeDefine.FRIEND_RELATION_NOT_FOUND); var message = _mapper.Map(dto); message.Sender = senderId; + //生成消息流唯一标识符 + message.StreamKey = StreamKeyBuilder.Private(dto.SenderId, dto.ReceiverId); _context.Messages.Add(message); await _context.SaveChangesAsync(); return true; diff --git a/backend/IM_API/Tools/SignalRMethodDefine.cs b/backend/IM_API/Tools/SignalRMethodDefine.cs new file mode 100644 index 0000000..829ec14 --- /dev/null +++ b/backend/IM_API/Tools/SignalRMethodDefine.cs @@ -0,0 +1,7 @@ +namespace IM_API.Tools +{ + public class SignalRMethodDefine + { + public static string ReceiveMessage = "ReceiveMessage"; + } +} diff --git a/backend/IM_API/Tools/StreamKeyBuilder.cs b/backend/IM_API/Tools/StreamKeyBuilder.cs new file mode 100644 index 0000000..c7eee60 --- /dev/null +++ b/backend/IM_API/Tools/StreamKeyBuilder.cs @@ -0,0 +1,17 @@ +namespace IM_API.Tools +{ + public static class StreamKeyBuilder + { + public static string Private(long a, long b) + { + if (a <= 0 || b <= 0) throw new ArgumentException(); + return $"p:{(a < b ? $"{a}_{b}" : $"{b}_{a}")}"; + } + + public static string Group(long groupId) + { + if (groupId <= 0) throw new ArgumentException(); + return $"g:{groupId}"; + } + } +}