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 10 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
11 11 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
12 12 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
13   -import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
14 13 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
15   -import cn.iocoder.yudao.module.im.service.group.ImGroupRequestService;
16 14 import cn.iocoder.yudao.module.im.service.group.ImGroupService;
17 15 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
18 16 import io.swagger.v3.oas.annotations.Operation;
... ... @@ -42,8 +40,6 @@ public class ImGroupController {
42 40 private ImGroupMemberService groupMemberService;
43 41 @Resource
44 42 private ImGroupMessageService groupMessageService;
45   - @Resource
46   - private ImGroupRequestService groupRequestService;
47 43  
48 44 // ==================== 群的写操作 ====================
49 45  
... ... @@ -181,34 +177,23 @@ public class ImGroupController {
181 177 }
182 178  
183 179 /**
184   - * 群列表批量转 VO + 关联回填 pinnedMessages 与 pendingRequestCount
  180 + * 群列表批量转 VO + 关联回填 pinnedMessages
185 181 * <p>
186   - * pinnedMessages 仅当前用户是该群有效成员时回填;pendingRequestCount 仅当前用户是该群 owner / admin 时回填
  182 + * 仅当登录用户是某群的有效成员时才回填该群的 pinnedMessages,避免非成员 / 已退群用户越权拿到置顶消息内容
187 183 */
188 184 private List<ImGroupRespVO> buildGroupRespVOList(List<ImGroupDO> groups, Long loginUserId) {
189 185 if (CollUtil.isEmpty(groups)) {
190 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 191 Set<Long> allMessageIds = convertSetByFlatMap(groups, group -> activeGroupIds.contains(group.getId())
200 192 ? CollUtil.emptyIfNull(group.getPinnedMessageIds()).stream() : Stream.empty());
201 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 195 return convertList(groups, group -> {
208 196 ImGroupRespVO vo = BeanUtils.toBean(group, ImGroupRespVO.class);
209   - if (ownerOrAdminGroupIds.contains(group.getId())) {
210   - vo.setPendingRequestCount(pendingCountMap.getOrDefault(group.getId(), 0L));
211   - }
212 197 if (!activeGroupIds.contains(group.getId()) || CollUtil.isEmpty(group.getPinnedMessageIds())) {
213 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 8 import cn.iocoder.yudao.module.im.controller.admin.group.vo.request.ImGroupRequestApplyReqVO;
9 9 import cn.iocoder.yudao.module.im.controller.admin.group.vo.request.ImGroupRequestRespVO;
10 10 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
  11 +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
11 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 15 import cn.iocoder.yudao.module.im.service.group.ImGroupRequestService;
13 16 import cn.iocoder.yudao.module.im.service.group.ImGroupService;
14 17 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
... ... @@ -47,6 +50,8 @@ public class ImGroupRequestController {
47 50 private ImGroupRequestService groupRequestService;
48 51 @Resource
49 52 private ImGroupService groupService;
  53 + @Resource
  54 + private ImGroupMemberService groupMemberService;
50 55  
51 56 @Resource
52 57 private AdminUserApi adminUserApi;
... ... @@ -79,71 +84,58 @@ public class ImGroupRequestController {
79 84  
80 85 @GetMapping("/list")
81 86 @Operation(summary = "查询「我相关」的加群申请列表(含我主动申请、我被邀请待审);游标分页")
82   - public CommonResult<List<ImGroupRequestRespVO>> getMyGroupRequestList(
  87 + public CommonResult<List<ImGroupRequestRespVO>> getGroupRequestList(
83 88 @Parameter(description = "当前列表最旧记录的 id;首页不传")
84 89 @RequestParam(value = "lastRequestId", required = false) Long lastRequestId,
85 90 @Parameter(description = "单次拉取条数", required = true)
86 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 104 @GetMapping("/get")
109   - @Operation(summary = "按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用)")
  105 + @Operation(summary = "按 id 单查申请记录(带越权过滤;WebSocket 通知到达后用)")
110 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 108 ImGroupRequestDO request = groupRequestService.getGroupRequest(id);
113   - // 越权过滤:申请人 / 被邀请人 / 邀请人 / 群主 / 管理员之外,当不存在返回 null
114 109 if (request == null) {
115 110 return success(null);
116 111 }
  112 + // 越权过滤:申请人 / 邀请人 / 群主 / 管理员之外,当不存在返回 null
117 113 Long currentUserId = getLoginUserId();
118 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 117 if (!canSee) {
132 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 131 /** 申请记录列表批量转 VO + 关联回填用户 / 群信息 */
138   - private List<ImGroupRequestRespVO> buildList(List<ImGroupRequestDO> list) {
  132 + private List<ImGroupRequestRespVO> buildVOList(List<ImGroupRequestDO> list) {
139 133 if (CollUtil.isEmpty(list)) {
140 134 return Collections.emptyList();
141 135 }
142   - // 1. 聚合 user / inviter 用户信息
143   - // TODO @AI:.filter(Objects::nonNull) 内部已经过滤了;
  136 + // 1. 聚合 user / inviter 用户信息;convertSetByFlatMap 内部已过滤 null
144 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 139 Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
148 140 // 2. 聚合群信息(封禁 / 解散群也要回填,便于前端展示历史)
149 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 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 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import jakarta.validation.constraints.NotBlank;
7 5 import lombok.Data;
... ... @@ -19,8 +17,7 @@ public class ImGroupCreateReqVO {
19 17 @Schema(description = "初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)", example = "[1024, 2048]")
20 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 32 @Schema(description = "是否全群禁言")
33 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 38 @Schema(description = "封禁时间")
43 39 private LocalDateTime bannedTime;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupUpdateReqVO.java
1 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 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import jakarta.validation.constraints.NotNull;
7 5 import lombok.Data;
... ... @@ -23,8 +21,7 @@ public class ImGroupUpdateReqVO {
23 21 @Schema(description = "群公告")
24 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 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 5 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
6   -import cn.iocoder.yudao.module.im.enums.group.ImGroupJoinTypeEnum;
7 6 import com.baomidou.mybatisplus.annotation.KeySequence;
8 7 import com.baomidou.mybatisplus.annotation.TableField;
9 8 import com.baomidou.mybatisplus.annotation.TableId;
... ... @@ -52,11 +51,11 @@ public class ImGroupDO extends BaseDO {
52 51 */
53 52 private String notice;
54 53 /**
55   - * 加群方式
  54 + * 进群是否需群主 / 管理员审批
56 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 18 @Mapper
19 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 22 return selectOne(new LambdaQueryWrapperX<ImFriendRequestDO>()
23 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 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 6 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupRequestManagerPageReqVO;
7 7 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO;
8   -import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum;
9 8 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
10 9 import org.apache.ibatis.annotations.Mapper;
11 10  
  11 +import java.util.Collection;
12 12 import java.util.List;
13 13  
14 14 /**
... ... @@ -19,31 +19,13 @@ import java.util.List;
19 19 @Mapper
20 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 29 return selectList(new LambdaQueryWrapperX<ImGroupRequestDO>()
48 30 .eq(ImGroupRequestDO::getUserId, userId)
49 31 .ltIfPresent(ImGroupRequestDO::getId, lastRequestId)
... ... @@ -51,26 +33,16 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; {
51 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 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 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 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 17 @Getter
18 18 public enum ImGroupAddSourceEnum implements ArrayValuable<Integer> {
19 19  
20   - // TODO @AI:没实现的方式,记得在后面加个 TODO @芋艿:未实现,原因。。。。
21   - SEARCH(1, "搜索"),
  20 + SEARCH(1, "搜索"), // TODO @芋艿:SEARCH 暂未实现,原因 - 搜群入口尚未开发
22 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 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 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 104 } else {
105 105 request = BeanUtils.toBean(reqVO, ImFriendRequestDO.class)
106 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 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 70 * 根据群编号查询有效成员的 userId 列表(仅 ENABLE 状态)
63 71 * <p>
64 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 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 89 * 只缓存 userId 列表而非整个 {@link ImGroupMemberDO},理由:
78 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 8  
9 9 import java.util.Collection;
10 10 import java.util.List;
11   -import java.util.Map;
12 11  
13 12 /**
14 13 * IM 加群申请 Service 接口
... ... @@ -20,8 +19,7 @@ public interface ImGroupRequestService {
20 19 /**
21 20 * 用户主动申请加群
22 21 * <p>
23   - * 群 joinType=FREE 时直接入群 + 1510 全员广播 + 1505 推送给申请人;
24   - * 否则落 im_group_request 待审批 + 1503 定向推送给群主 / 全部管理员
  22 + * 群未开启审批时直接入群 + 1510 全员广播;开启审批则 upsert 一条 inviterUserId=null 的待审批记录 + 1503 推送
25 23 *
26 24 * @param userId 申请人用户编号
27 25 * @param reqVO 申请请求
... ... @@ -30,9 +28,7 @@ public interface ImGroupRequestService {
30 28 ImGroupRequestDO applyJoinGroup(Long userId, @Valid ImGroupRequestApplyReqVO reqVO);
31 29  
32 30 /**
33   - * 同意加群申请(群主或管理员)
34   - * <p>
35   - * 处理前校验入群人数上限;通过后写 group_member(带 addSource / inviterUserId) + 推送通知
  31 + * 同意加群申请(群主或管理员);处理前校验入群人数上限
36 32 *
37 33 * @param userId 操作人用户编号
38 34 * @param requestId 申请记录编号
... ... @@ -49,15 +45,13 @@ public interface ImGroupRequestService {
49 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 50 * @param groupId 群编号
57 51 * @param inviterUserId 邀请人用户编号
58 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 61 * @param limit 单次拉取条数
68 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 77 * 按 id 单查申请记录;通用读接口,越权过滤交由调用方
... ... @@ -88,14 +81,6 @@ public interface ImGroupRequestService {
88 81 */
89 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 12 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
13 13 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO;
14 14 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupRequestMapper;
15   -import cn.iocoder.yudao.module.im.enums.group.ImGroupJoinTypeEnum;
16 15 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
17 16 import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum;
18 17 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
... ... @@ -37,9 +36,9 @@ import java.time.LocalDateTime;
37 36 import java.util.*;
38 37  
39 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 40 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
41 41  
42   -// TODO @AI:payload 能不能改成 notification 更像通知一点。
43 42 /**
44 43 * IM 加群申请 Service 实现类
45 44 *
... ... @@ -81,30 +80,32 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
81 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 86 groupService.validateMemberCountLimit(groupId, 1);
  87 + // 写群成员;addSource 来自前端入口(搜索 / 二维码 / 分享链接),inviterUserId=null(主动申请)
88 88 groupMemberService.addGroupMember(groupId, userId,
89 89 ImGroupMemberRoleEnum.NORMAL.getRole(), reqVO.getAddSource(), null);
  90 + // 推 1510 给全员;payload 含进群者 + 来源,前端按 entrantUserId 局部插入新成员
90 91 groupMessageService.sendGroupMessage(userId,
91 92 ImGroupMessageSendDTO.ofGroupMemberEnter(groupId, userId, reqVO.getAddSource()));
92 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 99 if (request != null) {
  100 + // TODO @AI:相同的放在一行里;然后 null 直接这样更新是清理不掉的;看看是不是在 basemapperx 里,增加一个 updateXXXX;可以根据传递的 DO,深度更新的方法?应该匹配这个场景的对哇?
99 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 109 } else {
109 110 request = BeanUtils.toBean(reqVO, ImGroupRequestDO.class)
110 111 .setUserId(userId).setInviterUserId(null)
... ... @@ -126,7 +127,7 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
126 127 @Override
127 128 @Transactional(rollbackFor = Exception.class)
128 129 public void agreeGroupRequest(Long userId, Long requestId) {
129   - // 1. 校验申请存在 + 未处理 + 操作人是 owner/admin
  130 + // 1. 校验申请存在 + 未处理 + 操作人是 owner / admin
130 131 ImGroupRequestDO request = validateRequestForHandle(userId, requestId);
131 132 // 2. 入群前校验人数上限;群已满抛错让操作人选择拒绝
132 133 groupService.validateMemberCountLimit(request.getGroupId(), 1);
... ... @@ -144,17 +145,18 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
144 145 request.setHandleResult(ImGroupRequestHandleResultEnum.AGREED.getResult())
145 146 .setHandleUserId(userId).setHandleTime(now);
146 147  
147   - // 4. 写群成员;带 addSource / inviterUserId
  148 + // 4. 写群成员;addSource / inviterUserId 沿用申请记录上的来源信息
148 149 groupMemberService.addGroupMember(request.getGroupId(), request.getUserId(),
149 150 ImGroupMemberRoleEnum.NORMAL.getRole(), request.getAddSource(), request.getInviterUserId());
150 151  
151 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 160 broadcastToOwnerAdminsAndApplicant(request.getGroupId(), request.getUserId(), payload,
159 161 ImMessageTypeEnum.GROUP_REQUEST_APPROVED.getType(), userId);
160 162 // 5.2 群事件:主动申请 → 1510 自由进群;被邀请 → 1509 成员加入
... ... @@ -187,32 +189,40 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
187 189 }
188 190  
189 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 199 broadcastToOwnerAdminsAndApplicant(request.getGroupId(), request.getUserId(), payload,
197 200 ImMessageTypeEnum.GROUP_REQUEST_REJECTED.getType(), userId);
198 201 }
199 202  
200   - // TODO @AI:createInviteRequestList;习惯性命名;
201 203 @Override
202 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 206 if (CollUtil.isEmpty(invitedUserIds)) {
205 207 return;
206 208 }
207 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 213 List<ImGroupRequestDO> requests = new ArrayList<>(invitedUserIds.size());
212 214 for (Long invitedUserId : invitedUserIds) {
213   - ImGroupRequestDO existing = groupRequestMapper.selectLatestPendingByGroupIdAndUserIdAndInviter(
214   - groupId, invitedUserId, inviterUserId);
  215 + ImGroupRequestDO existing = groupRequestMapper.selectByGroupIdAndUserId(groupId, invitedUserId);
215 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 226 requests.add(existing);
217 227 continue;
218 228 }
... ... @@ -238,19 +248,23 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
238 248 }
239 249  
240 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 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 270 @Override
... ... @@ -258,19 +272,6 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
258 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 275 @Override
275 276 public PageResult<ImGroupRequestDO> getGroupRequestPage(ImGroupRequestManagerPageReqVO reqVO) {
276 277 return groupRequestMapper.selectPage(reqVO);
... ... @@ -299,18 +300,18 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
299 300 */
300 301 private GroupRequestNotification buildRequestNotification(ImGroupDO group, ImGroupRequestDO request,
301 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 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 316 return payload;
316 317 }
... ... @@ -326,25 +327,21 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
326 327 }
327 328 Set<Long> receivers = new LinkedHashSet<>(listOwnerAndAdminUserIds(group));
328 329 receivers.add(applicantUserId);
329   - // TODO @AI:看看能不能用群的通道;
330 330 for (Long receiverUserId : receivers) {
331 331 websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification(
332 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 340 private List<Long> listOwnerAndAdminUserIds(ImGroupDO group) {
341   - List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(group.getId());
342 341 Set<Long> set = new LinkedHashSet<>();
343 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 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 28 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
29 29 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper;
30 30 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
31   -import cn.iocoder.yudao.module.im.enums.group.ImGroupJoinTypeEnum;
32 31 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
33 32 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
34 33 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
... ... @@ -194,7 +193,7 @@ public class ImGroupServiceImpl implements ImGroupService {
194 193 Long groupId = inviteReqVO.getGroupId();
195 194 // 1.1 校验群存在 + 当前用户是群成员
196 195 ImGroupDO group = validateGroupExists(groupId);
197   - ImGroupMemberDO operator = groupMemberService.validateMemberInGroup(groupId, userId);
  196 + groupMemberService.validateMemberInGroup(groupId, userId);
198 197 // 1.2 排除已在群中的用户
199 198 List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId);
200 199 activeMembers.forEach(member -> inviteReqVO.getMemberUserIds().remove(member.getUserId()));
... ... @@ -214,17 +213,13 @@ public class ImGroupServiceImpl implements ImGroupService {
214 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 219 return;
225 220 }
226 221  
227   - // 3. 直进:批量添加群成员,写 addSource=INVITE / inviterUserId=操作人,留痕
  222 + // 3. 情况二:未开审批,直进;批量添加群成员,写 addSource=INVITE / inviterUserId=操作人 留痕
228 223 groupMemberService.addGroupMembers(groupId, memberUserIds,
229 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 * 入群申请被同意通知
8 8 * <p>
9 9 * 走私聊通道定向推送给申请人 + 群主 + 全部管理员;
10   - * 申请人侧据此弹 toast、列表项更新;admin 侧据此把 pendingRequestCount 减 1 并从未处理列表移除
  10 + * 申请人侧据此弹 toast、列表项更新;admin 侧据此把 unhandledCount 减 1 并从未处理列表移除
11 11 */
12 12 @EqualsAndHashCode(callSuper = true)
13 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 6 /**
7 7 * 入群申请被拒绝通知
8 8 * <p>
9   - * 走私聊通道定向推送给申请人 + 群主 + 全部管理员;admin 侧据此把 pendingRequestCount 减 1 并从未处理列表移除
  9 + * 走私聊通道定向推送给申请人 + 群主 + 全部管理员;admin 侧据此把 unhandledCount 减 1 并从未处理列表移除
10 10 */
11 11 @EqualsAndHashCode(callSuper = true)
12 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 9 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO;
10 10 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupRequestMapper;
11 11 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
12   -import cn.iocoder.yudao.module.im.enums.group.ImGroupJoinTypeEnum;
13 12 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
14 13 import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum;
15 14 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
... ... @@ -60,7 +59,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
60 59 public void testApplyJoinGroup_freeMode_directJoin() {
61 60 // 准备:群是 FREE 模式
62 61 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
63   - .joinType(ImGroupJoinTypeEnum.FREE.getType())
  62 + .joinApproval(false)
64 63 .status(CommonStatusEnum.ENABLE.getStatus()).build();
65 64 when(groupService.validateGroupExists(10L)).thenReturn(group);
66 65  
... ... @@ -88,11 +87,11 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
88 87 public void testApplyJoinGroup_approvalMode_createsRequest() {
89 88 // 准备:群是 APPLY 模式
90 89 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
91   - .joinType(ImGroupJoinTypeEnum.APPLY.getType())
  90 + .joinApproval(true)
92 91 .status(CommonStatusEnum.ENABLE.getStatus()).build();
93 92 when(groupService.validateGroupExists(10L)).thenReturn(group);
94 93 // 群里有 owner + 一个 admin,作为 1503 推送目标
95   - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of(
  94 + when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of(
96 95 ImGroupMemberDO.builder().groupId(10L).userId(99L)
97 96 .role(ImGroupMemberRoleEnum.OWNER.getRole())
98 97 .status(CommonStatusEnum.ENABLE.getStatus()).build(),
... ... @@ -120,7 +119,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
120 119 @Test
121 120 public void testApplyJoinGroup_alreadyMember_throws() {
122 121 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
123   - .joinType(ImGroupJoinTypeEnum.APPLY.getType())
  122 + .joinApproval(true)
124 123 .status(CommonStatusEnum.ENABLE.getStatus()).build();
125 124 when(groupService.validateGroupExists(10L)).thenReturn(group);
126 125 when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(
... ... @@ -156,7 +155,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
156 155 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
157 156 .status(CommonStatusEnum.ENABLE.getStatus()).build();
158 157 when(groupService.getGroup(10L)).thenReturn(group);
159   - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of(
  158 + when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of(
160 159 ImGroupMemberDO.builder().groupId(10L).userId(99L)
161 160 .role(ImGroupMemberRoleEnum.OWNER.getRole())
162 161 .status(CommonStatusEnum.ENABLE.getStatus()).build()));
... ... @@ -234,7 +233,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
234 233 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L)
235 234 .status(CommonStatusEnum.ENABLE.getStatus()).build();
236 235 when(groupService.getGroup(10L)).thenReturn(group);
237   - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of(
  236 + when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of(
238 237 ImGroupMemberDO.builder().groupId(10L).userId(1L)
239 238 .role(ImGroupMemberRoleEnum.OWNER.getRole())
240 239 .status(CommonStatusEnum.ENABLE.getStatus()).build()));
... ... @@ -246,15 +245,15 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
246 245 verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
247 246 }
248 247  
249   - // ==================== createInviteRequests ====================
  248 + // ==================== createInviteRequestList ====================
250 249  
251 250 @Test
252   - public void testCreateInviteRequests_success() {
  251 + public void testCreateInviteRequestList_success() {
253 252 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
254   - .joinType(ImGroupJoinTypeEnum.APPLY_AND_NORMAL_INVITE.getType())
  253 + .joinApproval(true)
255 254 .status(CommonStatusEnum.ENABLE.getStatus()).build();
256 255 when(groupService.validateGroupExists(10L)).thenReturn(group);
257   - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of(
  256 + when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of(
258 257 ImGroupMemberDO.builder().groupId(10L).userId(99L)
259 258 .role(ImGroupMemberRoleEnum.OWNER.getRole())
260 259 .status(CommonStatusEnum.ENABLE.getStatus()).build()));
... ... @@ -263,7 +262,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
263 262 3L, buildUser(3L, "用户B")));
264 263  
265 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 267 // 断言:插入 2 条申请记录 + 推 1503 给 owner(每条 1 帧)共 2 帧
269 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 16 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
17 17 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper;
18 18 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
19   -import cn.iocoder.yudao.module.im.enums.group.ImGroupJoinTypeEnum;
20 19 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
21 20 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
22 21 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
... ... @@ -257,9 +256,9 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
257 256 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
258 257 .thenReturn(groupService);
259 258  
260   - // 准备:群存在 + joinType 默认 FREE + 当前用户是群主(操作人必走「直进」分支)
  259 + // 准备:群存在 + joinApproval=false(自由进群) + 当前用户是群主
261 260 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L)
262   - .joinType(ImGroupJoinTypeEnum.FREE.getType())
  261 + .joinApproval(false)
263 262 .status(CommonStatusEnum.ENABLE.getStatus()).build();
264 263 when(groupMapper.selectById(10L)).thenReturn(group);
265 264 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(
... ... @@ -293,7 +292,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
293 292 verify(groupMemberService).validateMemberInGroup(10L, 1L);
294 293 verify(groupMemberService).addGroupMembers(eq(10L), anyCollection(),
295 294 eq(ImGroupAddSourceEnum.INVITE.getSource()), eq(1L));
296   - verify(groupRequestService, never()).createInviteRequests(anyLong(), anyLong(), anyCollection());
  295 + verify(groupRequestService, never()).createInviteRequestList(anyLong(), anyLong(), anyCollection());
297 296 verify(webSocketService, never()).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class));
298 297 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
299 298 verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture());
... ... @@ -302,20 +301,16 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
302 301 }
303 302  
304 303 @Test
305   - public void testInviteGroupMember_strictMode_normalMember_routesToApproval() {
  304 + public void testInviteGroupMember_approval_routesToApproval() {
306 305 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
307 306 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
308 307 .thenReturn(groupService);
309 308  
310   - // 准备:joinType=APPLY_AND_NORMAL_INVITE 且操作人是普通成员;应走审批分支,落 group_request
  309 + // 准备:joinApproval=true,开启审批;任何邀请都需审批,落 group_request
311 310 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
312   - .joinType(ImGroupJoinTypeEnum.APPLY_AND_NORMAL_INVITE.getType())
  311 + .joinApproval(true)
313 312 .status(CommonStatusEnum.ENABLE.getStatus()).build();
314 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 314 when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of(
320 315 ImGroupMemberDO.builder().groupId(10L).userId(99L)
321 316 .role(ImGroupMemberRoleEnum.OWNER.getRole())
... ... @@ -336,51 +331,14 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
336 331 // 调用
337 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 336 verify(groupMemberService, never()).addGroupMembers(anyLong(), anyCollection(), any(), any());
342 337 verify(groupMessageService, never()).sendGroupMessage(anyLong(), any(ImGroupMessageSendDTO.class));
343 338 }
344 339 }
345 340  
346 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 342 public void testInviteGroupMember_notFriend() {
385 343 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
386 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 49 "status" tinyint NOT NULL COMMENT '群状态',
50 50 "dissolved_time" timestamp DEFAULT NULL COMMENT '解散时间',
51 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 53 "pinned_message_ids" varchar(128) DEFAULT NULL COMMENT '群置顶消息编号列表,逗号分隔',
54 54 "creator" varchar(64) DEFAULT '',
55 55 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
... ... @@ -121,7 +121,8 @@ CREATE TABLE IF NOT EXISTS &quot;im_friend_request&quot; (
121 121 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
122 122 "deleted" bit NOT NULL DEFAULT FALSE,
123 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 126 ) COMMENT 'IM 好友申请记录表';
126 127  
127 128 CREATE TABLE IF NOT EXISTS "im_group_request" (
... ... @@ -141,7 +142,8 @@ CREATE TABLE IF NOT EXISTS &quot;im_group_request&quot; (
141 142 "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
142 143 "deleted" bit NOT NULL DEFAULT FALSE,
143 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 147 ) COMMENT 'IM 加群申请记录表';
146 148  
147 149 CREATE TABLE IF NOT EXISTS "im_face_pack" (
... ...