Commit b320e3c19a733d41ee44dfa63fe1f8513b4fbe87

Authored by YunaiV
1 parent 9ebf7209

✨ feat(im): 初始化群申请 v0.1:第二把 review

Showing 21 changed files with 212 additions and 382 deletions
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java
@@ -10,9 +10,7 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessa @@ -10,9 +10,7 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessa
10 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; 10 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
11 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 11 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
12 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; 12 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
13 -import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;  
14 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService; 13 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
15 -import cn.iocoder.yudao.module.im.service.group.ImGroupRequestService;  
16 import cn.iocoder.yudao.module.im.service.group.ImGroupService; 14 import cn.iocoder.yudao.module.im.service.group.ImGroupService;
17 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; 15 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
18 import io.swagger.v3.oas.annotations.Operation; 16 import io.swagger.v3.oas.annotations.Operation;
@@ -42,8 +40,6 @@ public class ImGroupController { @@ -42,8 +40,6 @@ public class ImGroupController {
42 private ImGroupMemberService groupMemberService; 40 private ImGroupMemberService groupMemberService;
43 @Resource 41 @Resource
44 private ImGroupMessageService groupMessageService; 42 private ImGroupMessageService groupMessageService;
45 - @Resource  
46 - private ImGroupRequestService groupRequestService;  
47 43
48 // ==================== 群的写操作 ==================== 44 // ==================== 群的写操作 ====================
49 45
@@ -181,34 +177,23 @@ public class ImGroupController { @@ -181,34 +177,23 @@ public class ImGroupController {
181 } 177 }
182 178
183 /** 179 /**
184 - * 群列表批量转 VO + 关联回填 pinnedMessages 与 pendingRequestCount 180 + * 群列表批量转 VO + 关联回填 pinnedMessages
185 * <p> 181 * <p>
186 - * pinnedMessages 仅当前用户是该群有效成员时回填;pendingRequestCount 仅当前用户是该群 owner / admin 时回填 182 + * 仅当登录用户是某群的有效成员时才回填该群的 pinnedMessages,避免非成员 / 已退群用户越权拿到置顶消息内容
187 */ 183 */
188 private List<ImGroupRespVO> buildGroupRespVOList(List<ImGroupDO> groups, Long loginUserId) { 184 private List<ImGroupRespVO> buildGroupRespVOList(List<ImGroupDO> groups, Long loginUserId) {
189 if (CollUtil.isEmpty(groups)) { 185 if (CollUtil.isEmpty(groups)) {
190 return Collections.emptyList(); 186 return Collections.emptyList();
191 } 187 }
192 - // 1.1 当前用户的活跃群成员记录;用于 pinnedMessages 回填的「成员」可见性 + pendingRequestCount 回填的「owner / admin」可见性  
193 - List<ImGroupMemberDO> myMembers = groupMemberService.getActiveGroupMemberListByUserId(loginUserId);  
194 - Set<Long> activeGroupIds = convertSet(myMembers, ImGroupMemberDO::getGroupId);  
195 - Set<Long> ownerOrAdminGroupIds = convertSet(myMembers,  
196 - ImGroupMemberDO::getGroupId,  
197 - member -> ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole()));  
198 - // 1.2 批量查置顶消息 188 + // 仅当前用户是有效成员的群才允许回填置顶消息
  189 + Set<Long> activeGroupIds = convertSet(
  190 + groupMemberService.getActiveGroupMemberListByUserId(loginUserId), ImGroupMemberDO::getGroupId);
199 Set<Long> allMessageIds = convertSetByFlatMap(groups, group -> activeGroupIds.contains(group.getId()) 191 Set<Long> allMessageIds = convertSetByFlatMap(groups, group -> activeGroupIds.contains(group.getId())
200 ? CollUtil.emptyIfNull(group.getPinnedMessageIds()).stream() : Stream.empty()); 192 ? CollUtil.emptyIfNull(group.getPinnedMessageIds()).stream() : Stream.empty());
201 Map<Long, ImGroupMessageDO> messageMap = groupMessageService.getGroupMessageMap(allMessageIds); 193 Map<Long, ImGroupMessageDO> messageMap = groupMessageService.getGroupMessageMap(allMessageIds);
202 - // 1.3 仅 owner / admin 群批量统计未处理申请数  
203 - // TODO @AI:这块讨论中。。。  
204 - Map<Long, Long> pendingCountMap = groupRequestService.getPendingCountMap(  
205 - groups.stream().map(ImGroupDO::getId).filter(ownerOrAdminGroupIds::contains).toList());  
206 - // 2. 转换输出 194 + // 转换输出
207 return convertList(groups, group -> { 195 return convertList(groups, group -> {
208 ImGroupRespVO vo = BeanUtils.toBean(group, ImGroupRespVO.class); 196 ImGroupRespVO vo = BeanUtils.toBean(group, ImGroupRespVO.class);
209 - if (ownerOrAdminGroupIds.contains(group.getId())) {  
210 - vo.setPendingRequestCount(pendingCountMap.getOrDefault(group.getId(), 0L));  
211 - }  
212 if (!activeGroupIds.contains(group.getId()) || CollUtil.isEmpty(group.getPinnedMessageIds())) { 197 if (!activeGroupIds.contains(group.getId()) || CollUtil.isEmpty(group.getPinnedMessageIds())) {
213 return vo; 198 return vo;
214 } 199 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java
@@ -8,7 +8,10 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -8,7 +8,10 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
8 import cn.iocoder.yudao.module.im.controller.admin.group.vo.request.ImGroupRequestApplyReqVO; 8 import cn.iocoder.yudao.module.im.controller.admin.group.vo.request.ImGroupRequestApplyReqVO;
9 import cn.iocoder.yudao.module.im.controller.admin.group.vo.request.ImGroupRequestRespVO; 9 import cn.iocoder.yudao.module.im.controller.admin.group.vo.request.ImGroupRequestRespVO;
10 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; 10 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
  11 +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
11 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO; 12 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO;
  13 +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
  14 +import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
12 import cn.iocoder.yudao.module.im.service.group.ImGroupRequestService; 15 import cn.iocoder.yudao.module.im.service.group.ImGroupRequestService;
13 import cn.iocoder.yudao.module.im.service.group.ImGroupService; 16 import cn.iocoder.yudao.module.im.service.group.ImGroupService;
14 import cn.iocoder.yudao.module.system.api.user.AdminUserApi; 17 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -47,6 +50,8 @@ public class ImGroupRequestController { @@ -47,6 +50,8 @@ public class ImGroupRequestController {
47 private ImGroupRequestService groupRequestService; 50 private ImGroupRequestService groupRequestService;
48 @Resource 51 @Resource
49 private ImGroupService groupService; 52 private ImGroupService groupService;
  53 + @Resource
  54 + private ImGroupMemberService groupMemberService;
50 55
51 @Resource 56 @Resource
52 private AdminUserApi adminUserApi; 57 private AdminUserApi adminUserApi;
@@ -79,71 +84,58 @@ public class ImGroupRequestController { @@ -79,71 +84,58 @@ public class ImGroupRequestController {
79 84
80 @GetMapping("/list") 85 @GetMapping("/list")
81 @Operation(summary = "查询「我相关」的加群申请列表(含我主动申请、我被邀请待审);游标分页") 86 @Operation(summary = "查询「我相关」的加群申请列表(含我主动申请、我被邀请待审);游标分页")
82 - public CommonResult<List<ImGroupRequestRespVO>> getMyGroupRequestList( 87 + public CommonResult<List<ImGroupRequestRespVO>> getGroupRequestList(
83 @Parameter(description = "当前列表最旧记录的 id;首页不传") 88 @Parameter(description = "当前列表最旧记录的 id;首页不传")
84 @RequestParam(value = "lastRequestId", required = false) Long lastRequestId, 89 @RequestParam(value = "lastRequestId", required = false) Long lastRequestId,
85 @Parameter(description = "单次拉取条数", required = true) 90 @Parameter(description = "单次拉取条数", required = true)
86 @RequestParam("limit") @Min(1) @Max(200) Integer limit) { 91 @RequestParam("limit") @Min(1) @Max(200) Integer limit) {
87 - List<ImGroupRequestDO> list = groupRequestService.getMyGroupRequestList(getLoginUserId(), lastRequestId, limit);  
88 - return success(buildList(list)); 92 + List<ImGroupRequestDO> list = groupRequestService.getGroupRequestListByUserId(
  93 + getLoginUserId(), lastRequestId, limit);
  94 + return success(buildVOList(list));
89 } 95 }
90 96
91 - // TODO @AI:pending-list;  
92 - @GetMapping("/list-pending")  
93 - @Operation(summary = "查询指定群的未处理加群申请列表(仅群主或管理员可调);游标分页")  
94 - public CommonResult<List<ImGroupRequestRespVO>> getPendingGroupRequestList(  
95 - @RequestParam("groupId") @NotNull(message = "群编号不能为空") Long groupId,  
96 - @Parameter(description = "当前列表最旧记录的 id;首页不传")  
97 - @RequestParam(value = "lastRequestId", required = false) Long lastRequestId,  
98 - @Parameter(description = "单次拉取条数", required = true)  
99 - @RequestParam("limit") @Min(1) @Max(200) Integer limit) {  
100 - List<ImGroupRequestDO> list = groupRequestService.getPendingGroupRequestList(  
101 - getLoginUserId(), groupId, lastRequestId, limit);  
102 - return success(buildList(list)); 97 + @GetMapping("/unhandled-list")
  98 + @Operation(summary = "查询「我管理的所有群」下的未处理加群申请列表(不分页);前端 store 据此派生横幅红点 + Drawer 列表")
  99 + public CommonResult<List<ImGroupRequestRespVO>> getUnhandledRequestList() {
  100 + List<ImGroupRequestDO> list = groupRequestService.getUnhandledRequestListByOwnerOrAdmin(getLoginUserId());
  101 + return success(buildVOList(list));
103 } 102 }
104 103
105 - // TODO @AI:群管理里,也有个地方,可以查询这个群所有的 list;这样上面的 list、list-pending 有点重复。看看怎么组合。可以和我讨论下;  
106 -  
107 - // TODO @AI:方法上,不用 my;因为这个方法,都是查询我的;  
108 @GetMapping("/get") 104 @GetMapping("/get")
109 - @Operation(summary = "按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用)") 105 + @Operation(summary = "按 id 单查申请记录(带越权过滤;WebSocket 通知到达后用)")
110 @Parameter(name = "id", description = "申请记录编号", required = true) 106 @Parameter(name = "id", description = "申请记录编号", required = true)
111 - public CommonResult<ImGroupRequestRespVO> getMyGroupRequest(@RequestParam("id") Long id) { 107 + public CommonResult<ImGroupRequestRespVO> getGroupRequest(@RequestParam("id") Long id) {
112 ImGroupRequestDO request = groupRequestService.getGroupRequest(id); 108 ImGroupRequestDO request = groupRequestService.getGroupRequest(id);
113 - // 越权过滤:申请人 / 被邀请人 / 邀请人 / 群主 / 管理员之外,当不存在返回 null  
114 if (request == null) { 109 if (request == null) {
115 return success(null); 110 return success(null);
116 } 111 }
  112 + // 越权过滤:申请人 / 邀请人 / 群主 / 管理员之外,当不存在返回 null
117 Long currentUserId = getLoginUserId(); 113 Long currentUserId = getLoginUserId();
118 boolean canSee = ObjUtil.equal(request.getUserId(), currentUserId) 114 boolean canSee = ObjUtil.equal(request.getUserId(), currentUserId)
119 - || ObjUtil.equal(request.getInviterUserId(), currentUserId);  
120 - if (!canSee) {  
121 - // 群主 / 管理员可见;通过 list-pending 校验路径  
122 - // TODO @AI:不要 try catch;  
123 - try {  
124 - // TODO @AI:这个方法的实现,不要里面抛出异常;应该调用查询是否为管理员;而不应该这么搞哈。这样,是不是可以上面 || 后面在加一个条件?  
125 - groupRequestService.getPendingGroupRequestList(currentUserId, request.getGroupId(), null, 1);  
126 - canSee = true;  
127 - } catch (Exception ignored) {  
128 - // 非 owner / admin  
129 - }  
130 - } 115 + || ObjUtil.equal(request.getInviterUserId(), currentUserId)
  116 + || isGroupOwnerOrAdmin(request.getGroupId(), currentUserId);
131 if (!canSee) { 117 if (!canSee) {
132 return success(null); 118 return success(null);
133 } 119 }
134 - return success(CollUtil.getFirst(buildList(Collections.singletonList(request)))); 120 +
  121 + // 转换并返回
  122 + return success(CollUtil.getFirst(buildVOList(Collections.singletonList(request))));
  123 + }
  124 +
  125 + /** 当前用户是否该群的群主 / 管理员 */
  126 + private boolean isGroupOwnerOrAdmin(Long groupId, Long userId) {
  127 + ImGroupMemberDO member = groupMemberService.getGroupMember(groupId, userId);
  128 + return member != null && ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole());
135 } 129 }
136 130
137 /** 申请记录列表批量转 VO + 关联回填用户 / 群信息 */ 131 /** 申请记录列表批量转 VO + 关联回填用户 / 群信息 */
138 - private List<ImGroupRequestRespVO> buildList(List<ImGroupRequestDO> list) { 132 + private List<ImGroupRequestRespVO> buildVOList(List<ImGroupRequestDO> list) {
139 if (CollUtil.isEmpty(list)) { 133 if (CollUtil.isEmpty(list)) {
140 return Collections.emptyList(); 134 return Collections.emptyList();
141 } 135 }
142 - // 1. 聚合 user / inviter 用户信息  
143 - // TODO @AI:.filter(Objects::nonNull) 内部已经过滤了; 136 + // 1. 聚合 user / inviter 用户信息;convertSetByFlatMap 内部已过滤 null
144 Set<Long> userIds = convertSetByFlatMap(list, 137 Set<Long> userIds = convertSetByFlatMap(list,
145 - request -> Stream.of(request.getUserId(), request.getInviterUserId())  
146 - .filter(Objects::nonNull)); 138 + request -> Stream.of(request.getUserId(), request.getInviterUserId()));
147 Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds); 139 Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
148 // 2. 聚合群信息(封禁 / 解散群也要回填,便于前端展示历史) 140 // 2. 聚合群信息(封禁 / 解散群也要回填,便于前端展示历史)
149 Set<Long> groupIds = convertSet(list, ImGroupRequestDO::getGroupId); 141 Set<Long> groupIds = convertSet(list, ImGroupRequestDO::getGroupId);
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCreateReqVO.java
1 package cn.iocoder.yudao.module.im.controller.admin.group.vo; 1 package cn.iocoder.yudao.module.im.controller.admin.group.vo;
2 2
3 -import cn.iocoder.yudao.framework.common.validation.InEnum;  
4 -import cn.iocoder.yudao.module.im.enums.group.ImGroupJoinTypeEnum;  
5 import io.swagger.v3.oas.annotations.media.Schema; 3 import io.swagger.v3.oas.annotations.media.Schema;
6 import jakarta.validation.constraints.NotBlank; 4 import jakarta.validation.constraints.NotBlank;
7 import lombok.Data; 5 import lombok.Data;
@@ -19,8 +17,7 @@ public class ImGroupCreateReqVO { @@ -19,8 +17,7 @@ public class ImGroupCreateReqVO {
19 @Schema(description = "初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)", example = "[1024, 2048]") 17 @Schema(description = "初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)", example = "[1024, 2048]")
20 private List<Long> memberUserIds; 18 private List<Long> memberUserIds;
21 19
22 - @Schema(description = "加群方式;不传默认 0 自由进群", example = "0")  
23 - @InEnum(ImGroupJoinTypeEnum.class)  
24 - private Integer joinType; // 参见 ImGroupJoinTypeEnum 枚举类 20 + @Schema(description = "进群是否需群主 / 管理员审批;不传默认 false 自由进群", example = "false")
  21 + private Boolean joinApproval;
25 22
26 } 23 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java
@@ -32,12 +32,8 @@ public class ImGroupRespVO { @@ -32,12 +32,8 @@ public class ImGroupRespVO {
32 @Schema(description = "是否全群禁言") 32 @Schema(description = "是否全群禁言")
33 private Boolean mutedAll; 33 private Boolean mutedAll;
34 34
35 - @Schema(description = "加群方式", example = "0")  
36 - private Integer joinType; // 参见 ImGroupJoinTypeEnum 枚举类  
37 -  
38 - // TODO @AI:是不是不回填;前端自己查询 + 推送维护?类似微信,会展示有人加群,需要审批之类的。(晚点我可以给截图);  
39 - @Schema(description = "未处理加群申请数;仅当登录用户是该群群主 / 管理员时回填,否则为 null", example = "3")  
40 - private Long pendingRequestCount; 35 + @Schema(description = "进群是否需群主 / 管理员审批", example = "false")
  36 + private Boolean joinApproval;
41 37
42 @Schema(description = "封禁时间") 38 @Schema(description = "封禁时间")
43 private LocalDateTime bannedTime; 39 private LocalDateTime bannedTime;
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupUpdateReqVO.java
1 package cn.iocoder.yudao.module.im.controller.admin.group.vo; 1 package cn.iocoder.yudao.module.im.controller.admin.group.vo;
2 2
3 -import cn.iocoder.yudao.framework.common.validation.InEnum;  
4 -import cn.iocoder.yudao.module.im.enums.group.ImGroupJoinTypeEnum;  
5 import io.swagger.v3.oas.annotations.media.Schema; 3 import io.swagger.v3.oas.annotations.media.Schema;
6 import jakarta.validation.constraints.NotNull; 4 import jakarta.validation.constraints.NotNull;
7 import lombok.Data; 5 import lombok.Data;
@@ -23,8 +21,7 @@ public class ImGroupUpdateReqVO { @@ -23,8 +21,7 @@ public class ImGroupUpdateReqVO {
23 @Schema(description = "群公告") 21 @Schema(description = "群公告")
24 private String notice; 22 private String notice;
25 23
26 - @Schema(description = "加群方式", example = "1")  
27 - @InEnum(ImGroupJoinTypeEnum.class)  
28 - private Integer joinType; // 参见 ImGroupJoinTypeEnum 枚举类 24 + @Schema(description = "进群是否需群主 / 管理员审批", example = "true")
  25 + private Boolean joinApproval;
29 26
30 } 27 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupDO.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.im.dal.dataobject.group; @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.im.dal.dataobject.group;
3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; 5 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
6 -import cn.iocoder.yudao.module.im.enums.group.ImGroupJoinTypeEnum;  
7 import com.baomidou.mybatisplus.annotation.KeySequence; 6 import com.baomidou.mybatisplus.annotation.KeySequence;
8 import com.baomidou.mybatisplus.annotation.TableField; 7 import com.baomidou.mybatisplus.annotation.TableField;
9 import com.baomidou.mybatisplus.annotation.TableId; 8 import com.baomidou.mybatisplus.annotation.TableId;
@@ -52,11 +51,11 @@ public class ImGroupDO extends BaseDO { @@ -52,11 +51,11 @@ public class ImGroupDO extends BaseDO {
52 */ 51 */
53 private String notice; 52 private String notice;
54 /** 53 /**
55 - * 加群方式 54 + * 进群是否需群主 / 管理员审批
56 * <p> 55 * <p>
57 - * 枚举 {@link ImGroupJoinTypeEnum} 56 + * false 自由进群(默认);true 需审批,所有「申请」「邀请」路径都需群主 / 管理员同意
58 */ 57 */
59 - private Integer joinType; 58 + private Boolean joinApproval;
60 /** 59 /**
61 * 是否封禁 60 * 是否封禁
62 */ 61 */
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java
@@ -18,12 +18,10 @@ import java.util.List; @@ -18,12 +18,10 @@ import java.util.List;
18 @Mapper 18 @Mapper
19 public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> { 19 public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> {
20 20
21 - default ImFriendRequestDO selectLatestByFromUserIdAndToUserId(Long fromUserId, Long toUserId) { 21 + default ImFriendRequestDO selectByFromUserIdAndToUserId(Long fromUserId, Long toUserId) {
22 return selectOne(new LambdaQueryWrapperX<ImFriendRequestDO>() 22 return selectOne(new LambdaQueryWrapperX<ImFriendRequestDO>()
23 .eq(ImFriendRequestDO::getFromUserId, fromUserId) 23 .eq(ImFriendRequestDO::getFromUserId, fromUserId)
24 - .eq(ImFriendRequestDO::getToUserId, toUserId)  
25 - .orderByDesc(ImFriendRequestDO::getId)  
26 - .last("LIMIT 1")); 24 + .eq(ImFriendRequestDO::getToUserId, toUserId));
27 } 25 }
28 26
29 /** 27 /**
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java
@@ -5,10 +5,10 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; @@ -5,10 +5,10 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupRequestManagerPageReqVO; 6 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupRequestManagerPageReqVO;
7 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO; 7 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO;
8 -import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum;  
9 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 8 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
10 import org.apache.ibatis.annotations.Mapper; 9 import org.apache.ibatis.annotations.Mapper;
11 10
  11 +import java.util.Collection;
12 import java.util.List; 12 import java.util.List;
13 13
14 /** 14 /**
@@ -19,31 +19,13 @@ import java.util.List; @@ -19,31 +19,13 @@ import java.util.List;
19 @Mapper 19 @Mapper
20 public interface ImGroupRequestMapper extends BaseMapperX<ImGroupRequestDO> { 20 public interface ImGroupRequestMapper extends BaseMapperX<ImGroupRequestDO> {
21 21
22 - /**  
23 - * 取最新一条未处理申请;inviterUserId 为 null 表示主动申请  
24 - * <p>  
25 - * 主动申请、不同邀请人发起的邀请申请彼此互不覆盖,复用键含 inviterUserId  
26 - */  
27 - // TODO @AI:一般合理的,是不是可以覆盖????  
28 - default ImGroupRequestDO selectLatestPendingByGroupIdAndUserIdAndInviter(  
29 - Long groupId, Long userId, Long inviterUserId) {  
30 - LambdaQueryWrapperX<ImGroupRequestDO> wrapper = new LambdaQueryWrapperX<>();  
31 - wrapper.eq(ImGroupRequestDO::getGroupId, groupId)  
32 - .eq(ImGroupRequestDO::getUserId, userId)  
33 - .eq(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult());  
34 - if (inviterUserId == null) {  
35 - wrapper.isNull(ImGroupRequestDO::getInviterUserId);  
36 - } else {  
37 - wrapper.eq(ImGroupRequestDO::getInviterUserId, inviterUserId);  
38 - }  
39 - return selectOne(wrapper.orderByDesc(ImGroupRequestDO::getId).last("LIMIT 1")); 22 + default ImGroupRequestDO selectByGroupIdAndUserId(Long groupId, Long userId) {
  23 + return selectOne(new LambdaQueryWrapperX<ImGroupRequestDO>()
  24 + .eq(ImGroupRequestDO::getGroupId, groupId)
  25 + .eq(ImGroupRequestDO::getUserId, userId));
40 } 26 }
41 27
42 - /**  
43 - * 拉取「我相关」的加群申请列表(含我主动申请、我被邀请待审);游标分页  
44 - */  
45 - // TODO @AI:selectListByMy;保持风格一致;  
46 - default List<ImGroupRequestDO> selectMyList(Long userId, Long lastRequestId, int limit) { 28 + default List<ImGroupRequestDO> selectListByUserId(Long userId, Long lastRequestId, int limit) {
47 return selectList(new LambdaQueryWrapperX<ImGroupRequestDO>() 29 return selectList(new LambdaQueryWrapperX<ImGroupRequestDO>()
48 .eq(ImGroupRequestDO::getUserId, userId) 30 .eq(ImGroupRequestDO::getUserId, userId)
49 .ltIfPresent(ImGroupRequestDO::getId, lastRequestId) 31 .ltIfPresent(ImGroupRequestDO::getId, lastRequestId)
@@ -51,26 +33,16 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; { @@ -51,26 +33,16 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; {
51 .last("LIMIT " + limit)); 33 .last("LIMIT " + limit));
52 } 34 }
53 35
54 - // TODO @AI:类似下面的注释,减少 mapper 的业务型;  
55 - default List<ImGroupRequestDO> selectPendingListByGroupId(Long groupId, Long lastRequestId, int limit) { 36 + default List<ImGroupRequestDO> selectListByGroupIdsAndHandleResult(Collection<Long> groupIds, Integer handleResult) {
56 return selectList(new LambdaQueryWrapperX<ImGroupRequestDO>() 37 return selectList(new LambdaQueryWrapperX<ImGroupRequestDO>()
57 - .eq(ImGroupRequestDO::getGroupId, groupId)  
58 - .eq(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult())  
59 - .ltIfPresent(ImGroupRequestDO::getId, lastRequestId)  
60 - .orderByDesc(ImGroupRequestDO::getId)  
61 - .last("LIMIT " + limit));  
62 - }  
63 -  
64 - // TODO @AI:应该 bygroupidandhandleResult,减少不必要的条件  
65 - default Long selectPendingCountByGroupId(Long groupId) {  
66 - return selectCount(new LambdaQueryWrapperX<ImGroupRequestDO>()  
67 - .eq(ImGroupRequestDO::getGroupId, groupId)  
68 - .eq(ImGroupRequestDO::getHandleResult, ImGroupRequestHandleResultEnum.UNHANDLED.getResult())); 38 + .in(ImGroupRequestDO::getGroupId, groupIds)
  39 + .eq(ImGroupRequestDO::getHandleResult, handleResult)
  40 + .orderByDesc(ImGroupRequestDO::getId));
69 } 41 }
70 42
71 - default int updateByIdAndHandleResult(Long id, Integer handleResult, ImGroupRequestDO updateObj) { 43 + default int updateByIdAndHandleResult(Long id, Integer expectedHandleResult, ImGroupRequestDO updateObj) {
72 return update(updateObj, new LambdaUpdateWrapper<ImGroupRequestDO>() 44 return update(updateObj, new LambdaUpdateWrapper<ImGroupRequestDO>()
73 - .eq(ImGroupRequestDO::getId, id).eq(ImGroupRequestDO::getHandleResult, handleResult)); 45 + .eq(ImGroupRequestDO::getId, id).eq(ImGroupRequestDO::getHandleResult, expectedHandleResult));
74 } 46 }
75 47
76 default PageResult<ImGroupRequestDO> selectPage(ImGroupRequestManagerPageReqVO reqVO) { 48 default PageResult<ImGroupRequestDO> selectPage(ImGroupRequestManagerPageReqVO reqVO) {
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/group/ImGroupAddSourceEnum.java
@@ -17,11 +17,10 @@ import java.util.Arrays; @@ -17,11 +17,10 @@ import java.util.Arrays;
17 @Getter 17 @Getter
18 public enum ImGroupAddSourceEnum implements ArrayValuable<Integer> { 18 public enum ImGroupAddSourceEnum implements ArrayValuable<Integer> {
19 19
20 - // TODO @AI:没实现的方式,记得在后面加个 TODO @芋艿:未实现,原因。。。。  
21 - SEARCH(1, "搜索"), 20 + SEARCH(1, "搜索"), // TODO @芋艿:SEARCH 暂未实现,原因 - 搜群入口尚未开发
22 INVITE(2, "邀请"), 21 INVITE(2, "邀请"),
23 - QR_CODE(3, "扫码"),  
24 - SHARE_LINK(4, "分享链接"); 22 + QR_CODE(3, "扫码"), // TODO @芋艿:QR_CODE 暂未实现,原因 - 群二维码扫码进群入口尚未开发
  23 + SHARE_LINK(4, "分享链接"); // TODO @芋艿:SHARE_LINK 暂未实现,原因 - 群分享链接进群入口尚未开发
25 24
26 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImGroupAddSourceEnum::getSource).toArray(Integer[]::new); 25 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImGroupAddSourceEnum::getSource).toArray(Integer[]::new);
27 26
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/group/ImGroupJoinTypeEnum.java deleted
1 -package cn.iocoder.yudao.module.im.enums.group;  
2 -  
3 -import cn.iocoder.yudao.framework.common.core.ArrayValuable;  
4 -import lombok.Getter;  
5 -import lombok.RequiredArgsConstructor;  
6 -  
7 -import java.util.Arrays;  
8 -import java.util.Objects;  
9 -  
10 -// TODO @AI:这里是不是对齐微信;申请只有需要审批,和不需要审批。(不然感觉有点复杂、难理解?!)  
11 -/**  
12 - * IM 群加群方式枚举  
13 - * <p>  
14 - * 控制「外部用户主动申请」和「群成员邀请」两条入群路径是否需要群主 / 管理员审批  
15 - * <ul>  
16 - * <li>{@link #FREE}:用户搜到群可直接进群;任何成员邀请也直进</li>  
17 - * <li>{@link #APPLY}:用户申请进群需群主 / 管理员审批;任何成员邀请仍直进</li>  
18 - * <li>{@link #APPLY_AND_NORMAL_INVITE}:用户申请需审批;普通成员邀请也需审批;群主 / 管理员邀请直进</li>  
19 - * </ul>  
20 - *  
21 - * @author 芋道源码  
22 - */  
23 -@RequiredArgsConstructor  
24 -@Getter  
25 -public enum ImGroupJoinTypeEnum implements ArrayValuable<Integer> {  
26 -  
27 - FREE(0, "自由进群"),  
28 - APPLY(1, "申请需审批"),  
29 - APPLY_AND_NORMAL_INVITE(2, "申请、及普通成员邀请均需审批");  
30 -  
31 - public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImGroupJoinTypeEnum::getType).toArray(Integer[]::new);  
32 -  
33 - /**  
34 - * 类型  
35 - */  
36 - private final Integer type;  
37 - /**  
38 - * 名字  
39 - */  
40 - private final String name;  
41 -  
42 - @Override  
43 - public Integer[] array() {  
44 - return ARRAYS;  
45 - }  
46 -  
47 - /**  
48 - * 是否「自由进群」;自由进群路径无需创建申请记录  
49 - */  
50 - public static boolean isFree(Integer type) {  
51 - return Objects.equals(FREE.type, type);  
52 - }  
53 -  
54 - /**  
55 - * 普通成员邀请是否需要审批  
56 - */  
57 - public static boolean isInviteNeedApproval(Integer type) {  
58 - return Objects.equals(APPLY_AND_NORMAL_INVITE.type, type);  
59 - }  
60 -  
61 -}  
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java
@@ -88,19 +88,19 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { @@ -88,19 +88,19 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
88 return null; 88 return null;
89 } 89 }
90 90
91 - // 2. 落库:复用最新一条未处理记录走条件 update;否则新建  
92 - ImFriendRequestDO request = friendRequestMapper.selectLatestByFromUserIdAndToUserId(fromUserId, toUserId);  
93 - if (request != null && ImFriendRequestHandleResultEnum.isUnhandled(request.getHandleResult())) {  
94 - ImFriendRequestDO updateObj = new ImFriendRequestDO().setApplyContent(reqVO.getApplyContent())  
95 - .setDisplayName(reqVO.getDisplayName()).setAddSource(reqVO.getAddSource());  
96 - int affected = friendRequestMapper.updateByIdAndHandleResult(request.getId(),  
97 - ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), updateObj);  
98 - if (affected == 0) {  
99 - // 并发场景下另一方刚 agree/refuse 推进了状态,条件 update affected=0 抛已处理错让前端重试  
100 - throw exception(FRIEND_REQUEST_HANDLED);  
101 - }  
102 - // 同步更新字段,下面推送 payload 用  
103 - BeanUtil.copyProperties(reqVO, request, CopyOptions.create().setIgnoreNullValue(true)); 91 + // 2. 落库:upsert 语义;同一对 (from, to) 唯一,已有记录覆盖申请理由 / 备注 / 来源 + 重置为未处理 + 清空旧处理痕迹
  92 + ImFriendRequestDO request = friendRequestMapper.selectByFromUserIdAndToUserId(fromUserId, toUserId);
  93 + if (request != null) {
  94 + // TODO @AI:相同的放在一行里;然后 null 直接这样更新是清理不掉的;看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?
  95 + ImFriendRequestDO updateObj = new ImFriendRequestDO()
  96 + .setApplyContent(reqVO.getApplyContent())
  97 + .setDisplayName(reqVO.getDisplayName())
  98 + .setAddSource(reqVO.getAddSource())
  99 + .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult())
  100 + .setHandleContent(null).setHandleTime(null);
  101 + updateObj.setId(request.getId());
  102 + friendRequestMapper.updateById(updateObj);
  103 + BeanUtil.copyProperties(updateObj, request, CopyOptions.create().setIgnoreNullValue(true));
104 } else { 104 } else {
105 request = BeanUtils.toBean(reqVO, ImFriendRequestDO.class) 105 request = BeanUtils.toBean(reqVO, ImFriendRequestDO.class)
106 .setFromUserId(fromUserId).setToUserId(toUserId) 106 .setFromUserId(fromUserId).setToUserId(toUserId)
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java
@@ -59,6 +59,14 @@ public interface ImGroupMemberService { @@ -59,6 +59,14 @@ public interface ImGroupMemberService {
59 List<ImGroupMemberDO> getActiveGroupMemberListByGroupId(Long groupId); 59 List<ImGroupMemberDO> getActiveGroupMemberListByGroupId(Long groupId);
60 60
61 /** 61 /**
  62 + * 获取群里活跃的群主 + 管理员;用于审批通知定向推送
  63 + *
  64 + * @param groupId 群编号
  65 + * @return 群主 / 管理员的成员记录列表(按入群时间倒序的天然成员表顺序)
  66 + */
  67 + List<ImGroupMemberDO> getGroupMemberListByOwnerAndAdmin(Long groupId);
  68 +
  69 + /**
62 * 根据群编号查询有效成员的 userId 列表(仅 ENABLE 状态) 70 * 根据群编号查询有效成员的 userId 列表(仅 ENABLE 状态)
63 * <p> 71 * <p>
64 * 相比 {@link #getActiveGroupMemberListByGroupId(Long)},只返回 userId,结果体积小,并带有 Redis 缓存。 72 * 相比 {@link #getActiveGroupMemberListByGroupId(Long)},只返回 userId,结果体积小,并带有 Redis 缓存。
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java
@@ -73,6 +73,18 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { @@ -73,6 +73,18 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService {
73 return groupMemberMapper.selectListByGroupIdAndStatus(groupId, CommonStatusEnum.ENABLE.getStatus()); 73 return groupMemberMapper.selectListByGroupIdAndStatus(groupId, CommonStatusEnum.ENABLE.getStatus());
74 } 74 }
75 75
  76 + @Override
  77 + public List<ImGroupMemberDO> getGroupMemberListByOwnerAndAdmin(Long groupId) {
  78 + // TODO @AI:把条件往下传;这样减少加载数据量!
  79 + List<ImGroupMemberDO> members = getActiveGroupMemberListByGroupId(groupId);
  80 + if (CollUtil.isEmpty(members)) {
  81 + return Collections.emptyList();
  82 + }
  83 + return members.stream()
  84 + .filter(member -> ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole()))
  85 + .toList();
  86 + }
  87 +
76 /** 88 /**
77 * 只缓存 userId 列表而非整个 {@link ImGroupMemberDO},理由: 89 * 只缓存 userId 列表而非整个 {@link ImGroupMemberDO},理由:
78 * <ul> 90 * <ul>
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestService.java
@@ -8,7 +8,6 @@ import jakarta.validation.Valid; @@ -8,7 +8,6 @@ import jakarta.validation.Valid;
8 8
9 import java.util.Collection; 9 import java.util.Collection;
10 import java.util.List; 10 import java.util.List;
11 -import java.util.Map;  
12 11
13 /** 12 /**
14 * IM 加群申请 Service 接口 13 * IM 加群申请 Service 接口
@@ -20,8 +19,7 @@ public interface ImGroupRequestService { @@ -20,8 +19,7 @@ public interface ImGroupRequestService {
20 /** 19 /**
21 * 用户主动申请加群 20 * 用户主动申请加群
22 * <p> 21 * <p>
23 - * 群 joinType=FREE 时直接入群 + 1510 全员广播 + 1505 推送给申请人;  
24 - * 否则落 im_group_request 待审批 + 1503 定向推送给群主 / 全部管理员 22 + * 群未开启审批时直接入群 + 1510 全员广播;开启审批则 upsert 一条 inviterUserId=null 的待审批记录 + 1503 推送
25 * 23 *
26 * @param userId 申请人用户编号 24 * @param userId 申请人用户编号
27 * @param reqVO 申请请求 25 * @param reqVO 申请请求
@@ -30,9 +28,7 @@ public interface ImGroupRequestService { @@ -30,9 +28,7 @@ public interface ImGroupRequestService {
30 ImGroupRequestDO applyJoinGroup(Long userId, @Valid ImGroupRequestApplyReqVO reqVO); 28 ImGroupRequestDO applyJoinGroup(Long userId, @Valid ImGroupRequestApplyReqVO reqVO);
31 29
32 /** 30 /**
33 - * 同意加群申请(群主或管理员)  
34 - * <p>  
35 - * 处理前校验入群人数上限;通过后写 group_member(带 addSource / inviterUserId) + 推送通知 31 + * 同意加群申请(群主或管理员);处理前校验入群人数上限
36 * 32 *
37 * @param userId 操作人用户编号 33 * @param userId 操作人用户编号
38 * @param requestId 申请记录编号 34 * @param requestId 申请记录编号
@@ -49,15 +45,13 @@ public interface ImGroupRequestService { @@ -49,15 +45,13 @@ public interface ImGroupRequestService {
49 void refuseGroupRequest(Long userId, Long requestId, String handleContent); 45 void refuseGroupRequest(Long userId, Long requestId, String handleContent);
50 46
51 /** 47 /**
52 - * 邀请创建审批申请(用于 inviteGroupMember 在 joinType=APPLY_AND_NORMAL_INVITE 且邀请人是普通成员时调用)  
53 - * <p>  
54 - * 每个被邀请人创建一条 inviter_user_id=操作人 的待审批记录;通知群主 / 全部管理员 48 + * 邀请创建审批申请;inviteGroupMember 在群开启审批时调用,每个被邀请人 upsert 一条 inviterUserId=操作人 的记录
55 * 49 *
56 * @param groupId 群编号 50 * @param groupId 群编号
57 * @param inviterUserId 邀请人用户编号 51 * @param inviterUserId 邀请人用户编号
58 * @param invitedUserIds 被邀请人用户编号集合 52 * @param invitedUserIds 被邀请人用户编号集合
59 */ 53 */
60 - void createInviteRequests(Long groupId, Long inviterUserId, Collection<Long> invitedUserIds); 54 + void createInviteRequestList(Long groupId, Long inviterUserId, Collection<Long> invitedUserIds);
61 55
62 /** 56 /**
63 * 拉取「我相关」加群申请列表(含我主动申请、我被邀请待审);游标分页 57 * 拉取「我相关」加群申请列表(含我主动申请、我被邀请待审);游标分页
@@ -67,18 +61,17 @@ public interface ImGroupRequestService { @@ -67,18 +61,17 @@ public interface ImGroupRequestService {
67 * @param limit 单次拉取条数 61 * @param limit 单次拉取条数
68 * @return 申请记录列表,按 id 倒序 62 * @return 申请记录列表,按 id 倒序
69 */ 63 */
70 - List<ImGroupRequestDO> getMyGroupRequestList(Long userId, Long lastRequestId, Integer limit); 64 + List<ImGroupRequestDO> getGroupRequestListByUserId(Long userId, Long lastRequestId, Integer limit);
71 65
72 /** 66 /**
73 - * 拉取指定群下未处理申请;仅群主 / 管理员可调 67 + * 拉取「我管理的所有群」下的未处理申请列表
  68 + * <p>
  69 + * 前端 store 据此派生:每个群的未处理总数(用于群顶部横幅红点)+ 列表内容(用于 Drawer)
74 * 70 *
75 - * @param userId 操作人用户编号  
76 - * @param groupId 群编号  
77 - * @param lastRequestId 游标 id  
78 - * @param limit 单次拉取条数  
79 - * @return 未处理申请列表 71 + * @param userId 当前用户编号;后端按 ImGroupMember.role 过滤出我作为 OWNER / ADMIN 的群
  72 + * @return 未处理申请列表(不分页)
80 */ 73 */
81 - List<ImGroupRequestDO> getPendingGroupRequestList(Long userId, Long groupId, Long lastRequestId, Integer limit); 74 + List<ImGroupRequestDO> getUnhandledRequestListByOwnerOrAdmin(Long userId);
82 75
83 /** 76 /**
84 * 按 id 单查申请记录;通用读接口,越权过滤交由调用方 77 * 按 id 单查申请记录;通用读接口,越权过滤交由调用方
@@ -88,14 +81,6 @@ public interface ImGroupRequestService { @@ -88,14 +81,6 @@ public interface ImGroupRequestService {
88 */ 81 */
89 ImGroupRequestDO getGroupRequest(Long id); 82 ImGroupRequestDO getGroupRequest(Long id);
90 83
91 - /**  
92 - * 批量统计指定群的未处理申请数量;用于 ImGroupRespVO.pendingRequestCount 回填  
93 - *  
94 - * @param groupIds 群编号集合  
95 - * @return 群编号 → 未处理申请数 Map  
96 - */  
97 - Map<Long, Long> getPendingCountMap(Collection<Long> groupIds);  
98 -  
99 // ==================== 管理后台 ==================== 84 // ==================== 管理后台 ====================
100 85
101 /** 86 /**
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java
@@ -12,7 +12,6 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; @@ -12,7 +12,6 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
12 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 12 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
13 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO; 13 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO;
14 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupRequestMapper; 14 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupRequestMapper;
15 -import cn.iocoder.yudao.module.im.enums.group.ImGroupJoinTypeEnum;  
16 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; 15 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
17 import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum; 16 import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum;
18 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 17 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
@@ -37,9 +36,9 @@ import java.time.LocalDateTime; @@ -37,9 +36,9 @@ import java.time.LocalDateTime;
37 import java.util.*; 36 import java.util.*;
38 37
39 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 38 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  39 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
40 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; 40 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
41 41
42 -// TODO @AI:payload 能不能改成 notification 更像通知一点。  
43 /** 42 /**
44 * IM 加群申请 Service 实现类 43 * IM 加群申请 Service 实现类
45 * 44 *
@@ -81,30 +80,32 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -81,30 +80,32 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
81 throw exception(GROUP_REQUEST_ALREADY_MEMBER); 80 throw exception(GROUP_REQUEST_ALREADY_MEMBER);
82 } 81 }
83 82
84 - // 2. FREE 模式:直接入群 + 推 1510 全员广播;不落申请记录  
85 - if (ImGroupJoinTypeEnum.isFree(group.getJoinType())) {  
86 - // TODO @AI:这里还是写下注释;每一行的; 83 + // 2. 情况一:群未开启审批,直接入群;写群成员留痕 + 推 1510 全员广播;不落申请记录
  84 + if (!Boolean.TRUE.equals(group.getJoinApproval())) {
  85 + // 入群前校验人数上限
87 groupService.validateMemberCountLimit(groupId, 1); 86 groupService.validateMemberCountLimit(groupId, 1);
  87 + // 写群成员;addSource 来自前端入口(搜索 / 二维码 / 分享链接),inviterUserId=null(主动申请)
88 groupMemberService.addGroupMember(groupId, userId, 88 groupMemberService.addGroupMember(groupId, userId,
89 ImGroupMemberRoleEnum.NORMAL.getRole(), reqVO.getAddSource(), null); 89 ImGroupMemberRoleEnum.NORMAL.getRole(), reqVO.getAddSource(), null);
  90 + // 推 1510 给全员;payload 含进群者 + 来源,前端按 entrantUserId 局部插入新成员
90 groupMessageService.sendGroupMessage(userId, 91 groupMessageService.sendGroupMessage(userId,
91 ImGroupMessageSendDTO.ofGroupMemberEnter(groupId, userId, reqVO.getAddSource())); 92 ImGroupMessageSendDTO.ofGroupMemberEnter(groupId, userId, reqVO.getAddSource()));
92 return null; 93 return null;
93 } 94 }
94 95
95 - // 3. 非 FREE 模式:复用未处理记录或新建  
96 - ImGroupRequestDO request = groupRequestMapper.selectLatestPendingByGroupIdAndUserIdAndInviter(  
97 - groupId, userId, null); 96 + // 3. 情况二:群开启了审批;upsert 一条「主动申请」记录,inviterUserId=null
  97 + // 已有记录(无论之前是未处理 / 已同意 / 已拒绝),覆盖申请理由 + 来源 + 重置为未处理 + 清空旧处理痕迹
  98 + ImGroupRequestDO request = groupRequestMapper.selectByGroupIdAndUserId(groupId, userId);
98 if (request != null) { 99 if (request != null) {
  100 + // TODO @AI:相同的放在一行里;然后 null 直接这样更新是清理不掉的;看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?
99 ImGroupRequestDO updateObj = new ImGroupRequestDO() 101 ImGroupRequestDO updateObj = new ImGroupRequestDO()
100 - .setApplyContent(reqVO.getApplyContent()).setAddSource(reqVO.getAddSource());  
101 - int affected = groupRequestMapper.updateByIdAndHandleResult(request.getId(),  
102 - ImGroupRequestHandleResultEnum.UNHANDLED.getResult(), updateObj);  
103 - if (affected == 0) {  
104 - // 并发场景另一管理端刚处理过,提示重试  
105 - throw exception(GROUP_REQUEST_HANDLED);  
106 - }  
107 - BeanUtil.copyProperties(reqVO, request, CopyOptions.create().setIgnoreNullValue(true)); 102 + .setInviterUserId(null)
  103 + .setApplyContent(reqVO.getApplyContent()).setAddSource(reqVO.getAddSource())
  104 + .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult())
  105 + .setHandleUserId(null).setHandleContent(null).setHandleTime(null);
  106 + updateObj.setId(request.getId());
  107 + groupRequestMapper.updateById(updateObj);
  108 + BeanUtil.copyProperties(updateObj, request, CopyOptions.create().setIgnoreNullValue(true));
108 } else { 109 } else {
109 request = BeanUtils.toBean(reqVO, ImGroupRequestDO.class) 110 request = BeanUtils.toBean(reqVO, ImGroupRequestDO.class)
110 .setUserId(userId).setInviterUserId(null) 111 .setUserId(userId).setInviterUserId(null)
@@ -126,7 +127,7 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -126,7 +127,7 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
126 @Override 127 @Override
127 @Transactional(rollbackFor = Exception.class) 128 @Transactional(rollbackFor = Exception.class)
128 public void agreeGroupRequest(Long userId, Long requestId) { 129 public void agreeGroupRequest(Long userId, Long requestId) {
129 - // 1. 校验申请存在 + 未处理 + 操作人是 owner/admin 130 + // 1. 校验申请存在 + 未处理 + 操作人是 owner / admin
130 ImGroupRequestDO request = validateRequestForHandle(userId, requestId); 131 ImGroupRequestDO request = validateRequestForHandle(userId, requestId);
131 // 2. 入群前校验人数上限;群已满抛错让操作人选择拒绝 132 // 2. 入群前校验人数上限;群已满抛错让操作人选择拒绝
132 groupService.validateMemberCountLimit(request.getGroupId(), 1); 133 groupService.validateMemberCountLimit(request.getGroupId(), 1);
@@ -144,17 +145,18 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -144,17 +145,18 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
144 request.setHandleResult(ImGroupRequestHandleResultEnum.AGREED.getResult()) 145 request.setHandleResult(ImGroupRequestHandleResultEnum.AGREED.getResult())
145 .setHandleUserId(userId).setHandleTime(now); 146 .setHandleUserId(userId).setHandleTime(now);
146 147
147 - // 4. 写群成员;带 addSource / inviterUserId 148 + // 4. 写群成员;addSource / inviterUserId 沿用申请记录上的来源信息
148 groupMemberService.addGroupMember(request.getGroupId(), request.getUserId(), 149 groupMemberService.addGroupMember(request.getGroupId(), request.getUserId(),
149 ImGroupMemberRoleEnum.NORMAL.getRole(), request.getAddSource(), request.getInviterUserId()); 150 ImGroupMemberRoleEnum.NORMAL.getRole(), request.getAddSource(), request.getInviterUserId());
150 151
151 // 5.1 1505 私聊推送给申请人 + 群主 + 全部管理员(每端单推) 152 // 5.1 1505 私聊推送给申请人 + 群主 + 全部管理员(每端单推)
152 - GroupRequestApprovedNotification payload = new GroupRequestApprovedNotification();  
153 - // TODO @AI:能不能改成链式调用;  
154 - payload.setRequestId(request.getId());  
155 - payload.setGroupId(request.getGroupId());  
156 - payload.setUserId(request.getUserId());  
157 - payload.setOperatorUserId(userId); 153 + // TODO @AI:同类属性,放在一行,减少行占用;其它位置也是!
  154 + GroupRequestApprovedNotification payload = (GroupRequestApprovedNotification)
  155 + new GroupRequestApprovedNotification()
  156 + .setRequestId(request.getId())
  157 + .setGroupId(request.getGroupId())
  158 + .setUserId(request.getUserId())
  159 + .setOperatorUserId(userId);
158 broadcastToOwnerAdminsAndApplicant(request.getGroupId(), request.getUserId(), payload, 160 broadcastToOwnerAdminsAndApplicant(request.getGroupId(), request.getUserId(), payload,
159 ImMessageTypeEnum.GROUP_REQUEST_APPROVED.getType(), userId); 161 ImMessageTypeEnum.GROUP_REQUEST_APPROVED.getType(), userId);
160 // 5.2 群事件:主动申请 → 1510 自由进群;被邀请 → 1509 成员加入 162 // 5.2 群事件:主动申请 → 1510 自由进群;被邀请 → 1509 成员加入
@@ -187,32 +189,40 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -187,32 +189,40 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
187 } 189 }
188 190
189 // 3. 1506 私聊推送给申请人 + 群主 + 全部管理员 191 // 3. 1506 私聊推送给申请人 + 群主 + 全部管理员
190 - GroupRequestRejectedNotification payload = new GroupRequestRejectedNotification();  
191 - payload.setRequestId(request.getId());  
192 - payload.setGroupId(request.getGroupId());  
193 - payload.setUserId(request.getUserId());  
194 - payload.setHandleContent(handleContent);  
195 - payload.setOperatorUserId(userId); 192 + GroupRequestRejectedNotification payload = (GroupRequestRejectedNotification)
  193 + new GroupRequestRejectedNotification()
  194 + .setRequestId(request.getId())
  195 + .setGroupId(request.getGroupId())
  196 + .setUserId(request.getUserId())
  197 + .setHandleContent(handleContent)
  198 + .setOperatorUserId(userId);
196 broadcastToOwnerAdminsAndApplicant(request.getGroupId(), request.getUserId(), payload, 199 broadcastToOwnerAdminsAndApplicant(request.getGroupId(), request.getUserId(), payload,
197 ImMessageTypeEnum.GROUP_REQUEST_REJECTED.getType(), userId); 200 ImMessageTypeEnum.GROUP_REQUEST_REJECTED.getType(), userId);
198 } 201 }
199 202
200 - // TODO @AI:createInviteRequestList;习惯性命名;  
201 @Override 203 @Override
202 @Transactional(rollbackFor = Exception.class) 204 @Transactional(rollbackFor = Exception.class)
203 - public void createInviteRequests(Long groupId, Long inviterUserId, Collection<Long> invitedUserIds) { 205 + public void createInviteRequestList(Long groupId, Long inviterUserId, Collection<Long> invitedUserIds) {
204 if (CollUtil.isEmpty(invitedUserIds)) { 206 if (CollUtil.isEmpty(invitedUserIds)) {
205 return; 207 return;
206 } 208 }
207 ImGroupDO group = groupService.validateGroupExists(groupId); 209 ImGroupDO group = groupService.validateGroupExists(groupId);
208 210
209 - // TODO @AI:= 两侧,最好有空格;看看有没别的地方,也要改的;  
210 - // 1. 复用或新建 inviter_user_id=inviterUserId 的待审批记录 211 + // 1. 每个被邀请人 upsert 一条 inviter_user_id=邀请人 的记录
  212 + // 已有记录(同一对 group_id, user_id 唯一):覆盖 inviter_user_id + 重置 handle_result + 清空处理痕迹
211 List<ImGroupRequestDO> requests = new ArrayList<>(invitedUserIds.size()); 213 List<ImGroupRequestDO> requests = new ArrayList<>(invitedUserIds.size());
212 for (Long invitedUserId : invitedUserIds) { 214 for (Long invitedUserId : invitedUserIds) {
213 - ImGroupRequestDO existing = groupRequestMapper.selectLatestPendingByGroupIdAndUserIdAndInviter(  
214 - groupId, invitedUserId, inviterUserId); 215 + ImGroupRequestDO existing = groupRequestMapper.selectByGroupIdAndUserId(groupId, invitedUserId);
215 if (existing != null) { 216 if (existing != null) {
  217 + ImGroupRequestDO updateObj = new ImGroupRequestDO()
  218 + .setInviterUserId(inviterUserId)
  219 + .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult())
  220 + .setHandleUserId(null).setHandleContent(null).setHandleTime(null);
  221 + updateObj.setId(existing.getId());
  222 + groupRequestMapper.updateById(updateObj);
  223 + existing.setInviterUserId(inviterUserId)
  224 + .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult())
  225 + .setHandleUserId(null).setHandleContent(null).setHandleTime(null);
216 requests.add(existing); 226 requests.add(existing);
217 continue; 227 continue;
218 } 228 }
@@ -238,19 +248,23 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -238,19 +248,23 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
238 } 248 }
239 249
240 @Override 250 @Override
241 - public List<ImGroupRequestDO> getMyGroupRequestList(Long userId, Long lastRequestId, Integer limit) {  
242 - return groupRequestMapper.selectMyList(userId, lastRequestId, limit); 251 + public List<ImGroupRequestDO> getGroupRequestListByUserId(Long userId, Long lastRequestId, Integer limit) {
  252 + return groupRequestMapper.selectListByUserId(userId, lastRequestId, limit);
243 } 253 }
244 254
245 @Override 255 @Override
246 - public List<ImGroupRequestDO> getPendingGroupRequestList(Long userId, Long groupId,  
247 - Long lastRequestId, Integer limit) {  
248 - // 仅群主 / 管理员可拉  
249 - ImGroupMemberDO member = groupMemberService.validateMemberInGroup(groupId, userId);  
250 - if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())) {  
251 - throw exception(GROUP_REQUEST_NOT_TO_ME); 256 + public List<ImGroupRequestDO> getUnhandledRequestListByOwnerOrAdmin(Long userId) {
  257 + // 1. 找出当前用户作为 OWNER / ADMIN 的所有群
  258 + List<ImGroupMemberDO> myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId);
  259 + Set<Long> ownerOrAdminGroupIds = convertSet(myMembers,
  260 + ImGroupMemberDO::getGroupId,
  261 + m -> ImGroupMemberRoleEnum.isOwnerOrAdmin(m.getRole()));
  262 + if (CollUtil.isEmpty(ownerOrAdminGroupIds)) {
  263 + return Collections.emptyList();
252 } 264 }
253 - return groupRequestMapper.selectPendingListByGroupId(groupId, lastRequestId, limit); 265 + // 2. 一次拉所有群的未处理申请
  266 + return groupRequestMapper.selectListByGroupIdsAndHandleResult(
  267 + ownerOrAdminGroupIds, ImGroupRequestHandleResultEnum.UNHANDLED.getResult());
254 } 268 }
255 269
256 @Override 270 @Override
@@ -258,19 +272,6 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -258,19 +272,6 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
258 return groupRequestMapper.selectById(id); 272 return groupRequestMapper.selectById(id);
259 } 273 }
260 274
261 - // TODO @AI:【后面点改】应该批量查询;不然这样性能太差了。如果群太多;  
262 - @Override  
263 - public Map<Long, Long> getPendingCountMap(Collection<Long> groupIds) {  
264 - if (CollUtil.isEmpty(groupIds)) {  
265 - return Collections.emptyMap();  
266 - }  
267 - Map<Long, Long> result = new HashMap<>(groupIds.size());  
268 - for (Long groupId : groupIds) {  
269 - result.put(groupId, groupRequestMapper.selectPendingCountByGroupId(groupId));  
270 - }  
271 - return result;  
272 - }  
273 -  
274 @Override 275 @Override
275 public PageResult<ImGroupRequestDO> getGroupRequestPage(ImGroupRequestManagerPageReqVO reqVO) { 276 public PageResult<ImGroupRequestDO> getGroupRequestPage(ImGroupRequestManagerPageReqVO reqVO) {
276 return groupRequestMapper.selectPage(reqVO); 277 return groupRequestMapper.selectPage(reqVO);
@@ -299,18 +300,18 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -299,18 +300,18 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
299 */ 300 */
300 private GroupRequestNotification buildRequestNotification(ImGroupDO group, ImGroupRequestDO request, 301 private GroupRequestNotification buildRequestNotification(ImGroupDO group, ImGroupRequestDO request,
301 AdminUserRespDTO applyUser) { 302 AdminUserRespDTO applyUser) {
302 - // TODO @AI:看看能不能链式调用;  
303 - GroupRequestNotification payload = new GroupRequestNotification();  
304 - payload.setRequestId(request.getId());  
305 - payload.setGroupId(group.getId());  
306 - payload.setUserId(request.getUserId());  
307 - payload.setInviterUserId(request.getInviterUserId());  
308 - payload.setApplyContent(request.getApplyContent());  
309 - payload.setAddSource(request.getAddSource());  
310 - payload.setOperatorUserId(request.getInviterUserId() != null ? request.getInviterUserId() : request.getUserId()); 303 + // TODO @AI:看看能不能链式调用;同类属性,相同行;
  304 + GroupRequestNotification payload = (GroupRequestNotification) new GroupRequestNotification()
  305 + .setRequestId(request.getId())
  306 + .setGroupId(group.getId())
  307 + .setUserId(request.getUserId())
  308 + .setInviterUserId(request.getInviterUserId())
  309 + .setApplyContent(request.getApplyContent())
  310 + .setAddSource(request.getAddSource())
  311 + .setOperatorUserId(request.getInviterUserId() != null
  312 + ? request.getInviterUserId() : request.getUserId());
311 if (applyUser != null) { 313 if (applyUser != null) {
312 - payload.setUserNickname(applyUser.getNickname());  
313 - payload.setUserAvatar(applyUser.getAvatar()); 314 + payload.setUserNickname(applyUser.getNickname()).setUserAvatar(applyUser.getAvatar());
314 } 315 }
315 return payload; 316 return payload;
316 } 317 }
@@ -326,25 +327,21 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -326,25 +327,21 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
326 } 327 }
327 Set<Long> receivers = new LinkedHashSet<>(listOwnerAndAdminUserIds(group)); 328 Set<Long> receivers = new LinkedHashSet<>(listOwnerAndAdminUserIds(group));
328 receivers.add(applicantUserId); 329 receivers.add(applicantUserId);
329 - // TODO @AI:看看能不能用群的通道;  
330 for (Long receiverUserId : receivers) { 330 for (Long receiverUserId : receivers) {
331 websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification( 331 websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification(
332 messageType, operatorUserId, receiverUserId, payload)); 332 messageType, operatorUserId, receiverUserId, payload));
333 } 333 }
334 } 334 }
335 335
336 - // TODO @AI:这个方法,搞到到 ImGroupMemberService 里去;getGroupMemberListByOwnerAndAdmin;返回 list《do》;  
337 /** 336 /**
338 - * 列出群主 + 全部管理员的用户编号;activeMembers 通常 ≤ 500 + admin 上限 3,过滤成本可接受 337 + * 列出群主 + 全部管理员的用户编号;以 group.ownerUserId 兜底,避免 group_member 表里 owner 角色记录异常缺失
339 */ 338 */
  339 + // TODO @AI:有点过度设计了,直接 convertset 群成员即可!
340 private List<Long> listOwnerAndAdminUserIds(ImGroupDO group) { 340 private List<Long> listOwnerAndAdminUserIds(ImGroupDO group) {
341 - List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(group.getId());  
342 Set<Long> set = new LinkedHashSet<>(); 341 Set<Long> set = new LinkedHashSet<>();
343 set.add(group.getOwnerUserId()); 342 set.add(group.getOwnerUserId());
344 - for (ImGroupMemberDO member : activeMembers) {  
345 - if (ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())) {  
346 - set.add(member.getUserId());  
347 - } 343 + for (ImGroupMemberDO member : groupMemberService.getGroupMemberListByOwnerAndAdmin(group.getId())) {
  344 + set.add(member.getUserId());
348 } 345 }
349 return new ArrayList<>(set); 346 return new ArrayList<>(set);
350 } 347 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
@@ -28,7 +28,6 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; @@ -28,7 +28,6 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
28 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; 28 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
29 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; 29 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper;
30 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum; 30 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
31 -import cn.iocoder.yudao.module.im.enums.group.ImGroupJoinTypeEnum;  
32 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; 31 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
33 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 32 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
34 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 33 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
@@ -194,7 +193,7 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -194,7 +193,7 @@ public class ImGroupServiceImpl implements ImGroupService {
194 Long groupId = inviteReqVO.getGroupId(); 193 Long groupId = inviteReqVO.getGroupId();
195 // 1.1 校验群存在 + 当前用户是群成员 194 // 1.1 校验群存在 + 当前用户是群成员
196 ImGroupDO group = validateGroupExists(groupId); 195 ImGroupDO group = validateGroupExists(groupId);
197 - ImGroupMemberDO operator = groupMemberService.validateMemberInGroup(groupId, userId); 196 + groupMemberService.validateMemberInGroup(groupId, userId);
198 // 1.2 排除已在群中的用户 197 // 1.2 排除已在群中的用户
199 List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId); 198 List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId);
200 activeMembers.forEach(member -> inviteReqVO.getMemberUserIds().remove(member.getUserId())); 199 activeMembers.forEach(member -> inviteReqVO.getMemberUserIds().remove(member.getUserId()));
@@ -214,17 +213,13 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -214,17 +213,13 @@ public class ImGroupServiceImpl implements ImGroupService {
214 throw exception(GROUP_MEMBER_EXCEED, MAX_GROUP_MEMBER); 213 throw exception(GROUP_MEMBER_EXCEED, MAX_GROUP_MEMBER);
215 } 214 }
216 215
217 - // TODO @AI:是不是注释的风格,是情况一、情况二,这样搞?  
218 - // 2. 按群 joinType + 操作人角色分流:APPLY_AND_NORMAL_INVITE 模式下,普通成员邀请走审批,落 group_request;其余直进  
219 - // TODO @AI:是不是可以放到 isInviteNeedApproval 判断,是否要审批,这样更简洁一点?  
220 - boolean needApproval = ImGroupJoinTypeEnum.isInviteNeedApproval(group.getJoinType())  
221 - && !ImGroupMemberRoleEnum.isOwnerOrAdmin(operator.getRole());  
222 - if (needApproval) {  
223 - groupRequestService.createInviteRequests(groupId, userId, memberUserIds); 216 + // 2. 情况一:群开启了审批,邀请也走审批,落 group_request 等群主 / 管理员处理
  217 + if (Boolean.TRUE.equals(group.getJoinApproval())) {
  218 + groupRequestService.createInviteRequestList(groupId, userId, memberUserIds);
224 return; 219 return;
225 } 220 }
226 221
227 - // 3. 直进:批量添加群成员,写 addSource=INVITE / inviterUserId=操作人,留痕 222 + // 3. 情况二:未开审批,直进;批量添加群成员,写 addSource=INVITE / inviterUserId=操作人 留痕
228 groupMemberService.addGroupMembers(groupId, memberUserIds, 223 groupMemberService.addGroupMembers(groupId, memberUserIds,
229 ImGroupAddSourceEnum.INVITE.getSource(), userId); 224 ImGroupAddSourceEnum.INVITE.getSource(), userId);
230 225
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupRequestApprovedNotification.java
@@ -7,7 +7,7 @@ import lombok.EqualsAndHashCode; @@ -7,7 +7,7 @@ import lombok.EqualsAndHashCode;
7 * 入群申请被同意通知 7 * 入群申请被同意通知
8 * <p> 8 * <p>
9 * 走私聊通道定向推送给申请人 + 群主 + 全部管理员; 9 * 走私聊通道定向推送给申请人 + 群主 + 全部管理员;
10 - * 申请人侧据此弹 toast、列表项更新;admin 侧据此把 pendingRequestCount 减 1 并从未处理列表移除 10 + * 申请人侧据此弹 toast、列表项更新;admin 侧据此把 unhandledCount 减 1 并从未处理列表移除
11 */ 11 */
12 @EqualsAndHashCode(callSuper = true) 12 @EqualsAndHashCode(callSuper = true)
13 @Data 13 @Data
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupRequestRejectedNotification.java
@@ -6,7 +6,7 @@ import lombok.EqualsAndHashCode; @@ -6,7 +6,7 @@ import lombok.EqualsAndHashCode;
6 /** 6 /**
7 * 入群申请被拒绝通知 7 * 入群申请被拒绝通知
8 * <p> 8 * <p>
9 - * 走私聊通道定向推送给申请人 + 群主 + 全部管理员;admin 侧据此把 pendingRequestCount 减 1 并从未处理列表移除 9 + * 走私聊通道定向推送给申请人 + 群主 + 全部管理员;admin 侧据此把 unhandledCount 减 1 并从未处理列表移除
10 */ 10 */
11 @EqualsAndHashCode(callSuper = true) 11 @EqualsAndHashCode(callSuper = true)
12 @Data 12 @Data
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImplTest.java
@@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; @@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
9 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO; 9 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO;
10 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupRequestMapper; 10 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupRequestMapper;
11 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum; 11 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
12 -import cn.iocoder.yudao.module.im.enums.group.ImGroupJoinTypeEnum;  
13 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; 12 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
14 import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum; 13 import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum;
15 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 14 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
@@ -60,7 +59,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -60,7 +59,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
60 public void testApplyJoinGroup_freeMode_directJoin() { 59 public void testApplyJoinGroup_freeMode_directJoin() {
61 // 准备:群是 FREE 模式 60 // 准备:群是 FREE 模式
62 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) 61 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
63 - .joinType(ImGroupJoinTypeEnum.FREE.getType()) 62 + .joinApproval(false)
64 .status(CommonStatusEnum.ENABLE.getStatus()).build(); 63 .status(CommonStatusEnum.ENABLE.getStatus()).build();
65 when(groupService.validateGroupExists(10L)).thenReturn(group); 64 when(groupService.validateGroupExists(10L)).thenReturn(group);
66 65
@@ -88,11 +87,11 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -88,11 +87,11 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
88 public void testApplyJoinGroup_approvalMode_createsRequest() { 87 public void testApplyJoinGroup_approvalMode_createsRequest() {
89 // 准备:群是 APPLY 模式 88 // 准备:群是 APPLY 模式
90 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) 89 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
91 - .joinType(ImGroupJoinTypeEnum.APPLY.getType()) 90 + .joinApproval(true)
92 .status(CommonStatusEnum.ENABLE.getStatus()).build(); 91 .status(CommonStatusEnum.ENABLE.getStatus()).build();
93 when(groupService.validateGroupExists(10L)).thenReturn(group); 92 when(groupService.validateGroupExists(10L)).thenReturn(group);
94 // 群里有 owner + 一个 admin,作为 1503 推送目标 93 // 群里有 owner + 一个 admin,作为 1503 推送目标
95 - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of( 94 + when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of(
96 ImGroupMemberDO.builder().groupId(10L).userId(99L) 95 ImGroupMemberDO.builder().groupId(10L).userId(99L)
97 .role(ImGroupMemberRoleEnum.OWNER.getRole()) 96 .role(ImGroupMemberRoleEnum.OWNER.getRole())
98 .status(CommonStatusEnum.ENABLE.getStatus()).build(), 97 .status(CommonStatusEnum.ENABLE.getStatus()).build(),
@@ -120,7 +119,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -120,7 +119,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
120 @Test 119 @Test
121 public void testApplyJoinGroup_alreadyMember_throws() { 120 public void testApplyJoinGroup_alreadyMember_throws() {
122 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) 121 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
123 - .joinType(ImGroupJoinTypeEnum.APPLY.getType()) 122 + .joinApproval(true)
124 .status(CommonStatusEnum.ENABLE.getStatus()).build(); 123 .status(CommonStatusEnum.ENABLE.getStatus()).build();
125 when(groupService.validateGroupExists(10L)).thenReturn(group); 124 when(groupService.validateGroupExists(10L)).thenReturn(group);
126 when(groupMemberService.getGroupMember(10L, 1L)).thenReturn( 125 when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(
@@ -156,7 +155,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -156,7 +155,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
156 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) 155 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
157 .status(CommonStatusEnum.ENABLE.getStatus()).build(); 156 .status(CommonStatusEnum.ENABLE.getStatus()).build();
158 when(groupService.getGroup(10L)).thenReturn(group); 157 when(groupService.getGroup(10L)).thenReturn(group);
159 - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of( 158 + when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of(
160 ImGroupMemberDO.builder().groupId(10L).userId(99L) 159 ImGroupMemberDO.builder().groupId(10L).userId(99L)
161 .role(ImGroupMemberRoleEnum.OWNER.getRole()) 160 .role(ImGroupMemberRoleEnum.OWNER.getRole())
162 .status(CommonStatusEnum.ENABLE.getStatus()).build())); 161 .status(CommonStatusEnum.ENABLE.getStatus()).build()));
@@ -234,7 +233,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -234,7 +233,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
234 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) 233 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L)
235 .status(CommonStatusEnum.ENABLE.getStatus()).build(); 234 .status(CommonStatusEnum.ENABLE.getStatus()).build();
236 when(groupService.getGroup(10L)).thenReturn(group); 235 when(groupService.getGroup(10L)).thenReturn(group);
237 - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of( 236 + when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of(
238 ImGroupMemberDO.builder().groupId(10L).userId(1L) 237 ImGroupMemberDO.builder().groupId(10L).userId(1L)
239 .role(ImGroupMemberRoleEnum.OWNER.getRole()) 238 .role(ImGroupMemberRoleEnum.OWNER.getRole())
240 .status(CommonStatusEnum.ENABLE.getStatus()).build())); 239 .status(CommonStatusEnum.ENABLE.getStatus()).build()));
@@ -246,15 +245,15 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -246,15 +245,15 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
246 verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); 245 verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
247 } 246 }
248 247
249 - // ==================== createInviteRequests ==================== 248 + // ==================== createInviteRequestList ====================
250 249
251 @Test 250 @Test
252 - public void testCreateInviteRequests_success() { 251 + public void testCreateInviteRequestList_success() {
253 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) 252 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
254 - .joinType(ImGroupJoinTypeEnum.APPLY_AND_NORMAL_INVITE.getType()) 253 + .joinApproval(true)
255 .status(CommonStatusEnum.ENABLE.getStatus()).build(); 254 .status(CommonStatusEnum.ENABLE.getStatus()).build();
256 when(groupService.validateGroupExists(10L)).thenReturn(group); 255 when(groupService.validateGroupExists(10L)).thenReturn(group);
257 - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of( 256 + when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of(
258 ImGroupMemberDO.builder().groupId(10L).userId(99L) 257 ImGroupMemberDO.builder().groupId(10L).userId(99L)
259 .role(ImGroupMemberRoleEnum.OWNER.getRole()) 258 .role(ImGroupMemberRoleEnum.OWNER.getRole())
260 .status(CommonStatusEnum.ENABLE.getStatus()).build())); 259 .status(CommonStatusEnum.ENABLE.getStatus()).build()));
@@ -263,7 +262,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @@ -263,7 +262,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
263 3L, buildUser(3L, "用户B"))); 262 3L, buildUser(3L, "用户B")));
264 263
265 // 调用:邀请人 1L 邀请 2L、3L 264 // 调用:邀请人 1L 邀请 2L、3L
266 - groupRequestService.createInviteRequests(10L, 1L, List.of(2L, 3L)); 265 + groupRequestService.createInviteRequestList(10L, 1L, List.of(2L, 3L));
267 266
268 // 断言:插入 2 条申请记录 + 推 1503 给 owner(每条 1 帧)共 2 帧 267 // 断言:插入 2 条申请记录 + 推 1503 给 owner(每条 1 帧)共 2 帧
269 verify(groupRequestMapper, times(2)).insert(any(ImGroupRequestDO.class)); 268 verify(groupRequestMapper, times(2)).insert(any(ImGroupRequestDO.class));
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java
@@ -16,7 +16,6 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; @@ -16,7 +16,6 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
16 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 16 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
17 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; 17 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper;
18 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum; 18 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
19 -import cn.iocoder.yudao.module.im.enums.group.ImGroupJoinTypeEnum;  
20 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; 19 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
21 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 20 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
22 import cn.iocoder.yudao.module.im.service.friend.ImFriendService; 21 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
@@ -257,9 +256,9 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -257,9 +256,9 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
257 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) 256 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
258 .thenReturn(groupService); 257 .thenReturn(groupService);
259 258
260 - // 准备:群存在 + joinType 默认 FREE + 当前用户是群主(操作人必走「直进」分支) 259 + // 准备:群存在 + joinApproval=false(自由进群) + 当前用户是群主
261 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) 260 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L)
262 - .joinType(ImGroupJoinTypeEnum.FREE.getType()) 261 + .joinApproval(false)
263 .status(CommonStatusEnum.ENABLE.getStatus()).build(); 262 .status(CommonStatusEnum.ENABLE.getStatus()).build();
264 when(groupMapper.selectById(10L)).thenReturn(group); 263 when(groupMapper.selectById(10L)).thenReturn(group);
265 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( 264 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(
@@ -293,7 +292,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -293,7 +292,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
293 verify(groupMemberService).validateMemberInGroup(10L, 1L); 292 verify(groupMemberService).validateMemberInGroup(10L, 1L);
294 verify(groupMemberService).addGroupMembers(eq(10L), anyCollection(), 293 verify(groupMemberService).addGroupMembers(eq(10L), anyCollection(),
295 eq(ImGroupAddSourceEnum.INVITE.getSource()), eq(1L)); 294 eq(ImGroupAddSourceEnum.INVITE.getSource()), eq(1L));
296 - verify(groupRequestService, never()).createInviteRequests(anyLong(), anyLong(), anyCollection()); 295 + verify(groupRequestService, never()).createInviteRequestList(anyLong(), anyLong(), anyCollection());
297 verify(webSocketService, never()).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); 296 verify(webSocketService, never()).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class));
298 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class); 297 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
299 verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture()); 298 verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture());
@@ -302,20 +301,16 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -302,20 +301,16 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
302 } 301 }
303 302
304 @Test 303 @Test
305 - public void testInviteGroupMember_strictMode_normalMember_routesToApproval() { 304 + public void testInviteGroupMember_approval_routesToApproval() {
306 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { 305 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
307 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) 306 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
308 .thenReturn(groupService); 307 .thenReturn(groupService);
309 308
310 - // 准备:joinType=APPLY_AND_NORMAL_INVITE 且操作人是普通成员;应走审批分支,落 group_request 309 + // 准备:joinApproval=true,开启审批;任何邀请都需审批,落 group_request
311 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) 310 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
312 - .joinType(ImGroupJoinTypeEnum.APPLY_AND_NORMAL_INVITE.getType()) 311 + .joinApproval(true)
313 .status(CommonStatusEnum.ENABLE.getStatus()).build(); 312 .status(CommonStatusEnum.ENABLE.getStatus()).build();
314 when(groupMapper.selectById(10L)).thenReturn(group); 313 when(groupMapper.selectById(10L)).thenReturn(group);
315 - when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(  
316 - ImGroupMemberDO.builder().groupId(10L).userId(1L)  
317 - .role(ImGroupMemberRoleEnum.NORMAL.getRole())  
318 - .status(CommonStatusEnum.ENABLE.getStatus()).build());  
319 when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of( 314 when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of(
320 ImGroupMemberDO.builder().groupId(10L).userId(99L) 315 ImGroupMemberDO.builder().groupId(10L).userId(99L)
321 .role(ImGroupMemberRoleEnum.OWNER.getRole()) 316 .role(ImGroupMemberRoleEnum.OWNER.getRole())
@@ -336,51 +331,14 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -336,51 +331,14 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
336 // 调用 331 // 调用
337 groupService.inviteGroupMember(1L, reqVO); 332 groupService.inviteGroupMember(1L, reqVO);
338 333
339 - // 断言:走审批分支:调 createInviteRequests;不写群成员、不推 1509  
340 - verify(groupRequestService).createInviteRequests(eq(10L), eq(1L), anyCollection()); 334 + // 断言:走审批分支:调 createInviteRequestList;不写群成员、不推 1509
  335 + verify(groupRequestService).createInviteRequestList(eq(10L), eq(1L), anyCollection());
341 verify(groupMemberService, never()).addGroupMembers(anyLong(), anyCollection(), any(), any()); 336 verify(groupMemberService, never()).addGroupMembers(anyLong(), anyCollection(), any(), any());
342 verify(groupMessageService, never()).sendGroupMessage(anyLong(), any(ImGroupMessageSendDTO.class)); 337 verify(groupMessageService, never()).sendGroupMessage(anyLong(), any(ImGroupMessageSendDTO.class));
343 } 338 }
344 } 339 }
345 340
346 @Test 341 @Test
347 - public void testInviteGroupMember_strictMode_owner_directJoin() {  
348 - try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {  
349 - springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))  
350 - .thenReturn(groupService);  
351 -  
352 - // 准备:joinType=APPLY_AND_NORMAL_INVITE 但操作人是群主;走「直进」分支  
353 - ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L)  
354 - .joinType(ImGroupJoinTypeEnum.APPLY_AND_NORMAL_INVITE.getType())  
355 - .status(CommonStatusEnum.ENABLE.getStatus()).build();  
356 - when(groupMapper.selectById(10L)).thenReturn(group);  
357 - when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(  
358 - ImGroupMemberDO.builder().groupId(10L).userId(1L)  
359 - .role(ImGroupMemberRoleEnum.OWNER.getRole())  
360 - .status(CommonStatusEnum.ENABLE.getStatus()).build());  
361 - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of(  
362 - ImGroupMemberDO.builder().groupId(10L).userId(1L)  
363 - .role(ImGroupMemberRoleEnum.OWNER.getRole())  
364 - .status(CommonStatusEnum.ENABLE.getStatus()).build()));  
365 -  
366 - ImGroupMemberInviteReqVO reqVO = new ImGroupMemberInviteReqVO();  
367 - reqVO.setGroupId(10L);  
368 - reqVO.setMemberUserIds(new ArrayList<>(List.of(2L)));  
369 - when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(List.of(  
370 - ImFriendDO.builder().userId(1L).friendUserId(2L)  
371 - .status(CommonStatusEnum.ENABLE.getStatus()).build()));  
372 -  
373 - // 调用  
374 - groupService.inviteGroupMember(1L, reqVO);  
375 -  
376 - // 断言:走直进分支  
377 - verify(groupMemberService).addGroupMembers(eq(10L), anyCollection(),  
378 - eq(ImGroupAddSourceEnum.INVITE.getSource()), eq(1L));  
379 - verify(groupRequestService, never()).createInviteRequests(anyLong(), anyLong(), anyCollection());  
380 - }  
381 - }  
382 -  
383 - @Test  
384 public void testInviteGroupMember_notFriend() { 342 public void testInviteGroupMember_notFriend() {
385 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { 343 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
386 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) 344 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
yudao-module-im/src/test/resources/sql/create_tables.sql
@@ -49,7 +49,7 @@ CREATE TABLE IF NOT EXISTS &quot;im_group&quot; ( @@ -49,7 +49,7 @@ CREATE TABLE IF NOT EXISTS &quot;im_group&quot; (
49 "status" tinyint NOT NULL COMMENT '群状态', 49 "status" tinyint NOT NULL COMMENT '群状态',
50 "dissolved_time" timestamp DEFAULT NULL COMMENT '解散时间', 50 "dissolved_time" timestamp DEFAULT NULL COMMENT '解散时间',
51 "muted_all" bit DEFAULT FALSE COMMENT '是否全群禁言', 51 "muted_all" bit DEFAULT FALSE COMMENT '是否全群禁言',
52 - "join_type" tinyint NOT NULL DEFAULT 0 COMMENT '加群方式;0自由进群 1申请需审批 2申请和普通成员邀请均需审批', 52 + "join_approval" bit NOT NULL DEFAULT FALSE COMMENT '进群是否需群主 / 管理员审批;false 自由进群,true 需审批',
53 "pinned_message_ids" varchar(128) DEFAULT NULL COMMENT '群置顶消息编号列表,逗号分隔', 53 "pinned_message_ids" varchar(128) DEFAULT NULL COMMENT '群置顶消息编号列表,逗号分隔',
54 "creator" varchar(64) DEFAULT '', 54 "creator" varchar(64) DEFAULT '',
55 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 55 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -121,7 +121,8 @@ CREATE TABLE IF NOT EXISTS &quot;im_friend_request&quot; ( @@ -121,7 +121,8 @@ CREATE TABLE IF NOT EXISTS &quot;im_friend_request&quot; (
121 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 121 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
122 "deleted" bit NOT NULL DEFAULT FALSE, 122 "deleted" bit NOT NULL DEFAULT FALSE,
123 "tenant_id" bigint NOT NULL DEFAULT 0, 123 "tenant_id" bigint NOT NULL DEFAULT 0,
124 - PRIMARY KEY ("id") 124 + PRIMARY KEY ("id"),
  125 + CONSTRAINT "uk_im_friend_request" UNIQUE ("from_user_id", "to_user_id", "tenant_id")
125 ) COMMENT 'IM 好友申请记录表'; 126 ) COMMENT 'IM 好友申请记录表';
126 127
127 CREATE TABLE IF NOT EXISTS "im_group_request" ( 128 CREATE TABLE IF NOT EXISTS "im_group_request" (
@@ -141,7 +142,8 @@ CREATE TABLE IF NOT EXISTS &quot;im_group_request&quot; ( @@ -141,7 +142,8 @@ CREATE TABLE IF NOT EXISTS &quot;im_group_request&quot; (
141 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 142 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
142 "deleted" bit NOT NULL DEFAULT FALSE, 143 "deleted" bit NOT NULL DEFAULT FALSE,
143 "tenant_id" bigint NOT NULL DEFAULT 0, 144 "tenant_id" bigint NOT NULL DEFAULT 0,
144 - PRIMARY KEY ("id") 145 + PRIMARY KEY ("id"),
  146 + CONSTRAINT "uk_im_group_request" UNIQUE ("group_id", "user_id", "tenant_id")
145 ) COMMENT 'IM 加群申请记录表'; 147 ) COMMENT 'IM 加群申请记录表';
146 148
147 CREATE TABLE IF NOT EXISTS "im_face_pack" ( 149 CREATE TABLE IF NOT EXISTS "im_face_pack" (