fix:修复已知问题
This commit is contained in:
parent
10f79fb537
commit
dc6ecf224d
@ -8,6 +8,7 @@ using IM_API.Tools;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
using StackExchange.Redis;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@ -38,7 +39,8 @@ public class UserServiceTests
|
|||||||
var loggerMock = new Mock<ILogger<UserService>>();
|
var loggerMock = new Mock<ILogger<UserService>>();
|
||||||
var mapper = CreateMapper();
|
var mapper = CreateMapper();
|
||||||
var mockCache = new Mock<ICacheService>();
|
var mockCache = new Mock<ICacheService>();
|
||||||
return new UserService(context, loggerMock.Object, mapper , mockCache.Object);
|
var res = new Mock<IConnectionMultiplexer>();
|
||||||
|
return new UserService(context, loggerMock.Object, mapper , mockCache.Object, res.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== GetUserInfoAsync ==========
|
// ========== GetUserInfoAsync ==========
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -14,7 +14,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("IMTest")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("IMTest")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+139037085bdb672bb115b4b9506955f7bc733672")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+10f79fb537b71581876f17031e09898ddf99e367")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("IMTest")]
|
[assembly: System.Reflection.AssemblyProductAttribute("IMTest")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("IMTest")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("IMTest")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
07f6bfbf17c54c7629f2ad097053140e01b65bf5b6fd786f5da1ceb943f92ba1
|
6a1eca496991f1712085223e3079932051f23c0e5f7568bc971c393fde95395b
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -18,15 +18,18 @@ 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 IUserService _userSerivce;
|
private readonly IUserService _userSerivce;
|
||||||
|
private readonly IGroupService _groupService;
|
||||||
public ConversationEventHandler(
|
public ConversationEventHandler(
|
||||||
IConversationService conversationService,
|
IConversationService conversationService,
|
||||||
ILogger<ConversationEventHandler> logger,
|
ILogger<ConversationEventHandler> logger,
|
||||||
IUserService userService
|
IUserService userService,
|
||||||
|
IGroupService groupService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_conversationService = conversationService;
|
_conversationService = conversationService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_userSerivce = userService;
|
_userSerivce = userService;
|
||||||
|
_groupService = groupService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
|
public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
|
||||||
@ -35,14 +38,13 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
|
|||||||
if (@event.ChatType == ChatType.GROUP)
|
if (@event.ChatType == ChatType.GROUP)
|
||||||
{
|
{
|
||||||
var userinfo = await _userSerivce.GetUserInfoAsync(@event.MsgSenderId);
|
var userinfo = await _userSerivce.GetUserInfoAsync(@event.MsgSenderId);
|
||||||
await _conversationService.UpdateConversationAfterSentAsync(new Dtos.Conversation.UpdateConversationDto
|
await _groupService.UpdateGroupConversationAsync(new Dtos.Group.GroupUpdateConversationDto
|
||||||
{
|
{
|
||||||
LastMessage = $"{userinfo.NickName}:{@event.MessageContent}",
|
GroupId = @event.MsgRecipientId,
|
||||||
LastSequenceId = @event.SequenceId,
|
LastMessage = @event.MessageContent,
|
||||||
ReceiptId = @event.MsgRecipientId,
|
LastSenderName = userinfo.NickName,
|
||||||
SenderId = @event.MsgSenderId,
|
LastUpdateTime = @event.MessageCreated,
|
||||||
StreamKey = @event.StreamKey,
|
MaxSequenceId = @event.SequenceId
|
||||||
DateTime = @event.MessageCreated
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using IM_API.Application.Interfaces;
|
|||||||
using IM_API.Domain.Events;
|
using IM_API.Domain.Events;
|
||||||
using IM_API.Dtos;
|
using IM_API.Dtos;
|
||||||
using IM_API.Hubs;
|
using IM_API.Hubs;
|
||||||
|
using IM_API.Interface.Services;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
using IM_API.Tools;
|
using IM_API.Tools;
|
||||||
using IM_API.VOs.Message;
|
using IM_API.VOs.Message;
|
||||||
@ -15,10 +16,12 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
|
|||||||
{
|
{
|
||||||
private readonly IHubContext<ChatHub> _hub;
|
private readonly IHubContext<ChatHub> _hub;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
public SignalREventHandler(IHubContext<ChatHub> hub, IMapper mapper)
|
private readonly IUserService _userService;
|
||||||
|
public SignalREventHandler(IHubContext<ChatHub> hub, IMapper mapper,IUserService userService)
|
||||||
{
|
{
|
||||||
_hub = hub;
|
_hub = hub;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
|
_userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
|
public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
|
||||||
@ -29,6 +32,9 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
|
|||||||
{
|
{
|
||||||
var entity = _mapper.Map<Message>(@event);
|
var entity = _mapper.Map<Message>(@event);
|
||||||
var messageBaseVo = _mapper.Map<MessageBaseVo>(entity);
|
var messageBaseVo = _mapper.Map<MessageBaseVo>(entity);
|
||||||
|
var senderinfo = await _userService.GetUserInfoAsync(@event.MsgSenderId);
|
||||||
|
messageBaseVo.SenderName = senderinfo.NickName;
|
||||||
|
messageBaseVo.SenderAvatar = senderinfo.Avatar ?? "";
|
||||||
await _hub.Clients.Group(@event.StreamKey).SendAsync("ReceiveMessage", new HubResponse<MessageBaseVo>("Event", messageBaseVo));
|
await _hub.Clients.Group(@event.StreamKey).SendAsync("ReceiveMessage", new HubResponse<MessageBaseVo>("Event", messageBaseVo));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
11
backend/IM_API/Dtos/Group/GroupUpdateConversationDto.cs
Normal file
11
backend/IM_API/Dtos/Group/GroupUpdateConversationDto.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace IM_API.Dtos.Group
|
||||||
|
{
|
||||||
|
public class GroupUpdateConversationDto
|
||||||
|
{
|
||||||
|
public int GroupId { get; set; }
|
||||||
|
public long MaxSequenceId { get; set; }
|
||||||
|
public string LastMessage { get; set; }
|
||||||
|
public string LastSenderName { get; set; }
|
||||||
|
public DateTimeOffset LastUpdateTime { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -43,5 +43,6 @@ namespace IM_API.Interface.Services
|
|||||||
/// <param name="desc"></param>
|
/// <param name="desc"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<List<GroupInfoDto>> GetGroupListAsync(int userId, int page, int limit, bool desc);
|
Task<List<GroupInfoDto>> GetGroupListAsync(int userId, int page, int limit, bool desc);
|
||||||
|
Task UpdateGroupConversationAsync(GroupUpdateConversationDto dto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1115
backend/IM_API/Migrations/20260208124430_update-group.Designer.cs
generated
Normal file
1115
backend/IM_API/Migrations/20260208124430_update-group.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
65
backend/IM_API/Migrations/20260208124430_update-group.cs
Normal file
65
backend/IM_API/Migrations/20260208124430_update-group.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace IM_API.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class updategroup : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "LastMessage",
|
||||||
|
table: "groups",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
collation: "utf8mb4_general_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "LastSenderName",
|
||||||
|
table: "groups",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
collation: "utf8mb4_general_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||||
|
name: "LastUpdateTime",
|
||||||
|
table: "groups",
|
||||||
|
type: "datetime(6)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "MaxSequenceId",
|
||||||
|
table: "groups",
|
||||||
|
type: "bigint",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LastMessage",
|
||||||
|
table: "groups");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LastSenderName",
|
||||||
|
table: "groups");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LastUpdateTime",
|
||||||
|
table: "groups");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "MaxSequenceId",
|
||||||
|
table: "groups");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -354,6 +354,20 @@ namespace IM_API.Migrations
|
|||||||
.HasColumnType("int(11)")
|
.HasColumnType("int(11)")
|
||||||
.HasComment("群主");
|
.HasComment("群主");
|
||||||
|
|
||||||
|
b.Property<string>("LastMessage")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("LastSenderName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastUpdateTime")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<long>("MaxSequenceId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(20)
|
.HasMaxLength(20)
|
||||||
|
|||||||
@ -50,6 +50,11 @@ public partial class Group
|
|||||||
/// 群头像
|
/// 群头像
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Avatar { get; set; } = null!;
|
public string Avatar { get; set; } = null!;
|
||||||
|
public long MaxSequenceId { get; set; } = 0;
|
||||||
|
public string LastMessage { get; set; } = string.Empty;
|
||||||
|
public string LastSenderName { get; set; } = string.Empty;
|
||||||
|
public DateTimeOffset LastUpdateTime { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
|
||||||
public virtual ICollection<GroupInvite> GroupInvites { get; set; } = new List<GroupInvite>();
|
public virtual ICollection<GroupInvite> GroupInvites { get; set; } = new List<GroupInvite>();
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace IM_API.Models;
|
namespace IM_API.Models;
|
||||||
|
|
||||||
@ -58,32 +59,33 @@ public partial class User
|
|||||||
/// 用户头像链接
|
/// 用户头像链接
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Avatar { get; set; }
|
public string? Avatar { get; set; }
|
||||||
|
[JsonIgnore]
|
||||||
|
|
||||||
public virtual ICollection<Conversation> Conversations { get; set; } = new List<Conversation>();
|
public virtual ICollection<Conversation> Conversations { get; set; } = new List<Conversation>();
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ICollection<Device> Devices { get; set; } = new List<Device>();
|
public virtual ICollection<Device> Devices { get; set; } = new List<Device>();
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ICollection<Friend> FriendFriendNavigations { get; set; } = new List<Friend>();
|
public virtual ICollection<Friend> FriendFriendNavigations { get; set; } = new List<Friend>();
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ICollection<FriendRequest> FriendRequestRequestUserNavigations { get; set; } = new List<FriendRequest>();
|
public virtual ICollection<FriendRequest> FriendRequestRequestUserNavigations { get; set; } = new List<FriendRequest>();
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ICollection<FriendRequest> FriendRequestResponseUserNavigations { get; set; } = new List<FriendRequest>();
|
public virtual ICollection<FriendRequest> FriendRequestResponseUserNavigations { get; set; } = new List<FriendRequest>();
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ICollection<Friend> FriendUsers { get; set; } = new List<Friend>();
|
public virtual ICollection<Friend> FriendUsers { get; set; } = new List<Friend>();
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ICollection<GroupInvite> GroupInviteInviteUserNavigations { get; set; } = new List<GroupInvite>();
|
public virtual ICollection<GroupInvite> GroupInviteInviteUserNavigations { get; set; } = new List<GroupInvite>();
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ICollection<GroupInvite> GroupInviteInvitedUserNavigations { get; set; } = new List<GroupInvite>();
|
public virtual ICollection<GroupInvite> GroupInviteInvitedUserNavigations { get; set; } = new List<GroupInvite>();
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ICollection<GroupMember> GroupMembers { get; set; } = new List<GroupMember>();
|
public virtual ICollection<GroupMember> GroupMembers { get; set; } = new List<GroupMember>();
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ICollection<GroupRequest> GroupRequests { get; set; } = new List<GroupRequest>();
|
public virtual ICollection<GroupRequest> GroupRequests { get; set; } = new List<GroupRequest>();
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ICollection<Group> Groups { get; set; } = new List<Group>();
|
public virtual ICollection<Group> Groups { get; set; } = new List<Group>();
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ICollection<LoginLog> LoginLogs { get; set; } = new List<LoginLog>();
|
public virtual ICollection<LoginLog> LoginLogs { get; set; } = new List<LoginLog>();
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ICollection<Message> Messages { get; set; } = new List<Message>();
|
public virtual ICollection<Message> Messages { get; set; } = new List<Message>();
|
||||||
|
[JsonIgnore]
|
||||||
public virtual ICollection<Notification> Notifications { get; set; } = new List<Notification>();
|
public virtual ICollection<Notification> Notifications { get; set; } = new List<Notification>();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,7 @@ namespace IM_API.Services
|
|||||||
var groupList = await (from c in _context.Conversations
|
var groupList = await (from c in _context.Conversations
|
||||||
join g in _context.Groups on c.TargetId equals g.Id
|
join g in _context.Groups on c.TargetId equals g.Id
|
||||||
where c.UserId == userId && c.ChatType == ChatType.GROUP
|
where c.UserId == userId && c.ChatType == ChatType.GROUP
|
||||||
select new { c, g.Avatar, g.Name })
|
select new { c, g.Avatar, g.Name,g.MaxSequenceId,g.LastMessage })
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var privateDtos = privateList.Select(x =>
|
var privateDtos = privateList.Select(x =>
|
||||||
@ -58,6 +58,9 @@ namespace IM_API.Services
|
|||||||
var dto = _mapper.Map<ConversationVo>(x.c);
|
var dto = _mapper.Map<ConversationVo>(x.c);
|
||||||
dto.TargetAvatar = x.Avatar;
|
dto.TargetAvatar = x.Avatar;
|
||||||
dto.TargetName = x.Name;
|
dto.TargetName = x.Name;
|
||||||
|
dto.UnreadCount = (int)(x.MaxSequenceId - x.c.LastReadSequenceId ?? 0);
|
||||||
|
dto.LastSequenceId = x.MaxSequenceId;
|
||||||
|
dto.LastMessage = x.LastMessage;
|
||||||
return dto;
|
return dto;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -125,5 +125,16 @@ namespace IM_API.Services
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
public async Task UpdateGroupConversationAsync(GroupUpdateConversationDto dto)
|
||||||
|
{
|
||||||
|
var group = await _context.Groups.FirstOrDefaultAsync(x => x.Id == dto.GroupId);
|
||||||
|
if (group is null) return;
|
||||||
|
group.LastMessage = dto.LastMessage;
|
||||||
|
group.MaxSequenceId = dto.MaxSequenceId;
|
||||||
|
group.LastSenderName = dto.LastSenderName;
|
||||||
|
group.LastUpdateTime = dto.LastUpdateTime;
|
||||||
|
_context.Groups.Update(group);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ using IM_API.VOs.Message;
|
|||||||
using MassTransit;
|
using MassTransit;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using static MassTransit.Monitoring.Performance.BuiltInCounters;
|
using static MassTransit.Monitoring.Performance.BuiltInCounters;
|
||||||
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
|
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
|
||||||
@ -26,9 +27,11 @@ namespace IM_API.Services
|
|||||||
//private readonly IEventBus _eventBus;
|
//private readonly IEventBus _eventBus;
|
||||||
private readonly IPublishEndpoint _endpoint;
|
private readonly IPublishEndpoint _endpoint;
|
||||||
private readonly ISequenceIdService _sequenceIdService;
|
private readonly ISequenceIdService _sequenceIdService;
|
||||||
|
private readonly IUserService _userService;
|
||||||
public MessageService(
|
public MessageService(
|
||||||
ImContext context, ILogger<MessageService> logger, IMapper mapper,
|
ImContext context, ILogger<MessageService> logger, IMapper mapper,
|
||||||
IPublishEndpoint publishEndpoint, ISequenceIdService sequenceIdService
|
IPublishEndpoint publishEndpoint, ISequenceIdService sequenceIdService,
|
||||||
|
IUserService userService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
@ -37,6 +40,7 @@ namespace IM_API.Services
|
|||||||
//_eventBus = eventBus;
|
//_eventBus = eventBus;
|
||||||
_endpoint = publishEndpoint;
|
_endpoint = publishEndpoint;
|
||||||
_sequenceIdService = sequenceIdService;
|
_sequenceIdService = sequenceIdService;
|
||||||
|
_userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<MessageBaseVo>> GetMessagesAsync(int userId,MessageQueryDto dto)
|
public async Task<List<MessageBaseVo>> GetMessagesAsync(int userId,MessageQueryDto dto)
|
||||||
@ -48,6 +52,7 @@ namespace IM_API.Services
|
|||||||
if (conversation is null) throw new BaseException(CodeDefine.CONVERSATION_NOT_FOUND);
|
if (conversation is null) throw new BaseException(CodeDefine.CONVERSATION_NOT_FOUND);
|
||||||
|
|
||||||
var baseQuery = _context.Messages.Where(x => x.StreamKey == conversation.StreamKey);
|
var baseQuery = _context.Messages.Where(x => x.StreamKey == conversation.StreamKey);
|
||||||
|
List<MessageBaseVo> messages = new List<MessageBaseVo>();
|
||||||
if (dto.Direction == 0) // Before: 找比锚点小的,按倒序排
|
if (dto.Direction == 0) // Before: 找比锚点小的,按倒序排
|
||||||
{
|
{
|
||||||
if (dto.Cursor.HasValue)
|
if (dto.Cursor.HasValue)
|
||||||
@ -59,20 +64,41 @@ namespace IM_API.Services
|
|||||||
.Select(m => _mapper.Map<MessageBaseVo>(m))
|
.Select(m => _mapper.Map<MessageBaseVo>(m))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
return list.OrderBy(s => s.SequenceId).ToList();
|
messages = list.OrderBy(s => s.SequenceId).ToList();
|
||||||
}
|
}
|
||||||
else // After: 找比锚点大的,按正序排(用于补洞或刷新)
|
else // After: 找比锚点大的,按正序排(用于补洞或刷新)
|
||||||
{
|
{
|
||||||
// 如果 Cursor 为空且是 After,逻辑上说不通,通常直接返回空或报错
|
// 如果 Cursor 为空且是 After,逻辑上说不通,通常直接返回空或报错
|
||||||
if (!dto.Cursor.HasValue) return new List<MessageBaseVo>();
|
if (!dto.Cursor.HasValue) return new List<MessageBaseVo>();
|
||||||
|
|
||||||
return await baseQuery
|
messages = await baseQuery
|
||||||
.Where(m => m.SequenceId > dto.Cursor.Value)
|
.Where(m => m.SequenceId > dto.Cursor.Value)
|
||||||
.OrderBy(m => m.SequenceId) // 按时间线正序
|
.OrderBy(m => m.SequenceId) // 按时间线正序
|
||||||
.Take(dto.Limit)
|
.Take(dto.Limit)
|
||||||
.Select(m => _mapper.Map<MessageBaseVo>(m))
|
.Select(m => _mapper.Map<MessageBaseVo>(m))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
//取发送者信息,用于前端展示
|
||||||
|
if(messages.Count > 0)
|
||||||
|
{
|
||||||
|
var ids = messages
|
||||||
|
.Select(s => s.SenderId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var userinfoList = await _userService.GetUserInfoListAsync(ids);
|
||||||
|
// 转为字典,提高查询效率
|
||||||
|
var userDict = userinfoList.ToDictionary(x => x.Id, x => x);
|
||||||
|
|
||||||
|
foreach (var item in messages)
|
||||||
|
{
|
||||||
|
if(userDict.TryGetValue(item.SenderId, out var user))
|
||||||
|
{
|
||||||
|
item.SenderName = user.NickName;
|
||||||
|
item.SenderAvatar = user.Avatar ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<int> GetUnreadCountAsync(int userId)
|
public Task<int> GetUnreadCountAsync(int userId)
|
||||||
|
|||||||
@ -5,5 +5,7 @@ namespace IM_API.VOs.Message
|
|||||||
public record MessageBaseVo:MessageBaseDto
|
public record MessageBaseVo:MessageBaseDto
|
||||||
{
|
{
|
||||||
public int SequenceId { get; set; }
|
public int SequenceId { get; set; }
|
||||||
|
public string SenderName { get; set; } = "";
|
||||||
|
public string SenderAvatar { get; set; } = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
195
frontend/web/src/components/user/UserHoverCard.vue
Normal file
195
frontend/web/src/components/user/UserHoverCard.vue
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
<template>
|
||||||
|
<teleport to="body">
|
||||||
|
<transition name="fade">
|
||||||
|
<div
|
||||||
|
v-if="isVisible"
|
||||||
|
class="im-hover-card"
|
||||||
|
:style="cardStyle"
|
||||||
|
@mouseenter="clearTimer"
|
||||||
|
@mouseleave="hide"
|
||||||
|
>
|
||||||
|
<div class="card-inner">
|
||||||
|
<div class="user-profile">
|
||||||
|
<div class="info-text">
|
||||||
|
<h4 class="nickname">{{ currentUser.name }}</h4>
|
||||||
|
<p class="detail-item">
|
||||||
|
<span class="label">微信号:</span>
|
||||||
|
<span class="value">{{ currentUser.id }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="detail-item">
|
||||||
|
<span class="label">地 区:</span>
|
||||||
|
<span class="value">{{ currentUser.region || '未知' }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<img :src="currentUser.avatar" class="avatar-square" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-bio">
|
||||||
|
<p class="bio-text">{{ currentUser.bio || '暂无签名' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer">
|
||||||
|
<button class="action-btn primary" @click="onChat">发消息</button>
|
||||||
|
<button v-if="!currentUser.isFriend" class="action-btn secondary" @click="onAdd">添加好友</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue';
|
||||||
|
|
||||||
|
const isVisible = ref(false);
|
||||||
|
const currentUser = ref({});
|
||||||
|
const cardStyle = reactive({
|
||||||
|
position: 'fixed',
|
||||||
|
top: '0px',
|
||||||
|
left: '0px'
|
||||||
|
});
|
||||||
|
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
const show = (el, data) => {
|
||||||
|
clearTimer();
|
||||||
|
currentUser.value = data;
|
||||||
|
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
// IM 习惯:通常在头像右侧或下方弹出
|
||||||
|
// 这里设置为在头像中心水平对齐,下方弹出
|
||||||
|
cardStyle.top = `${rect.bottom + 8}px`;
|
||||||
|
cardStyle.left = `${rect.left}px`;
|
||||||
|
|
||||||
|
isVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
isVisible.value = false;
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearTimer = () => {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAdd = () => {
|
||||||
|
console.log('申请添加好友:', currentUser.value.id);
|
||||||
|
// 这里写你的逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChat = () => {
|
||||||
|
console.log('跳转聊天窗口:', currentUser.value.id);
|
||||||
|
isVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ show, hide });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.im-hover-card {
|
||||||
|
z-index: 9999;
|
||||||
|
width: 280px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 4px; /* IM 通常是小圆角或直角 */
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
color: #333;
|
||||||
|
font-family: "Microsoft YaHei", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-inner {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部布局:文字在左,头像在右(典型微信名片风) */
|
||||||
|
.user-profile {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item .label {
|
||||||
|
color: #999;
|
||||||
|
width: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item .value {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-square {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 签名区 */
|
||||||
|
.user-bio {
|
||||||
|
padding: 15px 0;
|
||||||
|
border-top: 1px solid #f2f2f2;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bio-text {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #888;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部按钮:去掉花哨渐变,改用纯色或文字链接感 */
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 12px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.primary {
|
||||||
|
color: #576b95; /* 经典的微信蓝/链接色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.primary:hover {
|
||||||
|
color: #3e4d6d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.secondary {
|
||||||
|
color: #576b95;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画:简单的淡入 */
|
||||||
|
.fade-enter-active, .fade-leave-active {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
.fade-enter-from, .fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -10,10 +10,12 @@
|
|||||||
|
|
||||||
<div class="chat-history" ref="historyRef">
|
<div class="chat-history" ref="historyRef">
|
||||||
<HistoryLoading ref="loadingRef" :loading="isLoading" :finished="isFinished" :error="hasError" @retry="loadHistoryMsg"/>
|
<HistoryLoading ref="loadingRef" :loading="isLoading" :finished="isFinished" :error="hasError" @retry="loadHistoryMsg"/>
|
||||||
|
<UserHoverCard ref="userHoverCardRef"/>
|
||||||
<div v-for="m in chatStore.messages" :key="m.id" :class="['msg', m.senderId == myInfo.id ? 'mine' : 'other']">
|
<div v-for="m in chatStore.messages" :key="m.id" :class="['msg', m.senderId == myInfo.id ? 'mine' : 'other']">
|
||||||
<img :src="m.senderId == myInfo.id ? (myInfo?.avatar || defaultAvatar) : getAvatar(m.senderId) ?? defaultAvatar" class="avatar-chat" />
|
<img @mouseenter="(e) => handleHoverCard(e,m)" @mouseleave="closeHoverCard" :src="m.senderId == myInfo.id ? (myInfo?.avatar || defaultAvatar) : m.senderAvatar ?? defaultAvatar" class="avatar-chat" />
|
||||||
|
|
||||||
<div class="msg-content">
|
<div class="msg-content">
|
||||||
|
<div class="group-sendername" v-if="m.chatType == MESSAGE_TYPE.GROUP && m.senderId != myInfo.id">{{ m.senderName }}</div>
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
<div v-if="m.type === 'Text'">{{ m.content }}</div>
|
<div v-if="m.type === 'Text'">{{ m.content }}</div>
|
||||||
<div v-else-if="m.type === 'emoji'" class="emoji-msg">{{ m.content }}</div>
|
<div v-else-if="m.type === 'emoji'" class="emoji-msg">{{ m.content }}</div>
|
||||||
@ -66,6 +68,7 @@ import { GetLocalIso } from '@/utils/dateTool';
|
|||||||
import HistoryLoading from '@/components/messages/HistoryLoading.vue';
|
import HistoryLoading from '@/components/messages/HistoryLoading.vue';
|
||||||
import { useMessage } from '@/components/messages/useAlert';
|
import { useMessage } from '@/components/messages/useAlert';
|
||||||
import { SYSTEM_BASE_STATUS } from '@/constants/systemBaseStatus';
|
import { SYSTEM_BASE_STATUS } from '@/constants/systemBaseStatus';
|
||||||
|
import UserHoverCard from '@/components/user/UserHoverCard.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id:{
|
id:{
|
||||||
@ -81,6 +84,7 @@ const conversationStore = useConversationStore();
|
|||||||
const input = ref(''); // 输入框内容
|
const input = ref(''); // 输入框内容
|
||||||
const historyRef = ref(null); // 绑定 DOM 用于滚动
|
const historyRef = ref(null); // 绑定 DOM 用于滚动
|
||||||
const loadingRef = ref(null)
|
const loadingRef = ref(null)
|
||||||
|
const userHoverCardRef = ref(null);
|
||||||
const myInfo = useAuthStore().userInfo;
|
const myInfo = useAuthStore().userInfo;
|
||||||
|
|
||||||
const conversationInfo = ref(null)
|
const conversationInfo = ref(null)
|
||||||
@ -122,6 +126,19 @@ const loadHistoryMsg = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleHoverCard = (e, m) => {
|
||||||
|
const userInfo = {
|
||||||
|
name: m.senderName,
|
||||||
|
avatar: m.senderAvatar,
|
||||||
|
id: m.senderId
|
||||||
|
}
|
||||||
|
userHoverCardRef.value.show(e.target, userInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeHoverCard = () => {
|
||||||
|
userHoverCardRef.value.hide();
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => chatStore.messages,
|
() => chatStore.messages,
|
||||||
async (newVal) => {
|
async (newVal) => {
|
||||||
@ -132,10 +149,6 @@ watch(
|
|||||||
{deep: true}
|
{deep: true}
|
||||||
);
|
);
|
||||||
|
|
||||||
const getAvatar = (userId) => {
|
|
||||||
return conversationStore.conversations.find(x => x.targetId == userId).targetAvatar;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动滚动到底部
|
// 自动滚动到底部
|
||||||
const scrollToBottom = async () => {
|
const scrollToBottom = async () => {
|
||||||
await nextTick(); // 等待 DOM 更新后执行
|
await nextTick(); // 等待 DOM 更新后执行
|
||||||
@ -216,6 +229,7 @@ const initChat = async (newId) => {
|
|||||||
const sessionid = generateSessionId(
|
const sessionid = generateSessionId(
|
||||||
conversationInfo.value.userId, conversationInfo.value.targetId, conversationInfo.value.chatType == MESSAGE_TYPE.GROUP)
|
conversationInfo.value.userId, conversationInfo.value.targetId, conversationInfo.value.chatType == MESSAGE_TYPE.GROUP)
|
||||||
await chatStore.swtichSession(sessionid,newId);
|
await chatStore.swtichSession(sessionid,newId);
|
||||||
|
isFinished.value = false;
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,6 +386,12 @@ onUnmounted(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group-sendername {
|
||||||
|
width: 55px;
|
||||||
|
font-size: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* 消息对齐逻辑 */
|
/* 消息对齐逻辑 */
|
||||||
.msg {
|
.msg {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<div class="scroll-area">
|
<div class="scroll-area">
|
||||||
<div v-for="s in filteredSessions" :key="s.id"
|
<div v-for="s in filteredSessions" :key="s.id"
|
||||||
class="list-item" :class="{active: activeId === s.id}" @click="selectSession(s)">
|
class="list-item" :class="{active: activeId == s.id}" @click="selectSession(s)">
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<img :src="s.targetAvatar ? s.targetAvatar : defaultAvatar" class="avatar-std" />
|
<img :src="s.targetAvatar ? s.targetAvatar : defaultAvatar" class="avatar-std" />
|
||||||
<span v-if="s.unreadCount > 0" class="unread-badge">{{ s.unreadCount ?? 0 }}</span>
|
<span v-if="s.unreadCount > 0" class="unread-badge">{{ s.unreadCount ?? 0 }}</span>
|
||||||
@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import defaultAvatar from '@/assets/default_avatar.png'
|
import defaultAvatar from '@/assets/default_avatar.png'
|
||||||
import { formatDate } from '@/utils/formatDate'
|
import { formatDate } from '@/utils/formatDate'
|
||||||
import { useConversationStore } from '@/stores/conversation'
|
import { useConversationStore } from '@/stores/conversation'
|
||||||
@ -50,13 +50,14 @@ import feather from 'feather-icons'
|
|||||||
import SearchUser from '@/components/user/SearchUser.vue'
|
import SearchUser from '@/components/user/SearchUser.vue'
|
||||||
import CreateGroup from '@/components/groups/CreateGroup.vue'
|
import CreateGroup from '@/components/groups/CreateGroup.vue'
|
||||||
import { useBrowserNotification } from '@/services/useBrowserNotification'
|
import { useBrowserNotification } from '@/services/useBrowserNotification'
|
||||||
|
import { useChatStore } from '@/stores/chat'
|
||||||
|
|
||||||
const conversationStore = useConversationStore();
|
const conversationStore = useConversationStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const browserNotification = useBrowserNotification();
|
const browserNotification = useBrowserNotification();
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const activeId = ref(1)
|
const activeId = ref(0)
|
||||||
const searchUserModal = ref(false);
|
const searchUserModal = ref(false);
|
||||||
const createGroupModal = ref(false);
|
const createGroupModal = ref(false);
|
||||||
const msgTitleShow = ref(false);
|
const msgTitleShow = ref(false);
|
||||||
@ -100,6 +101,18 @@ function actionHandler(type){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => chatStore.activeConversationId,
|
||||||
|
(newVal) => {
|
||||||
|
if(newVal && newVal != 0){
|
||||||
|
activeId.value = newVal;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate:true}
|
||||||
|
)
|
||||||
|
|
||||||
async function requestNotificationPermission(){
|
async function requestNotificationPermission(){
|
||||||
await browserNotification.requestPermission();
|
await browserNotification.requestPermission();
|
||||||
if(Notification.permission === "granted") msgTitleShow.value = false;
|
if(Notification.permission === "granted") msgTitleShow.value = false;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user