Commit 241064e7d07424892b33c2d0d840ec894123cf23

Authored by YunaiV
1 parent f5bec3ee

✨ feat(im): 初始化群申请 v0.3:第四把 review(优化界面,进一步对齐微信界面)

yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java
@@ -21,8 +21,6 @@ import io.swagger.v3.oas.annotations.Parameter; @@ -21,8 +21,6 @@ import io.swagger.v3.oas.annotations.Parameter;
21 import io.swagger.v3.oas.annotations.tags.Tag; 21 import io.swagger.v3.oas.annotations.tags.Tag;
22 import jakarta.annotation.Resource; 22 import jakarta.annotation.Resource;
23 import jakarta.validation.Valid; 23 import jakarta.validation.Valid;
24 -import jakarta.validation.constraints.Max;  
25 -import jakarta.validation.constraints.Min;  
26 import jakarta.validation.constraints.NotNull; 24 import jakarta.validation.constraints.NotNull;
27 import jakarta.validation.constraints.Size; 25 import jakarta.validation.constraints.Size;
28 import org.springframework.validation.annotation.Validated; 26 import org.springframework.validation.annotation.Validated;
@@ -82,18 +80,6 @@ public class ImGroupRequestController { @@ -82,18 +80,6 @@ public class ImGroupRequestController {
82 return success(true); 80 return success(true);
83 } 81 }
84 82
85 - @GetMapping("/list")  
86 - @Operation(summary = "查询「我相关」的加群申请列表(含我主动申请、我被邀请待审);游标分页")  
87 - public CommonResult<List<ImGroupRequestRespVO>> getGroupRequestList(  
88 - @Parameter(description = "当前列表最旧记录的 id;首页不传")  
89 - @RequestParam(value = "lastRequestId", required = false) Long lastRequestId,  
90 - @Parameter(description = "单次拉取条数", required = true)  
91 - @RequestParam("limit") @Min(1) @Max(200) Integer limit) {  
92 - List<ImGroupRequestDO> list = groupRequestService.getGroupRequestListByUserId(  
93 - getLoginUserId(), lastRequestId, limit);  
94 - return success(buildVOList(list));  
95 - }  
96 -  
97 @GetMapping("/unhandled-list") 83 @GetMapping("/unhandled-list")
98 @Operation(summary = "查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表") 84 @Operation(summary = "查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表")
99 public CommonResult<List<ImGroupRequestRespVO>> getUnhandledRequestList() { 85 public CommonResult<List<ImGroupRequestRespVO>> getUnhandledRequestList() {
@@ -101,6 +87,15 @@ public class ImGroupRequestController { @@ -101,6 +87,15 @@ public class ImGroupRequestController {
101 return success(buildVOList(list)); 87 return success(buildVOList(list));
102 } 88 }
103 89
  90 + @GetMapping("/list-by-group")
  91 + @Operation(summary = "查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查")
  92 + @Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
  93 + public CommonResult<List<ImGroupRequestRespVO>> getGroupRequestListByGroupId(
  94 + @RequestParam("groupId") @NotNull(message = "群编号不能为空") Long groupId) {
  95 + List<ImGroupRequestDO> list = groupRequestService.getGroupRequestListByGroupId(getLoginUserId(), groupId);
  96 + return success(buildVOList(list));
  97 + }
  98 +
104 @GetMapping("/get") 99 @GetMapping("/get")
105 @Operation(summary = "按 id 单查申请记录(带越权过滤;WebSocket 通知到达后用)") 100 @Operation(summary = "按 id 单查申请记录(带越权过滤;WebSocket 通知到达后用)")
106 @Parameter(name = "id", description = "申请记录编号", required = true) 101 @Parameter(name = "id", description = "申请记录编号", required = true)
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java
@@ -25,14 +25,6 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; { @@ -25,14 +25,6 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; {
25 .eq(ImGroupRequestDO::getUserId, userId)); 25 .eq(ImGroupRequestDO::getUserId, userId));
26 } 26 }
27 27
28 - default List<ImGroupRequestDO> selectListByUserId(Long userId, Long lastRequestId, int limit) {  
29 - return selectList(new LambdaQueryWrapperX<ImGroupRequestDO>()  
30 - .eq(ImGroupRequestDO::getUserId, userId)  
31 - .ltIfPresent(ImGroupRequestDO::getId, lastRequestId)  
32 - .orderByDesc(ImGroupRequestDO::getId)  
33 - .last("LIMIT " + limit));  
34 - }  
35 -  
36 default List<ImGroupRequestDO> selectListByGroupIdsAndHandleResult(Collection<Long> groupIds, Integer handleResult) { 28 default List<ImGroupRequestDO> selectListByGroupIdsAndHandleResult(Collection<Long> groupIds, Integer handleResult) {
37 return selectList(new LambdaQueryWrapperX<ImGroupRequestDO>() 29 return selectList(new LambdaQueryWrapperX<ImGroupRequestDO>()
38 .in(ImGroupRequestDO::getGroupId, groupIds) 30 .in(ImGroupRequestDO::getGroupId, groupIds)
@@ -40,6 +32,12 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; { @@ -40,6 +32,12 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; {
40 .orderByDesc(ImGroupRequestDO::getId)); 32 .orderByDesc(ImGroupRequestDO::getId));
41 } 33 }
42 34
  35 + default List<ImGroupRequestDO> selectListByGroupId(Long groupId) {
  36 + return selectList(new LambdaQueryWrapperX<ImGroupRequestDO>()
  37 + .eq(ImGroupRequestDO::getGroupId, groupId)
  38 + .orderByDesc(ImGroupRequestDO::getId));
  39 + }
  40 +
43 default int updateByIdAndHandleResult(Long id, Integer expectedHandleResult, ImGroupRequestDO updateObj) { 41 default int updateByIdAndHandleResult(Long id, Integer expectedHandleResult, ImGroupRequestDO updateObj) {
44 return update(updateObj, new LambdaUpdateWrapper<ImGroupRequestDO>() 42 return update(updateObj, new LambdaUpdateWrapper<ImGroupRequestDO>()
45 .eq(ImGroupRequestDO::getId, id).eq(ImGroupRequestDO::getHandleResult, expectedHandleResult)); 43 .eq(ImGroupRequestDO::getId, id).eq(ImGroupRequestDO::getHandleResult, expectedHandleResult));
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestService.java
@@ -54,16 +54,6 @@ public interface ImGroupRequestService { @@ -54,16 +54,6 @@ public interface ImGroupRequestService {
54 void createInviteRequestList(Long groupId, Long inviterUserId, Collection<Long> invitedUserIds); 54 void createInviteRequestList(Long groupId, Long inviterUserId, Collection<Long> invitedUserIds);
55 55
56 /** 56 /**
57 - * 拉取「我相关」加群申请列表(含我主动申请、我被邀请待审);游标分页  
58 - *  
59 - * @param userId 用户编号  
60 - * @param lastRequestId 当前列表最旧记录的 id;首页传 null  
61 - * @param limit 单次拉取条数  
62 - * @return 申请记录列表,按 id 倒序  
63 - */  
64 - List<ImGroupRequestDO> getGroupRequestListByUserId(Long userId, Long lastRequestId, Integer limit);  
65 -  
66 - /**  
67 * 拉取「我管理的所有群」下的未处理申请列表 57 * 拉取「我管理的所有群」下的未处理申请列表
68 * <p> 58 * <p>
69 * 前端 store 据此派生:每个群的未处理总数(用于群顶部横幅红点)+ 列表内容(用于 Drawer) 59 * 前端 store 据此派生:每个群的未处理总数(用于群顶部横幅红点)+ 列表内容(用于 Drawer)
@@ -74,6 +64,17 @@ public interface ImGroupRequestService { @@ -74,6 +64,17 @@ public interface ImGroupRequestService {
74 List<ImGroupRequestDO> getUnhandledRequestListByOwnerOrAdmin(Long userId); 64 List<ImGroupRequestDO> getUnhandledRequestListByOwnerOrAdmin(Long userId);
75 65
76 /** 66 /**
  67 + * 拉取指定群下的全部加群申请(含已处理);仅群主 / 管理员可查
  68 + * <p>
  69 + * 用于群「进群申请」子页:最新一条卡片化突出 + 历史申请按 id 倒序
  70 + *
  71 + * @param userId 当前用户编号;用于校验 owner / admin 身份
  72 + * @param groupId 群编号
  73 + * @return 申请记录列表,按 id 倒序
  74 + */
  75 + List<ImGroupRequestDO> getGroupRequestListByGroupId(Long userId, Long groupId);
  76 +
  77 + /**
77 * 按 id 单查申请记录;通用读接口,越权过滤交由调用方 78 * 按 id 单查申请记录;通用读接口,越权过滤交由调用方
78 * 79 *
79 * @param id 申请记录编号 80 * @param id 申请记录编号
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java
@@ -246,11 +246,6 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -246,11 +246,6 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
246 } 246 }
247 247
248 @Override 248 @Override
249 - public List<ImGroupRequestDO> getGroupRequestListByUserId(Long userId, Long lastRequestId, Integer limit) {  
250 - return groupRequestMapper.selectListByUserId(userId, lastRequestId, limit);  
251 - }  
252 -  
253 - @Override  
254 public List<ImGroupRequestDO> getUnhandledRequestListByOwnerOrAdmin(Long userId) { 249 public List<ImGroupRequestDO> getUnhandledRequestListByOwnerOrAdmin(Long userId) {
255 // 1. 找出当前用户作为 OWNER / ADMIN 的所有群 250 // 1. 找出当前用户作为 OWNER / ADMIN 的所有群
256 List<ImGroupMemberDO> myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId); 251 List<ImGroupMemberDO> myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId);
@@ -266,6 +261,18 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -266,6 +261,18 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
266 } 261 }
267 262
268 @Override 263 @Override
  264 + public List<ImGroupRequestDO> getGroupRequestListByGroupId(Long userId, Long groupId) {
  265 + // 1. 校验群存在 + 当前用户是群主 / 管理员
  266 + groupService.validateGroupExists(groupId);
  267 + ImGroupMemberDO operator = groupMemberService.validateMemberInGroup(groupId, userId);
  268 + if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(operator.getRole())) {
  269 + throw exception(GROUP_REQUEST_NOT_TO_ME);
  270 + }
  271 + // 2. 拉取该群下全部申请(含已处理);按 id 倒序,前端首条卡片化展示
  272 + return groupRequestMapper.selectListByGroupId(groupId);
  273 + }
  274 +
  275 + @Override
269 public ImGroupRequestDO getGroupRequest(Long id) { 276 public ImGroupRequestDO getGroupRequest(Long id) {
270 return groupRequestMapper.selectById(id); 277 return groupRequestMapper.selectById(id);
271 } 278 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
@@ -191,9 +191,9 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -191,9 +191,9 @@ public class ImGroupServiceImpl implements ImGroupService {
191 @Override 191 @Override
192 public void inviteGroupMember(Long userId, ImGroupMemberInviteReqVO inviteReqVO) { 192 public void inviteGroupMember(Long userId, ImGroupMemberInviteReqVO inviteReqVO) {
193 Long groupId = inviteReqVO.getGroupId(); 193 Long groupId = inviteReqVO.getGroupId();
194 - // 1.1 校验群存在 + 当前用户是群成员 194 + // 1.1 校验群存在 + 当前用户是群成员;同时拿到 role 供下面审批分支判断
195 ImGroupDO group = validateGroupExists(groupId); 195 ImGroupDO group = validateGroupExists(groupId);
196 - groupMemberService.validateMemberInGroup(groupId, userId); 196 + ImGroupMemberDO operator = groupMemberService.validateMemberInGroup(groupId, userId);
197 // 1.2 排除已在群中的用户 197 // 1.2 排除已在群中的用户
198 List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId); 198 List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId);
199 activeMembers.forEach(member -> inviteReqVO.getMemberUserIds().remove(member.getUserId())); 199 activeMembers.forEach(member -> inviteReqVO.getMemberUserIds().remove(member.getUserId()));
@@ -213,13 +213,15 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -213,13 +213,15 @@ public class ImGroupServiceImpl implements ImGroupService {
213 throw exception(GROUP_MEMBER_EXCEED, MAX_GROUP_MEMBER); 213 throw exception(GROUP_MEMBER_EXCEED, MAX_GROUP_MEMBER);
214 } 214 }
215 215
216 - // 2. 情况一:群开启了审批,邀请也走审批,落 group_request 等群主 / 管理员处理  
217 - if (Boolean.TRUE.equals(group.getJoinApproval())) { 216 + // 2. 情况一:群开启审批 + 邀请人是普通成员,落 group_request 等群主 / 管理员处理
  217 + // 群主 / 管理员邀请视同已审批通过,绕过审批直接拉人进群(对齐 OpenIM / 微信)
  218 + if (Boolean.TRUE.equals(group.getJoinApproval())
  219 + && !ImGroupMemberRoleEnum.isOwnerOrAdmin(operator.getRole())) {
218 groupRequestService.createInviteRequestList(groupId, userId, memberUserIds); 220 groupRequestService.createInviteRequestList(groupId, userId, memberUserIds);
219 return; 221 return;
220 } 222 }
221 223
222 - // 3. 情况二:未开审批,直进;批量添加群成员,写 addSource=INVITE / inviterUserId=操作人 留痕 224 + // 3. 情况二:未开审批 / 群主 / 管理员邀请,直进;批量添加群成员,写 addSource=INVITE / inviterUserId=操作人 留痕
223 groupMemberService.addGroupMembers(groupId, memberUserIds, 225 groupMemberService.addGroupMembers(groupId, memberUserIds,
224 ImGroupAddSourceEnum.INVITE.getSource(), userId); 226 ImGroupAddSourceEnum.INVITE.getSource(), userId);
225 227
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java
@@ -301,16 +301,20 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -301,16 +301,20 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
301 } 301 }
302 302
303 @Test 303 @Test
304 - public void testInviteGroupMember_approval_routesToApproval() { 304 + public void testInviteGroupMember_approval_normalRoutesToApproval() {
305 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { 305 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
306 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) 306 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
307 .thenReturn(groupService); 307 .thenReturn(groupService);
308 308
309 - // 准备:joinApproval=true,开启审批;任何邀请都需审批,落 group_request 309 + // 准备:joinApproval=true,开启审批;普通成员邀请走审批,落 group_request
310 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) 310 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
311 .joinApproval(true) 311 .joinApproval(true)
312 .status(CommonStatusEnum.ENABLE.getStatus()).build(); 312 .status(CommonStatusEnum.ENABLE.getStatus()).build();
313 when(groupMapper.selectById(10L)).thenReturn(group); 313 when(groupMapper.selectById(10L)).thenReturn(group);
  314 + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(
  315 + ImGroupMemberDO.builder().groupId(10L).userId(1L)
  316 + .role(ImGroupMemberRoleEnum.NORMAL.getRole())
  317 + .status(CommonStatusEnum.ENABLE.getStatus()).build());
314 when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of( 318 when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of(
315 ImGroupMemberDO.builder().groupId(10L).userId(99L) 319 ImGroupMemberDO.builder().groupId(10L).userId(99L)
316 .role(ImGroupMemberRoleEnum.OWNER.getRole()) 320 .role(ImGroupMemberRoleEnum.OWNER.getRole())
@@ -339,6 +343,48 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -339,6 +343,48 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
339 } 343 }
340 344
341 @Test 345 @Test
  346 + public void testInviteGroupMember_approval_ownerBypassesApproval() {
  347 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  348 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
  349 + .thenReturn(groupService);
  350 +
  351 + // 准备:joinApproval=true,但邀请人是群主;视同已审批,直进群
  352 + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L)
  353 + .joinApproval(true)
  354 + .status(CommonStatusEnum.ENABLE.getStatus()).build();
  355 + when(groupMapper.selectById(10L)).thenReturn(group);
  356 + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(
  357 + ImGroupMemberDO.builder().groupId(10L).userId(1L)
  358 + .role(ImGroupMemberRoleEnum.OWNER.getRole())
  359 + .status(CommonStatusEnum.ENABLE.getStatus()).build());
  360 + when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of(
  361 + ImGroupMemberDO.builder().groupId(10L).userId(1L)
  362 + .role(ImGroupMemberRoleEnum.OWNER.getRole())
  363 + .status(CommonStatusEnum.ENABLE.getStatus()).build()));
  364 +
  365 + ImGroupMemberInviteReqVO reqVO = new ImGroupMemberInviteReqVO();
  366 + reqVO.setGroupId(10L);
  367 + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L, 3L)));
  368 + when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(List.of(
  369 + ImFriendDO.builder().userId(1L).friendUserId(2L)
  370 + .status(CommonStatusEnum.ENABLE.getStatus()).build(),
  371 + ImFriendDO.builder().userId(1L).friendUserId(3L)
  372 + .status(CommonStatusEnum.ENABLE.getStatus()).build()));
  373 +
  374 + // 调用
  375 + groupService.inviteGroupMember(1L, reqVO);
  376 +
  377 + // 断言:绕过审批,直接 addGroupMembers + 推 1509;不落 group_request
  378 + verify(groupRequestService, never()).createInviteRequestList(anyLong(), anyLong(), anyCollection());
  379 + verify(groupMemberService).addGroupMembers(eq(10L), anyCollection(),
  380 + eq(ImGroupAddSourceEnum.INVITE.getSource()), eq(1L));
  381 + ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
  382 + verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture());
  383 + assertEquals(ImMessageTypeEnum.GROUP_MEMBER_INVITE.getType(), dtoCaptor.getValue().getType());
  384 + }
  385 + }
  386 +
  387 + @Test
342 public void testInviteGroupMember_notFriend() { 388 public void testInviteGroupMember_notFriend() {
343 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { 389 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
344 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) 390 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))