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 21 import io.swagger.v3.oas.annotations.tags.Tag;
22 22 import jakarta.annotation.Resource;
23 23 import jakarta.validation.Valid;
24   -import jakarta.validation.constraints.Max;
25   -import jakarta.validation.constraints.Min;
26 24 import jakarta.validation.constraints.NotNull;
27 25 import jakarta.validation.constraints.Size;
28 26 import org.springframework.validation.annotation.Validated;
... ... @@ -82,18 +80,6 @@ public class ImGroupRequestController {
82 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 83 @GetMapping("/unhandled-list")
98 84 @Operation(summary = "查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表")
99 85 public CommonResult<List<ImGroupRequestRespVO>> getUnhandledRequestList() {
... ... @@ -101,6 +87,15 @@ public class ImGroupRequestController {
101 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 99 @GetMapping("/get")
105 100 @Operation(summary = "按 id 单查申请记录(带越权过滤;WebSocket 通知到达后用)")
106 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 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 28 default List<ImGroupRequestDO> selectListByGroupIdsAndHandleResult(Collection<Long> groupIds, Integer handleResult) {
37 29 return selectList(new LambdaQueryWrapperX<ImGroupRequestDO>()
38 30 .in(ImGroupRequestDO::getGroupId, groupIds)
... ... @@ -40,6 +32,12 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; {
40 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 41 default int updateByIdAndHandleResult(Long id, Integer expectedHandleResult, ImGroupRequestDO updateObj) {
44 42 return update(updateObj, new LambdaUpdateWrapper<ImGroupRequestDO>()
45 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 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 58 * <p>
69 59 * 前端 store 据此派生:每个群的未处理总数(用于群顶部横幅红点)+ 列表内容(用于 Drawer)
... ... @@ -74,6 +64,17 @@ public interface ImGroupRequestService {
74 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 78 * 按 id 单查申请记录;通用读接口,越权过滤交由调用方
78 79 *
79 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 246 }
247 247  
248 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 249 public List<ImGroupRequestDO> getUnhandledRequestListByOwnerOrAdmin(Long userId) {
255 250 // 1. 找出当前用户作为 OWNER / ADMIN 的所有群
256 251 List<ImGroupMemberDO> myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId);
... ... @@ -266,6 +261,18 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
266 261 }
267 262  
268 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 276 public ImGroupRequestDO getGroupRequest(Long id) {
270 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 191 @Override
192 192 public void inviteGroupMember(Long userId, ImGroupMemberInviteReqVO inviteReqVO) {
193 193 Long groupId = inviteReqVO.getGroupId();
194   - // 1.1 校验群存在 + 当前用户是群成员
  194 + // 1.1 校验群存在 + 当前用户是群成员;同时拿到 role 供下面审批分支判断
195 195 ImGroupDO group = validateGroupExists(groupId);
196   - groupMemberService.validateMemberInGroup(groupId, userId);
  196 + ImGroupMemberDO operator = groupMemberService.validateMemberInGroup(groupId, userId);
197 197 // 1.2 排除已在群中的用户
198 198 List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId);
199 199 activeMembers.forEach(member -> inviteReqVO.getMemberUserIds().remove(member.getUserId()));
... ... @@ -213,13 +213,15 @@ public class ImGroupServiceImpl implements ImGroupService {
213 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 220 groupRequestService.createInviteRequestList(groupId, userId, memberUserIds);
219 221 return;
220 222 }
221 223  
222   - // 3. 情况二:未开审批,直进;批量添加群成员,写 addSource=INVITE / inviterUserId=操作人 留痕
  224 + // 3. 情况二:未开审批 / 群主 / 管理员邀请,直进;批量添加群成员,写 addSource=INVITE / inviterUserId=操作人 留痕
223 225 groupMemberService.addGroupMembers(groupId, memberUserIds,
224 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 301 }
302 302  
303 303 @Test
304   - public void testInviteGroupMember_approval_routesToApproval() {
  304 + public void testInviteGroupMember_approval_normalRoutesToApproval() {
305 305 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
306 306 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
307 307 .thenReturn(groupService);
308 308  
309   - // 准备:joinApproval=true,开启审批;任何邀请都需审批,落 group_request
  309 + // 准备:joinApproval=true,开启审批;普通成员邀请走审批,落 group_request
310 310 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
311 311 .joinApproval(true)
312 312 .status(CommonStatusEnum.ENABLE.getStatus()).build();
313 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 318 when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of(
315 319 ImGroupMemberDO.builder().groupId(10L).userId(99L)
316 320 .role(ImGroupMemberRoleEnum.OWNER.getRole())
... ... @@ -339,6 +343,48 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
339 343 }
340 344  
341 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 388 public void testInviteGroupMember_notFriend() {
343 389 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
344 390 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
... ...