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);
}
}
@@ -28,11 +28,11 @@ namespace GroupService.WebApi.Controllers.Group
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
return Ok(await service.GetAllAsync(Guid.Parse(userId)));
}
[HttpGet("~/api/[controller]/{userId}")]
[HttpGet]
[ProducesDefaultResponseType(typeof(Result<GroupResponse>))]
public async Task<IActionResult> GetOne([FromRoute] Guid userId)
public async Task<IActionResult> GetOne(Guid groupId)
{
return Ok(await service.GetByIdAsync(userId));
return Ok(await service.GetByIdAsync(groupId));
}
[HttpPost]
@@ -42,5 +42,12 @@ namespace GroupService.WebApi.Controllers.Group
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
return Ok(await service.CreateAsync(new GroupCreateCommand(Guid.Parse(userId), request.Name)));
}
[HttpPost]
[UnitOfWork(typeof(GroupDbContext))]
public async Task<IActionResult> Update(GroupUpdateRequest request)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
return Ok(await service.UpdateAsync(new GroupUpdateCommand(Guid.Parse(userId), request.GroupId, request.Avatar, request.GroupName, request.Description)));
}
}
}
@@ -0,0 +1,22 @@
using FluentValidation;
namespace GroupService.WebApi.Controllers.Group
{
public class GroupUpdateRequest
{
public Guid GroupId { get; set; }
public string? GroupName { get; set; }
public string? Avatar { get; set; }
public string? Description { get; set; }
}
public class GroupUpDateRequestValidator : AbstractValidator<GroupUpdateRequest>
{
public GroupUpDateRequestValidator()
{
RuleFor(r => r.GroupId)
.NotNull()
.NotEmpty();
}
}
}
@@ -41,5 +41,12 @@ namespace GroupService.WebApi.Controllers.GroupRequest
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
return Ok(await service.GetByIdAsync(id, Guid.Parse(userId)));
}
[HttpGet]
public async Task<IActionResult> List()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
return Ok(await service.GetListAsync(Guid.Parse(userId)));
}
}
}
+3 -17
View File
@@ -1,27 +1,13 @@
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY IM_API_NEW.sln ./
COPY GroupService.WebApi/GroupService.WebApi.csproj GroupService.WebApi/
COPY GroupService.Domain/GroupService.Domain.csproj GroupService.Domain/
COPY GroupService.Infrastructure/GroupService.Infrastructure.csproj GroupService.Infrastructure/
COPY DomainCommons/IM.DomainCommons.csproj DomainCommons/
COPY Infrastructure/IM.Infrastructure.csproj Infrastructure/
COPY IM.ASPNETCore/IM.ASPNETCore.csproj IM.ASPNETCore/
COPY IM.Commons/IM.Commons.csproj IM.Commons/
COPY IM.InitCommon/IM.InitCommon.csproj IM.InitCommon/
COPY IM.Jwt/IM.Jwt.csproj IM.Jwt/
COPY IM.Protocols/IM.Protocols.csproj IM.Protocols/
RUN dotnet restore GroupService.WebApi/GroupService.WebApi.csproj
COPY . .
RUN find . -type d \( -name obj -o -name bin \) -prune -exec rm -rf {} + 2>/dev/null || true
RUN dotnet publish GroupService.WebApi/GroupService.WebApi.csproj \
-c Release \
-o /app/publish \
--no-restore
-o /app/publish
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app