This commit is contained in:
西街长安 2026-02-08 15:13:55 +08:00
parent ed95bdddac
commit eff83a9f68
11 changed files with 132 additions and 39 deletions

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: "在此粘贴浏览器或后端的报错信息..."

View File

@ -17,33 +17,47 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
{
private readonly IConversationService _conversationService;
private readonly ILogger<ConversationEventHandler> _logger;
private readonly ImContext _context;
private readonly IMapper _mapper;
private readonly IUserService _userSerivce;
public ConversationEventHandler(
IConversationService conversationService,
ILogger<ConversationEventHandler> logger,
ImContext imContext,
IMapper mapper
IUserService userService
)
{
_conversationService = conversationService;
_logger = logger;
_context = imContext;
_mapper = mapper;
_userSerivce = userService;
}
public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
{
var @event = context.Message;
await _conversationService.UpdateConversationAfterSentAsync(new Dtos.Conversation.UpdateConversationDto
if (@event.ChatType == ChatType.GROUP)
{
LastMessage = @event.MessageContent,
LastSequenceId = @event.SequenceId,
ReceiptId = @event.MsgRecipientId,
SenderId = @event.MsgSenderId,
StreamKey = @event.StreamKey,
DateTime = @event.MessageCreated
});
var userinfo = await _userSerivce.GetUserInfoAsync(@event.MsgSenderId);
await _conversationService.UpdateConversationAfterSentAsync(new Dtos.Conversation.UpdateConversationDto
{
LastMessage = $"{userinfo.NickName}{@event.MessageContent}",
LastSequenceId = @event.SequenceId,
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
});
}
}
}

View File

@ -25,29 +25,17 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
{
Console.ForegroundColor = ConsoleColor.Red;
var @event = context.Message;
Console.WriteLine($"[SignalR]handlerCreated!");
try
{
// 先转成实体,如果这一步都报错,说明之前的 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();
await _hub.Clients.Group(@event.StreamKey).SendAsync("ReceiveMessage", new HubResponse<MessageBaseVo>("Event", messageBaseVo));
}
catch (Exception ex)
{
Console.WriteLine($"[SignalR] 发送失败: {ex.Message}");
Console.ResetColor();
throw; // 抛出异常触发 MassTransit 重试
throw;
}
}
}

View File

@ -8,6 +8,7 @@ using IM_API.VOs.Auth;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
namespace IM_API.Controllers
{
@ -39,13 +40,20 @@ namespace IM_API.Controllers
[HttpPost]
public async Task<IActionResult> Login(LoginRequestDto dto)
{
Stopwatch sw = Stopwatch.StartNew();
var user = await _authService.LoginAsync(dto);
_logger.LogInformation("服务耗时: {ms}ms", sw.ElapsedMilliseconds);
var userInfo = _mapper.Map<UserInfoDto>(user);
_logger.LogInformation("序列化耗时: {ms}ms", sw.ElapsedMilliseconds);
//生成凭证
(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);
_logger.LogInformation("RefreshToken生成耗时: {ms}ms", sw.ElapsedMilliseconds);
var res = new BaseResponse<LoginVo>(new LoginVo(userInfo,token,refreshToken, expiresAt));
_logger.LogInformation("总耗时: {ms}ms", sw.ElapsedMilliseconds);
return Ok(res);
}
[HttpPost]

View File

@ -5,6 +5,7 @@ using IM_API.Tools;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
@ -45,7 +46,7 @@ namespace IM_API.Controllers
{
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
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);
return Ok(res);
@ -84,7 +85,7 @@ namespace IM_API.Controllers
{
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
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?>());
}
/// <summary>
@ -100,5 +101,14 @@ namespace IM_API.Controllers
await _userService.UpdateOlineStatusAsync(userId, dto.OnlineStatus);
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);
}
}
}

View File

@ -35,9 +35,9 @@ namespace IM_API.Hubs
//将用户加入已加入聊天的会话组
string userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
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();
}

View File

@ -35,5 +35,11 @@ namespace IM_API.Interface.Services
/// <param name="onlineStatus"></param>
/// <returns></returns>
Task<bool> UpdateOlineStatusAsync(int userId, UserOnlineStatus onlineStatus);
/// <summary>
/// 批量获取用户信息
/// </summary>
/// <param name="ids">用户id列表</param>
/// <returns></returns>
Task<List<UserInfoDto>> GetUserInfoListAsync(List<int> ids);
}
}

View File

@ -6,6 +6,7 @@ using IM_API.Models;
using IM_API.Tools;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
using System.Text.Json;
namespace IM_API.Services
{
@ -15,12 +16,16 @@ namespace IM_API.Services
private readonly ILogger<UserService> _logger;
private readonly IMapper _mapper;
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._logger = logger;
this._mapper = mapper;
_cacheService = cacheService;
_redis = connectionMultiplexer.GetDatabase();
}
#region
public async Task<UserInfoDto> GetUserInfoAsync(int userId)
@ -87,5 +92,38 @@ namespace IM_API.Services
return _mapper.Map<UserInfoDto>(user);
}
#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
}
}

View File

@ -1,4 +1,4 @@
#VITE_API_BASE_URL = http://localhost:5202/api
#VITE_SIGNALR_BASE_URL = http://localhost:5202/chat/
VITE_API_BASE_URL = https://im.test.nxsir.cn/api
VITE_SIGNALR_BASE_URL = https://im.test.nxsir.cn/chat/
VITE_API_BASE_URL = http://localhost:5202/api
VITE_SIGNALR_BASE_URL = http://localhost:5202/chat/
#VITE_API_BASE_URL = https://im.test.nxsir.cn/api
#VITE_SIGNALR_BASE_URL = https://im.test.nxsir.cn/chat/

View File

@ -5,12 +5,13 @@ import { useChatStore } from "@/stores/chat";
import { messageHandler } from "@/handler/messageHandler";
import { generateSessionId } from "../sessionIdTools";
import { useConversationStore } from "@/stores/conversation";
import { MESSAGE_TYPE } from "@/constants/MessageType";
export const SignalRMessageHandler = (data) => {
const msg = data.data;
const chatStore = useChatStore()
const browserNotification = useBrowserNotification();
const sessionId = generateSessionId(msg.senderId, msg.receiverId);
const sessionId = generateSessionId(msg.senderId, msg.receiverId, msg.chatType == MESSAGE_TYPE.GROUP);
messageHandler(msg);
chatStore.pushAndSortMessagesAsync([msg], sessionId);
const conversation = useConversationStore().conversations.find(x => x.targetId == msg.senderId);

View File

@ -178,7 +178,7 @@ input.value = ''; // 清空输入框
updateMsg.isError = true;
}finally{
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;
}