前端:

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
@@ -26,10 +26,10 @@ namespace IM_API.Application.EventHandlers.FriendAddHandler
{
ChatType = ChatType.PRIVATE,
Content = "您有新的好友关系已添加",
MsgId = @event.EventId.ToString(),
//MsgId = @event.EventId.ToString(),
ReceiverId = @event.ResponseUserId,
SenderId = @event.RequestUserId,
TimeStamp = DateTime.UtcNow
TimeStamp = DateTime.Now
});
await _chathub.Clients.Users(usersList).SendAsync("ReceiveMessage", res);
}
@@ -35,28 +35,15 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
{
var @event = context.Message;
if (@event.ChatType == ChatType.PRIVATE)
await _conversationService.UpdateConversationAfterSentAsync(new Dtos.Conversation.UpdateConversationDto
{
Conversation? userAConversation = await _context.Conversations.FirstOrDefaultAsync(
x => x.UserId == @event.MsgSenderId && x.TargetId == @event.MsgRecipientId
);
Conversation? userBConversation = await _context.Conversations.FirstOrDefaultAsync(
x => x.UserId == @event.MsgRecipientId && x.TargetId == @event.MsgSenderId
);
if (userAConversation is null || userBConversation is null)
{
_logger.LogError("消息事件更新会话信息失败:{@event}", @event);
}
userAConversation.LastMessage = @event.MessageContent;
userAConversation.LastReadMessageId = @event.MessageId;
userAConversation.LastMessageTime = @event.MessageCreated;
userBConversation.LastMessage = @event.MessageContent;
userBConversation.UnreadCount += 1;
userBConversation.LastMessageTime = @event.MessageCreated;
_context.UpdateRange(userAConversation, userBConversation);
await _context.SaveChangesAsync();
}
LastMessage = @event.MessageContent,
LastSequenceId = @event.SequenceId,
ReceiptId = @event.MsgRecipientId,
SenderId = @event.MsgSenderId,
StreamKey = @event.StreamKey,
DateTime = @event.MessageCreated
});
}
}
@@ -0,0 +1,26 @@
using IM_API.Domain.Events;
using MassTransit;
using IM_API.Interface.Services;
using AutoMapper;
using IM_API.Models;
namespace IM_API.Application.EventHandlers.MessageCreatedHandler
{
public class MessageCreatedDbHandler : IConsumer<MessageCreatedEvent>
{
private readonly IMessageSevice _messageService;
public readonly IMapper _mapper;
public MessageCreatedDbHandler(IMessageSevice messageSevice, IMapper mapper)
{
_messageService = messageSevice;
_mapper = mapper;
}
public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
{
var @event = context.Message;
var msg = _mapper.Map<Message>(@event);
await _messageService.MakeMessageAsync(msg);
}
}
}
@@ -5,6 +5,7 @@ using IM_API.Dtos;
using IM_API.Hubs;
using IM_API.Models;
using IM_API.Tools;
using IM_API.VOs.Message;
using MassTransit;
using Microsoft.AspNetCore.SignalR;
@@ -22,21 +23,31 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
{
Console.ForegroundColor = ConsoleColor.Red;
var @event = context.Message;
if (@event.ChatType == Models.ChatType.PRIVATE)
Console.WriteLine($"[SignalR]handlerCreated!");
try
{
MessageBaseDto messageBaseDto = new MessageBaseDto
{
MsgId = @event.MessageId.ToString(),
ChatType = @event.ChatType,
Content = @event.MessageContent,
GroupMemberId = null,
ReceiverId = @event.MsgRecipientId,
SenderId = @event.MsgSenderId,
TimeStamp = @event.MessageCreated,
Type = @event.MessageMsgType
};
await _hub.Clients.Users(@event.MsgRecipientId.ToString()).SendAsync("ReceiveMessage", messageBaseDto);
// 先转成实体,如果这一步都报错,说明之前的 sbyte -> enum 还没改好
var entity = _mapper.Map<Message>(@event);
// 再从实体转 VO,这是最稳妥的路径,因为这两者的映射你肯定配过了
var messageBaseVo = _mapper.Map<MessageBaseVo>(entity);
// 2. 打印日志确认逻辑执行到这里了
Console.WriteLine($"[SignalR] 准备向所有人广播消息: {messageBaseVo.Content}");
// 3. 执行广播
await _hub.Clients.User(@event.MsgRecipientId.ToString()).SendAsync("ReceiveMessage", new HubResponse<MessageBaseVo>("Event", messageBaseVo));
Console.WriteLine("[SignalR] 广播指令已发出");
Console.ResetColor();
}
catch (Exception ex)
{
Console.WriteLine($"[SignalR] 发送失败: {ex.Message}");
Console.ResetColor();
throw; // 抛出异常触发 MassTransit 重试
}
}
}
+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));
};
});
+5 -4
View File
@@ -4,6 +4,7 @@ using IM_API.Dtos.Auth;
using IM_API.Dtos.User;
using IM_API.Interface.Services;
using IM_API.Tools;
using IM_API.VOs.Auth;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
@@ -44,7 +45,7 @@ namespace IM_API.Controllers
(string token,DateTime expiresAt) = _jwtService.CreateAccessTokenForUser(user.Id,user.Username,"user");
//生成刷新凭证
string refreshToken = await _refreshTokenService.CreateRefreshTokenAsync(user.Id);
var res = new BaseResponse<LoginDto>(new LoginDto(userInfo,token,refreshToken, expiresAt));
var res = new BaseResponse<LoginVo>(new LoginVo(userInfo,token,refreshToken, expiresAt));
return Ok(res);
}
[HttpPost]
@@ -55,18 +56,18 @@ namespace IM_API.Controllers
return Ok(res);
}
[HttpPost]
[ProducesResponseType(typeof(BaseResponse<LoginDto>),StatusCodes.Status200OK)]
[ProducesResponseType(typeof(BaseResponse<LoginVo>),StatusCodes.Status200OK)]
public async Task<IActionResult> Refresh(RefreshDto dto)
{
(bool ok,int userId) = await _refreshTokenService.ValidateRefreshTokenAsync(dto.refreshToken);
if (!ok)
{
var err = new BaseResponse<LoginDto>(CodeDefine.AUTH_FAILED);
var err = new BaseResponse<LoginVo>(CodeDefine.AUTH_FAILED);
return Unauthorized(err);
}
var userinfo = await _userService.GetUserInfoAsync(userId);
(string token,DateTime expiresAt) = _jwtService.CreateAccessTokenForUser(userinfo.Id,userinfo.Username,"user");
var res = new BaseResponse<LoginDto>(new LoginDto(userinfo,token, dto.refreshToken, expiresAt));
var res = new BaseResponse<LoginVo>(new LoginVo(userinfo,token, dto.refreshToken, expiresAt));
return Ok(res);
}
}
@@ -1,7 +1,7 @@
using IM_API.Dtos;
using IM_API.Dtos.Conversation;
using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.VOs.Conversation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -26,7 +26,7 @@ namespace IM_API.Controllers
{
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
var list = await _conversationSerivice.GetConversationsAsync(int.Parse(userIdStr));
var res = new BaseResponse<List<ConversationDto>>(list);
var res = new BaseResponse<List<ConversationVo>>(list);
return Ok(res);
}
[HttpGet]
@@ -34,7 +34,7 @@ namespace IM_API.Controllers
{
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
var conversation = await _conversationSerivice.GetConversationByIdAsync(int.Parse(userIdStr), conversationId);
var res = new BaseResponse<ConversationDto>(conversation);
var res = new BaseResponse<ConversationVo>(conversation);
return Ok(res);
}
[HttpPost]
@@ -0,0 +1,43 @@
using IM_API.Dtos;
using IM_API.Dtos.Group;
using IM_API.Interface.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace IM_API.Controllers
{
[Authorize]
[Route("api/[controller]/[action]")]
[ApiController]
public class GroupController : ControllerBase
{
private readonly IGroupService _groupService;
private readonly ILogger<GroupController> _logger;
public GroupController(IGroupService groupService, ILogger<GroupController> logger)
{
_groupService = groupService;
_logger = logger;
}
[HttpGet]
[ProducesResponseType(typeof(BaseResponse<List<GroupInfoDto>>) ,StatusCodes.Status200OK)]
public async Task<IActionResult> GetGroups(int page = 1, int limit = 100, bool desc = false)
{
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
var list = await _groupService.GetGroupListAsync(int.Parse(userIdStr), page, limit, desc);
var res = new BaseResponse<List<GroupInfoDto>>(list);
return Ok(res);
}
[HttpPost]
[ProducesResponseType(typeof(BaseResponse<GroupInfoDto>), StatusCodes.Status200OK)]
public async Task<ActionResult> CreateGroup([FromBody]GroupCreateDto groupCreateDto)
{
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
var groupInfo = await _groupService.CreateGroupAsync(int.Parse(userIdStr), groupCreateDto);
var res = new BaseResponse<GroupInfoDto>(groupInfo);
return Ok(res);
}
}
}
+24 -15
View File
@@ -1,5 +1,9 @@
using IM_API.Dtos;
using IM_API.Application.Interfaces;
using IM_API.Domain.Events;
using IM_API.Dtos;
using IM_API.Dtos.Message;
using IM_API.Interface.Services;
using IM_API.VOs.Message;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -15,31 +19,36 @@ namespace IM_API.Controllers
{
private readonly IMessageSevice _messageService;
private readonly ILogger<MessageController> _logger;
public MessageController(IMessageSevice messageService, ILogger<MessageController> logger)
private readonly IEventBus _eventBus;
public MessageController(IMessageSevice messageService, ILogger<MessageController> logger, IEventBus eventBus)
{
_messageService = messageService;
_logger = logger;
_eventBus = eventBus;
}
[HttpPost]
public async Task<IActionResult> SendPrivateMessage(MessageBaseDto dto)
[ProducesResponseType(typeof(BaseResponse<MessageBaseVo>), StatusCodes.Status200OK)]
public async Task<IActionResult> SendMessage(MessageBaseDto dto)
{
var userIdstr = User.FindFirstValue(ClaimTypes.NameIdentifier);
await _messageService.SendPrivateMessageAsync(int.Parse(userIdstr), dto.ReceiverId, dto);
return Ok(new BaseResponse<object?>());
}
[HttpPost]
public async Task<IActionResult> SendGroupMessage(MessageBaseDto dto)
{
var userIdstr = User.FindFirstValue(ClaimTypes.NameIdentifier);
await _messageService.SendGroupMessageAsync(int.Parse(userIdstr), dto.ReceiverId, dto);
return Ok(new BaseResponse<object?>());
MessageBaseVo messageBaseVo = new MessageBaseVo();
if(dto.ChatType == Models.ChatType.PRIVATE)
{
messageBaseVo = await _messageService.SendPrivateMessageAsync(int.Parse(userIdstr), dto.ReceiverId, dto);
}
else
{
messageBaseVo = await _messageService.SendGroupMessageAsync(int.Parse(userIdstr), dto.ReceiverId, dto);
}
return Ok(new BaseResponse<MessageBaseVo>(messageBaseVo));
}
[HttpGet]
public async Task<IActionResult> GetMessageList([Required]int conversationId, int? msgId, int? pageSize)
[ProducesResponseType(typeof(BaseResponse<List<MessageBaseVo>>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetMessageList([FromQuery]MessageQueryDto dto)
{
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
var msgList = await _messageService.GetMessagesAsync(int.Parse(userIdStr), conversationId, msgId, pageSize, false);
var res = new BaseResponse<List<MessageBaseDto>>(msgList);
var msgList = await _messageService.GetMessagesAsync(int.Parse(userIdStr),dto);
var res = new BaseResponse<List<MessageBaseVo>>(msgList);
return Ok(res);
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ 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 DateTimeOffset OccurredAt { get; init; } = DateTime.Now;
public long OperatorId { get; init; }
public string AggregateId { get; init; } = "";
public abstract string EventType { get; }
@@ -19,7 +19,7 @@ namespace IM_API.Domain.Events
/// <summary>
/// 好友关系创建时间
/// </summary>
public DateTime Created { get; init; }
public DateTimeOffset Created { get; init; }
}
}
@@ -8,13 +8,14 @@ namespace IM_API.Domain.Events
public override string EventType => "IM.MESSAGE.MESSAGE_CREATED";
public ChatType ChatType { get; set; }
public MessageMsgType MessageMsgType { get; set; }
public int MessageId { get; set; }
public long SequenceId { 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 DateTimeOffset MessageCreated { get; set; }
public string StreamKey { get; set; }
public Guid ClientMsgId { get; set; }
@@ -0,0 +1,12 @@
namespace IM_API.Dtos.Conversation
{
public class UpdateConversationDto
{
public string StreamKey { get; set; }
public int SenderId { get; set; }
public int ReceiptId { get; set; }
public string LastMessage { get; set; }
public long LastSequenceId { get; set; }
public DateTimeOffset DateTime { get; set; }
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ namespace IM_API.Dtos.Friend
public FriendStatus StatusEnum { get; init; }
public DateTime Created { get; init; }
public DateTimeOffset Created { get; init; }
public string RemarkName { get; init; } = string.Empty;
@@ -21,7 +21,7 @@ namespace IM_API.Dtos.Friend
/// <summary>
/// 申请时间
/// </summary>
public DateTime Created { get; set; }
public DateTimeOffset Created { get; set; }
/// <summary>
/// 申请附言
@@ -1,4 +1,5 @@
using IM_API.Models;
using System.ComponentModel.DataAnnotations;
namespace IM_API.Dtos.Group
{
@@ -7,11 +8,15 @@ namespace IM_API.Dtos.Group
/// <summary>
/// 群聊名称
/// </summary>
[Required(ErrorMessage = "群名称必填")]
[MaxLength(20, ErrorMessage = "群名称不能大于20字符")]
public string Name { get; set; } = null!;
/// <summary>
/// 群头像
/// </summary>
[Required(ErrorMessage = "群头像必填")]
public string Avatar { get; set; } = null!;
public List<int>? UserIDs { get; set; }
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ namespace IM_API.Dtos.Group
/// <summary>
/// 群聊创建时间
/// </summary>
public DateTime Created { get; set; }
public DateTimeOffset Created { get; set; }
/// <summary>
/// 群头像
+2
View File
@@ -14,6 +14,7 @@ namespace IM_API.Dtos
Code = CodeDefine.SUCCESS.Code;
Message = CodeDefine.SUCCESS.Message;
Type = HubResponseType.ActionStatus;
Method = method;
}
public HubResponse(string method,T data)
{
@@ -21,6 +22,7 @@ namespace IM_API.Dtos
Message = CodeDefine.SUCCESS.Message;
Type = HubResponseType.ActionStatus;
Data = data;
Method = method;
}
public HubResponse(CodeDefine codedefine,string method)
{
@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace IM_API.Dtos.Message
{
public class MessageQueryDto
{
[Required(ErrorMessage = "会话ID必填")]
public int ConversationId { get; set; }
// 锚点序号(如果为空,说明是第一次进聊天框,拉最新的)
public long? Cursor { get; set; }
// 查询方向:0 - 查旧(Before), 1 - 查新(After)
public int Direction { get; set; } = 0;
public int Limit { get; set; } = 20;
}
}
+2 -3
View File
@@ -7,12 +7,11 @@ namespace IM_API.Dtos
// 使用 { get; init; } 确保对象创建后不可修改,且支持无参构造
public MessageMsgType Type { get; init; } = default!;
public ChatType ChatType { get; init; } = default!;
public string? MsgId { get; init; }
public Guid MsgId { get; init; }
public int SenderId { get; init; }
public int ReceiverId { get; init; }
public int? GroupMemberId { get; init; }
public string Content { get; init; } = default!;
public DateTime TimeStamp { get; init; }
public DateTimeOffset TimeStamp { get; init; }
public MessageBaseDto() { }
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ namespace IM_API.Dtos.User
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
public DateTimeOffset Created { get; set; }
/// <summary>
/// 账户状态
@@ -18,6 +18,7 @@ namespace IM_API.Filters
{
if(context.Exception is BaseException)
{
_logger.LogWarning(context.Exception, context.Exception.Message);
var exception = (BaseException)context.Exception;
BaseResponse<object?> res = new BaseResponse<object?>(
code:exception.Code,
+5 -4
View File
@@ -5,6 +5,7 @@ using IM_API.Dtos;
using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.Tools;
using IM_API.VOs.Message;
using Microsoft.AspNetCore.SignalR;
using System.Security.Claims;
@@ -40,16 +41,16 @@ namespace IM_API.Hubs
}
await base.OnConnectedAsync();
}
public async Task<HubResponse<MessageBaseDto?>> SendMessage(MessageBaseDto dto)
public async Task<HubResponse<MessageBaseVo?>> SendMessage(MessageBaseDto dto)
{
if (!Context.User.Identity.IsAuthenticated)
{
await Clients.Caller.SendAsync("ReceiveMessage", new BaseResponse<object?>(CodeDefine.AUTH_FAILED));
Context.Abort();
return new HubResponse<MessageBaseDto?>(CodeDefine.AUTH_FAILED, "SendMessage");
return new HubResponse<MessageBaseVo?>(CodeDefine.AUTH_FAILED, "SendMessage");
}
var userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
MessageBaseDto msgInfo = null;
MessageBaseVo? msgInfo = null;
if(dto.ChatType == ChatType.PRIVATE)
{
msgInfo = await _messageService.SendPrivateMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
@@ -58,7 +59,7 @@ namespace IM_API.Hubs
{
msgInfo = await _messageService.SendGroupMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
}
return new HubResponse<MessageBaseDto?>("SendMessage", msgInfo);
return new HubResponse<MessageBaseVo?>("SendMessage", msgInfo);
}
public async Task<HubResponse<object?>> ClearUnreadCount(int conversationId)
{
+2
View File
@@ -23,9 +23,11 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.2" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
<PackageReference Include="RedLock.net" Version="2.3.2" />
<PackageReference Include="StackExchange.Redis" Version="2.9.32" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
@@ -0,0 +1,49 @@
using IM_API.Models;
namespace IM_API.Interface.Services
{
public interface ICacheService
{
/// <summary>
/// 设置缓存
/// </summary>
/// <typeparam name="T">缓存对象类型</typeparam>
/// <param name="key">缓存索引值</param>
/// <param name="value">要缓存的对象</param>
/// <param name="expiration">过期时间</param>
/// <returns></returns>
Task SetAsync<T>(string key, T value, TimeSpan? expiration = null);
/// <summary>
/// 获取缓存
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">缓存索引</param>
/// <returns></returns>
Task<T?> GetAsync<T>(string key);
/// <summary>
/// 删除缓存
/// </summary>
/// <param name="key">缓存索引</param>
/// <returns></returns>
Task RemoveAsync(string key);
/// <summary>
/// 设置用户缓存
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
Task SetUserCacheAsync(User user);
/// <summary>
/// 通过用户名获取用户缓存
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
Task<User?> GetUserCacheAsync(string username);
/// <summary>
/// 删除用户缓存
/// </summary>
/// <param name="id"></param>
/// <param name="username"></param>
/// <returns></returns>
Task RemoveUserCacheAsync(string username);
}
}
@@ -1,5 +1,6 @@
using IM_API.Dtos.Conversation;
using IM_API.Models;
using IM_API.VOs.Conversation;
namespace IM_API.Interface.Services
{
@@ -22,7 +23,7 @@ namespace IM_API.Interface.Services
/// </summary>
/// <param name="userId">用户id</param>
/// <returns></returns>
Task<List<ConversationDto>> GetConversationsAsync(int userId);
Task<List<ConversationVo>> GetConversationsAsync(int userId);
/// <summary>
/// 获取指定用户的所有推送标识符
/// </summary>
@@ -34,7 +35,7 @@ namespace IM_API.Interface.Services
/// </summary>
/// <param name="conversationId"></param>
/// <returns></returns>
Task<ConversationDto> GetConversationByIdAsync(int userId, int conversationId);
Task<ConversationVo> GetConversationByIdAsync(int userId, int conversationId);
/// <summary>
/// 清空未读消息
/// </summary>
@@ -50,5 +51,6 @@ namespace IM_API.Interface.Services
/// <param name="chatType"></param>
/// <returns></returns>
Task MakeConversationAsync(int userAId, int userBId, ChatType chatType);
Task UpdateConversationAfterSentAsync(UpdateConversationDto dto);
}
}
@@ -11,14 +11,14 @@ namespace IM_API.Interface.Services
/// <param name="groupId">群ID</param>
/// <param name="userIds">邀请的用户列表</param>
/// <returns></returns>
Task InviteUsers(int userId,int groupId, List<int> userIds);
Task InviteUsersAsync(int userId,int groupId, List<int> userIds);
/// <summary>
/// 加入群聊
/// </summary>
/// <param name="userId">操作者ID</param>
/// <param name="groupId">群ID</param>
/// <returns></returns>
Task JoinGroup(int userId,int groupId);
Task JoinGroupAsync(int userId,int groupId);
/// <summary>
/// 创建群聊
/// </summary>
@@ -26,14 +26,22 @@ namespace IM_API.Interface.Services
/// <param name="groupCreateDto">群信息</param>
/// <param name="userIds">邀请用户列表</param>
/// <returns></returns>
Task<GroupInfoDto> CreateGroup(int userId, GroupCreateDto groupCreateDto, List<int> userIds);
Task<GroupInfoDto> CreateGroupAsync(int userId, GroupCreateDto groupCreateDto);
/// <summary>
/// 删除群
/// </summary>
/// <param name="userId">操作者ID</param>
/// <param name="groupId">群ID</param>
/// <returns></returns>
Task DeleteGroup(int userId, int groupId);
//Task UpdateGroupAuthori
Task DeleteGroupAsync(int userId, int groupId);
/// <summary>
/// 获取当前用户群列表
/// </summary>
/// <param name="userId">操作者ID</param>
/// <param name="page"></param>
/// <param name="limit"></param>
/// <param name="desc"></param>
/// <returns></returns>
Task<List<GroupInfoDto>> GetGroupListAsync(int userId, int page, int limit, bool desc);
}
}
@@ -1,4 +1,7 @@
using IM_API.Dtos;
using IM_API.Dtos.Message;
using IM_API.Models;
using IM_API.VOs.Message;
namespace IM_API.Interface.Services
{
@@ -11,7 +14,7 @@ namespace IM_API.Interface.Services
/// <param name="receiverId">接收人</param>
/// <param name="dto"></param>
/// <returns></returns>
Task<MessageBaseDto> SendPrivateMessageAsync(int senderId,int receiverId,MessageBaseDto dto);
Task<MessageBaseVo> SendPrivateMessageAsync(int senderId,int receiverId,MessageBaseDto dto);
/// <summary>
/// 发送群聊消息
/// </summary>
@@ -19,16 +22,21 @@ namespace IM_API.Interface.Services
/// <param name="groupId">接收群id</param>
/// <param name="dto"></param>
/// <returns></returns>
Task<MessageBaseDto> SendGroupMessageAsync(int senderId,int groupId,MessageBaseDto dto);
Task<MessageBaseVo> SendGroupMessageAsync(int senderId,int groupId,MessageBaseDto dto);
/// <summary>
/// 获取消息列表
/// 消息入库
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
Task MakeMessageAsync(Message message);
/// <summary>
/// 获取历史消息列表
/// </summary>
/// <param name="conversationId">会话id(用于获取指定用户间聊天消息)</param>
/// <param name="msgId">消息id</param>
/// <param name="sequenceId">消息id</param>
/// <param name="pageSize">获取消息数量</param>
/// <param name="desc"></param>
/// <returns></returns>
Task<List<MessageBaseDto>> GetMessagesAsync(int userId, int conversationId,int? msgId,int? pageSize,bool desc);
Task<List<MessageBaseVo>> GetMessagesAsync(int userId,MessageQueryDto dto);
/// <summary>
/// 获取未读消息数
/// </summary>
@@ -0,0 +1,12 @@
namespace IM_API.Interface.Services
{
public interface ISequenceIdService
{
/// <summary>
/// 创建消息序号
/// </summary>
/// <param name="streamKey">聊天唯一标识/param>
/// <returns></returns>
Task<long> GetNextSquenceIdAsync(string streamKey);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,73 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "admins");
migrationBuilder.DropTable(
name: "conversations");
migrationBuilder.DropTable(
name: "devices");
migrationBuilder.DropTable(
name: "files");
migrationBuilder.DropTable(
name: "friend_request");
migrationBuilder.DropTable(
name: "friends");
migrationBuilder.DropTable(
name: "group_invite");
migrationBuilder.DropTable(
name: "group_member");
migrationBuilder.DropTable(
name: "group_request");
migrationBuilder.DropTable(
name: "login_log");
migrationBuilder.DropTable(
name: "notifications");
migrationBuilder.DropTable(
name: "permissionarole");
migrationBuilder.DropTable(
name: "messages");
migrationBuilder.DropTable(
name: "groups");
migrationBuilder.DropTable(
name: "roles");
migrationBuilder.DropTable(
name: "permissions");
migrationBuilder.DropTable(
name: "users");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class change_file : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Type",
table: "files",
newName: "FileType");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "FileType",
table: "files",
newName: "Type");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class addSequenceId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "GroupMemberId",
table: "messages");
migrationBuilder.AddColumn<long>(
name: "SequenceId",
table: "messages",
type: "bigint",
nullable: false,
defaultValue: 0L);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SequenceId",
table: "messages");
migrationBuilder.AddColumn<int>(
name: "GroupMemberId",
table: "messages",
type: "int(11)",
nullable: true,
comment: "若为群消息则表示具体的成员id");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class updateconversationlastreadmessageid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "conversations_ibfk_2",
table: "conversations");
migrationBuilder.AddForeignKey(
name: "conversations_ibfk_2",
table: "conversations",
column: "lastReadMessageId",
principalTable: "messages",
principalColumn: "ID",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "conversations_ibfk_2",
table: "conversations");
migrationBuilder.AddForeignKey(
name: "conversations_ibfk_2",
table: "conversations",
column: "lastReadMessageId",
principalTable: "messages",
principalColumn: "ID");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,93 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class updatemessage : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "conversations_ibfk_2",
table: "conversations");
migrationBuilder.RenameIndex(
name: "lastMessageId",
table: "conversations",
newName: "LastReadSequenceId");
migrationBuilder.AddColumn<Guid>(
name: "ClientMsgId",
table: "messages",
type: "char(36)",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
collation: "ascii_general_ci");
migrationBuilder.AddColumn<int>(
name: "MessageId",
table: "conversations",
type: "int(11)",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_messages_SequenceId_StreamKey",
table: "messages",
columns: new[] { "SequenceId", "StreamKey" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_conversations_MessageId",
table: "conversations",
column: "MessageId");
migrationBuilder.AddForeignKey(
name: "FK_conversations_messages_MessageId",
table: "conversations",
column: "MessageId",
principalTable: "messages",
principalColumn: "ID");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_conversations_messages_MessageId",
table: "conversations");
migrationBuilder.DropIndex(
name: "IX_messages_SequenceId_StreamKey",
table: "messages");
migrationBuilder.DropIndex(
name: "IX_conversations_MessageId",
table: "conversations");
migrationBuilder.DropColumn(
name: "ClientMsgId",
table: "messages");
migrationBuilder.DropColumn(
name: "MessageId",
table: "conversations");
migrationBuilder.RenameIndex(
name: "LastReadSequenceId",
table: "conversations",
newName: "lastMessageId");
migrationBuilder.AddForeignKey(
name: "conversations_ibfk_2",
table: "conversations",
column: "lastReadMessageId",
principalTable: "messages",
principalColumn: "ID",
onDelete: ReferentialAction.SetNull);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class updatedatetimeToDateTimeOffset : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class updateDateTimeOffsettype : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -30,12 +30,12 @@ public partial class Admin
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
public DateTimeOffset Created { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime Updated { get; set; }
public DateTimeOffset Updated { get; set; }
public virtual Role Role { get; set; } = null!;
}
+5 -4
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -20,14 +21,14 @@ public partial class Conversation
/// <summary>
/// 最后一条未读消息ID
/// </summary>
public int? LastReadMessageId { get; set; }
public long? LastReadSequenceId { get; set; }
/// <summary>
/// 未读消息数
/// </summary>
public int UnreadCount { get; set; }
public int ChatType { get; set; }
public ChatType ChatType { get; set; }
/// <summary>
/// 消息推送唯一标识符
@@ -42,9 +43,9 @@ public partial class Conversation
/// <summary>
/// 最后一条消息发送时间
/// </summary>
public DateTime LastMessageTime { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset LastMessageTime { get; set; }
public virtual Message? LastReadMessage { get; set; }
public virtual User User { get; set; } = null!;
}
+18
View File
@@ -0,0 +1,18 @@
using IM_API.Dtos.Conversation;
namespace IM_API.Models
{
public partial class Conversation
{
public ChatType ChatTypeEnum {
get
{
return ChatType;
}
set
{
ChatType = value;
}
}
}
}
+3 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -21,7 +22,8 @@ public partial class Device
/// <summary>
/// 最后一次登录
/// </summary>
public DateTime LastLogin { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset LastLogin { get; set; }
public virtual User User { get; set; } = null!;
}
+4 -2
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -25,7 +26,7 @@ public partial class File
/// <summary>
/// 文件类型
/// </summary>
public string Type { get; set; } = null!;
public string FileType { get; set; } = null!;
/// <summary>
/// 关联消息ID
@@ -35,7 +36,8 @@ public partial class File
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
public virtual Message Message { get; set; } = null!;
}
+3 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -26,7 +27,8 @@ public partial class Friend
/// <summary>
/// 好友关系创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
/// <summary>
/// 好友备注名
+3 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -20,7 +21,8 @@ public partial class FriendRequest
/// <summary>
/// 申请时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
/// <summary>
/// 申请附言
+3 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -42,7 +43,8 @@ public partial class Group
/// <summary>
/// 群聊创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
/// <summary>
/// 群头像
+1 -1
View File
@@ -31,7 +31,7 @@ public partial class GroupInvite
/// <summary>
/// 创建时间
/// </summary>
public DateTime? Created { get; set; }
public DateTimeOffset? Created { get; set; }
public virtual Group Group { get; set; } = null!;
+3 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -25,7 +26,8 @@ public partial class GroupMember
/// <summary>
/// 加入群聊时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
public virtual Group Group { get; set; } = null!;
+3 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -31,7 +32,8 @@ public partial class GroupRequest
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
public virtual Group Group { get; set; } = null!;
+1 -1
View File
@@ -19,7 +19,7 @@ namespace IM_API.Models
});
modelBuilder.Entity<Conversation>(entity =>
{
entity.Ignore(e => e.ChatTypeEnum);
});
modelBuilder.Entity<Device>(entity =>
+9 -10
View File
@@ -101,7 +101,8 @@ public partial class ImContext : DbContext
entity.HasIndex(e => e.UserId, "Userid");
entity.HasIndex(e => e.LastReadMessageId, "lastMessageId");
entity.HasIndex(e => e.LastReadSequenceId, "LastReadSequenceId");
entity.Property(e => e.Id)
.HasColumnType("int(11)")
@@ -113,7 +114,7 @@ public partial class ImContext : DbContext
entity.Property(e => e.LastMessageTime)
.HasComment("最后一条消息发送时间")
.HasColumnType("datetime");
entity.Property(e => e.LastReadMessageId)
entity.Property(e => e.LastReadSequenceId)
.HasComment("最后一条未读消息ID ")
.HasColumnType("int(11)")
.HasColumnName("lastReadMessageId");
@@ -130,10 +131,6 @@ public partial class ImContext : DbContext
.HasComment("用户")
.HasColumnType("int(11)");
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)
.HasForeignKey(d => d.UserId)
.OnDelete(DeleteBehavior.ClientSetNull)
@@ -197,7 +194,7 @@ public partial class ImContext : DbContext
entity.Property(e => e.Size)
.HasComment("文件大小(单位:KB ")
.HasColumnType("int(11)");
entity.Property(e => e.Type)
entity.Property(e => e.FileType)
.HasMaxLength(10)
.HasComment("文件类型 ");
entity.Property(e => e.Url)
@@ -529,6 +526,11 @@ public partial class ImContext : DbContext
entity.HasIndex(e => e.Sender, "Sender");
//设置联合唯一索引
entity.HasIndex(e => new { e.SequenceId, e.StreamKey })
.IsUnique();
entity.Property(e => e.Id)
.HasColumnType("int(11)")
.HasColumnName("ID");
@@ -541,9 +543,6 @@ public partial class ImContext : DbContext
entity.Property(e => e.Created)
.HasComment("发送时间 ")
.HasColumnType("datetime");
entity.Property(e => e.GroupMemberId)
.HasComment("若为群消息则表示具体的成员id")
.HasColumnType("int(11)");
entity.Property(e => e.MsgType)
.HasComment("消息类型\r\n(0:文本,1:图片,2:语音,3:视频,4:文件,5:语音聊天,6:视频聊天)")
.HasColumnType("tinyint(4)");
+3 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -15,7 +16,8 @@ public partial class LoginLog
/// <summary>
/// 登录时间
/// </summary>
public DateTime Logined { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Logined { get; set; }
/// <summary>
/// 登录用户
+7 -3
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -18,6 +19,7 @@ public partial class Message
/// (0:文本,1:图片,2:语音,3:视频,4:文件,5:语音聊天,6:视频聊天)
/// </summary>
public sbyte MsgType { get; set; }
public Guid ClientMsgId { get; set; }
/// <summary>
/// 消息内容
@@ -42,7 +44,8 @@ public partial class Message
/// <summary>
/// 发送时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
/// <summary>
/// 消息推送唯一标识符
@@ -50,9 +53,10 @@ public partial class Message
public string StreamKey { get; set; } = null!;
/// <summary>
/// 若为群消息则表示具体的成员id
/// 消息排序标识
/// </summary>
public int? GroupMemberId { get; set; }
public long SequenceId { get; set; }
public virtual ICollection<Conversation> Conversations { get; set; } = new List<Conversation>();
+3 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -30,7 +31,8 @@ public partial class Notification
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
public virtual User User { get; set; } = null!;
}
+3 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -25,7 +26,8 @@ public partial class Permission
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
public virtual ICollection<Permissionarole> Permissionaroles { get; set; } = new List<Permissionarole>();
}
+3 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -20,7 +21,8 @@ public partial class Role
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
public virtual ICollection<Admin> Admins { get; set; } = new List<Admin>();
+4 -2
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@@ -32,12 +33,13 @@ public partial class User
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
public DateTimeOffset Created { get; set; }
/// <summary>
/// 修改时间
/// </summary>
public DateTime? Updated { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset? Updated { get; set; }
/// <summary>
/// 账户状态
+11 -6
View File
@@ -1,5 +1,6 @@
using IM_API.Configs;
using IM_API.Configs.Options;
using IM_API.Filters;
using IM_API.Hubs;
using IM_API.Models;
@@ -25,18 +26,22 @@ namespace IM_API
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
string conStr = configuration.GetConnectionString("DefaultConnection")!;
string redisConStr = configuration.GetConnectionString("Redis");
var conOptions = configuration.GetSection("ConnectionStrings").Get<ConnectionOptions>();
//注入数据库上下文
builder.Services.AddDbContext<ImContext>(options =>
{
options.UseMySql(conStr,ServerVersion.AutoDetect(conStr));
options.UseMySql(conOptions.DefaultConnection,ServerVersion.AutoDetect(conOptions.DefaultConnection));
});
//注入redis
var redis = ConnectionMultiplexer.Connect(redisConStr);
var redis = ConnectionMultiplexer.Connect(conOptions.Redis);
builder.Services.AddSingleton<IConnectionMultiplexer>(redis);
builder.Services.AddRabbitMQ(configuration.GetSection("RabbitMqOptions").Get<RabbitMqOptions>());
builder.Services.AddStackExchangeRedisCache(options =>
{
options.ConnectionMultiplexerFactory = () => Task.FromResult<IConnectionMultiplexer>(redis);
});
builder.Services.AddRabbitMQ(configuration.GetSection("RabbitMqOptions").Get<RabbitMQOptions>());
builder.Services.AddAllService(configuration);
@@ -116,7 +121,7 @@ namespace IM_API
}).AddJsonOptions(options =>
{
// 保持 ISO 8601 格式
options.JsonSerializerOptions.Converters.Add(new UtcDateTimeConverter());
//options.JsonSerializerOptions.Converters.Add(new UtcDateTimeConverter());
// 将枚举转换为字符串
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
// 建议:保持驼峰命名
+6 -1
View File
@@ -14,15 +14,19 @@ namespace IM_API.Services
private readonly ImContext _context;
private readonly ILogger<AuthService> _logger;
private readonly IMapper _mapper;
public AuthService(ImContext context, ILogger<AuthService> logger, IMapper mapper)
private readonly ICacheService _cache;
public AuthService(ImContext context, ILogger<AuthService> logger, IMapper mapper, ICacheService cache)
{
_context = context;
_logger = logger;
_mapper = mapper;
_cache = cache;
}
public async Task<User> LoginAsync(LoginRequestDto dto)
{
var userinfo = await _cache.GetUserCacheAsync(dto.Username);
if (userinfo != null && userinfo.Password == dto.Password) return userinfo;
string username = dto.Username;
string password = dto.Password;
var user = await _context.Users.FirstOrDefaultAsync(x => x.Username == username && x.Password == password);
@@ -30,6 +34,7 @@ namespace IM_API.Services
{
throw new BaseException(CodeDefine.PASSWORD_ERROR);
}
await _cache.SetUserCacheAsync(user);
return user;
}
+40 -21
View File
@@ -4,6 +4,7 @@ using IM_API.Exceptions;
using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.Tools;
using IM_API.VOs.Conversation;
using Microsoft.EntityFrameworkCore;
namespace IM_API.Services
@@ -27,26 +28,26 @@ namespace IM_API.Services
#endregion
#region
public async Task<List<ConversationDto>> GetConversationsAsync(int userId)
public async Task<List<ConversationVo>> GetConversationsAsync(int userId)
{
// 1. 获取私聊会话
var privateList = await (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
where c.UserId == userId && c.ChatType == ChatType.PRIVATE
select new { c, f.Avatar, f.RemarkName })
.ToListAsync();
// 2. 获取群聊会话
var groupList = await (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
where c.UserId == userId && c.ChatType == ChatType.GROUP
select new { c, g.Avatar, g.Name })
.ToListAsync();
var privateDtos = privateList.Select(x =>
{
var dto = _mapper.Map<ConversationDto>(x.c);
var dto = _mapper.Map<ConversationVo>(x.c);
dto.TargetAvatar = x.Avatar;
dto.TargetName = x.RemarkName;
return dto;
@@ -54,7 +55,7 @@ namespace IM_API.Services
var groupDtos = groupList.Select(x =>
{
var dto = _mapper.Map<ConversationDto>(x.c);
var dto = _mapper.Map<ConversationVo>(x.c);
dto.TargetAvatar = x.Avatar;
dto.TargetName = x.Name;
return dto;
@@ -89,25 +90,23 @@ namespace IM_API.Services
#endregion
#region
public async Task<ConversationDto> GetConversationByIdAsync(int userId, int conversationId)
public async Task<ConversationVo> GetConversationByIdAsync(int userId, int conversationId)
{
var conversation = await _context.Conversations
.Include(x => x.LastReadMessage)
.FirstOrDefaultAsync(
x => x.UserId == userId && x.Id == conversationId
);
if (conversation is null) throw new BaseException(CodeDefine.CONVERSATION_NOT_FOUND);
var dto = _mapper.Map<ConversationDto>(conversation);
//dto.LastReadMessage = _mapper.Map<MessageBaseDto>(conversation);
if(conversation.ChatType == (int)ChatType.PRIVATE)
var dto = _mapper.Map<ConversationVo>(conversation);
if(conversation.ChatType == ChatType.PRIVATE)
{
var friendInfo = await _context.Friends.FirstOrDefaultAsync(
x => x.UserId == userId && x.FriendId == conversation.TargetId
var friendInfo = await _context.Friends.Include(n => n.FriendNavigation).FirstOrDefaultAsync(
x => x.UserId == conversation.UserId && x.FriendId == conversation.TargetId
);
if (friendInfo is null) throw new BaseException(CodeDefine.FRIEND_RELATION_NOT_FOUND);
_mapper.Map(friendInfo,dto);
}
if(conversation.ChatType == (int)ChatType.GROUP)
if(conversation.ChatType == ChatType.GROUP)
{
var groupInfo = await _context.Groups.FirstOrDefaultAsync(
x => x.Id == conversation.TargetId
@@ -125,12 +124,18 @@ namespace IM_API.Services
if (conversation is null) throw new BaseException(CodeDefine.CONVERSATION_NOT_FOUND);
var message = await _context.Messages
.Where(x => x.StreamKey == conversation.StreamKey)
.OrderByDescending(x => x.Id)
.OrderByDescending(x => x.SequenceId)
.FirstOrDefaultAsync();
conversation.LastReadMessage = message;
conversation.UnreadCount = 0;
_context.Conversations.Update(conversation);
await _context.SaveChangesAsync();
if(message != null)
{
conversation.UnreadCount = 0;
conversation.LastMessage = message.Content;
conversation.LastReadSequenceId = message.SequenceId;
conversation.LastMessageTime = message.Created;
_context.Conversations.Update(conversation);
await _context.SaveChangesAsync();
}
return true;
}
@@ -143,10 +148,10 @@ namespace IM_API.Services
StreamKeyBuilder.Private(userAId, userBId) : StreamKeyBuilder.Group(userBId);
var conversation = new Conversation()
{
ChatType = (int)chatType,
ChatType = chatType,
LastMessage = "",
LastMessageTime = DateTime.UtcNow,
LastReadMessageId = -1,
LastMessageTime = DateTime.Now,
LastReadSequenceId = null,
StreamKey = streamKey,
TargetId = userBId,
UnreadCount = 0,
@@ -156,5 +161,19 @@ namespace IM_API.Services
_context.Conversations.Add(conversation);
await _context.SaveChangesAsync();
}
public async Task UpdateConversationAfterSentAsync(UpdateConversationDto dto)
{
var cList = await _context.Conversations.Where(x => x.StreamKey == dto.StreamKey).ToListAsync();
foreach(var c in cList)
{
bool isSender = dto.SenderId == c.UserId;
c.LastMessage = dto.LastMessage;
c.LastMessageTime = dto.DateTime;
c.LastReadSequenceId = isSender ? dto.LastSequenceId : c.LastReadSequenceId;
c.UnreadCount = isSender ? 0 : c.UnreadCount + 1;
}
_context.Conversations.UpdateRange(cList);
await _context.SaveChangesAsync();
}
}
}
+4 -4
View File
@@ -132,8 +132,8 @@ namespace IM_API.Services
await _endpoint.Publish(new FriendAddEvent()
{
AggregateId = friendRequest.Id.ToString(),
OccurredAt = DateTime.UtcNow,
Created = DateTime.UtcNow,
OccurredAt = DateTime.Now,
Created = DateTime.Now,
EventId = Guid.NewGuid(),
OperatorId = friendRequest.ResponseUser,
RequestInfo = _mapper.Map<FriendRequestDto>(friendRequest),
@@ -183,7 +183,7 @@ namespace IM_API.Services
await _endpoint.Publish(new RequestFriendEvent()
{
AggregateId = friendRequst.Id.ToString(),
OccurredAt = friendRequst.Created,
OccurredAt = friendRequst.Created.UtcDateTime,
Description = friendRequst.Description,
EventId = Guid.NewGuid(),
FromUserId = friendRequst.RequestUser,
@@ -205,7 +205,7 @@ namespace IM_API.Services
Friend friendA = new Friend()
{
Avatar = userbInfo.Avatar,
Created = DateTime.UtcNow,
Created = DateTime.Now,
FriendId = userbInfo.Id,
RemarkName = remarkName ?? userbInfo.NickName,
StatusEnum = FriendStatus.Added,
+25 -6
View File
@@ -1,4 +1,5 @@
using AutoMapper;
using AutoMapper.QueryableExtensions;
using IM_API.Domain.Events;
using IM_API.Dtos.Group;
using IM_API.Exceptions;
@@ -27,7 +28,7 @@ namespace IM_API.Services
private async Task<List<GroupInvite>> GetGroupInvites(int userId, int groupId, List<int> ids)
{
DateTime dateTime = DateTime.UtcNow;
DateTime dateTime = DateTime.Now;
//验证被邀请用户是否为好友
var validFriendIds = await _context.Friends
.Where(f => f.UserId == userId && ids.Contains(f.FriendId))
@@ -44,13 +45,14 @@ namespace IM_API.Services
}).ToList();
}
public async Task<GroupInfoDto> CreateGroup(int userId, GroupCreateDto groupCreateDto, List<int> userIds)
public async Task<GroupInfoDto> CreateGroupAsync(int userId, GroupCreateDto groupCreateDto)
{
List<int> userIds = groupCreateDto.UserIDs ?? [];
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
//先创建群
DateTime dateTime = DateTime.UtcNow;
DateTime dateTime = DateTime.Now;
Group group = _mapper.Map<Group>(groupCreateDto);
group.GroupMaster = userId;
_context.Groups.Add(group);
@@ -90,21 +92,38 @@ namespace IM_API.Services
}
}
public Task DeleteGroup(int userId, int groupId)
public Task DeleteGroupAsync(int userId, int groupId)
{
throw new NotImplementedException();
}
public async Task InviteUsers(int userId, int groupId, List<int> userIds)
public async Task InviteUsersAsync(int userId, int groupId, List<int> userIds)
{
var group = await _context.Groups.FirstOrDefaultAsync(
x => x.Id == groupId) ?? throw new BaseException(CodeDefine.GROUP_NOT_FOUND);
}
public Task JoinGroup(int userId, int groupId)
public Task JoinGroupAsync(int userId, int groupId)
{
throw new NotImplementedException();
}
public async Task<List<GroupInfoDto>> GetGroupListAsync(int userId, int page, int limit, bool desc)
{
var query = _context.GroupMembers
.Where(x => x.UserId == userId)
.Select(s => s.Group);
if (desc)
{
query = query.OrderByDescending(x => x.Id);
}
var list = await query
.Skip((page - 1) * limit)
.Take(limit)
.ProjectTo<GroupInfoDto>(_mapper.ConfigurationProvider)
.ToListAsync();
return list;
}
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ namespace IM_API.Services
public (string token, DateTime expiresAt) CreateAccessTokenForUser(int userId, string username, string role)
{
var expiresAt = DateTime.UtcNow.AddMinutes(_accessMinutes);
var expiresAt = DateTime.Now.AddMinutes(_accessMinutes);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
+49 -40
View File
@@ -2,12 +2,18 @@
using IM_API.Application.Interfaces;
using IM_API.Domain.Events;
using IM_API.Dtos;
using IM_API.Dtos.Message;
using IM_API.Exceptions;
using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.Tools;
using IM_API.VOs.Message;
using MassTransit;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
using System.Text.RegularExpressions;
using static MassTransit.Monitoring.Performance.BuiltInCounters;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
namespace IM_API.Services
{
@@ -19,9 +25,10 @@ namespace IM_API.Services
//废弃,此处已使用rabbitMQ替代
//private readonly IEventBus _eventBus;
private readonly IPublishEndpoint _endpoint;
private readonly ISequenceIdService _sequenceIdService;
public MessageService(
ImContext context, ILogger<MessageService> logger, IMapper mapper, IEventBus eventBus,
IPublishEndpoint publishEndpoint
ImContext context, ILogger<MessageService> logger, IMapper mapper,
IPublishEndpoint publishEndpoint, ISequenceIdService sequenceIdService
)
{
_context = context;
@@ -29,40 +36,43 @@ namespace IM_API.Services
_mapper = mapper;
//_eventBus = eventBus;
_endpoint = publishEndpoint;
_sequenceIdService = sequenceIdService;
}
public async Task<List<MessageBaseDto>> GetMessagesAsync(int userId, int conversationId, int? msgId, int? pageSize, bool desc)
public async Task<List<MessageBaseVo>> GetMessagesAsync(int userId,MessageQueryDto dto)
{
//获取会话信息,用于获取双方聊天的唯一标识streamkey
Conversation? conversation = await _context.Conversations.FirstOrDefaultAsync(
x => x.Id == conversationId && x.UserId == userId
x => x.Id == dto.ConversationId && x.UserId == userId
);
if (conversation is null) throw new BaseException(CodeDefine.CONVERSATION_NOT_FOUND);
var query = _context.Messages.AsQueryable();
if(msgId != null)
var baseQuery = _context.Messages.Where(x => x.StreamKey == conversation.StreamKey);
if (dto.Direction == 0) // Before: 找比锚点小的,按倒序排
{
query = query.Where(
x => x.StreamKey == conversation.StreamKey && x.Id < msgId.Value
)
.OrderByDescending(x => x.Id);
if (dto.Cursor.HasValue)
baseQuery = baseQuery.Where(m => m.SequenceId < dto.Cursor.Value);
var list = await baseQuery
.OrderByDescending(m => m.SequenceId) // 最新消息在最前
.Take(dto.Limit)
.Select(m => _mapper.Map<MessageBaseVo>(m))
.ToListAsync();
return list.OrderBy(s => s.SequenceId).ToList();
}
else
else // After: 找比锚点大的,按正序排(用于补洞或刷新)
{
query = query.Where(
x => x.StreamKey == conversation.StreamKey && x.Id > conversation.LastReadMessageId
);
// 如果 Cursor 为空且是 After,逻辑上说不通,通常直接返回空或报错
if (!dto.Cursor.HasValue) return new List<MessageBaseVo>();
return await baseQuery
.Where(m => m.SequenceId > dto.Cursor.Value)
.OrderBy(m => m.SequenceId) // 按时间线正序
.Take(dto.Limit)
.Select(m => _mapper.Map<MessageBaseVo>(m))
.ToListAsync();
}
if(pageSize != null)
{
query = query.Take(pageSize.Value);
}
var msgList = await query
.ToListAsync();
msgList = msgList
.OrderBy(x => x.Created)
.ThenBy(t => t.Id)
.ToList();
return _mapper.Map<List<MessageBaseDto>>(msgList);
}
public Task<int> GetUnreadCountAsync(int userId)
@@ -89,8 +99,14 @@ namespace IM_API.Services
{
throw new NotImplementedException();
}
public async Task MakeMessageAsync(Message message)
{
_context.Messages.Add(message);
await _context.SaveChangesAsync();
}
#region
public async Task<MessageBaseDto> SendGroupMessageAsync(int senderId, int groupId, MessageBaseDto dto)
public async Task<MessageBaseVo> SendGroupMessageAsync(int senderId, int groupId, MessageBaseDto dto)
{
//判断群存在
var isExist = await _context.Groups.AnyAsync(x => x.Id == groupId);
@@ -99,30 +115,23 @@ namespace IM_API.Services
var isMember = await _context.GroupMembers.AnyAsync(x => x.GroupId == groupId && x.UserId == senderId);
if (!isMember) throw new BaseException(CodeDefine.NO_GROUP_PERMISSION);
var message = _mapper.Map<Message>(dto);
message.Sender = senderId;
message.StreamKey = StreamKeyBuilder.Group(
groupId);
_context.Messages.Add(message);
await _context.SaveChangesAsync();
message.StreamKey = StreamKeyBuilder.Group(groupId);
message.SequenceId = await _sequenceIdService.GetNextSquenceIdAsync(message.StreamKey);
await _endpoint.Publish(_mapper.Map<MessageCreatedEvent>(message));
return _mapper.Map<MessageBaseDto>(message);
return _mapper.Map<MessageBaseVo>(message);
}
#endregion
#region
public async Task<MessageBaseDto> SendPrivateMessageAsync(int senderId, int receiverId, MessageBaseDto dto)
public async Task<MessageBaseVo> SendPrivateMessageAsync(int senderId, int receiverId, MessageBaseDto dto)
{
bool isExist = await _context.Friends.AnyAsync(x => x.FriendId == receiverId);
if (!isExist) throw new BaseException(CodeDefine.FRIEND_RELATION_NOT_FOUND);
var message = _mapper.Map<Message>(dto);
message.Sender = senderId;
//生成消息流唯一标识符
message.StreamKey = StreamKeyBuilder.Private(dto.SenderId, dto.ReceiverId);
_context.Messages.Add(message);
await _context.SaveChangesAsync();
message.StreamKey = StreamKeyBuilder.Private(senderId, receiverId);
message.SequenceId = await _sequenceIdService.GetNextSquenceIdAsync(message.StreamKey);
await _endpoint.Publish(_mapper.Map<MessageCreatedEvent>(message));
return _mapper.Map<MessageBaseDto>(message);
return _mapper.Map<MessageBaseVo>(message);
}
#endregion
}
@@ -0,0 +1,62 @@
using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.Tools;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
namespace IM_API.Services
{
public class RedisCacheService:ICacheService
{
private readonly IDistributedCache _cache;
public RedisCacheService(IDistributedCache cache)
{
_cache = cache;
}
public async Task<T?> GetAsync<T>(string key)
{
var valueBytes= await _cache.GetAsync(key);
if (valueBytes is null || valueBytes.Length == 0) return default;
return JsonSerializer.Deserialize<T>(valueBytes);
}
public async Task<User?> GetUserCacheAsync(string username)
{
var usernameKey = RedisKeys.GetUserinfoKeyByUsername(username);
var userid = await GetAsync<string>(usernameKey);
if (userid is null) return default;
var key = RedisKeys.GetUserinfoKey(userid);
return await GetAsync<User>(key);
}
public async Task RemoveAsync(string key) => await _cache.RemoveAsync(key);
public async Task RemoveUserCacheAsync(string username)
{
var usernameKey = RedisKeys.GetUserinfoKeyByUsername(username);
var userid = await GetAsync<string>(usernameKey);
if (userid is null) return;
var key = RedisKeys.GetUserinfoKey(userid);
await RemoveAsync(key);
}
public async Task SetAsync<T>(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);
}
public async Task SetUserCacheAsync(User user)
{
var idKey = RedisKeys.GetUserinfoKey(user.Id.ToString());
await SetAsync(idKey, user);
var usernameKey = RedisKeys.GetUserinfoKeyByUsername(user.Username);
await SetAsync(usernameKey, user.Id.ToString());
}
}
}
@@ -0,0 +1,46 @@
using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.Tools;
using Microsoft.EntityFrameworkCore;
using RedLockNet;
using StackExchange.Redis;
namespace IM_API.Services
{
public class SequenceIdService : ISequenceIdService
{
private IDatabase _database;
private IDistributedLockFactory _lockFactory;
private ImContext _context;
public SequenceIdService(IConnectionMultiplexer connectionMultiplexer,
IDistributedLockFactory distributedLockFactory, ImContext imContext)
{
_database = connectionMultiplexer.GetDatabase();
_lockFactory = distributedLockFactory;
_context = imContext;
}
public async Task<long> GetNextSquenceIdAsync(string streamKey)
{
string key = RedisKeys.GetSequenceIdKey(streamKey);
string lockKey = RedisKeys.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 _context.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);
}
}
}
+16 -2
View File
@@ -5,6 +5,7 @@ using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.Tools;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
namespace IM_API.Services
{
@@ -13,31 +14,43 @@ namespace IM_API.Services
private readonly ImContext _context;
private readonly ILogger<UserService> _logger;
private readonly IMapper _mapper;
public UserService(ImContext imContext,ILogger<UserService> logger,IMapper mapper)
private readonly ICacheService _cacheService;
public UserService(ImContext imContext,ILogger<UserService> logger,IMapper mapper, ICacheService cacheService)
{
this._context = imContext;
this._logger = logger;
this._mapper = mapper;
_cacheService = cacheService;
}
#region
public async Task<UserInfoDto> GetUserInfoAsync(int userId)
{
var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == userId);
//查询redis缓存,如果存在直接返回不走查库逻辑
var key = RedisKeys.GetUserinfoKey(userId.ToString());
var userinfoCache = await _cacheService.GetAsync<User>(key);
if (userinfoCache != null) return _mapper.Map<UserInfoDto>(userinfoCache);
//无缓存查库
var user = await _context.Users
.FirstOrDefaultAsync(x => x.Id == userId);
if (user == null)
{
throw new BaseException(CodeDefine.USER_NOT_FOUND);
}
await _cacheService.SetUserCacheAsync(user);
return _mapper.Map<UserInfoDto>(user);
}
#endregion
#region
public async Task<UserInfoDto> GetUserInfoByUsernameAsync(string username)
{
var userinfo = await _cacheService.GetUserCacheAsync(username);
if (userinfo != null) return _mapper.Map<UserInfoDto>(userinfo);
var user = await _context.Users.FirstOrDefaultAsync(x => x.Username == username);
if (user == null)
{
throw new BaseException(CodeDefine.USER_NOT_FOUND);
}
await _cacheService.SetUserCacheAsync(user);
return _mapper.Map<UserInfoDto>(user);
}
#endregion
@@ -70,6 +83,7 @@ namespace IM_API.Services
if (user is null) throw new BaseException(CodeDefine.USER_NOT_FOUND);
_mapper.Map(dto,user);
await _context.SaveChangesAsync();
await _cacheService.SetUserCacheAsync(user);
return _mapper.Map<UserInfoDto>(user);
}
#endregion
+22
View File
@@ -0,0 +1,22 @@
namespace IM_API.Tools
{
public static class RedisKeys
{
public static string GetUserinfoKey(string userId)
{
return $"user::uinfo::{userId}";
}
public static string GetUserinfoKeyByUsername(string username)
{
return $"user::uinfobyid::{username}";
}
public static string GetSequenceIdKey(string streamKey)
{
return $"chat::seq::{streamKey}";
}
public static string GetSequenceIdLockKey(string streamKey)
{
return $"lock::seq::{streamKey}";
}
}
}
@@ -1,14 +1,14 @@
using IM_API.Dtos.User;
namespace IM_API.Dtos.Auth
namespace IM_API.VOs.Auth
{
public class LoginDto
public class LoginVo
{
public UserInfoDto userInfo { get; set; }
public string Token { get; set; }
public string RefreshToken { get; set; }
public DateTime ExpireAt { get; set; }
public LoginDto(UserInfoDto userInfo,string token, string refreshToken, DateTime expireAt) {
public DateTimeOffset ExpireAt { get; set; }
public LoginVo(UserInfoDto userInfo,string token, string refreshToken, DateTime expireAt) {
this.userInfo = userInfo;
Token = token;
RefreshToken = refreshToken;
@@ -1,8 +1,10 @@
using IM_API.Models;
using IM_API.Dtos;
using IM_API.Models;
using IM_API.VOs.Message;
namespace IM_API.Dtos.Conversation
namespace IM_API.VOs.Conversation
{
public class ConversationDto
public class ConversationVo
{
public int Id { get; set; }
@@ -19,14 +21,14 @@ namespace IM_API.Dtos.Conversation
/// <summary>
/// 最后一条未读消息ID
/// </summary>
public int? LastReadMessageId { get; set; }
public long? LastSequenceId { get; set; }
/// <summary>
/// 未读消息数
/// </summary>
public int UnreadCount { get; set; }
public int ChatType { get; set; }
public ChatType ChatType { get; set; }
/// <summary>
@@ -36,7 +38,7 @@ namespace IM_API.Dtos.Conversation
/// <summary>
/// 最后一条消息时间
/// </summary>
public DateTime? DateTime { get; set; } = null;
public DateTimeOffset? DateTime { get; set; } = null;
/// <summary>
/// 对方昵称
@@ -46,6 +48,6 @@ namespace IM_API.Dtos.Conversation
/// 对方头像
/// </summary>
public string? TargetAvatar { get; set; }
public MessageBaseDto? LastReadMessage { get; set; }
}
}
@@ -0,0 +1,9 @@
using IM_API.Dtos;
namespace IM_API.VOs.Message
{
public record MessageBaseVo:MessageBaseDto
{
public int SequenceId { get; set; }
}
}