This commit is contained in:
2026-02-08 18:43:20 +08:00
parent ed95bdddac
commit eff83a9f68
11 changed files with 132 additions and 39 deletions
+28
View File
@@ -0,0 +1,28 @@
name: "IM 缺陷报告"
description: "记录聊天消息、SignalR 或缓存相关的问题"
title: "[BUG]简要描述问题"
labels: ["bug", "high-priority"]
body:
- type: markdown
attributes:
value: "请详细填写 Bug 信息,这将有助于快速定位后端 Redis 或 SignalR 的问题。"
- type: input
id: stream_key
attributes:
label: "相关 StreamKey / 群组 ID"
placeholder: "例如:group:123"
- type: textarea
id: steps
attributes:
label: "复现步骤"
value: |
1. 登录用户 A 和用户 B
2. 用户 A 向群组发送消息
3. 用户 B 界面没有反应
validations:
required: true
- type: textarea
id: logs
attributes:
label: "控制台错误日志 (Console Log)"
placeholder: "在此粘贴浏览器或后端的报错信息..."
@@ -17,33 +17,47 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
{ {
private readonly IConversationService _conversationService; private readonly IConversationService _conversationService;
private readonly ILogger<ConversationEventHandler> _logger; private readonly ILogger<ConversationEventHandler> _logger;
private readonly ImContext _context; private readonly IUserService _userSerivce;
private readonly IMapper _mapper;
public ConversationEventHandler( public ConversationEventHandler(
IConversationService conversationService, IConversationService conversationService,
ILogger<ConversationEventHandler> logger, ILogger<ConversationEventHandler> logger,
ImContext imContext, IUserService userService
IMapper mapper
) )
{ {
_conversationService = conversationService; _conversationService = conversationService;
_logger = logger; _logger = logger;
_context = imContext; _userSerivce = userService;
_mapper = mapper;
} }
public async Task Consume(ConsumeContext<MessageCreatedEvent> context) public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
{ {
var @event = context.Message; var @event = context.Message;
await _conversationService.UpdateConversationAfterSentAsync(new Dtos.Conversation.UpdateConversationDto if (@event.ChatType == ChatType.GROUP)
{ {
LastMessage = @event.MessageContent, var userinfo = await _userSerivce.GetUserInfoAsync(@event.MsgSenderId);
LastSequenceId = @event.SequenceId, await _conversationService.UpdateConversationAfterSentAsync(new Dtos.Conversation.UpdateConversationDto
ReceiptId = @event.MsgRecipientId, {
SenderId = @event.MsgSenderId, LastMessage = $"{userinfo.NickName}{@event.MessageContent}",
StreamKey = @event.StreamKey, LastSequenceId = @event.SequenceId,
DateTime = @event.MessageCreated ReceiptId = @event.MsgRecipientId,
}); SenderId = @event.MsgSenderId,
StreamKey = @event.StreamKey,
DateTime = @event.MessageCreated
});
}
else
{
await _conversationService.UpdateConversationAfterSentAsync(new Dtos.Conversation.UpdateConversationDto
{
LastMessage = @event.MessageContent,
LastSequenceId = @event.SequenceId,
ReceiptId = @event.MsgRecipientId,
SenderId = @event.MsgSenderId,
StreamKey = @event.StreamKey,
DateTime = @event.MessageCreated
});
}
} }
} }
@@ -25,29 +25,17 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
{ {
Console.ForegroundColor = ConsoleColor.Red; Console.ForegroundColor = ConsoleColor.Red;
var @event = context.Message; var @event = context.Message;
Console.WriteLine($"[SignalR]handlerCreated!");
try try
{ {
// 先转成实体,如果这一步都报错,说明之前的 sbyte -> enum 还没改好
var entity = _mapper.Map<Message>(@event); var entity = _mapper.Map<Message>(@event);
// 再从实体转 VO,这是最稳妥的路径,因为这两者的映射你肯定配过了
var messageBaseVo = _mapper.Map<MessageBaseVo>(entity); var messageBaseVo = _mapper.Map<MessageBaseVo>(entity);
await _hub.Clients.Group(@event.StreamKey).SendAsync("ReceiveMessage", new HubResponse<MessageBaseVo>("Event", messageBaseVo));
// 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) catch (Exception ex)
{ {
Console.WriteLine($"[SignalR] 发送失败: {ex.Message}"); Console.WriteLine($"[SignalR] 发送失败: {ex.Message}");
Console.ResetColor(); Console.ResetColor();
throw; // 抛出异常触发 MassTransit 重试 throw;
} }
} }
} }
@@ -8,6 +8,7 @@ using IM_API.VOs.Auth;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
namespace IM_API.Controllers namespace IM_API.Controllers
{ {
@@ -39,13 +40,20 @@ namespace IM_API.Controllers
[HttpPost] [HttpPost]
public async Task<IActionResult> Login(LoginRequestDto dto) public async Task<IActionResult> Login(LoginRequestDto dto)
{ {
Stopwatch sw = Stopwatch.StartNew();
var user = await _authService.LoginAsync(dto); var user = await _authService.LoginAsync(dto);
_logger.LogInformation("服务耗时: {ms}ms", sw.ElapsedMilliseconds);
var userInfo = _mapper.Map<UserInfoDto>(user); var userInfo = _mapper.Map<UserInfoDto>(user);
_logger.LogInformation("序列化耗时: {ms}ms", sw.ElapsedMilliseconds);
//生成凭证 //生成凭证
(string token,DateTime expiresAt) = _jwtService.CreateAccessTokenForUser(user.Id,user.Username,"user"); (string token,DateTime expiresAt) = _jwtService.CreateAccessTokenForUser(user.Id,user.Username,"user");
_logger.LogInformation("Token生成耗时: {ms}ms", sw.ElapsedMilliseconds);
//生成刷新凭证 //生成刷新凭证
string refreshToken = await _refreshTokenService.CreateRefreshTokenAsync(user.Id); string refreshToken = await _refreshTokenService.CreateRefreshTokenAsync(user.Id);
_logger.LogInformation("RefreshToken生成耗时: {ms}ms", sw.ElapsedMilliseconds);
var res = new BaseResponse<LoginVo>(new LoginVo(userInfo,token,refreshToken, expiresAt)); var res = new BaseResponse<LoginVo>(new LoginVo(userInfo,token,refreshToken, expiresAt));
_logger.LogInformation("总耗时: {ms}ms", sw.ElapsedMilliseconds);
return Ok(res); return Ok(res);
} }
[HttpPost] [HttpPost]
+12 -2
View File
@@ -5,6 +5,7 @@ using IM_API.Tools;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
@@ -45,7 +46,7 @@ namespace IM_API.Controllers
{ {
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier); var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
int userId = int.Parse(userIdStr); int userId = int.Parse(userIdStr);
var userinfo = await _userService.UpdateUserAsync(userId,dto); var userinfo = await _userService.UpdateUserAsync(userId, dto);
var res = new BaseResponse<UserInfoDto>(userinfo); var res = new BaseResponse<UserInfoDto>(userinfo);
return Ok(res); return Ok(res);
@@ -84,7 +85,7 @@ namespace IM_API.Controllers
{ {
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier); var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
int userId = int.Parse(userIdStr); int userId = int.Parse(userIdStr);
await _userService.ResetPasswordAsync(userId,dto.OldPassword,dto.Password); await _userService.ResetPasswordAsync(userId, dto.OldPassword, dto.Password);
return Ok(new BaseResponse<object?>()); return Ok(new BaseResponse<object?>());
} }
/// <summary> /// <summary>
@@ -100,5 +101,14 @@ namespace IM_API.Controllers
await _userService.UpdateOlineStatusAsync(userId, dto.OnlineStatus); await _userService.UpdateOlineStatusAsync(userId, dto.OnlineStatus);
return Ok(new BaseResponse<object?>()); return Ok(new BaseResponse<object?>());
} }
[HttpPost]
[ProducesResponseType(typeof(BaseResponse<List<UserInfoDto>>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetUserList([FromBody][Required]List<int> ids)
{
var users = await _userService.GetUserInfoListAsync(ids);
var res = new BaseResponse<List<UserInfoDto>>(users);
return Ok(res);
}
} }
} }
+2 -2
View File
@@ -35,9 +35,9 @@ namespace IM_API.Hubs
//将用户加入已加入聊天的会话组 //将用户加入已加入聊天的会话组
string userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier); string userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
var keys = await _conversationService.GetUserAllStreamKeyAsync(int.Parse(userIdStr)); var keys = await _conversationService.GetUserAllStreamKeyAsync(int.Parse(userIdStr));
foreach(var key in keys) foreach (var key in keys)
{ {
await Groups.AddToGroupAsync(Context.ConnectionId,key); await Groups.AddToGroupAsync(Context.ConnectionId, key);
} }
await base.OnConnectedAsync(); await base.OnConnectedAsync();
} }
@@ -35,5 +35,11 @@ namespace IM_API.Interface.Services
/// <param name="onlineStatus"></param> /// <param name="onlineStatus"></param>
/// <returns></returns> /// <returns></returns>
Task<bool> UpdateOlineStatusAsync(int userId, UserOnlineStatus onlineStatus); Task<bool> UpdateOlineStatusAsync(int userId, UserOnlineStatus onlineStatus);
/// <summary>
/// 批量获取用户信息
/// </summary>
/// <param name="ids">用户id列表</param>
/// <returns></returns>
Task<List<UserInfoDto>> GetUserInfoListAsync(List<int> ids);
} }
} }
+39 -1
View File
@@ -6,6 +6,7 @@ using IM_API.Models;
using IM_API.Tools; using IM_API.Tools;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using StackExchange.Redis; using StackExchange.Redis;
using System.Text.Json;
namespace IM_API.Services namespace IM_API.Services
{ {
@@ -15,12 +16,16 @@ namespace IM_API.Services
private readonly ILogger<UserService> _logger; private readonly ILogger<UserService> _logger;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly ICacheService _cacheService; private readonly ICacheService _cacheService;
public UserService(ImContext imContext,ILogger<UserService> logger,IMapper mapper, ICacheService cacheService) private readonly IDatabase _redis;
public UserService(
ImContext imContext,ILogger<UserService> logger,
IMapper mapper, ICacheService cacheService, IConnectionMultiplexer connectionMultiplexer)
{ {
this._context = imContext; this._context = imContext;
this._logger = logger; this._logger = logger;
this._mapper = mapper; this._mapper = mapper;
_cacheService = cacheService; _cacheService = cacheService;
_redis = connectionMultiplexer.GetDatabase();
} }
#region #region
public async Task<UserInfoDto> GetUserInfoAsync(int userId) public async Task<UserInfoDto> GetUserInfoAsync(int userId)
@@ -87,5 +92,38 @@ namespace IM_API.Services
return _mapper.Map<UserInfoDto>(user); return _mapper.Map<UserInfoDto>(user);
} }
#endregion #endregion
#region
public async Task<List<UserInfoDto>> GetUserInfoListAsync(List<int> ids)
{
//读取缓存中存在的用户,存在直接添加到结果列表
var idKeyArr = ids.Select(s => (RedisKey)RedisKeys.GetUserinfoKey(s.ToString())).ToArray();
var values = await _redis.StringGetAsync(idKeyArr);
List<User> results = [];
List<int> missingIds = [];
for(int i = 0; i < ids.Count; i++)
{
if (values[i].HasValue)
{
results.Add(JsonSerializer.Deserialize<User>(values[i]));
}
else
{
missingIds.Add(ids[i]);
}
}
//如果存在没有缓存的用户则进行查库
if (missingIds.Any())
{
var dbUsers = await _context.Users
.Where(x => ids.Contains(x.Id)).ToListAsync();
results.AddRange(dbUsers);
var setTasks = dbUsers.Select(s => _cacheService.SetUserCacheAsync(s)).ToList();
await Task.WhenAll(setTasks);
}
return _mapper.Map<List<UserInfoDto>>(results);
}
#endregion
} }
} }
+4 -4
View File
@@ -1,4 +1,4 @@
#VITE_API_BASE_URL = http://localhost:5202/api VITE_API_BASE_URL = http://localhost:5202/api
#VITE_SIGNALR_BASE_URL = http://localhost:5202/chat/ VITE_SIGNALR_BASE_URL = http://localhost:5202/chat/
VITE_API_BASE_URL = https://im.test.nxsir.cn/api #VITE_API_BASE_URL = https://im.test.nxsir.cn/api
VITE_SIGNALR_BASE_URL = https://im.test.nxsir.cn/chat/ #VITE_SIGNALR_BASE_URL = https://im.test.nxsir.cn/chat/
@@ -5,12 +5,13 @@ import { useChatStore } from "@/stores/chat";
import { messageHandler } from "@/handler/messageHandler"; import { messageHandler } from "@/handler/messageHandler";
import { generateSessionId } from "../sessionIdTools"; import { generateSessionId } from "../sessionIdTools";
import { useConversationStore } from "@/stores/conversation"; import { useConversationStore } from "@/stores/conversation";
import { MESSAGE_TYPE } from "@/constants/MessageType";
export const SignalRMessageHandler = (data) => { export const SignalRMessageHandler = (data) => {
const msg = data.data; const msg = data.data;
const chatStore = useChatStore() const chatStore = useChatStore()
const browserNotification = useBrowserNotification(); const browserNotification = useBrowserNotification();
const sessionId = generateSessionId(msg.senderId, msg.receiverId); const sessionId = generateSessionId(msg.senderId, msg.receiverId, msg.chatType == MESSAGE_TYPE.GROUP);
messageHandler(msg); messageHandler(msg);
chatStore.pushAndSortMessagesAsync([msg], sessionId); chatStore.pushAndSortMessagesAsync([msg], sessionId);
const conversation = useConversationStore().conversations.find(x => x.targetId == msg.senderId); const conversation = useConversationStore().conversations.find(x => x.targetId == msg.senderId);
@@ -178,7 +178,7 @@ input.value = ''; // 清空输入框
updateMsg.isError = true; updateMsg.isError = true;
}finally{ }finally{
updateMsg.isLoading = false; updateMsg.isLoading = false;
chatStore.pushAndSortMessagesAsync([updateMsg], generateSessionId(msg.senderId, msg.receiverId), true); chatStore.pushAndSortMessagesAsync([updateMsg], generateSessionId(msg.senderId, msg.receiverId, msg.chatType == MESSAGE_TYPE.GROUP), true);
msg.isLoading = false; msg.isLoading = false;
} }