This commit is contained in:
2026-08-31 14:42:22 +08:00
72 changed files with 2479 additions and 168 deletions
@@ -1,5 +1,5 @@
using GroupService.Domain.Events;
using GroupService.WebApi.Application.GroupRequest;
using GroupService.WebApi.Application.GroupMember;
using IM.Commons;
using IM.Commons.IntegrationEvents;
using MassTransit;
@@ -12,20 +12,19 @@ namespace GroupService.WebApi.Application.EventHandler
, INotificationHandler<GroupInvitationCreateDomainEvent>
{
private readonly IPublishEndpoint endpoint;
private readonly GroupRequestService requestService;
private readonly GroupMemberService memberService;
public GroupInvitationEventHandler(IPublishEndpoint endpoint, GroupRequestService requestService)
public GroupInvitationEventHandler(IPublishEndpoint endpoint, GroupMemberService memberService)
{
this.endpoint = endpoint;
this.requestService = requestService;
this.memberService = memberService;
}
public async Task Handle(GroupInvitationAcceptDomainEvent notification, CancellationToken cancellationToken)
{
var invitation = notification.Invitation;
var res = await requestService.CreateAsync(invitation.GroupId, invitation.UserId,
$"邀请入群"
);
// 邀请接受 → 直接加入群聊,不走入群申请审批流程
var res = await memberService.CreateAsync(invitation.GroupId, invitation.UserId);
await endpoint.Publish(new GroupInvitationAcceptEvent(invitation.Id, invitation.UserId, invitation.UserProfile.NickName, invitation.UserProfile.Avatar
, invitation.GroupId, invitation.GroupProfile.GroupName, invitation.GroupProfile.Avatar, invitation.OperatorId
@@ -34,7 +33,6 @@ namespace GroupService.WebApi.Application.EventHandler
if (!res.Succeeded)
{
throw new EventHandlerException(res.Message);
}
}
@@ -1,4 +1,5 @@
using GroupService.Domain.Events;
using GroupService.Infrastructure;
using IM.Commons.IntegrationEvents;
using MassTransit;
using MediatR;
@@ -8,18 +9,25 @@ namespace GroupService.WebApi.Application.EventHandler
public class GroupMemberJoinedHandler : INotificationHandler<GroupMemberJoinedDomainEvent>
{
private readonly IPublishEndpoint endpoint;
private readonly GroupDbContext db;
public GroupMemberJoinedHandler(IPublishEndpoint endpoint)
public GroupMemberJoinedHandler(IPublishEndpoint endpoint, GroupDbContext db)
{
this.endpoint = endpoint;
this.db = db;
}
public async Task Handle(GroupMemberJoinedDomainEvent notification, CancellationToken cancellationToken)
{
var member = notification.Member;
// 用 FindAsync 而非 FirstOrDefaultAsync:群可能刚创建尚未提交到数据库,FindAsync 先查内存跟踪器
var group = await db.Set<Domain.Entities.Group>().FindAsync(member.GroupId);
var groupName = group?.Name ?? "群聊";
var groupAvatar = group?.Avatar;
await endpoint.Publish(new GroupMemberJoinedEvent(member.Id,
member.UserId, member.GroupId, member.GroupNickName, member.Avatar,
member.Role.ToString()), cancellationToken);
member.Role.ToString(), groupName, groupAvatar), cancellationToken);
}
}
}
@@ -8,11 +8,13 @@ namespace GroupService.WebApi.Application.Group
public class GroupService
{
private readonly IGroupReposity reposity;
private readonly IGroupMemberReposity memberReposity;
private readonly IMapper mapper;
public GroupService(IGroupReposity reposity, IMapper mapper)
public GroupService(IGroupReposity reposity, IGroupMemberReposity memberReposity, IMapper mapper)
{
this.reposity = reposity;
this.memberReposity = memberReposity;
this.mapper = mapper;
}
@@ -39,5 +41,19 @@ namespace GroupService.WebApi.Application.Group
return Result<GroupResponse>.Success(mapper.Map<GroupResponse>(group));
}
public async Task<Result<GroupResponse>> UpdateAsync(GroupUpdateCommand command)
{
var group = await reposity.FindByIdAsync(command.GroupId);
if(group is null)
return Result.Fail<GroupResponse>(ResultCode.GROUP_NOT_FOUND);
var isAdmin = await memberReposity.CheckMemberAdminAsync(group.Id ,command.UserId);
if (!isAdmin)
return Result.Fail<GroupResponse>(ResultCode.ADMIN_PERMISSION_DENIED);
group.Update(command.GroupName, null, command.Description, command.Avatar);
return Result.Success(mapper.Map<GroupResponse>(group));
}
}
}
@@ -0,0 +1,4 @@
namespace GroupService.WebApi.Application.Group
{
public record GroupUpdateCommand(Guid UserId, Guid GroupId, string? Avatar, string? GroupName, string? Description);
}
@@ -39,6 +39,13 @@ namespace GroupService.WebApi.Application.GroupInvitation
return Result.Fail<GroupInvitationResponse>(ResultCode.PERMISSION_DENIED);
}
// 检查是否已存在对该用户的未处理邀请,避免 Duplicate entry 报错
var existing = await reposity.FindByGroupIdAndUserIdAsync(groupId, userId);
if (existing != null)
{
return Result.Success(mapper.Map<GroupInvitationResponse>(existing));
}
var group = await groupReposity.FindByIdAsync(groupId);
var userProfile = new UserProfile()
@@ -66,6 +73,54 @@ namespace GroupService.WebApi.Application.GroupInvitation
return Result.Success(mapper.Map<GroupInvitationResponse>(invitation));
}
public async Task<Result<object>> CreateBatchAsync(Guid operatorId, List<Guid> userIds, Guid groupId)
{
var group = await groupReposity.FindByIdAsync(groupId);
if (group is null)
return Result.Fail(ResultCode.GROUP_NOT_FOUND);
// 校验操作者是否有权限(群成员/管理员/群主)
var operatorMember = await memberReposity.FindOneByGroupIdAndUserIdAsync(groupId, operatorId);
if (operatorMember == null)
return Result.Fail(ResultCode.PERMISSION_DENIED);
var operatorInfo = await idService.FindUserByIdAsync(operatorId);
var operatorProfile = new UserProfile()
{
Avatar = operatorInfo.Data?.Avatar,
NickName = operatorInfo.Data?.NickName ?? string.Empty
};
var groupProfile = new GroupProfile()
{
Avatar = group.Avatar,
GroupName = group.Name
};
// 批量创建邀请,跳过已存在未处理邀请的用户
foreach (var userId in userIds)
{
var existing = await reposity.FindByGroupIdAndUserIdAsync(groupId, userId);
if (existing != null)
continue;
var userRes = await idService.FindUserByIdAsync(userId);
if (!userRes.Succeeded)
continue;
var userProfile = new UserProfile()
{
Avatar = userRes.Data?.Avatar,
NickName = userRes.Data?.NickName ?? string.Empty
};
var invitation = new Domain.Entities.GroupInvitation(
groupId, groupProfile, userId, userProfile, operatorId, operatorProfile);
reposity.Create(invitation);
}
return Result.Success();
}
public async Task<Result<object>> HandleAsync(GroupInvitationHandleCommand command)
{
var invitation = await reposity.FindByIdAsync(command.InvitationId);
@@ -96,6 +96,11 @@ namespace GroupService.WebApi.Application.GroupRequest
return Result.Success(mapper.Map<GroupRequestResponse>(request));
}
public async Task<Result<List<GroupRequestResponse>>> GetListAsync(Guid userId)
{
var list = await reposity.FindByUserIdAsync(userId);
return Result.Success(mapper.Map<List<GroupRequestResponse>>(list.ToList()));
}
}
}
@@ -14,6 +14,35 @@ namespace GroupService.WebApi.Application.IntegrationServices
this.client = client;
}
public async Task<Result<List<UserInfoDto>>> FindByIdsAsync(List<Guid> ids)
{
try
{
var request = new GetUserListRequest();
request.UserIds.AddRange(ids.Select(x => x.ToString()));
var response = await client.GetUserListAsyncAsync(request);
var users = response.Users.Select(x => new UserInfoDto
{
Avatar = x.Avatar,
CreationTime = x.CreationTime.ToDateTimeOffset(),
Deletion = x.Deletion?.ToDateTimeOffset(),
Description = x.Description,
Email = x.Email,
Id = Guid.Parse(x.Id),
NickName = x.NickName,
Phone = x.Phone,
Region = x.Region,
UserName = x.UserName
}).ToList();
return Result.Success(users);
}catch(RpcException e)
{
return Result.Fail<List<UserInfoDto>>(ResultCode.USER_NOT_FOUND);
}
}
public async Task<Result<UserInfoDto>> FindUserByIdAsync(Guid id)
{
try
@@ -6,5 +6,6 @@ namespace GroupService.WebApi.Application.IntegrationServices
public interface IIdentityIntegrationService
{
Task<Result<UserInfoDto>> FindUserByIdAsync(Guid id);
Task<Result<List<UserInfoDto>>> FindByIdsAsync(List<Guid> ids);
}
}