Merge pull request '前端:' (#65) from feature-nxdev into main
Reviewed-on: #65
This commit is contained in:
commit
f1a9af3393
@ -11,6 +11,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MassTransit;
|
||||
using IM_API.Configs;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace IM_API.Tests.Services
|
||||
{
|
||||
@ -61,7 +62,9 @@ namespace IM_API.Tests.Services
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var service = new GroupService(_context, _mapper, _loggerMock.Object, _publishMock.Object);
|
||||
var userService = new Mock<UserService>();
|
||||
|
||||
var service = new GroupService(_context, _mapper, _loggerMock.Object, _publishMock.Object,userService.Object);
|
||||
|
||||
// Act (执行测试: 第1页,每页2条,倒序)
|
||||
var result = await service.GetGroupListAsync(userId, page: 1, limit: 2, desc: true);
|
||||
@ -76,8 +79,10 @@ namespace IM_API.Tests.Services
|
||||
[Fact]
|
||||
public async Task GetGroupList_ShouldReturnEmpty_WhenUserHasNoGroups()
|
||||
{
|
||||
|
||||
var userSerivce = new Mock<UserService>();
|
||||
// Arrange
|
||||
var service = new GroupService(_context, _mapper, _loggerMock.Object, _publishMock.Object);
|
||||
var service = new GroupService(_context, _mapper, _loggerMock.Object, _publishMock.Object, userSerivce.Object);
|
||||
|
||||
// Act
|
||||
var result = await service.GetGroupListAsync(userId: 999, page: 1, limit: 10, desc: false);
|
||||
|
||||
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.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+10f79fb537b71581876f17031e09898ddf99e367")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+dc6ecf224df4e8714171e8b5d23afaa90b3a1f81")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("IMTest")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("IMTest")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@ -1 +1 @@
|
||||
6a1eca496991f1712085223e3079932051f23c0e5f7568bc971c393fde95395b
|
||||
1b9e709aa84e0b4f6260cd10cf25bfc3a30c60e75a3966fc7d4cdf489eae898b
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,24 @@
|
||||
using IM_API.Domain.Events;
|
||||
using IM_API.Interface.Services;
|
||||
using MassTransit;
|
||||
|
||||
namespace IM_API.Application.EventHandlers.GroupInviteActionUpdateHandler
|
||||
{
|
||||
public class RequestDbHandler : IConsumer<GroupInviteActionUpdateEvent>
|
||||
{
|
||||
private readonly IGroupService _groupService;
|
||||
public RequestDbHandler(IGroupService groupService)
|
||||
{
|
||||
_groupService = groupService;
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<GroupInviteActionUpdateEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
if(@event.Action == Models.GroupInviteState.Passed)
|
||||
{
|
||||
await _groupService.MakeGroupRequestAsync(@event.UserId, @event.InviteUserId,@event.GroupId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
using IM_API.Domain.Events;
|
||||
using IM_API.Dtos;
|
||||
using IM_API.Hubs;
|
||||
using IM_API.VOs.Group;
|
||||
using MassTransit;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace IM_API.Application.EventHandlers.GroupInviteActionUpdateHandler
|
||||
{
|
||||
public class SignalRHandler : IConsumer<GroupInviteActionUpdateEvent>
|
||||
{
|
||||
private IHubContext<ChatHub> _hub;
|
||||
public SignalRHandler(IHubContext<ChatHub> hub)
|
||||
{
|
||||
_hub = hub;
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<GroupInviteActionUpdateEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
var msg = new HubResponse<GroupInviteActionUpdateVo>("Event", new GroupInviteActionUpdateVo
|
||||
{
|
||||
Action = @event.Action,
|
||||
GroupId = @event.GroupId,
|
||||
InvitedUserId = @event.UserId,
|
||||
InviteUserId = @event.InviteUserId,
|
||||
InviteId = @event.InviteId
|
||||
});
|
||||
|
||||
await _hub.Clients.Users([@event.UserId.ToString(), @event.InviteUserId.ToString()])
|
||||
.SendAsync("ReceiveMessage",msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
using IM_API.Domain.Events;
|
||||
using IM_API.Dtos;
|
||||
using IM_API.Hubs;
|
||||
using IM_API.VOs.Group;
|
||||
using MassTransit;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace IM_API.Application.EventHandlers.GroupInviteHandler
|
||||
{
|
||||
public class GroupInviteSignalRHandler : IConsumer<GroupInviteEvent>
|
||||
{
|
||||
private readonly IHubContext<ChatHub> _hub;
|
||||
public GroupInviteSignalRHandler(IHubContext<ChatHub> hub)
|
||||
{
|
||||
_hub = hub;
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<GroupInviteEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
var list = @event.Ids.Select(id => id.ToString()).ToArray();
|
||||
var msg = new HubResponse<GroupInviteVo>("Event", new GroupInviteVo { GroupId = @event.GroupId, UserId = @event.UserId });
|
||||
await _hub.Clients.Users(list).SendAsync("ReceiveMessage", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
using IM_API.Domain.Events;
|
||||
using IM_API.Interface.Services;
|
||||
using MassTransit;
|
||||
|
||||
namespace IM_API.Application.EventHandlers.GroupJoinHandler
|
||||
{
|
||||
public class GroupJoinConversationHandler : IConsumer<GroupJoinEvent>
|
||||
{
|
||||
private IConversationService _conversationService;
|
||||
public GroupJoinConversationHandler(IConversationService conversationService)
|
||||
{
|
||||
_conversationService = conversationService;
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<GroupJoinEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
await _conversationService.MakeConversationAsync(@event.UserId, @event.GroupId, Models.ChatType.GROUP);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
using IM_API.Domain.Events;
|
||||
using IM_API.Interface.Services;
|
||||
using MassTransit;
|
||||
|
||||
namespace IM_API.Application.EventHandlers.GroupJoinHandler
|
||||
{
|
||||
public class GroupJoinDbHandler : IConsumer<GroupJoinEvent>
|
||||
{
|
||||
private readonly IGroupService _groupService;
|
||||
public GroupJoinDbHandler(IGroupService groupService)
|
||||
{
|
||||
_groupService = groupService;
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<GroupJoinEvent> context)
|
||||
{
|
||||
await _groupService.MakeGroupMemberAsync(context.Message.UserId,
|
||||
context.Message.GroupId, context.Message.IsCreated ?
|
||||
Models.GroupMemberRole.Master : Models.GroupMemberRole.Normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
using IM_API.Domain.Events;
|
||||
using IM_API.Dtos;
|
||||
using IM_API.Hubs;
|
||||
using IM_API.Tools;
|
||||
using IM_API.VOs.Group;
|
||||
using MassTransit;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace IM_API.Application.EventHandlers.GroupJoinHandler
|
||||
{
|
||||
public class GroupJoinSignalrHandler : IConsumer<GroupJoinEvent>
|
||||
{
|
||||
private readonly IHubContext<ChatHub> _hub;
|
||||
private readonly IDatabase _redis;
|
||||
public GroupJoinSignalrHandler(IHubContext<ChatHub> hub, IConnectionMultiplexer connectionMultiplexer)
|
||||
{
|
||||
_hub = hub;
|
||||
_redis = connectionMultiplexer.GetDatabase();
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<GroupJoinEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
string stramKey = StreamKeyBuilder.Group(@event.GroupId);
|
||||
//将用户加入群组通知
|
||||
var list = await _redis.SetMembersAsync(RedisKeys.GetConnectionIdKey(@event.UserId.ToString()));
|
||||
if(list != null && list.Length > 0)
|
||||
{
|
||||
var tasks = list.Select(connectionId =>
|
||||
_hub.Groups.AddToGroupAsync(connectionId!, stramKey)
|
||||
).ToList();
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
//发送通知给群成员
|
||||
var msg = new GroupJoinVo
|
||||
{
|
||||
GroupId = @event.GroupId,
|
||||
UserId = @event.UserId
|
||||
};
|
||||
await _hub.Clients.Group(stramKey).SendAsync("ReceiveMessage",new HubResponse<GroupJoinVo>("Event",msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,17 @@
|
||||
using IM_API.Domain.Events;
|
||||
using IM_API.Hubs;
|
||||
using MassTransit;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace IM_API.Application.EventHandlers.GroupRequestHandler
|
||||
{
|
||||
public class GroupRequestSignalRHandler : IConsumer<GroupRequestEvent>
|
||||
public class GroupRequestSignalRHandler(IHubContext<ChatHub> hubContext) : IConsumer<GroupRequestEvent>
|
||||
{
|
||||
Task IConsumer<GroupRequestEvent>.Consume(ConsumeContext<GroupRequestEvent> context)
|
||||
private readonly IHubContext<ChatHub> _hub = hubContext;
|
||||
|
||||
public async Task Consume(ConsumeContext<GroupRequestEvent> context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
using IM_API.Domain.Events;
|
||||
using MassTransit;
|
||||
|
||||
namespace IM_API.Application.EventHandlers.GroupRequestHandler
|
||||
{
|
||||
public class NextEventHandler : IConsumer<GroupRequestEvent>
|
||||
{
|
||||
private readonly IPublishEndpoint _endpoint;
|
||||
public NextEventHandler(IPublishEndpoint endpoint)
|
||||
{
|
||||
_endpoint = endpoint;
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<GroupRequestEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
if(@event.Action == Models.GroupRequestState.Passed)
|
||||
{
|
||||
await _endpoint.Publish(new GroupJoinEvent
|
||||
{
|
||||
AggregateId = @event.AggregateId,
|
||||
OccurredAt = @event.OccurredAt,
|
||||
EventId = Guid.NewGuid(),
|
||||
GroupId = @event.GroupId,
|
||||
OperatorId = @event.OperatorId,
|
||||
UserId = @event.UserId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
using IM_API.Domain.Events;
|
||||
using MassTransit;
|
||||
|
||||
namespace IM_API.Application.EventHandlers.GroupRequestUpdateHandler
|
||||
{
|
||||
public class NextEventHandler : IConsumer<GroupRequestUpdateEvent>
|
||||
{
|
||||
private readonly IPublishEndpoint _endpoint;
|
||||
public NextEventHandler(IPublishEndpoint endpoint)
|
||||
{
|
||||
_endpoint = endpoint;
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<GroupRequestUpdateEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
if(@event.Action == Models.GroupRequestState.Passed)
|
||||
{
|
||||
await _endpoint.Publish(new GroupJoinEvent
|
||||
{
|
||||
AggregateId = @event.AggregateId,
|
||||
OccurredAt = @event.OccurredAt,
|
||||
EventId = Guid.NewGuid(),
|
||||
GroupId = @event.GroupId,
|
||||
OperatorId = @event.OperatorId,
|
||||
UserId = @event.UserId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
using IM_API.Domain.Events;
|
||||
using IM_API.Dtos;
|
||||
using IM_API.Hubs;
|
||||
using IM_API.VOs.Group;
|
||||
using MassTransit;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace IM_API.Application.EventHandlers.GroupRequestUpdateHandler
|
||||
{
|
||||
public class RequestUpdateSignalrHandler : IConsumer<GroupRequestUpdateEvent>
|
||||
{
|
||||
private readonly IHubContext<ChatHub> _hub;
|
||||
public RequestUpdateSignalrHandler(IHubContext<ChatHub> hub)
|
||||
{
|
||||
_hub = hub;
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<GroupRequestUpdateEvent> context)
|
||||
{
|
||||
var msg = new HubResponse<GroupRequestUpdateVo>("Event", new GroupRequestUpdateVo
|
||||
{
|
||||
GroupId = context.Message.GroupId,
|
||||
RequestId = context.Message.RequestId,
|
||||
UserId = context.Message.UserId
|
||||
});
|
||||
await _hub.Clients.User(context.Message.UserId.ToString()).SendAsync("ReceiveMessage", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,14 @@
|
||||
using AutoMapper;
|
||||
using IM_API.Application.EventHandlers.FriendAddHandler;
|
||||
using IM_API.Application.EventHandlers.GroupInviteActionUpdateHandler;
|
||||
using IM_API.Application.EventHandlers.GroupInviteHandler;
|
||||
using IM_API.Application.EventHandlers.GroupJoinHandler;
|
||||
using IM_API.Application.EventHandlers.GroupRequestHandler;
|
||||
using IM_API.Application.EventHandlers.GroupRequestUpdateHandler;
|
||||
using IM_API.Application.EventHandlers.MessageCreatedHandler;
|
||||
using IM_API.Application.EventHandlers.RequestFriendHandler;
|
||||
using IM_API.Configs.Options;
|
||||
using IM_API.Domain.Events;
|
||||
using MassTransit;
|
||||
using MySqlConnector;
|
||||
|
||||
@ -21,6 +27,16 @@ namespace IM_API.Configs
|
||||
x.AddConsumer<RequestFriendSignalRHandler>();
|
||||
x.AddConsumer<FriendAddConversationHandler>();
|
||||
x.AddConsumer<MessageCreatedDbHandler>();
|
||||
x.AddConsumer<GroupJoinConversationHandler>();
|
||||
x.AddConsumer<GroupJoinDbHandler>();
|
||||
x.AddConsumer<GroupJoinSignalrHandler>();
|
||||
x.AddConsumer<GroupRequestSignalRHandler>();
|
||||
x.AddConsumer<Application.EventHandlers.GroupRequestHandler.NextEventHandler>();
|
||||
x.AddConsumer<Application.EventHandlers.GroupRequestUpdateHandler.NextEventHandler>();
|
||||
x.AddConsumer<GroupInviteSignalRHandler>();
|
||||
x.AddConsumer<RequestDbHandler>();
|
||||
x.AddConsumer<SignalRHandler>();
|
||||
x.AddConsumer<RequestUpdateSignalrHandler>();
|
||||
x.UsingRabbitMq((ctx,cfg) =>
|
||||
{
|
||||
cfg.Host(options.Host, "/", h =>
|
||||
|
||||
@ -32,12 +32,40 @@ namespace IM_API.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(BaseResponse<GroupInfoDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult> CreateGroup([FromBody]GroupCreateDto groupCreateDto)
|
||||
public async Task<IActionResult> 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);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(BaseResponse<object?>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> HandleGroupInvite([FromBody]HandleGroupInviteDto dto)
|
||||
{
|
||||
string userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
|
||||
await _groupService.HandleGroupInviteAsync(int.Parse(userIdStr), dto);
|
||||
var res = new BaseResponse<object?>();
|
||||
return Ok(res);
|
||||
}
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(BaseResponse<object?>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> HandleGroupRequest([FromBody]HandleGroupRequestDto dto)
|
||||
{
|
||||
string userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
await _groupService.HandleGroupRequestAsync(int.Parse(userIdStr),dto);
|
||||
var res = new BaseResponse<object?>();
|
||||
return Ok(res);
|
||||
}
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(BaseResponse<object?>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> InviteUser([FromBody]GroupInviteUserDto dto)
|
||||
{
|
||||
string userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
await _groupService.InviteUsersAsync(int.Parse(userIdStr), dto.GroupId, dto.Ids);
|
||||
var res = new BaseResponse<object?>();
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/IM_API/Domain/Events/GroupInviteActionUpdateEvent.cs
Normal file
14
backend/IM_API/Domain/Events/GroupInviteActionUpdateEvent.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using IM_API.Models;
|
||||
|
||||
namespace IM_API.Domain.Events
|
||||
{
|
||||
public record GroupInviteActionUpdateEvent : DomainEvent
|
||||
{
|
||||
public override string EventType => "IM.GROUPS_INVITE_UPDATE";
|
||||
public int UserId { get; set; }
|
||||
public int InviteUserId { get; set; }
|
||||
public int InviteId { get; set; }
|
||||
public int GroupId { get; set; }
|
||||
public GroupInviteState Action { get; set; }
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
{
|
||||
public record GroupInviteEvent : DomainEvent
|
||||
{
|
||||
public override string EventType => "IM.GROUPS_GROUP_INVITE";
|
||||
public override string EventType => "IM.GROUPS_INVITE_ADD";
|
||||
public required List<int> Ids { get; init; }
|
||||
public int GroupId { get; init; }
|
||||
public int UserId { get; init; }
|
||||
|
||||
10
backend/IM_API/Domain/Events/GroupJoinEvent.cs
Normal file
10
backend/IM_API/Domain/Events/GroupJoinEvent.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace IM_API.Domain.Events
|
||||
{
|
||||
public record GroupJoinEvent : DomainEvent
|
||||
{
|
||||
public override string EventType => "IM.GROUPS_MEMBER_ADD";
|
||||
public int UserId { get; set; }
|
||||
public int GroupId { get; set; }
|
||||
public bool IsCreated { get; set; } = false;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using IM_API.Dtos.Group;
|
||||
using IM_API.Models;
|
||||
|
||||
namespace IM_API.Domain.Events
|
||||
{
|
||||
@ -8,6 +9,7 @@ namespace IM_API.Domain.Events
|
||||
public int GroupId { get; init; }
|
||||
public int UserId { get; set; }
|
||||
public string Description { get; set; }
|
||||
public GroupRequestState Action { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/IM_API/Domain/Events/GroupRequestUpdateEvent.cs
Normal file
14
backend/IM_API/Domain/Events/GroupRequestUpdateEvent.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using IM_API.Models;
|
||||
|
||||
namespace IM_API.Domain.Events
|
||||
{
|
||||
public record GroupRequestUpdateEvent : DomainEvent
|
||||
{
|
||||
public override string EventType => "IM.GROUPS_REQUEST_UPDATE";
|
||||
public int UserId { get; set; }
|
||||
public int GroupId { get; set; }
|
||||
public int AdminUserId { get; set; }
|
||||
public int RequestId { get; set; }
|
||||
public GroupRequestState Action { get; set; }
|
||||
}
|
||||
}
|
||||
8
backend/IM_API/Dtos/Group/GroupInviteUserDto.cs
Normal file
8
backend/IM_API/Dtos/Group/GroupInviteUserDto.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace IM_API.Dtos.Group
|
||||
{
|
||||
public class GroupInviteUserDto
|
||||
{
|
||||
public int GroupId { get; set; }
|
||||
public List<int> Ids { get; set; }
|
||||
}
|
||||
}
|
||||
10
backend/IM_API/Dtos/Group/HandleGroupInviteDto.cs
Normal file
10
backend/IM_API/Dtos/Group/HandleGroupInviteDto.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using IM_API.Models;
|
||||
|
||||
namespace IM_API.Dtos.Group
|
||||
{
|
||||
public class HandleGroupInviteDto
|
||||
{
|
||||
public int InviteId { get; set; }
|
||||
public GroupInviteState Action { get; set; }
|
||||
}
|
||||
}
|
||||
10
backend/IM_API/Dtos/Group/HandleGroupRequestDto.cs
Normal file
10
backend/IM_API/Dtos/Group/HandleGroupRequestDto.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using IM_API.Models;
|
||||
|
||||
namespace IM_API.Dtos.Group
|
||||
{
|
||||
public class HandleGroupRequestDto
|
||||
{
|
||||
public int RequestId { get; set; }
|
||||
public GroupRequestState Action { get; set; }
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@ using IM_API.Models;
|
||||
using IM_API.Tools;
|
||||
using IM_API.VOs.Message;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using StackExchange.Redis;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace IM_API.Hubs
|
||||
@ -15,14 +16,13 @@ namespace IM_API.Hubs
|
||||
{
|
||||
private IMessageSevice _messageService;
|
||||
private readonly IConversationService _conversationService;
|
||||
private readonly IEventBus _eventBus;
|
||||
private readonly IMapper _mapper;
|
||||
public ChatHub(IMessageSevice messageService, IConversationService conversationService, IEventBus eventBus, IMapper mapper)
|
||||
private readonly IDatabase _redis;
|
||||
public ChatHub(IMessageSevice messageService, IConversationService conversationService,
|
||||
IConnectionMultiplexer connectionMultiplexer)
|
||||
{
|
||||
_messageService = messageService;
|
||||
_conversationService = conversationService;
|
||||
_eventBus = eventBus;
|
||||
_mapper = mapper;
|
||||
_redis = connectionMultiplexer.GetDatabase();
|
||||
}
|
||||
|
||||
public async override Task OnConnectedAsync()
|
||||
@ -32,6 +32,7 @@ namespace IM_API.Hubs
|
||||
Context.Abort();
|
||||
return;
|
||||
}
|
||||
|
||||
//将用户加入已加入聊天的会话组
|
||||
string userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var keys = await _conversationService.GetUserAllStreamKeyAsync(int.Parse(userIdStr));
|
||||
@ -39,8 +40,20 @@ namespace IM_API.Hubs
|
||||
{
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, key);
|
||||
}
|
||||
//储存用户对应的连接id
|
||||
await _redis.SetAddAsync(RedisKeys.GetConnectionIdKey(userIdStr), Context.ConnectionId);
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
if (!Context.User.Identity.IsAuthenticated)
|
||||
{
|
||||
string useridStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
await _redis.SetRemoveAsync(RedisKeys.GetConnectionIdKey(useridStr), Context.ConnectionId);
|
||||
}
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
public async Task<HubResponse<MessageBaseVo?>> SendMessage(MessageBaseDto dto)
|
||||
{
|
||||
if (!Context.User.Identity.IsAuthenticated)
|
||||
|
||||
@ -33,8 +33,4 @@
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Application\EventHandlers\GroupInviteHandler\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using IM_API.Dtos.Group;
|
||||
using IM_API.Models;
|
||||
|
||||
namespace IM_API.Interface.Services
|
||||
{
|
||||
@ -43,6 +44,12 @@ namespace IM_API.Interface.Services
|
||||
/// <param name="desc"></param>
|
||||
/// <returns></returns>
|
||||
Task<List<GroupInfoDto>> GetGroupListAsync(int userId, int page, int limit, bool desc);
|
||||
|
||||
Task UpdateGroupConversationAsync(GroupUpdateConversationDto dto);
|
||||
|
||||
Task HandleGroupInviteAsync(int userid, HandleGroupInviteDto dto);
|
||||
Task HandleGroupRequestAsync(int userid, HandleGroupRequestDto dto);
|
||||
Task MakeGroupRequestAsync(int userId,int? adminUserId,int groupId);
|
||||
Task MakeGroupMemberAsync(int userId, int groupId, GroupMemberRole? role);
|
||||
}
|
||||
}
|
||||
|
||||
1117
backend/IM_API/Migrations/20260211065853_update-group-groupid-userid.Designer.cs
generated
Normal file
1117
backend/IM_API/Migrations/20260211065853_update-group-groupid-userid.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,49 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace IM_API.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class updategroupgroupiduserid : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "group_request_ibfk_2",
|
||||
table: "group_request");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_group_request_UserId",
|
||||
table: "group_request",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "group_request_ibfk_2",
|
||||
table: "group_request",
|
||||
column: "UserId",
|
||||
principalTable: "users",
|
||||
principalColumn: "ID");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "group_request_ibfk_2",
|
||||
table: "group_request");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_group_request_UserId",
|
||||
table: "group_request");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "group_request_ibfk_2",
|
||||
table: "group_request",
|
||||
column: "GroupId",
|
||||
principalTable: "users",
|
||||
principalColumn: "ID");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -513,6 +513,8 @@ namespace IM_API.Migrations
|
||||
b.HasKey("Id")
|
||||
.HasName("PRIMARY");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex(new[] { "GroupId" }, "GroupId")
|
||||
.HasDatabaseName("GroupId1");
|
||||
|
||||
@ -985,15 +987,15 @@ namespace IM_API.Migrations
|
||||
.IsRequired()
|
||||
.HasConstraintName("group_request_ibfk_1");
|
||||
|
||||
b.HasOne("IM_API.Models.User", "GroupNavigation")
|
||||
b.HasOne("IM_API.Models.User", "User")
|
||||
.WithMany("GroupRequests")
|
||||
.HasForeignKey("GroupId")
|
||||
.HasForeignKey("UserId")
|
||||
.IsRequired()
|
||||
.HasConstraintName("group_request_ibfk_2");
|
||||
|
||||
b.Navigation("Group");
|
||||
|
||||
b.Navigation("GroupNavigation");
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IM_API.Models.LoginLog", b =>
|
||||
|
||||
@ -37,5 +37,5 @@ public partial class GroupRequest
|
||||
|
||||
public virtual Group Group { get; set; } = null!;
|
||||
|
||||
public virtual User GroupNavigation { get; set; } = null!;
|
||||
public virtual User User { get; set; } = null!;
|
||||
}
|
||||
|
||||
@ -475,8 +475,8 @@ public partial class ImContext : DbContext
|
||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||
.HasConstraintName("group_request_ibfk_1");
|
||||
|
||||
entity.HasOne(d => d.GroupNavigation).WithMany(p => p.GroupRequests)
|
||||
.HasForeignKey(d => d.GroupId)
|
||||
entity.HasOne(d => d.User).WithMany(p => p.GroupRequests)
|
||||
.HasForeignKey(d => d.UserId)
|
||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||
.HasConstraintName("group_request_ibfk_2");
|
||||
});
|
||||
|
||||
@ -18,31 +18,25 @@ namespace IM_API.Services
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ILogger<GroupService> _logger;
|
||||
private readonly IPublishEndpoint _endPoint;
|
||||
public GroupService(ImContext context, IMapper mapper, ILogger<GroupService> logger, IPublishEndpoint publishEndpoint)
|
||||
private readonly IUserService _userService;
|
||||
public GroupService(ImContext context, IMapper mapper, ILogger<GroupService> logger,
|
||||
IPublishEndpoint publishEndpoint, IUserService userService)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
_logger = logger;
|
||||
_endPoint = publishEndpoint;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
private async Task<List<GroupInvite>> GetGroupInvites(int userId, int groupId, List<int> ids)
|
||||
private async Task<List<int>> validFriendshipAsync (int userId, List<int> ids)
|
||||
{
|
||||
DateTime dateTime = DateTime.Now;
|
||||
DateTime dateTime = DateTime.UtcNow;
|
||||
//验证被邀请用户是否为好友
|
||||
var validFriendIds = await _context.Friends
|
||||
return await _context.Friends
|
||||
.Where(f => f.UserId == userId && ids.Contains(f.FriendId))
|
||||
.Select(f => f.FriendId)
|
||||
.ToListAsync();
|
||||
//创建群成员对象
|
||||
return validFriendIds.Select(fid => new GroupInvite
|
||||
{
|
||||
Created = dateTime,
|
||||
GroupId = groupId,
|
||||
InvitedUser = fid,
|
||||
StateEnum = GroupInviteState.Pending,
|
||||
InviteUser = userId
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<GroupInfoDto> CreateGroupAsync(int userId, GroupCreateDto groupCreateDto)
|
||||
@ -57,31 +51,21 @@ namespace IM_API.Services
|
||||
group.GroupMaster = userId;
|
||||
_context.Groups.Add(group);
|
||||
await _context.SaveChangesAsync();
|
||||
var groupInvites = new List<GroupInvite>();
|
||||
if (userIds.Count > 0)
|
||||
{
|
||||
groupInvites = await GetGroupInvites(userId,group.Id, userIds);
|
||||
_context.GroupInvites.AddRange(groupInvites);
|
||||
//邀请好友
|
||||
await InviteUsersAsync(userId, group.Id ,userIds);
|
||||
}
|
||||
var groupMember = new GroupMember
|
||||
{
|
||||
UserId = userId,
|
||||
Created = dateTime,
|
||||
RoleEnum = GroupMemberRole.Master,
|
||||
GroupId = group.Id
|
||||
};
|
||||
_context.GroupMembers.Add(groupMember);
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
await _endPoint.Publish(new GroupInviteEvent
|
||||
await _endPoint.Publish(new GroupJoinEvent
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
AggregateId = userId.ToString(),
|
||||
GroupId = group.Id,
|
||||
EventId = Guid.NewGuid(),
|
||||
OccurredAt = dateTime,
|
||||
Ids = groupInvites.Select(x => x.Id).ToList(),
|
||||
OperatorId = userId,
|
||||
UserId = userId
|
||||
UserId = userId,
|
||||
IsCreated = true
|
||||
});
|
||||
return _mapper.Map<GroupInfoDto>(group);
|
||||
}
|
||||
@ -101,7 +85,43 @@ namespace IM_API.Services
|
||||
{
|
||||
var group = await _context.Groups.FirstOrDefaultAsync(
|
||||
x => x.Id == groupId) ?? throw new BaseException(CodeDefine.GROUP_NOT_FOUND);
|
||||
|
||||
//过滤非好友
|
||||
var groupInviteIds = await validFriendshipAsync(userId, userIds);
|
||||
var inviteList = groupInviteIds.Select(id => new GroupInvite
|
||||
{
|
||||
Created = DateTime.UtcNow,
|
||||
GroupId = group.Id,
|
||||
InviteUser = userId,
|
||||
InvitedUser = id,
|
||||
StateEnum = GroupInviteState.Pending
|
||||
}).ToList();
|
||||
_context.GroupInvites.AddRange(inviteList);
|
||||
await _context.SaveChangesAsync();
|
||||
await _endPoint.Publish(new GroupInviteEvent
|
||||
{
|
||||
GroupId = groupId,
|
||||
AggregateId = userId.ToString(),
|
||||
OccurredAt = DateTime.UtcNow,
|
||||
EventId = Guid.NewGuid(),
|
||||
Ids = userIds,
|
||||
OperatorId = userId,
|
||||
UserId = userId
|
||||
});
|
||||
}
|
||||
|
||||
public async Task MakeGroupMemberAsync(int userId, int groupId ,GroupMemberRole? role)
|
||||
{
|
||||
var isExist = await _context.GroupMembers.AnyAsync(x => x.GroupId == groupId && x.UserId == userId);
|
||||
if (isExist) return;
|
||||
var groupMember = new GroupMember
|
||||
{
|
||||
UserId = userId,
|
||||
Created = DateTime.UtcNow,
|
||||
RoleEnum = role ?? GroupMemberRole.Normal,
|
||||
GroupId = groupId
|
||||
};
|
||||
_context.GroupMembers.Add(groupMember);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public Task JoinGroupAsync(int userId, int groupId)
|
||||
@ -136,5 +156,96 @@ namespace IM_API.Services
|
||||
_context.Groups.Update(group);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
public async Task HandleGroupInviteAsync(int userid, HandleGroupInviteDto dto)
|
||||
{
|
||||
var user = _userService.GetUserInfoAsync(userid);
|
||||
var inviteInfo = await _context.GroupInvites.FirstOrDefaultAsync(x => x.Id == dto.InviteId)
|
||||
?? throw new BaseException(CodeDefine.INVALID_ACTION);
|
||||
if (inviteInfo.InvitedUser != userid) throw new BaseException(CodeDefine.AUTH_FAILED);
|
||||
inviteInfo.StateEnum = dto.Action;
|
||||
_context.GroupInvites.Update(inviteInfo);
|
||||
await _context.SaveChangesAsync();
|
||||
await _endPoint.Publish(new GroupInviteActionUpdateEvent
|
||||
{
|
||||
Action = dto.Action,
|
||||
AggregateId = userid.ToString(),
|
||||
OccurredAt = DateTime.UtcNow,
|
||||
EventId = Guid.NewGuid(),
|
||||
GroupId = inviteInfo.GroupId,
|
||||
InviteId = inviteInfo.Id,
|
||||
InviteUserId = inviteInfo.InviteUser.Value,
|
||||
OperatorId = userid,
|
||||
UserId = userid
|
||||
|
||||
});
|
||||
}
|
||||
public async Task HandleGroupRequestAsync(int userid, HandleGroupRequestDto dto)
|
||||
{
|
||||
var user = _userService.GetUserInfoAsync(userid);
|
||||
//判断请求存在
|
||||
var requestInfo = await _context.GroupRequests.FirstOrDefaultAsync(x => x.Id == dto.RequestId)
|
||||
?? throw new BaseException(CodeDefine.INVALID_ACTION);
|
||||
//判断成员存在
|
||||
var memberInfo = await _context.GroupMembers.FirstOrDefaultAsync(x => x.UserId == userid)
|
||||
?? throw new BaseException(CodeDefine.NO_GROUP_PERMISSION);
|
||||
//判断成员权限
|
||||
if (memberInfo.RoleEnum != GroupMemberRole.Master && memberInfo.RoleEnum != GroupMemberRole.Administrator)
|
||||
throw new BaseException(CodeDefine.NO_GROUP_PERMISSION);
|
||||
|
||||
requestInfo.StateEnum = dto.Action;
|
||||
_context.GroupRequests.Update(requestInfo);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
await _endPoint.Publish(new GroupRequestUpdateEvent
|
||||
{
|
||||
Action = requestInfo.StateEnum,
|
||||
AdminUserId = userid,
|
||||
AggregateId = userid.ToString(),
|
||||
OccurredAt = DateTime.UtcNow,
|
||||
EventId = Guid.NewGuid(),
|
||||
GroupId = requestInfo.GroupId,
|
||||
OperatorId = userid,
|
||||
UserId = requestInfo.UserId,
|
||||
RequestId = requestInfo.Id
|
||||
});
|
||||
}
|
||||
public async Task MakeGroupRequestAsync(int userId, int? adminUserId, int groupId)
|
||||
{
|
||||
var requestInfo = await _context.GroupRequests
|
||||
.FirstOrDefaultAsync(x => x.UserId == userId && x.GroupId == groupId);
|
||||
if (requestInfo != null) return;
|
||||
|
||||
var member = await _context.GroupMembers.FirstOrDefaultAsync(
|
||||
x => x.UserId == adminUserId && x.GroupId == groupId);
|
||||
var request = new GroupRequest
|
||||
{
|
||||
Created = DateTime.UtcNow,
|
||||
Description = string.Empty,
|
||||
GroupId = groupId,
|
||||
UserId = userId,
|
||||
StateEnum = GroupRequestState.Pending
|
||||
};
|
||||
if(member != null && (
|
||||
member.RoleEnum == GroupMemberRole.Administrator || member.RoleEnum == GroupMemberRole.Master))
|
||||
{
|
||||
request.StateEnum = GroupRequestState.Passed;
|
||||
}
|
||||
|
||||
_context.GroupRequests.Add(request);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
await _endPoint.Publish(new GroupRequestEvent
|
||||
{
|
||||
OccurredAt = DateTime.UtcNow,
|
||||
Description = request.Description,
|
||||
GroupId = request.GroupId,
|
||||
Action = request.StateEnum,
|
||||
UserId = userId,
|
||||
AggregateId = userId.ToString(),
|
||||
EventId = Guid.NewGuid(),
|
||||
OperatorId = userId
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,21 +2,10 @@
|
||||
{
|
||||
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}";
|
||||
}
|
||||
public static string GetUserinfoKey(string userId) => $"user::uinfo::{userId}";
|
||||
public static string GetUserinfoKeyByUsername(string username) => $"user::uinfobyid::{username}";
|
||||
public static string GetSequenceIdKey(string streamKey) => $"chat::seq::{streamKey}";
|
||||
public static string GetSequenceIdLockKey(string streamKey) => $"lock::seq::{streamKey}";
|
||||
public static string GetConnectionIdKey(string userId) => $"signalr::user::con::{userId}";
|
||||
}
|
||||
}
|
||||
|
||||
13
backend/IM_API/VOs/Group/GroupInviteActionUpdateVo.cs
Normal file
13
backend/IM_API/VOs/Group/GroupInviteActionUpdateVo.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using IM_API.Models;
|
||||
|
||||
namespace IM_API.VOs.Group
|
||||
{
|
||||
public class GroupInviteActionUpdateVo
|
||||
{
|
||||
public int GroupId { get; set; }
|
||||
public int InviteUserId { get; set; }
|
||||
public int InvitedUserId { get; set; }
|
||||
public int InviteId { get; set; }
|
||||
public GroupInviteState Action { get; set; }
|
||||
}
|
||||
}
|
||||
9
backend/IM_API/VOs/Group/GroupInviteVo.cs
Normal file
9
backend/IM_API/VOs/Group/GroupInviteVo.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace IM_API.VOs.Group
|
||||
{
|
||||
public class GroupInviteVo
|
||||
{
|
||||
public int GroupId { get; set; }
|
||||
public int UserId { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
8
backend/IM_API/VOs/Group/GroupJoinVo.cs
Normal file
8
backend/IM_API/VOs/Group/GroupJoinVo.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace IM_API.VOs.Group
|
||||
{
|
||||
public class GroupJoinVo
|
||||
{
|
||||
public int UserId { get; set; }
|
||||
public int GroupId { get; set; }
|
||||
}
|
||||
}
|
||||
9
backend/IM_API/VOs/Group/GroupRequestUpdateVo.cs
Normal file
9
backend/IM_API/VOs/Group/GroupRequestUpdateVo.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace IM_API.VOs.Group
|
||||
{
|
||||
public class GroupRequestUpdateVo
|
||||
{
|
||||
public int RequestId { get; set; }
|
||||
public int GroupId { get; set; }
|
||||
public int UserId { get; set; }
|
||||
}
|
||||
}
|
||||
@ -20,8 +20,7 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@ -29,4 +28,8 @@ onMounted(async () => {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
98
frontend/web/src/components/ContextMenu.vue
Normal file
98
frontend/web/src/components/ContextMenu.vue
Normal file
@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="context-menu"
|
||||
:style="{ top: style.top, left: style.left }"
|
||||
@click="hide"
|
||||
>
|
||||
<div
|
||||
v-for="item in menuItems"
|
||||
:key="item.label"
|
||||
class="menu-item"
|
||||
:class="{ danger: item.type === 'danger' }"
|
||||
@click="item.action"
|
||||
>
|
||||
<span class="icon">{{ item.icon }}</span>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
const visible = ref(false);
|
||||
const menuItems = ref([]);
|
||||
const style = reactive({
|
||||
top: '0px',
|
||||
left: '0px'
|
||||
});
|
||||
|
||||
// 打开菜单:传入原生事件 e 和菜单配置
|
||||
const show = (e, items) => {
|
||||
e.preventDefault(); // 再次确保拦截
|
||||
menuItems.value = items;
|
||||
|
||||
// 核心:使用 client 坐标定位
|
||||
style.top = `${e.clientY}px`;
|
||||
style.left = `${e.clientX}px`;
|
||||
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 点击外部自动关闭
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', hide);
|
||||
window.addEventListener('contextmenu', hide); // 再次右键也关闭旧的
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('click', hide);
|
||||
window.removeEventListener('contextmenu', hide);
|
||||
});
|
||||
|
||||
defineExpose({ show, hide });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
min-width: 140px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #eee;
|
||||
padding: 5px 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.menu-item.danger {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.menu-item .icon {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@ -1,14 +1,16 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useContactStore } from '@/stores/contact';
|
||||
import { groupService } from '@/services/group';
|
||||
import { SYSTEM_BASE_STATUS } from '@/constants/systemBaseStatus';
|
||||
import { useMessage } from '../messages/useAlert';
|
||||
|
||||
const contactStore = useContactStore();
|
||||
const message = useMessage();
|
||||
|
||||
const props = defineProps({ modelValue: Boolean });
|
||||
const emit = defineEmits(['update:modelValue', 'create']);
|
||||
|
||||
const friends = ref([
|
||||
{ id: 1, name: '张三', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=1' },
|
||||
{ id: 2, name: '李四', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=2' },
|
||||
{ id: 3, name: '王五', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=3' },
|
||||
]);
|
||||
const friends = ref([])
|
||||
|
||||
const groupName = ref('');
|
||||
const selected = ref(new Set()); // 使用 Set 处理选中逻辑更简洁
|
||||
@ -17,10 +19,23 @@ const toggle = (id) => {
|
||||
selected.value.has(id) ? selected.value.delete(id) : selected.value.add(id);
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
emit('create', { name: groupName.value, members: Array.from(selected.value) });
|
||||
emit('update:modelValue', false);
|
||||
const submit = async () => {
|
||||
const res = await groupService.createGroup({
|
||||
name: groupName.value,
|
||||
avatar: "https://baidu.com",
|
||||
userIDs: [...selected.value]
|
||||
});
|
||||
|
||||
if(res.code == SYSTEM_BASE_STATUS.SUCCESS){
|
||||
message.show('群聊创建成功。');
|
||||
}else{
|
||||
message.error(res.message);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () =>{
|
||||
friends.value = contactStore.contacts;
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -36,10 +51,10 @@ const submit = () => {
|
||||
<input v-model="groupName" placeholder="群组名称..." class="mini-input" />
|
||||
|
||||
<div class="list">
|
||||
<div v-for="f in friends" :key="f.id" @click="toggle(f.id)" class="item">
|
||||
<img :src="f.avatar" class="avatar" />
|
||||
<span class="name">{{ f.name }}</span>
|
||||
<input type="checkbox" :checked="selected.has(f.id)" />
|
||||
<div v-for="f in friends" :key="f.friendId" @click="toggle(f.friendId)" class="item">
|
||||
<img :src="f.userInfo.avatar" class="avatar" />
|
||||
<span class="name">{{ f.remarkName }}</span>
|
||||
<input type="checkbox" :checked="selected.has(f.friendId)" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@ -30,7 +30,7 @@ const routes = [
|
||||
{
|
||||
path: '/messages/chat/:id',
|
||||
name: '/msgChat',
|
||||
component: () => import('@/views/messages/MessageContent.vue'),
|
||||
component: () => import('@/views/messages/messageContent/MessageContent.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
|
||||
10
frontend/web/src/services/group.js
Normal file
10
frontend/web/src/services/group.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { request } from "./api"
|
||||
|
||||
export const groupService = {
|
||||
/**
|
||||
* 创建群聊
|
||||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
createGroup: (data) => request.post('/Group/CreateGroup', data)
|
||||
}
|
||||
@ -49,9 +49,9 @@ function handleStartChat(contact) {
|
||||
/* 1. 基础容器:锁定宽高,禁止抖动 */
|
||||
.im-container {
|
||||
display: flex;
|
||||
width: 1000px;
|
||||
height: 650px;
|
||||
margin: 40px auto;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
@ -3,20 +3,21 @@
|
||||
<header class="chat-header">
|
||||
<span class="title">{{ conversationInfo?.targetName || '未选择会话' }}</span>
|
||||
<div class="actions">
|
||||
<button @click="startCall('video')" v-html="feather.icons['video'].toSvg({width:16, height: 16})"></button>
|
||||
<button @click="startCall('voice')" v-html="feather.icons['phone'].toSvg({width:16, height: 16})"></button>
|
||||
<button class="tool-btn" @click="startCall('video')" v-html="feather.icons['video'].toSvg({width:20, height: 20})"></button>
|
||||
<button class="tool-btn" @click="startCall('voice')" v-html="feather.icons['phone-call'].toSvg({width:20, height: 20})"></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="chat-history" ref="historyRef">
|
||||
<HistoryLoading ref="loadingRef" :loading="isLoading" :finished="isFinished" :error="hasError" @retry="loadHistoryMsg"/>
|
||||
<UserHoverCard ref="userHoverCardRef"/>
|
||||
<ContextMenu ref="menuRef"/>
|
||||
<div v-for="m in chatStore.messages" :key="m.id" :class="['msg', m.senderId == myInfo.id ? 'mine' : 'other']">
|
||||
<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="group-sendername" v-if="m.chatType == MESSAGE_TYPE.GROUP && m.senderId != myInfo.id">{{ m.senderName }}</div>
|
||||
<div class="bubble">
|
||||
<div class="bubble" @contextmenu.prevent="(e) => handleRightClick(e, m)">
|
||||
<div v-if="m.type === 'Text'">{{ m.content }}</div>
|
||||
<div v-else-if="m.type === 'emoji'" class="emoji-msg">{{ m.content }}</div>
|
||||
<div class="status" v-if="m.senderId == myInfo.id">
|
||||
@ -69,6 +70,7 @@ import HistoryLoading from '@/components/messages/HistoryLoading.vue';
|
||||
import { useMessage } from '@/components/messages/useAlert';
|
||||
import { SYSTEM_BASE_STATUS } from '@/constants/systemBaseStatus';
|
||||
import UserHoverCard from '@/components/user/UserHoverCard.vue';
|
||||
import ContextMenu from '@/components/ContextMenu.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id:{
|
||||
@ -85,6 +87,7 @@ const input = ref(''); // 输入框内容
|
||||
const historyRef = ref(null); // 绑定 DOM 用于滚动
|
||||
const loadingRef = ref(null)
|
||||
const userHoverCardRef = ref(null);
|
||||
const menuRef = ref(null);
|
||||
const myInfo = useAuthStore().userInfo;
|
||||
|
||||
const conversationInfo = ref(null)
|
||||
@ -139,6 +142,38 @@ const closeHoverCard = () => {
|
||||
userHoverCardRef.value.hide();
|
||||
}
|
||||
|
||||
const handleRightClick = (e, m) => {
|
||||
e.stopPropagation();
|
||||
const items = [
|
||||
{
|
||||
label: '复制',
|
||||
action: () => console.log('打开之前的悬浮卡片', user)
|
||||
},
|
||||
{
|
||||
label: '转发',
|
||||
action: () => console.log('进入私聊', user.id)
|
||||
},
|
||||
{
|
||||
label: '多选',
|
||||
action: () => {}
|
||||
},
|
||||
{
|
||||
label: '翻译',
|
||||
action: () => {}
|
||||
},
|
||||
{
|
||||
label: '引用',
|
||||
action: () => {}
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'danger',
|
||||
action: () => alert('删除成功')
|
||||
}
|
||||
];
|
||||
menuRef.value.show(e, items);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => chatStore.messages,
|
||||
async (newVal) => {
|
||||
@ -366,9 +401,11 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
border: 0;
|
||||
background-color: white;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
|
||||
/* 历史区域:自动撑开并处理滚动 */
|
||||
.chat-history {
|
||||
flex: 1;
|
||||
@ -0,0 +1,21 @@
|
||||
export function useRightClickHandler() {
|
||||
const items = [
|
||||
{
|
||||
label: '查看资料',
|
||||
action: () => console.log('打开之前的悬浮卡片', user)
|
||||
},
|
||||
{
|
||||
label: '发送消息',
|
||||
action: () => console.log('进入私聊', user.id)
|
||||
},
|
||||
{
|
||||
label: '修改备注',
|
||||
action: () => { }
|
||||
},
|
||||
{
|
||||
label: '删除好友',
|
||||
type: 'danger',
|
||||
action: () => alert('删除成功')
|
||||
}
|
||||
];
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user