前端:

1、优化消息排序逻辑
2、新增加载历史消息
3、修复已知问题
后端:
1、优化消息排序逻辑
2、增加用户信息缓存机制
3、修改日期类型为DateTimeOffset改善时区信息丢失问题
3、修复了已知问题
数据库:
1、新增SequenceId字段用于消息排序
2、新增ClientMsgId字段用于客户端消息回执
This commit is contained in:
2026-02-07 22:37:56 +08:00
118 changed files with 10691 additions and 452 deletions
+16 -11
View File
@@ -1,13 +1,16 @@
using IM_API.Application.EventHandlers.FriendAddHandler;
using AutoMapper;
using IM_API.Application.EventHandlers.FriendAddHandler;
using IM_API.Application.EventHandlers.MessageCreatedHandler;
using IM_API.Application.EventHandlers.RequestFriendHandler;
using IM_API.Configs.Options;
using MassTransit;
using MySqlConnector;
namespace IM_API.Configs
{
public static class MQConfig
{
public static IServiceCollection AddRabbitMQ(this IServiceCollection services, RabbitMqOptions options)
public static IServiceCollection AddRabbitMQ(this IServiceCollection services, RabbitMQOptions options)
{
services.AddMassTransit(x =>
{
@@ -17,7 +20,7 @@ namespace IM_API.Configs
x.AddConsumer<FriendAddSignalRHandler>();
x.AddConsumer<RequestFriendSignalRHandler>();
x.AddConsumer<FriendAddConversationHandler>();
x.AddConsumer<MessageCreatedDbHandler>();
x.UsingRabbitMq((ctx,cfg) =>
{
cfg.Host(options.Host, "/", h =>
@@ -26,7 +29,15 @@ namespace IM_API.Configs
h.Password(options.Password);
});
cfg.ConfigureEndpoints(ctx);
});
cfg.UseMessageRetry(r =>
{
r.Handle<IOException>();
r.Handle<MySqlException>();
r.Ignore<AutoMapperMappingException>();
r.Exponential(5, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(2));
});
cfg.ConfigureEndpoints(ctx);
});
});
@@ -34,11 +45,5 @@ namespace IM_API.Configs
}
}
public class RabbitMqOptions
{
public string Host { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get;set; }
}
}
+42 -22
View File
@@ -2,11 +2,13 @@
using IM_API.Domain.Events;
using IM_API.Dtos;
using IM_API.Dtos.Auth;
using IM_API.Dtos.Conversation;
using IM_API.Dtos.Friend;
using IM_API.Dtos.Group;
using IM_API.Dtos.User;
using IM_API.Models;
using IM_API.Tools;
using IM_API.VOs.Conversation;
using IM_API.VOs.Message;
namespace IM_API.Configs
{
@@ -41,13 +43,13 @@ namespace IM_API.Configs
.ForMember(dest => dest.FriendId , opt => opt.MapFrom(src => src.ToUserId))
.ForMember(dest => dest.StatusEnum , opt =>opt.MapFrom(src => FriendStatus.Pending))
.ForMember(dest => dest.RemarkName , opt => opt.MapFrom(src => src.RemarkName))
.ForMember(dest => dest.Created , opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(dest => dest.Created , opt => opt.MapFrom(src => DateTime.Now))
;
//发起好友请求转换请求对象
CreateMap<FriendRequestDto, FriendRequest>()
.ForMember(dest => dest.RequestUser , opt => opt.MapFrom(src => src.FromUserId))
.ForMember(dest => dest.ResponseUser , opt => opt.MapFrom(src => src.ToUserId))
.ForMember(dest => dest.Created , opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(dest => dest.Created , opt => opt.MapFrom(src => DateTime.Now))
.ForMember(dest => dest.StateEnum , opt => opt.MapFrom(src => FriendRequestState.Pending))
.ForMember(dest => dest.Description , opt => opt.MapFrom(src => src.Description))
;
@@ -59,15 +61,15 @@ namespace IM_API.Configs
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
;
//消息模型转换
CreateMap<Message, MessageBaseDto>()
.ForMember(dest => dest.Type , opt => opt.MapFrom(src => src.MsgTypeEnum.ToString()))
.ForMember(dest => dest.MsgId , opt => opt.MapFrom(src => src.Id))
CreateMap<Message, MessageBaseVo>()
.ForMember(dest => dest.Type , opt => opt.MapFrom(src => src.MsgTypeEnum))
.ForMember(dest => dest.MsgId , opt => opt.MapFrom(src => src.ClientMsgId))
.ForMember(dest => dest.SenderId , opt => opt.MapFrom(src => src.Sender))
.ForMember(dest => dest.ChatType , opt => opt.MapFrom(src => src.ChatTypeEnum.ToString()))
.ForMember(dest => dest.ChatType , opt => opt.MapFrom(src => src.ChatTypeEnum))
.ForMember(dest => dest.ReceiverId, opt => opt.MapFrom(src => src.Recipient))
.ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.Content))
.ForMember(dest => dest.TimeStamp, opt => opt.MapFrom(src => src.Created))
.ForMember(dest => dest.GroupMemberId , opt => opt.MapFrom(src => src.GroupMemberId))
.ForMember(dest => dest.SequenceId, opt => opt.MapFrom(src => src.SequenceId))
;
CreateMap<MessageBaseDto, Message>()
.ForMember(dest => dest.Sender, opt => opt.MapFrom(src => src.SenderId))
@@ -78,9 +80,9 @@ namespace IM_API.Configs
.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.GroupMemberId, opt => opt.MapFrom(src => src.GroupMemberId))
.ForMember(dest => dest.ChatType, opt => opt.Ignore())
.ForMember(dest => dest.MsgType, opt => opt.Ignore())
.ForMember(dest => dest.ClientMsgId, opt => opt.MapFrom(src => src.MsgId))
;
//会话对象深拷贝
@@ -99,41 +101,58 @@ namespace IM_API.Configs
.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.EventId, opt => opt.MapFrom(src => Guid.NewGuid()))
.ForMember(dest => dest.AggregateId, opt => opt.MapFrom(src => src.Sender.ToString()))
.ForMember(dest => dest.OccurredAt , opt => opt.MapFrom(src => DateTime.Now))
.ForMember(dest => dest.OperatorId, opt => opt.MapFrom(src => src.Sender))
.ForMember(dest => dest.StreamKey, opt => opt.MapFrom(src => src.StreamKey))
;
CreateMap<MessageCreatedEvent, Message>()
.ForMember(dest => dest.SequenceId, opt => opt.MapFrom(src => src.SequenceId))
.ForMember(dest => dest.ClientMsgId, opt => opt.MapFrom(src => src.ClientMsgId))
.ForMember(dest => dest.StateEnum, opt => opt.MapFrom(src => src.State))
.ForMember(dest => dest.ChatTypeEnum, opt => opt.MapFrom(src => src.ChatType))
.ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.MessageContent))
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.MessageCreated))
.ForMember(dest => dest.MsgTypeEnum, opt => opt.MapFrom(src => src.MessageMsgType))
.ForMember(dest => dest.Recipient, opt => opt.MapFrom(src => src.MsgRecipientId))
.ForMember(dest => dest.Sender, opt => opt.MapFrom(src => src.MsgSenderId))
.ForMember(dest => dest.StreamKey, opt => opt.MapFrom(src => src.StreamKey))
.ForMember(dest => dest.ChatType, opt => opt.Ignore())
.ForMember(dest => dest.State, opt => opt.Ignore())
.ForMember(dest => dest.MsgType, opt => opt.Ignore());
//消息发送事件转换会话对象
CreateMap<MessageCreatedEvent, Conversation>()
.ForMember(dest => dest.LastReadMessageId, opt => opt.MapFrom(src => src.MessageId))
//.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.ChatType, opt => opt.MapFrom(src => 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.UtcNow))
.ForMember(dest => dest.LastMessageTime, opt => opt.MapFrom(src => DateTime.Now))
;
//创建会话对象
CreateMap<Conversation, ConversationDto>()
CreateMap<Conversation, ConversationVo>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.LastMessage, opt => opt.MapFrom(src => src.LastMessage))
.ForMember(dest => dest.LastReadMessage, opt => opt.MapFrom(src => src.LastReadMessage))
.ForMember(dest => dest.LastReadMessageId, opt => opt.MapFrom(src => src.LastReadMessageId))
.ForMember(dest => dest.ChatType, opt => opt.MapFrom(src => src.ChatType))
.ForMember(dest => dest.LastSequenceId, opt => opt.MapFrom(src => src.LastReadSequenceId))
.ForMember(dest => dest.ChatType, opt => opt.MapFrom(src => src.ChatTypeEnum))
.ForMember(dest => dest.DateTime, opt => opt.MapFrom(src => src.LastMessageTime))
.ForMember(dest => dest.TargetId, opt => opt.MapFrom(src => src.TargetId))
.ForMember(dest => dest.UnreadCount, opt => opt.MapFrom(src => src.UnreadCount))
.ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.UserId));
CreateMap<Friend, ConversationDto>()
.ForMember(dest => dest.TargetAvatar, opt => opt.MapFrom(src => src.Avatar))
CreateMap<Friend, ConversationVo>()
.ForMember(dest => dest.TargetAvatar, opt => opt.MapFrom(src => src.FriendNavigation.Avatar))
.ForMember(dest => dest.TargetName, opt => opt.MapFrom(src => src.RemarkName));
CreateMap<Group, ConversationDto>()
CreateMap<Group, ConversationVo>()
.ForMember(dest => dest.TargetAvatar, opt => opt.MapFrom(src => src.Avatar))
.ForMember(dest => dest.TargetName, opt => opt.MapFrom(src => src.Name));
@@ -147,10 +166,11 @@ namespace IM_API.Configs
CreateMap<GroupCreateDto, Group>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.Avatar, opt => opt.MapFrom(src => src.Avatar))
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => DateTime.Now))
.ForMember(dest => dest.AllMembersBannedEnum, opt => opt.MapFrom(src => GroupAllMembersBanned.ALLOWED))
.ForMember(dest => dest.AuhorityEnum, opt => opt.MapFrom(src => GroupAuhority.REQUIRE_CONSENT))
.ForMember(dest => dest.StatusEnum, opt => opt.MapFrom(src => GroupStatus.Normal));
.ForMember(dest => dest.StatusEnum, opt => opt.MapFrom(src => GroupStatus.Normal))
;
}
}
}
@@ -0,0 +1,8 @@
namespace IM_API.Configs.Options
{
public class ConnectionOptions
{
public string DefaultConnection { get; set; }
public string Redis { get; set; }
}
}
@@ -0,0 +1,10 @@
namespace IM_API.Configs.Options
{
public class RabbitMQOptions
{
public string Host { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}
@@ -8,6 +8,10 @@ using IM_API.Services;
using IM_API.Tools;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using RedLockNet;
using RedLockNet.SERedis;
using RedLockNet.SERedis.Configuration;
using StackExchange.Redis;
namespace IM_API.Configs
{
@@ -17,14 +21,23 @@ namespace IM_API.Configs
{
services.AddAutoMapper(typeof(MapperConfig));
services.AddTransient<IAuthService, AuthService>();
services.AddTransient<IUserService, UserService>();
services.AddTransient<IFriendSerivce, FriendService>();
services.AddTransient<IMessageSevice, MessageService>();
services.AddTransient<IConversationService, ConversationService>();
services.AddScoped<IAuthService, AuthService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IFriendSerivce, FriendService>();
services.AddScoped<IMessageSevice, MessageService>();
services.AddScoped<IConversationService, ConversationService>();
services.AddScoped<IGroupService, GroupService>();
services.AddScoped<ISequenceIdService, SequenceIdService>();
services.AddScoped<ICacheService, RedisCacheService>();
services.AddScoped<IEventBus, InMemoryEventBus>();
services.AddSingleton<IJWTService, JWTService>();
services.AddSingleton<IRefreshTokenService, RedisRefreshTokenService>();
services.AddSingleton<IDistributedLockFactory>(sp =>
{
var connection = sp.GetRequiredService<IConnectionMultiplexer>();
// 这里可以配置多个 Redis 节点提高安全性,单机运行传一个即可
return RedLockFactory.Create(new List<RedLockMultiplexer> { new RedLockMultiplexer(connection) });
});
return services;
}
public static IServiceCollection AddModelValidation(this IServiceCollection services, IConfiguration configuration)
@@ -40,7 +53,7 @@ namespace IM_API.Configs
Field = e.Key,
Message = e.Value.Errors.First().ErrorMessage
});
Console.WriteLine(errors);
return new BadRequestObjectResult(new BaseResponse<object?>(CodeDefine.PARAMETER_ERROR.Code, errors.First().Message));
};
});