Commit b320e3c19a733d41ee44dfa63fe1f8513b4fbe87
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<ImGroupRequestDO> { | @@ -51,26 +33,16 @@ public interface ImGroupRequestMapper extends BaseMapperX<ImGroupRequestDO> { | ||
| 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 "im_group" ( | @@ -49,7 +49,7 @@ CREATE TABLE IF NOT EXISTS "im_group" ( | ||
| 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 "im_friend_request" ( | @@ -121,7 +121,8 @@ CREATE TABLE IF NOT EXISTS "im_friend_request" ( | ||
| 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 "im_group_request" ( | @@ -141,7 +142,8 @@ CREATE TABLE IF NOT EXISTS "im_group_request" ( | ||
| 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" ( |