From eff83a9f6896959d33e8c3a15b0c819b61b9eb84 Mon Sep 17 00:00:00 2001 From: nanxun Date: Sun, 8 Feb 2026 15:13:55 +0800 Subject: [PATCH] 222 --- .gitea/ISSUE_TEMPLATE/bug.yaml | 28 +++++++++++++ .../ConversationEventHandler.cs | 42 ++++++++++++------- .../SignalREventHandler.cs | 16 +------ backend/IM_API/Controllers/AuthController.cs | 8 ++++ backend/IM_API/Controllers/UserController.cs | 14 ++++++- backend/IM_API/Hubs/ChatHub.cs | 4 +- .../IM_API/Interface/Services/IUserService.cs | 6 +++ backend/IM_API/Services/UserService.cs | 40 +++++++++++++++++- frontend/web/.env | 8 ++-- .../src/utils/signalr/SignalMessageHandler.js | 3 +- .../web/src/views/messages/MessageContent.vue | 2 +- 11 files changed, 132 insertions(+), 39 deletions(-) create mode 100644 .gitea/ISSUE_TEMPLATE/bug.yaml diff --git a/.gitea/ISSUE_TEMPLATE/bug.yaml b/.gitea/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 0000000..f94e41d --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug.yaml @@ -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: "在此粘贴浏览器或后端的报错信息..." \ No newline at end of file diff --git a/backend/IM_API/Application/EventHandlers/MessageCreatedHandler/ConversationEventHandler.cs b/backend/IM_API/Application/EventHandlers/MessageCreatedHandler/ConversationEventHandler.cs index d4aab33..02cf786 100644 --- a/backend/IM_API/Application/EventHandlers/MessageCreatedHandler/ConversationEventHandler.cs +++ b/backend/IM_API/Application/EventHandlers/MessageCreatedHandler/ConversationEventHandler.cs @@ -17,33 +17,47 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler { private readonly IConversationService _conversationService; private readonly ILogger _logger; - private readonly ImContext _context; - private readonly IMapper _mapper; + private readonly IUserService _userSerivce; public ConversationEventHandler( IConversationService conversationService, ILogger logger, - ImContext imContext, - IMapper mapper + IUserService userService ) { _conversationService = conversationService; _logger = logger; - _context = imContext; - _mapper = mapper; + _userSerivce = userService; } public async Task Consume(ConsumeContext 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 + }); + } + } } diff --git a/backend/IM_API/Application/EventHandlers/MessageCreatedHandler/SignalREventHandler.cs b/backend/IM_API/Application/EventHandlers/MessageCreatedHandler/SignalREventHandler.cs index b2a2e0e..caebb7b 100644 --- a/backend/IM_API/Application/EventHandlers/MessageCreatedHandler/SignalREventHandler.cs +++ b/backend/IM_API/Application/EventHandlers/MessageCreatedHandler/SignalREventHandler.cs @@ -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(@event); - - // 再从实体转 VO,这是最稳妥的路径,因为这两者的映射你肯定配过了 var messageBaseVo = _mapper.Map(entity); - - // 2. 打印日志确认逻辑执行到这里了 - Console.WriteLine($"[SignalR] 准备向所有人广播消息: {messageBaseVo.Content}"); - - // 3. 执行广播 - await _hub.Clients.User(@event.MsgRecipientId.ToString()).SendAsync("ReceiveMessage", new HubResponse("Event", messageBaseVo)); - - Console.WriteLine("[SignalR] 广播指令已发出"); - Console.ResetColor(); + await _hub.Clients.Group(@event.StreamKey).SendAsync("ReceiveMessage", new HubResponse("Event", messageBaseVo)); } catch (Exception ex) { Console.WriteLine($"[SignalR] 发送失败: {ex.Message}"); Console.ResetColor(); - throw; // 抛出异常触发 MassTransit 重试 + throw; } } } diff --git a/backend/IM_API/Controllers/AuthController.cs b/backend/IM_API/Controllers/AuthController.cs index 2e7cf0d..5d17d89 100644 --- a/backend/IM_API/Controllers/AuthController.cs +++ b/backend/IM_API/Controllers/AuthController.cs @@ -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 Login(LoginRequestDto dto) { + Stopwatch sw = Stopwatch.StartNew(); var user = await _authService.LoginAsync(dto); + _logger.LogInformation("服务耗时: {ms}ms", sw.ElapsedMilliseconds); var userInfo = _mapper.Map(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(new LoginVo(userInfo,token,refreshToken, expiresAt)); + _logger.LogInformation("总耗时: {ms}ms", sw.ElapsedMilliseconds); return Ok(res); } [HttpPost] diff --git a/backend/IM_API/Controllers/UserController.cs b/backend/IM_API/Controllers/UserController.cs index 6b8587e..f4b4c3a 100644 --- a/backend/IM_API/Controllers/UserController.cs +++ b/backend/IM_API/Controllers/UserController.cs @@ -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(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()); } /// @@ -100,5 +101,14 @@ namespace IM_API.Controllers await _userService.UpdateOlineStatusAsync(userId, dto.OnlineStatus); return Ok(new BaseResponse()); } + + [HttpPost] + [ProducesResponseType(typeof(BaseResponse>), StatusCodes.Status200OK)] + public async Task GetUserList([FromBody][Required]List ids) + { + var users = await _userService.GetUserInfoListAsync(ids); + var res = new BaseResponse>(users); + return Ok(res); + } } } diff --git a/backend/IM_API/Hubs/ChatHub.cs b/backend/IM_API/Hubs/ChatHub.cs index 3b3d857..e6f5bb3 100644 --- a/backend/IM_API/Hubs/ChatHub.cs +++ b/backend/IM_API/Hubs/ChatHub.cs @@ -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(); } diff --git a/backend/IM_API/Interface/Services/IUserService.cs b/backend/IM_API/Interface/Services/IUserService.cs index 147ae9a..68a5d38 100644 --- a/backend/IM_API/Interface/Services/IUserService.cs +++ b/backend/IM_API/Interface/Services/IUserService.cs @@ -35,5 +35,11 @@ namespace IM_API.Interface.Services /// /// Task UpdateOlineStatusAsync(int userId, UserOnlineStatus onlineStatus); + /// + /// 批量获取用户信息 + /// + /// 用户id列表 + /// + Task> GetUserInfoListAsync(List ids); } } diff --git a/backend/IM_API/Services/UserService.cs b/backend/IM_API/Services/UserService.cs index dc07845..1ffbe57 100644 --- a/backend/IM_API/Services/UserService.cs +++ b/backend/IM_API/Services/UserService.cs @@ -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 _logger; private readonly IMapper _mapper; private readonly ICacheService _cacheService; - public UserService(ImContext imContext,ILogger logger,IMapper mapper, ICacheService cacheService) + private readonly IDatabase _redis; + public UserService( + ImContext imContext,ILogger 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 GetUserInfoAsync(int userId) @@ -87,5 +92,38 @@ namespace IM_API.Services return _mapper.Map(user); } #endregion + #region 批量获取用户信息 + public async Task> GetUserInfoListAsync(List ids) + { + //读取缓存中存在的用户,存在直接添加到结果列表 + var idKeyArr = ids.Select(s => (RedisKey)RedisKeys.GetUserinfoKey(s.ToString())).ToArray(); + var values = await _redis.StringGetAsync(idKeyArr); + List results = []; + List missingIds = []; + for(int i = 0; i < ids.Count; i++) + { + if (values[i].HasValue) + { + results.Add(JsonSerializer.Deserialize(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>(results); + } + #endregion } } diff --git a/frontend/web/.env b/frontend/web/.env index d3d8fa0..4651767 100644 --- a/frontend/web/.env +++ b/frontend/web/.env @@ -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/ \ No newline at end of file +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/ \ No newline at end of file diff --git a/frontend/web/src/utils/signalr/SignalMessageHandler.js b/frontend/web/src/utils/signalr/SignalMessageHandler.js index 15ca6f7..3af6a9e 100644 --- a/frontend/web/src/utils/signalr/SignalMessageHandler.js +++ b/frontend/web/src/utils/signalr/SignalMessageHandler.js @@ -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); diff --git a/frontend/web/src/views/messages/MessageContent.vue b/frontend/web/src/views/messages/MessageContent.vue index ecdc5ab..26a3cf9 100644 --- a/frontend/web/src/views/messages/MessageContent.vue +++ b/frontend/web/src/views/messages/MessageContent.vue @@ -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; }