Commit 1c29398da594acd50c987b11b11c9d69e4c6e80f
1 parent
37dfd6c0
✨ feat(im): 增加群角色(管理员)
Showing
20 changed files
with
754 additions
and
69 deletions
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java
| ... | ... | @@ -2,8 +2,11 @@ package cn.iocoder.yudao.module.im.controller.admin.group; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 5 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; | |
| 6 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; | |
| 5 | 7 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; |
| 6 | 8 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupRespVO; |
| 9 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; | |
| 7 | 10 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; |
| 8 | 11 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; |
| 9 | 12 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; |
| ... | ... | @@ -14,7 +17,6 @@ import io.swagger.v3.oas.annotations.Parameter; |
| 14 | 17 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 15 | 18 | import jakarta.annotation.Resource; |
| 16 | 19 | import jakarta.validation.Valid; |
| 17 | -import org.springframework.security.access.prepost.PreAuthorize; | |
| 18 | 20 | import org.springframework.validation.annotation.Validated; |
| 19 | 21 | import org.springframework.web.bind.annotation.*; |
| 20 | 22 | |
| ... | ... | @@ -36,7 +38,6 @@ public class ImGroupController { |
| 36 | 38 | |
| 37 | 39 | @PostMapping("/create") |
| 38 | 40 | @Operation(summary = "创建群") |
| 39 | - @PreAuthorize("@ss.hasPermission('im:group:create')") | |
| 40 | 41 | public CommonResult<ImGroupRespVO> createGroup(@Valid @RequestBody ImGroupCreateReqVO createReqVO) { |
| 41 | 42 | ImGroupDO group = groupService.createGroup(createReqVO, getLoginUserId()); |
| 42 | 43 | return success(BeanUtils.toBean(group, ImGroupRespVO.class)); |
| ... | ... | @@ -44,7 +45,6 @@ public class ImGroupController { |
| 44 | 45 | |
| 45 | 46 | @PutMapping("/update") |
| 46 | 47 | @Operation(summary = "更新群") |
| 47 | - @PreAuthorize("@ss.hasPermission('im:group:update')") | |
| 48 | 48 | public CommonResult<ImGroupRespVO> updateGroup(@Valid @RequestBody ImGroupUpdateReqVO updateReqVO) { |
| 49 | 49 | ImGroupDO group = groupService.updateGroup(updateReqVO, getLoginUserId()); |
| 50 | 50 | return success(BeanUtils.toBean(group, ImGroupRespVO.class)); |
| ... | ... | @@ -53,7 +53,6 @@ public class ImGroupController { |
| 53 | 53 | @DeleteMapping("/dissolve") |
| 54 | 54 | @Operation(summary = "解散群") |
| 55 | 55 | @Parameter(name = "id", description = "群编号", required = true) |
| 56 | - @PreAuthorize("@ss.hasPermission('im:group:delete')") | |
| 57 | 56 | public CommonResult<Boolean> dissolveGroup(@RequestParam("id") Long id) { |
| 58 | 57 | groupService.dissolveGroup(id, getLoginUserId()); |
| 59 | 58 | return success(true); |
| ... | ... | @@ -64,7 +63,6 @@ public class ImGroupController { |
| 64 | 63 | @GetMapping("/get") |
| 65 | 64 | @Operation(summary = "获得群") |
| 66 | 65 | @Parameter(name = "id", description = "编号", required = true, example = "1024") |
| 67 | - @PreAuthorize("@ss.hasPermission('im:group:query')") | |
| 68 | 66 | public CommonResult<ImGroupRespVO> getGroup(@RequestParam("id") Long id) { |
| 69 | 67 | ImGroupDO group = groupService.getGroup(id); |
| 70 | 68 | return success(BeanUtils.toBean(group, ImGroupRespVO.class)); |
| ... | ... | @@ -81,7 +79,6 @@ public class ImGroupController { |
| 81 | 79 | |
| 82 | 80 | @PostMapping("/invite") |
| 83 | 81 | @Operation(summary = "邀请用户加入群") |
| 84 | - @PreAuthorize("@ss.hasPermission('im:group:update')") | |
| 85 | 82 | public CommonResult<Boolean> inviteGroupMember(@Valid @RequestBody ImGroupMemberInviteReqVO inviteReqVO) { |
| 86 | 83 | groupService.inviteGroupMember(getLoginUserId(), inviteReqVO); |
| 87 | 84 | return success(true); |
| ... | ... | @@ -90,7 +87,6 @@ public class ImGroupController { |
| 90 | 87 | @DeleteMapping("/quit") |
| 91 | 88 | @Operation(summary = "退出群") |
| 92 | 89 | @Parameter(name = "groupId", description = "群编号", required = true) |
| 93 | - @PreAuthorize("@ss.hasPermission('im:group:update')") | |
| 94 | 90 | public CommonResult<Boolean> quitGroup(@RequestParam("groupId") Long groupId) { |
| 95 | 91 | groupService.quitGroup(groupId, getLoginUserId()); |
| 96 | 92 | return success(true); |
| ... | ... | @@ -98,10 +94,30 @@ public class ImGroupController { |
| 98 | 94 | |
| 99 | 95 | @DeleteMapping("/kicking") |
| 100 | 96 | @Operation(summary = "移除群成员") |
| 101 | - @PreAuthorize("@ss.hasPermission('im:group:update')") | |
| 102 | 97 | public CommonResult<Boolean> removeGroupMember(@Valid @RequestBody ImGroupMemberRemoveReqVO removeReqVO) { |
| 103 | 98 | groupService.removeGroupMember(getLoginUserId(), removeReqVO); |
| 104 | 99 | return success(true); |
| 105 | 100 | } |
| 106 | 101 | |
| 102 | + @PutMapping("/add-admin") | |
| 103 | + @Operation(summary = "添加群管理员") | |
| 104 | + public CommonResult<Boolean> addGroupAdmin(@Valid @RequestBody ImGroupAdminAddReqVO reqVO) { | |
| 105 | + groupService.addGroupAdmin(getLoginUserId(), reqVO); | |
| 106 | + return success(true); | |
| 107 | + } | |
| 108 | + | |
| 109 | + @PutMapping("/remove-admin") | |
| 110 | + @Operation(summary = "撤销群管理员") | |
| 111 | + public CommonResult<Boolean> removeGroupAdmin(@Valid @RequestBody ImGroupAdminRemoveReqVO reqVO) { | |
| 112 | + groupService.removeGroupAdmin(getLoginUserId(), reqVO); | |
| 113 | + return success(true); | |
| 114 | + } | |
| 115 | + | |
| 116 | + @PutMapping("/transfer-owner") | |
| 117 | + @Operation(summary = "转让群主") | |
| 118 | + public CommonResult<Boolean> transferGroupOwner(@Valid @RequestBody ImGroupTransferOwnerReqVO transferReqVO) { | |
| 119 | + groupService.transferGroupOwner(getLoginUserId(), transferReqVO); | |
| 120 | + return success(true); | |
| 121 | + } | |
| 122 | + | |
| 107 | 123 | } | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java
| ... | ... | @@ -17,7 +17,6 @@ import io.swagger.v3.oas.annotations.Parameters; |
| 17 | 17 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 18 | 18 | import jakarta.annotation.Resource; |
| 19 | 19 | import jakarta.validation.Valid; |
| 20 | -import org.springframework.security.access.prepost.PreAuthorize; | |
| 21 | 20 | import org.springframework.validation.annotation.Validated; |
| 22 | 21 | import org.springframework.web.bind.annotation.*; |
| 23 | 22 | |
| ... | ... | @@ -44,7 +43,6 @@ public class ImGroupMemberController { |
| 44 | 43 | |
| 45 | 44 | @PutMapping("/update") |
| 46 | 45 | @Operation(summary = "更新群成员") |
| 47 | - @PreAuthorize("@ss.hasPermission('im:group-member:update')") | |
| 48 | 46 | public CommonResult<Boolean> updateGroupMember(@Valid @RequestBody ImGroupMemberUpdateReqVO updateReqVO) { |
| 49 | 47 | groupMemberService.updateGroupMember(getLoginUserId(), updateReqVO); |
| 50 | 48 | return success(true); |
| ... | ... | @@ -57,7 +55,6 @@ public class ImGroupMemberController { |
| 57 | 55 | @Parameter(name = "groupId", description = "群编号(与 userId 配合查)", example = "1"), |
| 58 | 56 | @Parameter(name = "userId", description = "用户编号(与 groupId 配合查)", example = "100") |
| 59 | 57 | }) |
| 60 | - @PreAuthorize("@ss.hasPermission('im:group-member:query')") | |
| 61 | 58 | public CommonResult<ImGroupMemberRespVO> getGroupMember(@RequestParam(value = "id", required = false) Long id, |
| 62 | 59 | @RequestParam(value = "groupId", required = false) Long groupId, |
| 63 | 60 | @RequestParam(value = "userId", required = false) Long userId) { | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminAddReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.group.vo; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import jakarta.validation.constraints.NotEmpty; | |
| 5 | +import jakarta.validation.constraints.NotNull; | |
| 6 | +import lombok.Data; | |
| 7 | + | |
| 8 | +import java.util.List; | |
| 9 | + | |
| 10 | +@Schema(description = "管理后台 - 添加群管理员 Request VO") | |
| 11 | +@Data | |
| 12 | +public class ImGroupAdminAddReqVO { | |
| 13 | + | |
| 14 | + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279") | |
| 15 | + @NotNull(message = "群编号不能为空") | |
| 16 | + private Long groupId; | |
| 17 | + | |
| 18 | + @Schema(description = "目标用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[101, 102]") | |
| 19 | + @NotEmpty(message = "目标用户编号列表不能为空") | |
| 20 | + private List<Long> userIds; | |
| 21 | + | |
| 22 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminRemoveReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.group.vo; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import jakarta.validation.constraints.NotEmpty; | |
| 5 | +import jakarta.validation.constraints.NotNull; | |
| 6 | +import lombok.Data; | |
| 7 | + | |
| 8 | +import java.util.List; | |
| 9 | + | |
| 10 | +@Schema(description = "管理后台 - 撤销群管理员 Request VO") | |
| 11 | +@Data | |
| 12 | +public class ImGroupAdminRemoveReqVO { | |
| 13 | + | |
| 14 | + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279") | |
| 15 | + @NotNull(message = "群编号不能为空") | |
| 16 | + private Long groupId; | |
| 17 | + | |
| 18 | + @Schema(description = "目标用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[101, 102]") | |
| 19 | + @NotEmpty(message = "目标用户编号列表不能为空") | |
| 20 | + private List<Long> userIds; | |
| 21 | + | |
| 22 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupTransferOwnerReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.group.vo; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import jakarta.validation.constraints.NotNull; | |
| 5 | +import lombok.Data; | |
| 6 | + | |
| 7 | +@Schema(description = "管理后台 - 群主转让 Request VO") | |
| 8 | +@Data | |
| 9 | +public class ImGroupTransferOwnerReqVO { | |
| 10 | + | |
| 11 | + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279") | |
| 12 | + @NotNull(message = "群编号不能为空") | |
| 13 | + private Long groupId; | |
| 14 | + | |
| 15 | + @Schema(description = "新群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "202") | |
| 16 | + @NotNull(message = "新群主用户编号不能为空") | |
| 17 | + private Long newOwnerUserId; | |
| 18 | + | |
| 19 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberRespVO.java
| ... | ... | @@ -21,8 +21,8 @@ public class ImGroupMemberRespVO { |
| 21 | 21 | @Schema(description = "组内显示名", example = "芋艿") |
| 22 | 22 | private String displayUserName; |
| 23 | 23 | |
| 24 | - @Schema(description = "群显示备注", example = "核心群") | |
| 25 | - private String displayGroupName; | |
| 24 | + @Schema(description = "群备注", example = "核心群") | |
| 25 | + private String groupRemark; | |
| 26 | 26 | |
| 27 | 27 | @Schema(description = "是否免打扰") |
| 28 | 28 | private Boolean muted; |
| ... | ... | @@ -30,6 +30,9 @@ public class ImGroupMemberRespVO { |
| 30 | 30 | @Schema(description = "成员状态", example = "0") |
| 31 | 31 | private Integer status; |
| 32 | 32 | |
| 33 | + @Schema(description = "成员角色", example = "3") | |
| 34 | + private Integer role; // 参见 ImGroupMemberRoleEnum 枚举类 | |
| 35 | + | |
| 33 | 36 | @Schema(description = "入群时间") |
| 34 | 37 | private LocalDateTime joinTime; |
| 35 | 38 | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberUpdateReqVO.java
| ... | ... | @@ -17,8 +17,8 @@ public class ImGroupMemberUpdateReqVO { |
| 17 | 17 | @Schema(description = "群内昵称", example = "芋头") |
| 18 | 18 | private String displayUserName; |
| 19 | 19 | |
| 20 | - @Schema(description = "群名备注", example = "公司群") | |
| 21 | - private String displayGroupName; | |
| 20 | + @Schema(description = "群备注", example = "公司群") | |
| 21 | + private String groupRemark; | |
| 22 | 22 | |
| 23 | 23 | @Schema(description = "是否免打扰") |
| 24 | 24 | private Boolean muted; | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/member/ImGroupMemberManagerRespVO.java
| ... | ... | @@ -21,8 +21,8 @@ public class ImGroupMemberManagerRespVO { |
| 21 | 21 | @Schema(description = "组内显示名", example = "三哥") |
| 22 | 22 | private String displayUserName; |
| 23 | 23 | |
| 24 | - @Schema(description = "群显示备注", example = "技术交流群") | |
| 25 | - private String displayGroupName; | |
| 24 | + @Schema(description = "群备注", example = "技术交流群") | |
| 25 | + private String groupRemark; | |
| 26 | 26 | |
| 27 | 27 | @Schema(description = "是否免打扰", example = "false") |
| 28 | 28 | private Boolean muted; |
| ... | ... | @@ -30,6 +30,9 @@ public class ImGroupMemberManagerRespVO { |
| 30 | 30 | @Schema(description = "成员状态", example = "0") |
| 31 | 31 | private Integer status; // 参见 CommonStatusEnum 枚举类 |
| 32 | 32 | |
| 33 | + @Schema(description = "成员角色;1=群主 2=管理员 3=普通成员", example = "3") | |
| 34 | + private Integer role; // 参见 ImGroupMemberRoleEnum 枚举类 | |
| 35 | + | |
| 33 | 36 | @Schema(description = "入群时间") |
| 34 | 37 | private LocalDateTime joinTime; |
| 35 | 38 | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupMemberDO.java
| ... | ... | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.im.dal.dataobject.group; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 5 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; | |
| 5 | 6 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 6 | 7 | import com.baomidou.mybatisplus.annotation.TableId; |
| 7 | 8 | import com.baomidou.mybatisplus.annotation.TableName; |
| ... | ... | @@ -46,9 +47,9 @@ public class ImGroupMemberDO extends BaseDO { |
| 46 | 47 | */ |
| 47 | 48 | private String displayUserName; |
| 48 | 49 | /** |
| 49 | - * 群显示备注 | |
| 50 | + * 群备注 | |
| 50 | 51 | */ |
| 51 | - private String displayGroupName; | |
| 52 | + private String groupRemark; | |
| 52 | 53 | /** |
| 53 | 54 | * 是否免打扰 |
| 54 | 55 | */ |
| ... | ... | @@ -60,6 +61,12 @@ public class ImGroupMemberDO extends BaseDO { |
| 60 | 61 | */ |
| 61 | 62 | private Integer status; |
| 62 | 63 | /** |
| 64 | + * 成员角色 | |
| 65 | + * <p> | |
| 66 | + * 枚举 {@link ImGroupMemberRoleEnum} | |
| 67 | + */ | |
| 68 | + private Integer role; | |
| 69 | + /** | |
| 63 | 70 | * 入群时间 |
| 64 | 71 | */ |
| 65 | 72 | private LocalDateTime joinTime; | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java
| ... | ... | @@ -83,6 +83,20 @@ public interface ImGroupMemberMapper extends BaseMapperX<ImGroupMemberDO> { |
| 83 | 83 | .eq(ImGroupMemberDO::getStatus, oldStatus)); |
| 84 | 84 | } |
| 85 | 85 | |
| 86 | + @SuppressWarnings("UnusedReturnValue") | |
| 87 | + default int updateListByGroupIdAndUserIds(Long groupId, Collection<Long> userIds, ImGroupMemberDO updateObj) { | |
| 88 | + return update(updateObj, new LambdaQueryWrapperX<ImGroupMemberDO>() | |
| 89 | + .eq(ImGroupMemberDO::getGroupId, groupId) | |
| 90 | + .in(ImGroupMemberDO::getUserId, userIds)); | |
| 91 | + } | |
| 92 | + | |
| 93 | + default Long selectCountByGroupIdAndRoleAndStatus(Long groupId, Integer role, Integer status) { | |
| 94 | + return selectCount(new LambdaQueryWrapperX<ImGroupMemberDO>() | |
| 95 | + .eq(ImGroupMemberDO::getGroupId, groupId) | |
| 96 | + .eq(ImGroupMemberDO::getRole, role) | |
| 97 | + .eq(ImGroupMemberDO::getStatus, status)); | |
| 98 | + } | |
| 99 | + | |
| 86 | 100 | /** |
| 87 | 101 | * 批量按 group_id 统计指定状态的成员数 |
| 88 | 102 | */ | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java
| ... | ... | @@ -23,6 +23,8 @@ public interface ErrorCodeConstants { |
| 23 | 23 | ErrorCode GROUP_BANNED = new ErrorCode(1_040_400_001, "群已被封禁"); |
| 24 | 24 | ErrorCode GROUP_DISSOLVED = new ErrorCode(1_040_400_002, "群已解散"); |
| 25 | 25 | ErrorCode GROUP_NOT_OWNER = new ErrorCode(1_040_400_003, "仅群主可执行该操作"); |
| 26 | + ErrorCode GROUP_NOT_OWNER_OR_ADMIN = new ErrorCode(1_040_400_004, "仅群主或管理员可执行该操作"); | |
| 27 | + ErrorCode GROUP_TRANSFER_OWNER_TO_SELF = new ErrorCode(1_040_400_005, "不能将群主转让给自己"); | |
| 26 | 28 | |
| 27 | 29 | // ========== 群成员 (1-040-500-000) ========== |
| 28 | 30 | ErrorCode GROUP_MEMBER_NOT_IN_GROUP = new ErrorCode(1_040_500_001, "您已不在该群中"); |
| ... | ... | @@ -30,6 +32,11 @@ public interface ErrorCodeConstants { |
| 30 | 32 | ErrorCode GROUP_CANNOT_REMOVE_SELF = new ErrorCode(1_040_500_004, "不能将自己移出群聊"); |
| 31 | 33 | ErrorCode GROUP_MEMBER_EXCEED = new ErrorCode(1_040_500_005, "群聊人数不能超过 {} 人"); |
| 32 | 34 | ErrorCode GROUP_INVITE_NOT_FRIEND = new ErrorCode(1_040_500_006, "'{}' 不是您的好友,邀请失败"); |
| 35 | + ErrorCode GROUP_ADMIN_TARGET_NOT_IN_GROUP = new ErrorCode(1_040_500_007, "目标用户已不在该群中"); | |
| 36 | + ErrorCode GROUP_ADMIN_TARGET_IS_OWNER = new ErrorCode(1_040_500_008, "群主无法被设为或撤销管理员"); | |
| 37 | + ErrorCode GROUP_ADMIN_MAX_LIMIT = new ErrorCode(1_040_500_009, "群管理员数量不能超过 {} 人"); | |
| 38 | + ErrorCode GROUP_REMOVE_OWNER_DENIED = new ErrorCode(1_040_500_010, "群主无法被移出群聊"); | |
| 39 | + ErrorCode GROUP_REMOVE_ADMIN_DENIED = new ErrorCode(1_040_500_011, "管理员无法移出其他管理员,请先由群主撤销其管理员身份"); | |
| 33 | 40 | |
| 34 | 41 | // ========== 好友 (1-040-600-000) ========== |
| 35 | 42 | ErrorCode FRIEND_NOT_FRIEND = new ErrorCode(1_040_600_001, "对方不是您的好友"); | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java
| ... | ... | @@ -18,6 +18,16 @@ public interface ImCommonConstants { |
| 18 | 18 | int MAX_GROUP_MEMBER = 500; |
| 19 | 19 | |
| 20 | 20 | /** |
| 21 | + * 单群管理员人数上限 | |
| 22 | + */ | |
| 23 | + int GROUP_ADMIN_MAX_COUNT = 3; | |
| 24 | + | |
| 25 | + /** | |
| 26 | + * 单群置顶消息条数上限 | |
| 27 | + */ | |
| 28 | + int GROUP_PIN_MAX_COUNT = 5; | |
| 29 | + | |
| 30 | + /** | |
| 21 | 31 | * 群消息 @ 所有人的特殊用户编号 |
| 22 | 32 | */ |
| 23 | 33 | long AT_USER_ID_ALL = -1L; | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/group/ImGroupMemberRoleEnum.java
0 → 100644
| 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 | +/** | |
| 11 | + * IM 群成员角色枚举 | |
| 12 | + * | |
| 13 | + * @author 芋道源码 | |
| 14 | + */ | |
| 15 | +@RequiredArgsConstructor | |
| 16 | +@Getter | |
| 17 | +public enum ImGroupMemberRoleEnum implements ArrayValuable<Integer> { | |
| 18 | + | |
| 19 | + OWNER(1, "群主"), | |
| 20 | + ADMIN(2, "管理员"), | |
| 21 | + NORMAL(3, "普通成员"); | |
| 22 | + | |
| 23 | + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImGroupMemberRoleEnum::getRole).toArray(Integer[]::new); | |
| 24 | + | |
| 25 | + /** | |
| 26 | + * 角色 | |
| 27 | + */ | |
| 28 | + private final Integer role; | |
| 29 | + /** | |
| 30 | + * 名字 | |
| 31 | + */ | |
| 32 | + private final String name; | |
| 33 | + | |
| 34 | + @Override | |
| 35 | + public Integer[] array() { | |
| 36 | + return ARRAYS; | |
| 37 | + } | |
| 38 | + | |
| 39 | + public static boolean isOwner(Integer role) { | |
| 40 | + return Objects.equals(OWNER.role, role); | |
| 41 | + } | |
| 42 | + | |
| 43 | + public static boolean isAdmin(Integer role) { | |
| 44 | + return Objects.equals(ADMIN.role, role); | |
| 45 | + } | |
| 46 | + | |
| 47 | + public static boolean isOwnerOrAdmin(Integer role) { | |
| 48 | + return isOwner(role) || isAdmin(role); | |
| 49 | + } | |
| 50 | + | |
| 51 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java
| ... | ... | @@ -34,6 +34,15 @@ public interface ImGroupMemberService { |
| 34 | 34 | ImGroupMemberDO getGroupMember(Long groupId, Long userId); |
| 35 | 35 | |
| 36 | 36 | /** |
| 37 | + * 批量查询群成员(包含所有状态) | |
| 38 | + * | |
| 39 | + * @param groupId 群编号 | |
| 40 | + * @param userIds 用户编号集合 | |
| 41 | + * @return 群成员列表 | |
| 42 | + */ | |
| 43 | + List<ImGroupMemberDO> getGroupMembers(Long groupId, Collection<Long> userIds); | |
| 44 | + | |
| 45 | + /** | |
| 37 | 46 | * 根据群组 id 查询群成员(包含所有状态) |
| 38 | 47 | * |
| 39 | 48 | * @param groupId 群组id |
| ... | ... | @@ -79,10 +88,7 @@ public interface ImGroupMemberService { |
| 79 | 88 | List<ImGroupMemberDO> getQuitGroupMemberListByUserId(Long userId, LocalDateTime minQuitTime); |
| 80 | 89 | |
| 81 | 90 | /** |
| 82 | - * 添加群成员(入群) | |
| 83 | - * <p> | |
| 84 | - * 插入一条 ENABLE 状态的群成员记录,并设置 joinTime。 | |
| 85 | - * 可用于创建群时添加群主、邀请入群等场景。 | |
| 91 | + * 添加群成员(入群),角色默认 MEMBER | |
| 86 | 92 | * |
| 87 | 93 | * @param groupId 群编号 |
| 88 | 94 | * @param userId 用户编号 |
| ... | ... | @@ -92,6 +98,19 @@ public interface ImGroupMemberService { |
| 92 | 98 | ImGroupMemberDO addGroupMember(Long groupId, Long userId); |
| 93 | 99 | |
| 94 | 100 | /** |
| 101 | + * 添加群成员(入群),并指定角色 | |
| 102 | + * <p> | |
| 103 | + * 重置旧成员行也会强制重置 role,避免离群期间残留管理员身份被复用。 | |
| 104 | + * | |
| 105 | + * @param groupId 群编号 | |
| 106 | + * @param userId 用户编号 | |
| 107 | + * @param role 成员角色,见 {@link cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum} | |
| 108 | + * @return 群成员记录 | |
| 109 | + */ | |
| 110 | + @SuppressWarnings("UnusedReturnValue") | |
| 111 | + ImGroupMemberDO addGroupMember(Long groupId, Long userId, Integer role); | |
| 112 | + | |
| 113 | + /** | |
| 95 | 114 | * 批量添加群成员(入群) |
| 96 | 115 | * |
| 97 | 116 | * @param groupId 群编号 |
| ... | ... | @@ -119,6 +138,24 @@ public interface ImGroupMemberService { |
| 119 | 138 | void updateGroupMember(Long userId, @Valid ImGroupMemberUpdateReqVO updateReqVO); |
| 120 | 139 | |
| 121 | 140 | /** |
| 141 | + * 批量更新群成员角色 | |
| 142 | + * | |
| 143 | + * @param groupId 群编号 | |
| 144 | + * @param userIds 用户编号集合 | |
| 145 | + * @param role 新角色,见 {@link cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum} | |
| 146 | + */ | |
| 147 | + void updateGroupMemberRole(Long groupId, Collection<Long> userIds, Integer role); | |
| 148 | + | |
| 149 | + /** | |
| 150 | + * 统计群内指定 role 的活跃成员数量 | |
| 151 | + * | |
| 152 | + * @param groupId 群编号 | |
| 153 | + * @param role 成员角色 | |
| 154 | + * @return 数量 | |
| 155 | + */ | |
| 156 | + Long getGroupMemberCountByRole(Long groupId, Integer role); | |
| 157 | + | |
| 158 | + /** | |
| 122 | 159 | * 移除指定群成员(设置为 DISABLE 状态) |
| 123 | 160 | * <p> |
| 124 | 161 | * 用于退群、踢出场景 | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java
| ... | ... | @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 6 | 6 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; |
| 7 | 7 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; |
| 8 | 8 | import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; |
| 9 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; | |
| 9 | 10 | import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; |
| 10 | 11 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; |
| 11 | 12 | import jakarta.annotation.Resource; |
| ... | ... | @@ -51,6 +52,14 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { |
| 51 | 52 | } |
| 52 | 53 | |
| 53 | 54 | @Override |
| 55 | + public List<ImGroupMemberDO> getGroupMembers(Long groupId, Collection<Long> userIds) { | |
| 56 | + if (CollUtil.isEmpty(userIds)) { | |
| 57 | + return Collections.emptyList(); | |
| 58 | + } | |
| 59 | + return groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds); | |
| 60 | + } | |
| 61 | + | |
| 62 | + @Override | |
| 54 | 63 | public List<ImGroupMemberDO> getGroupMemberListByGroupId(Long groupId) { |
| 55 | 64 | return groupMemberMapper.selectListByGroupId(groupId); |
| 56 | 65 | } |
| ... | ... | @@ -88,27 +97,37 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { |
| 88 | 97 | return groupMemberMapper.selectQuitListByUserId(userId, minQuitTime); |
| 89 | 98 | } |
| 90 | 99 | |
| 100 | + @Override | |
| 101 | + public ImGroupMemberDO addGroupMember(Long groupId, Long userId) { | |
| 102 | + return addGroupMember(groupId, userId, ImGroupMemberRoleEnum.NORMAL.getRole()); | |
| 103 | + } | |
| 104 | + | |
| 91 | 105 | /** |
| 92 | 106 | * 并发安全:依靠 im_group_member 表的唯一索引 uk_im_group_member_group_user(group_id, user_id) 保证幂等, |
| 93 | 107 | * 当并发 insert 触发 {@link DuplicateKeyException} 时降级为 select + update。 |
| 108 | + * <p> | |
| 109 | + * 重置旧成员行时强制重置 role,避免离群期间残留管理员身份在重新入群后被复用。 | |
| 94 | 110 | */ |
| 95 | 111 | @Override |
| 96 | 112 | @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") |
| 97 | - public ImGroupMemberDO addGroupMember(Long groupId, Long userId) { | |
| 113 | + public ImGroupMemberDO addGroupMember(Long groupId, Long userId, Integer role) { | |
| 98 | 114 | LocalDateTime now = LocalDateTime.now(); |
| 99 | - // 情况一:已存在记录 → 复活或跳过 | |
| 115 | + // 情况一:已存在记录 → 重置或跳过 | |
| 100 | 116 | ImGroupMemberDO exists = groupMemberMapper.selectByGroupIdAndUserId(groupId, userId); |
| 101 | 117 | if (exists != null) { |
| 102 | 118 | if (CommonStatusEnum.isDisable(exists.getStatus())) { |
| 103 | 119 | groupMemberMapper.updateById(new ImGroupMemberDO().setId(exists.getId()) |
| 104 | - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now)); | |
| 120 | + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now) | |
| 121 | + .setRole(role)); | |
| 122 | + exists.setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now).setRole(role); | |
| 105 | 123 | } |
| 106 | - return groupMemberMapper.selectById(exists.getId()); | |
| 124 | + return exists; | |
| 107 | 125 | } |
| 108 | 126 | // 情况二:新增成员 |
| 109 | 127 | ImGroupMemberDO member = new ImGroupMemberDO() |
| 110 | 128 | .setGroupId(groupId).setUserId(userId) |
| 111 | - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now); | |
| 129 | + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now) | |
| 130 | + .setRole(role); | |
| 112 | 131 | try { |
| 113 | 132 | groupMemberMapper.insert(member); |
| 114 | 133 | return member; |
| ... | ... | @@ -123,6 +142,7 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { |
| 123 | 142 | @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") |
| 124 | 143 | public void addGroupMembers(Long groupId, Collection<Long> userIds) { |
| 125 | 144 | LocalDateTime now = LocalDateTime.now(); |
| 145 | + Integer role = ImGroupMemberRoleEnum.NORMAL.getRole(); | |
| 126 | 146 | // 1.1 查询已有记录(含已退群的 DISABLE 记录) |
| 127 | 147 | List<ImGroupMemberDO> existMembers = groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds); |
| 128 | 148 | Map<Long, ImGroupMemberDO> existMap = convertMap(existMembers, ImGroupMemberDO::getUserId); |
| ... | ... | @@ -133,10 +153,10 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { |
| 133 | 153 | ImGroupMemberDO exist = existMap.get(userId); |
| 134 | 154 | if (exist == null) { |
| 135 | 155 | inserts.add(new ImGroupMemberDO().setGroupId(groupId).setUserId(userId) |
| 136 | - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now)); | |
| 156 | + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setRole(role).setJoinTime(now)); | |
| 137 | 157 | } else if (CommonStatusEnum.DISABLE.getStatus().equals(exist.getStatus())) { |
| 138 | 158 | updates.add(new ImGroupMemberDO().setId(exist.getId()) |
| 139 | - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now)); | |
| 159 | + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setRole(role).setJoinTime(now)); | |
| 140 | 160 | } |
| 141 | 161 | } |
| 142 | 162 | |
| ... | ... | @@ -168,6 +188,20 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { |
| 168 | 188 | } |
| 169 | 189 | |
| 170 | 190 | @Override |
| 191 | + public void updateGroupMemberRole(Long groupId, Collection<Long> userIds, Integer role) { | |
| 192 | + if (CollUtil.isEmpty(userIds)) { | |
| 193 | + return; | |
| 194 | + } | |
| 195 | + groupMemberMapper.updateListByGroupIdAndUserIds(groupId, userIds, new ImGroupMemberDO().setRole(role)); | |
| 196 | + } | |
| 197 | + | |
| 198 | + @Override | |
| 199 | + public Long getGroupMemberCountByRole(Long groupId, Integer role) { | |
| 200 | + return groupMemberMapper.selectCountByGroupIdAndRoleAndStatus( | |
| 201 | + groupId, role, CommonStatusEnum.ENABLE.getStatus()); | |
| 202 | + } | |
| 203 | + | |
| 204 | + @Override | |
| 171 | 205 | public void updateGroupMember(Long userId, ImGroupMemberUpdateReqVO updateReqVO) { |
| 172 | 206 | // 1. 校验是群的有效成员 |
| 173 | 207 | ImGroupMemberDO member = validateMemberInGroup(updateReqVO.getGroupId(), userId); | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java
| 1 | 1 | package cn.iocoder.yudao.module.im.service.group; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; | |
| 5 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; | |
| 4 | 6 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; |
| 7 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; | |
| 5 | 8 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; |
| 6 | 9 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; |
| 7 | 10 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; |
| ... | ... | @@ -80,16 +83,6 @@ public interface ImGroupService { |
| 80 | 83 | ImGroupDO validateGroupExists(Long groupId); |
| 81 | 84 | |
| 82 | 85 | /** |
| 83 | - * 校验当前用户是否为群主 | |
| 84 | - * | |
| 85 | - * @param groupId 群编号 | |
| 86 | - * @param userId 用户编号 | |
| 87 | - * @return 群信息 | |
| 88 | - */ | |
| 89 | - @SuppressWarnings("UnusedReturnValue") | |
| 90 | - ImGroupDO validateGroupOwner(Long groupId, Long userId); | |
| 91 | - | |
| 92 | - /** | |
| 93 | 86 | * 获取指定用户的群列表 |
| 94 | 87 | * <p> |
| 95 | 88 | * 返回用户当前仍有效的群,以及最近 {@link cn.iocoder.yudao.module.im.enums.ImCommonConstants#MESSAGE_GROUP_PULL_MAX_DAYS} |
| ... | ... | @@ -128,13 +121,39 @@ public interface ImGroupService { |
| 128 | 121 | /** |
| 129 | 122 | * 移除群成员(踢人) |
| 130 | 123 | * <p> |
| 131 | - * 仅群主可执行,且不能移除自己,支持批量 | |
| 124 | + * 群主可踢管理员和普通成员;管理员仅能踢普通成员;群主不可被踢。 | |
| 132 | 125 | * |
| 133 | - * @param userId 当前登录用户编号(群主) | |
| 126 | + * @param userId 当前登录用户编号 | |
| 134 | 127 | * @param removeReqVO 移除信息 |
| 135 | 128 | */ |
| 136 | 129 | void removeGroupMember(Long userId, @Valid ImGroupMemberRemoveReqVO removeReqVO); |
| 137 | 130 | |
| 131 | + /** | |
| 132 | + * 添加群管理员(仅群主可执行) | |
| 133 | + * | |
| 134 | + * @param userId 当前登录用户编号(群主) | |
| 135 | + * @param reqVO 添加信息(含群编号、目标用户编号列表) | |
| 136 | + */ | |
| 137 | + void addGroupAdmin(Long userId, @Valid ImGroupAdminAddReqVO reqVO); | |
| 138 | + | |
| 139 | + /** | |
| 140 | + * 撤销群管理员(仅群主可执行) | |
| 141 | + * | |
| 142 | + * @param userId 当前登录用户编号(群主) | |
| 143 | + * @param reqVO 撤销信息(含群编号、目标用户编号列表) | |
| 144 | + */ | |
| 145 | + void removeGroupAdmin(Long userId, @Valid ImGroupAdminRemoveReqVO reqVO); | |
| 146 | + | |
| 147 | + /** | |
| 148 | + * 转让群主(仅老群主可执行) | |
| 149 | + * <p> | |
| 150 | + * 转让后:旧群主 role 降为 MEMBER,新群主 role 升为 OWNER | |
| 151 | + * | |
| 152 | + * @param userId 当前登录用户编号(旧群主) | |
| 153 | + * @param transferReqVO 转让信息 | |
| 154 | + */ | |
| 155 | + void transferGroupOwner(Long userId, @Valid ImGroupTransferOwnerReqVO transferReqVO); | |
| 156 | + | |
| 138 | 157 | // ==================== 管理后台 ==================== |
| 139 | 158 | |
| 140 | 159 | /** | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
| ... | ... | @@ -12,6 +12,9 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 12 | 12 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; |
| 13 | 13 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; |
| 14 | 14 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; |
| 15 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; | |
| 16 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; | |
| 17 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; | |
| 15 | 18 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; |
| 16 | 19 | import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO; |
| 17 | 20 | import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO; |
| ... | ... | @@ -19,6 +22,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; |
| 19 | 22 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; |
| 20 | 23 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; |
| 21 | 24 | import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; |
| 25 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; | |
| 22 | 26 | import cn.iocoder.yudao.module.im.service.friend.ImFriendService; |
| 23 | 27 | import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; |
| 24 | 28 | import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; |
| ... | ... | @@ -44,7 +48,6 @@ import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP; |
| 44 | 48 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; |
| 45 | 49 | import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.*; |
| 46 | 50 | |
| 47 | -// TODO @芋艿:群主的调整,暂时不考虑。后续在弄; | |
| 48 | 51 | /** |
| 49 | 52 | * 群 Service 实现类 |
| 50 | 53 | * |
| ... | ... | @@ -82,8 +85,8 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 82 | 85 | .setOwnerUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); |
| 83 | 86 | groupMapper.insert(group); |
| 84 | 87 | |
| 85 | - // 2. 将群主插入为群成员 | |
| 86 | - groupMemberService.addGroupMember(group.getId(), userId); | |
| 88 | + // 2. 将群主插入为群成员;显式 setRole(OWNER) 避免依赖 DDL DEFAULT 把群主行误认成 MEMBER | |
| 89 | + groupMemberService.addGroupMember(group.getId(), userId, ImGroupMemberRoleEnum.OWNER.getRole()); | |
| 87 | 90 | |
| 88 | 91 | // 3. 推送群创建事件给群主(多端同步) |
| 89 | 92 | webSocketService.sendGroupMessageAsync(userId, |
| ... | ... | @@ -207,26 +210,126 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 207 | 210 | @Override |
| 208 | 211 | public void removeGroupMember(Long userId, ImGroupMemberRemoveReqVO removeReqVO) { |
| 209 | 212 | Long groupId = removeReqVO.getGroupId(); |
| 210 | - // 1. 校验群存在 + 当前用户是群主 | |
| 211 | - validateGroupOwner(groupId, userId); | |
| 212 | - // 2. 不能移除自己 | |
| 213 | - if (removeReqVO.getMemberUserIds().contains(userId)) { | |
| 213 | + Set<Long> targetUserIds = new HashSet<>(removeReqVO.getMemberUserIds()); | |
| 214 | + // 1.1 校验群存在 + 操作者是群主或管理员 | |
| 215 | + ImGroupMemberDO operator = validateGroupOwnerOrAdmin(groupId, userId); | |
| 216 | + // 1.2 不能移除自己 | |
| 217 | + if (targetUserIds.contains(userId)) { | |
| 214 | 218 | throw exception(GROUP_CANNOT_REMOVE_SELF); |
| 215 | 219 | } |
| 220 | + // 1.3 三档权限校验:群主不可被移出;管理员不能移出管理员 | |
| 221 | + List<ImGroupMemberDO> targets = groupMemberService.getGroupMembers(groupId, targetUserIds); | |
| 222 | + boolean operatorIsAdmin = ImGroupMemberRoleEnum.isAdmin(operator.getRole()); | |
| 223 | + for (ImGroupMemberDO target : targets) { | |
| 224 | + if (ImGroupMemberRoleEnum.isOwner(target.getRole())) { | |
| 225 | + throw exception(GROUP_REMOVE_OWNER_DENIED); | |
| 226 | + } | |
| 227 | + if (operatorIsAdmin && ImGroupMemberRoleEnum.isAdmin(target.getRole())) { | |
| 228 | + throw exception(GROUP_REMOVE_ADMIN_DENIED); | |
| 229 | + } | |
| 230 | + } | |
| 216 | 231 | |
| 217 | - // 3.1 批量移除群成员 | |
| 218 | - Set<Long> memberUserIds = new HashSet<>(removeReqVO.getMemberUserIds()); | |
| 219 | - groupMemberService.removeGroupMembers(groupId, memberUserIds); | |
| 220 | - // 3.2 批量清理已读缓存 | |
| 221 | - groupMessageService.deleteReadMaxMessageIds(groupId, memberUserIds); | |
| 232 | + // 2.1 批量移除群成员 | |
| 233 | + groupMemberService.removeGroupMembers(groupId, targetUserIds); | |
| 234 | + // 2.2 批量清理已读缓存 | |
| 235 | + groupMessageService.deleteReadMaxMessageIds(groupId, targetUserIds); | |
| 222 | 236 | |
| 223 | - // 4.1 发送踢出提示消息(TIP_TEXT) | |
| 224 | - groupMessageService.sendTipGroupMessage(userId, groupId, memberUserIds, GROUP_REMOVE_TIP_MESSAGE); | |
| 225 | - // 4.2 推送群删除事件 | |
| 226 | - webSocketService.sendGroupMessageAsync(memberUserIds, | |
| 237 | + // 3.1 发送移出提示消息(TIP_TEXT) | |
| 238 | + groupMessageService.sendTipGroupMessage(userId, groupId, targetUserIds, GROUP_REMOVE_TIP_MESSAGE); | |
| 239 | + // 3.2 推送群删除事件 | |
| 240 | + webSocketService.sendGroupMessageAsync(targetUserIds, | |
| 227 | 241 | ImGroupMessageDTO.ofGroupDelete(userId, groupId)); |
| 228 | 242 | } |
| 229 | 243 | |
| 244 | + @Override | |
| 245 | + @Transactional(rollbackFor = Exception.class) | |
| 246 | + public void addGroupAdmin(Long userId, ImGroupAdminAddReqVO reqVO) { | |
| 247 | + Long groupId = reqVO.getGroupId(); | |
| 248 | + Set<Long> targetUserIds = new HashSet<>(reqVO.getUserIds()); | |
| 249 | + // 1.1 仅群主可操作 | |
| 250 | + validateGroupOwner(groupId, userId); | |
| 251 | + // 1.2 校验目标都是有效成员且非群主 | |
| 252 | + Map<Long, ImGroupMemberDO> targetMap = convertMap( | |
| 253 | + groupMemberService.getGroupMembers(groupId, targetUserIds), ImGroupMemberDO::getUserId); | |
| 254 | + validateAdminTargets(targetUserIds, targetMap); | |
| 255 | + // 1.3 幂等过滤:跳过已是 ADMIN | |
| 256 | + Set<Long> changedUserIds = convertSet(targetUserIds, | |
| 257 | + id -> id, | |
| 258 | + id -> !ImGroupMemberRoleEnum.isAdmin(targetMap.get(id).getRole())); | |
| 259 | + if (CollUtil.isEmpty(changedUserIds)) { | |
| 260 | + return; | |
| 261 | + } | |
| 262 | + // 1.4 校验上限 | |
| 263 | + Long existAdminCount = groupMemberService.getGroupMemberCountByRole( | |
| 264 | + groupId, ImGroupMemberRoleEnum.ADMIN.getRole()); | |
| 265 | + if (existAdminCount + changedUserIds.size() > GROUP_ADMIN_MAX_COUNT) { | |
| 266 | + throw exception(GROUP_ADMIN_MAX_LIMIT, GROUP_ADMIN_MAX_COUNT); | |
| 267 | + } | |
| 268 | + | |
| 269 | + // 2. 批量更新角色 | |
| 270 | + groupMemberService.updateGroupMemberRole(groupId, changedUserIds, ImGroupMemberRoleEnum.ADMIN.getRole()); | |
| 271 | + } | |
| 272 | + | |
| 273 | + @Override | |
| 274 | + @Transactional(rollbackFor = Exception.class) | |
| 275 | + public void removeGroupAdmin(Long userId, ImGroupAdminRemoveReqVO reqVO) { | |
| 276 | + Long groupId = reqVO.getGroupId(); | |
| 277 | + Set<Long> targetUserIds = new HashSet<>(reqVO.getUserIds()); | |
| 278 | + // 1.1 仅群主可操作 | |
| 279 | + validateGroupOwner(groupId, userId); | |
| 280 | + // 1.2 校验目标都是有效成员且非群主 | |
| 281 | + Map<Long, ImGroupMemberDO> targetMap = convertMap( | |
| 282 | + groupMemberService.getGroupMembers(groupId, targetUserIds), ImGroupMemberDO::getUserId); | |
| 283 | + validateAdminTargets(targetUserIds, targetMap); | |
| 284 | + // 1.3 幂等过滤:跳过已是 MEMBER | |
| 285 | + Set<Long> changedUserIds = convertSet(targetUserIds, | |
| 286 | + id -> id, | |
| 287 | + id -> ImGroupMemberRoleEnum.isAdmin(targetMap.get(id).getRole())); | |
| 288 | + if (CollUtil.isEmpty(changedUserIds)) { | |
| 289 | + return; | |
| 290 | + } | |
| 291 | + | |
| 292 | + // 2. 批量更新角色 | |
| 293 | + groupMemberService.updateGroupMemberRole(groupId, changedUserIds, ImGroupMemberRoleEnum.NORMAL.getRole()); | |
| 294 | + } | |
| 295 | + | |
| 296 | + /** | |
| 297 | + * 校验管理员变更目标都是当前群的有效成员(status=ENABLE)且非群主 | |
| 298 | + */ | |
| 299 | + private void validateAdminTargets(Set<Long> targetUserIds, Map<Long, ImGroupMemberDO> targetMap) { | |
| 300 | + for (Long targetUserId : targetUserIds) { | |
| 301 | + ImGroupMemberDO target = targetMap.get(targetUserId); | |
| 302 | + if (target == null || CommonStatusEnum.DISABLE.getStatus().equals(target.getStatus())) { | |
| 303 | + throw exception(GROUP_ADMIN_TARGET_NOT_IN_GROUP); | |
| 304 | + } | |
| 305 | + if (ImGroupMemberRoleEnum.isOwner(target.getRole())) { | |
| 306 | + throw exception(GROUP_ADMIN_TARGET_IS_OWNER); | |
| 307 | + } | |
| 308 | + } | |
| 309 | + } | |
| 310 | + | |
| 311 | + @Override | |
| 312 | + @CacheEvict(cacheNames = GROUP, key = "#transferReqVO.groupId") | |
| 313 | + @Transactional(rollbackFor = Exception.class) | |
| 314 | + public void transferGroupOwner(Long userId, ImGroupTransferOwnerReqVO transferReqVO) { | |
| 315 | + Long groupId = transferReqVO.getGroupId(); | |
| 316 | + Long newOwnerUserId = transferReqVO.getNewOwnerUserId(); | |
| 317 | + // 1.1 仅老群主可执行 | |
| 318 | + validateGroupOwner(groupId, userId); | |
| 319 | + // 1.2 不能转让给自己 | |
| 320 | + if (ObjUtil.equal(userId, newOwnerUserId)) { | |
| 321 | + throw exception(GROUP_TRANSFER_OWNER_TO_SELF); | |
| 322 | + } | |
| 323 | + // 1.3 新群主必须是群的有效成员 | |
| 324 | + ImGroupMemberDO newOwner = groupMemberService.validateMemberInGroup(groupId, newOwnerUserId); | |
| 325 | + | |
| 326 | + // 2.1 更新群表 owner_user_id | |
| 327 | + groupMapper.updateById(new ImGroupDO().setId(groupId).setOwnerUserId(newOwnerUserId)); | |
| 328 | + // 2.2 旧群主 role → MEMBER;新群主 role → OWNER | |
| 329 | + groupMemberService.updateGroupMemberRole(groupId, Set.of(userId), ImGroupMemberRoleEnum.NORMAL.getRole()); | |
| 330 | + groupMemberService.updateGroupMemberRole(groupId, Set.of(newOwner.getUserId()), ImGroupMemberRoleEnum.OWNER.getRole()); | |
| 331 | + } | |
| 332 | + | |
| 230 | 333 | // ==================== 群的读操作 ==================== |
| 231 | 334 | |
| 232 | 335 | @Override |
| ... | ... | @@ -244,8 +347,11 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 244 | 347 | return group; |
| 245 | 348 | } |
| 246 | 349 | |
| 247 | - @Override | |
| 248 | - public ImGroupDO validateGroupOwner(Long groupId, Long userId) { | |
| 350 | + /** | |
| 351 | + * 校验当前用户是群主,否则抛 GROUP_NOT_OWNER | |
| 352 | + */ | |
| 353 | + @SuppressWarnings("UnusedReturnValue") | |
| 354 | + private ImGroupDO validateGroupOwner(Long groupId, Long userId) { | |
| 249 | 355 | ImGroupDO group = validateGroupExists(groupId); |
| 250 | 356 | if (ObjUtil.notEqual(group.getOwnerUserId(), userId)) { |
| 251 | 357 | throw exception(GROUP_NOT_OWNER); |
| ... | ... | @@ -253,6 +359,18 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 253 | 359 | return group; |
| 254 | 360 | } |
| 255 | 361 | |
| 362 | + /** | |
| 363 | + * 校验当前用户是群主或管理员,否则抛 GROUP_NOT_OWNER_OR_ADMIN | |
| 364 | + */ | |
| 365 | + private ImGroupMemberDO validateGroupOwnerOrAdmin(Long groupId, Long userId) { | |
| 366 | + validateGroupExists(groupId); | |
| 367 | + ImGroupMemberDO member = groupMemberService.validateMemberInGroup(groupId, userId); | |
| 368 | + if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())) { | |
| 369 | + throw exception(GROUP_NOT_OWNER_OR_ADMIN); | |
| 370 | + } | |
| 371 | + return member; | |
| 372 | + } | |
| 373 | + | |
| 256 | 374 | @Override |
| 257 | 375 | @Cacheable(cacheNames = GROUP, key = "#id", unless = "#result == null") |
| 258 | 376 | public ImGroupDO getGroup(Long id) { | ... | ... |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImplTest.java
| ... | ... | @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
| 6 | 6 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; |
| 7 | 7 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; |
| 8 | 8 | import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; |
| 9 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; | |
| 9 | 10 | import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; |
| 10 | 11 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; |
| 11 | 12 | import org.junit.jupiter.api.Test; |
| ... | ... | @@ -61,7 +62,6 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { |
| 61 | 62 | ImGroupMemberDO exists = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) |
| 62 | 63 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); |
| 63 | 64 | when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(exists); |
| 64 | - when(groupMemberMapper.selectById(50L)).thenReturn(exists); | |
| 65 | 65 | |
| 66 | 66 | ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); |
| 67 | 67 | |
| ... | ... | @@ -72,13 +72,11 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { |
| 72 | 72 | |
| 73 | 73 | @Test |
| 74 | 74 | public void testAddGroupMember_existingDisabledRecovers() { |
| 75 | - // 准备:已存在且 DISABLE,应更新为 ENABLE | |
| 75 | + // 准备:已存在且 DISABLE,应重置为 ENABLE 并重置 role 为 MEMBER | |
| 76 | 76 | ImGroupMemberDO exists = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) |
| 77 | - .status(CommonStatusEnum.DISABLE.getStatus()).build(); | |
| 77 | + .status(CommonStatusEnum.DISABLE.getStatus()) | |
| 78 | + .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build(); | |
| 78 | 79 | when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(exists); |
| 79 | - ImGroupMemberDO recovered = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) | |
| 80 | - .status(CommonStatusEnum.ENABLE.getStatus()).build(); | |
| 81 | - when(groupMemberMapper.selectById(50L)).thenReturn(recovered); | |
| 82 | 80 | |
| 83 | 81 | ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); |
| 84 | 82 | |
| ... | ... | @@ -86,8 +84,10 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { |
| 86 | 84 | verify(groupMemberMapper).updateById(captor.capture()); |
| 87 | 85 | assertEquals(50L, captor.getValue().getId()); |
| 88 | 86 | assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); |
| 87 | + assertEquals(ImGroupMemberRoleEnum.NORMAL.getRole(), captor.getValue().getRole()); | |
| 89 | 88 | assertNotNull(captor.getValue().getJoinTime()); |
| 90 | 89 | assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); |
| 90 | + assertEquals(ImGroupMemberRoleEnum.NORMAL.getRole(), result.getRole()); | |
| 91 | 91 | } |
| 92 | 92 | |
| 93 | 93 | @Test | ... | ... |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java
| ... | ... | @@ -4,7 +4,10 @@ import cn.hutool.extra.spring.SpringUtil; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 5 | 5 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 6 | 6 | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
| 7 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; | |
| 8 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; | |
| 7 | 9 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; |
| 10 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; | |
| 8 | 11 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; |
| 9 | 12 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; |
| 10 | 13 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; |
| ... | ... | @@ -12,6 +15,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; |
| 12 | 15 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; |
| 13 | 16 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; |
| 14 | 17 | import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; |
| 18 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; | |
| 15 | 19 | import cn.iocoder.yudao.module.im.service.friend.ImFriendService; |
| 16 | 20 | import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; |
| 17 | 21 | import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; |
| ... | ... | @@ -29,6 +33,7 @@ import java.util.ArrayList; |
| 29 | 33 | import java.util.HashMap; |
| 30 | 34 | import java.util.List; |
| 31 | 35 | import java.util.Map; |
| 36 | +import java.util.Set; | |
| 32 | 37 | |
| 33 | 38 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; |
| 34 | 39 | import static org.junit.jupiter.api.Assertions.*; |
| ... | ... | @@ -78,8 +83,8 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { |
| 78 | 83 | assertEquals(100L, result.getId()); |
| 79 | 84 | assertEquals(1L, result.getOwnerUserId()); |
| 80 | 85 | assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); |
| 81 | - // 验证:群主加入群 + 推送群创建事件 | |
| 82 | - verify(groupMemberService).addGroupMember(100L, 1L); | |
| 86 | + // 验证:群主加入群(带 OWNER role)+ 推送群创建事件 | |
| 87 | + verify(groupMemberService).addGroupMember(100L, 1L, ImGroupMemberRoleEnum.OWNER.getRole()); | |
| 83 | 88 | verify(webSocketService).sendGroupMessageAsync(eq(1L), any(ImGroupMessageDTO.class)); |
| 84 | 89 | } |
| 85 | 90 | |
| ... | ... | @@ -394,6 +399,9 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { |
| 394 | 399 | ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) |
| 395 | 400 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); |
| 396 | 401 | when(groupMapper.selectById(10L)).thenReturn(group); |
| 402 | + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( | |
| 403 | + ImGroupMemberDO.builder().groupId(10L).userId(1L) | |
| 404 | + .role(ImGroupMemberRoleEnum.OWNER.getRole()).build()); | |
| 397 | 405 | |
| 398 | 406 | ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); |
| 399 | 407 | reqVO.setGroupId(10L); |
| ... | ... | @@ -414,6 +422,16 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { |
| 414 | 422 | ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) |
| 415 | 423 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); |
| 416 | 424 | when(groupMapper.selectById(10L)).thenReturn(group); |
| 425 | + // 操作者:群主 | |
| 426 | + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( | |
| 427 | + ImGroupMemberDO.builder().groupId(10L).userId(1L) | |
| 428 | + .role(ImGroupMemberRoleEnum.OWNER.getRole()).build()); | |
| 429 | + // 目标:两个普通成员 | |
| 430 | + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( | |
| 431 | + ImGroupMemberDO.builder().groupId(10L).userId(2L) | |
| 432 | + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build(), | |
| 433 | + ImGroupMemberDO.builder().groupId(10L).userId(3L) | |
| 434 | + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build())); | |
| 417 | 435 | |
| 418 | 436 | ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); |
| 419 | 437 | reqVO.setGroupId(10L); |
| ... | ... | @@ -428,6 +446,293 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { |
| 428 | 446 | } |
| 429 | 447 | } |
| 430 | 448 | |
| 449 | + @Test | |
| 450 | + public void testRemoveGroupMember_adminCannotRemoveAdmin() { | |
| 451 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | |
| 452 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | |
| 453 | + .thenReturn(groupService); | |
| 454 | + | |
| 455 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) | |
| 456 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | |
| 457 | + when(groupMapper.selectById(10L)).thenReturn(group); | |
| 458 | + // 操作者:管理员 | |
| 459 | + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( | |
| 460 | + ImGroupMemberDO.builder().groupId(10L).userId(1L) | |
| 461 | + .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build()); | |
| 462 | + // 目标:另一个管理员 | |
| 463 | + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( | |
| 464 | + ImGroupMemberDO.builder().groupId(10L).userId(2L) | |
| 465 | + .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build())); | |
| 466 | + | |
| 467 | + ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); | |
| 468 | + reqVO.setGroupId(10L); | |
| 469 | + reqVO.setMemberUserIds(List.of(2L)); | |
| 470 | + | |
| 471 | + ServiceException exception = assertThrows(ServiceException.class, | |
| 472 | + () -> groupService.removeGroupMember(1L, reqVO)); | |
| 473 | + assertEquals(GROUP_REMOVE_ADMIN_DENIED.getCode(), exception.getCode()); | |
| 474 | + verify(groupMemberService, never()).removeGroupMembers(anyLong(), anyCollection()); | |
| 475 | + } | |
| 476 | + } | |
| 477 | + | |
| 478 | + @Test | |
| 479 | + public void testRemoveGroupMember_ownerCannotBeRemoved() { | |
| 480 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | |
| 481 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | |
| 482 | + .thenReturn(groupService); | |
| 483 | + | |
| 484 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) | |
| 485 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | |
| 486 | + when(groupMapper.selectById(10L)).thenReturn(group); | |
| 487 | + // 操作者:群主(不能踢自己;这里换成另一个 userId 的群主语义 — 用 ADMIN 操作群主) | |
| 488 | + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( | |
| 489 | + ImGroupMemberDO.builder().groupId(10L).userId(1L) | |
| 490 | + .role(ImGroupMemberRoleEnum.OWNER.getRole()).build()); | |
| 491 | + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( | |
| 492 | + ImGroupMemberDO.builder().groupId(10L).userId(99L) | |
| 493 | + .role(ImGroupMemberRoleEnum.OWNER.getRole()).build())); | |
| 494 | + | |
| 495 | + ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); | |
| 496 | + reqVO.setGroupId(10L); | |
| 497 | + reqVO.setMemberUserIds(List.of(99L)); | |
| 498 | + | |
| 499 | + ServiceException exception = assertThrows(ServiceException.class, | |
| 500 | + () -> groupService.removeGroupMember(1L, reqVO)); | |
| 501 | + assertEquals(GROUP_REMOVE_OWNER_DENIED.getCode(), exception.getCode()); | |
| 502 | + } | |
| 503 | + } | |
| 504 | + | |
| 505 | + // ========== addGroupAdmin ========== | |
| 506 | + | |
| 507 | + @Test | |
| 508 | + public void testAddGroupAdmin_success() { | |
| 509 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | |
| 510 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | |
| 511 | + .thenReturn(groupService); | |
| 512 | + | |
| 513 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | |
| 514 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | |
| 515 | + when(groupMapper.selectById(10L)).thenReturn(group); | |
| 516 | + // 目标 3 是普通成员 | |
| 517 | + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( | |
| 518 | + ImGroupMemberDO.builder().userId(3L).status(CommonStatusEnum.ENABLE.getStatus()) | |
| 519 | + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build())); | |
| 520 | + // 群里已有 1 个 ADMIN,1 + 1 ≤ 3 不超上限 | |
| 521 | + when(groupMemberService.getGroupMemberCountByRole(10L, ImGroupMemberRoleEnum.ADMIN.getRole())) | |
| 522 | + .thenReturn(1L); | |
| 523 | + | |
| 524 | + ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO(); | |
| 525 | + reqVO.setGroupId(10L); | |
| 526 | + reqVO.setUserIds(List.of(3L)); | |
| 527 | + | |
| 528 | + groupService.addGroupAdmin(1L, reqVO); | |
| 529 | + | |
| 530 | + verify(groupMemberService).updateGroupMemberRole(eq(10L), argThat((Set<Long> ids) -> | |
| 531 | + ids.size() == 1 && ids.contains(3L)), | |
| 532 | + eq(ImGroupMemberRoleEnum.ADMIN.getRole())); | |
| 533 | + } | |
| 534 | + } | |
| 535 | + | |
| 536 | + @Test | |
| 537 | + public void testAddGroupAdmin_exceedsLimit() { | |
| 538 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | |
| 539 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | |
| 540 | + .thenReturn(groupService); | |
| 541 | + | |
| 542 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | |
| 543 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | |
| 544 | + when(groupMapper.selectById(10L)).thenReturn(group); | |
| 545 | + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( | |
| 546 | + ImGroupMemberDO.builder().userId(5L).status(CommonStatusEnum.ENABLE.getStatus()) | |
| 547 | + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build())); | |
| 548 | + // 群里已有 3 个 ADMIN(达到上限),再加 1 会超 | |
| 549 | + when(groupMemberService.getGroupMemberCountByRole(10L, ImGroupMemberRoleEnum.ADMIN.getRole())) | |
| 550 | + .thenReturn(3L); | |
| 551 | + | |
| 552 | + ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO(); | |
| 553 | + reqVO.setGroupId(10L); | |
| 554 | + reqVO.setUserIds(List.of(5L)); | |
| 555 | + | |
| 556 | + ServiceException exception = assertThrows(ServiceException.class, | |
| 557 | + () -> groupService.addGroupAdmin(1L, reqVO)); | |
| 558 | + assertEquals(GROUP_ADMIN_MAX_LIMIT.getCode(), exception.getCode()); | |
| 559 | + verify(groupMemberService, never()).updateGroupMemberRole(anyLong(), anyCollection(), anyInt()); | |
| 560 | + } | |
| 561 | + } | |
| 562 | + | |
| 563 | + @Test | |
| 564 | + public void testAddGroupAdmin_targetIsOwner() { | |
| 565 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | |
| 566 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | |
| 567 | + .thenReturn(groupService); | |
| 568 | + | |
| 569 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | |
| 570 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | |
| 571 | + when(groupMapper.selectById(10L)).thenReturn(group); | |
| 572 | + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( | |
| 573 | + ImGroupMemberDO.builder().userId(1L).status(CommonStatusEnum.ENABLE.getStatus()) | |
| 574 | + .role(ImGroupMemberRoleEnum.OWNER.getRole()).build())); | |
| 575 | + | |
| 576 | + ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO(); | |
| 577 | + reqVO.setGroupId(10L); | |
| 578 | + reqVO.setUserIds(List.of(1L)); | |
| 579 | + | |
| 580 | + ServiceException exception = assertThrows(ServiceException.class, | |
| 581 | + () -> groupService.addGroupAdmin(1L, reqVO)); | |
| 582 | + assertEquals(GROUP_ADMIN_TARGET_IS_OWNER.getCode(), exception.getCode()); | |
| 583 | + } | |
| 584 | + } | |
| 585 | + | |
| 586 | + @Test | |
| 587 | + public void testAddGroupAdmin_idempotentSkipWhenAlreadyAdmin() { | |
| 588 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | |
| 589 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | |
| 590 | + .thenReturn(groupService); | |
| 591 | + | |
| 592 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | |
| 593 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | |
| 594 | + when(groupMapper.selectById(10L)).thenReturn(group); | |
| 595 | + // 目标已是 ADMIN:再加无需操作 | |
| 596 | + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( | |
| 597 | + ImGroupMemberDO.builder().userId(2L).status(CommonStatusEnum.ENABLE.getStatus()) | |
| 598 | + .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build())); | |
| 599 | + | |
| 600 | + ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO(); | |
| 601 | + reqVO.setGroupId(10L); | |
| 602 | + reqVO.setUserIds(List.of(2L)); | |
| 603 | + | |
| 604 | + groupService.addGroupAdmin(1L, reqVO); | |
| 605 | + | |
| 606 | + verify(groupMemberService, never()).updateGroupMemberRole(anyLong(), anyCollection(), anyInt()); | |
| 607 | + verify(groupMemberService, never()).getGroupMemberCountByRole(anyLong(), anyInt()); | |
| 608 | + } | |
| 609 | + } | |
| 610 | + | |
| 611 | + // ========== removeGroupAdmin ========== | |
| 612 | + | |
| 613 | + @Test | |
| 614 | + public void testRemoveGroupAdmin_success() { | |
| 615 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | |
| 616 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | |
| 617 | + .thenReturn(groupService); | |
| 618 | + | |
| 619 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | |
| 620 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | |
| 621 | + when(groupMapper.selectById(10L)).thenReturn(group); | |
| 622 | + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( | |
| 623 | + ImGroupMemberDO.builder().userId(2L).status(CommonStatusEnum.ENABLE.getStatus()) | |
| 624 | + .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build())); | |
| 625 | + | |
| 626 | + ImGroupAdminRemoveReqVO reqVO = new ImGroupAdminRemoveReqVO(); | |
| 627 | + reqVO.setGroupId(10L); | |
| 628 | + reqVO.setUserIds(List.of(2L)); | |
| 629 | + | |
| 630 | + groupService.removeGroupAdmin(1L, reqVO); | |
| 631 | + | |
| 632 | + verify(groupMemberService).updateGroupMemberRole(eq(10L), argThat((Set<Long> ids) -> | |
| 633 | + ids.size() == 1 && ids.contains(2L)), | |
| 634 | + eq(ImGroupMemberRoleEnum.NORMAL.getRole())); | |
| 635 | + } | |
| 636 | + } | |
| 637 | + | |
| 638 | + @Test | |
| 639 | + public void testRemoveGroupAdmin_idempotentSkipWhenAlreadyMember() { | |
| 640 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | |
| 641 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | |
| 642 | + .thenReturn(groupService); | |
| 643 | + | |
| 644 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | |
| 645 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | |
| 646 | + when(groupMapper.selectById(10L)).thenReturn(group); | |
| 647 | + // 目标已是 MEMBER:撤销无需操作 | |
| 648 | + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( | |
| 649 | + ImGroupMemberDO.builder().userId(2L).status(CommonStatusEnum.ENABLE.getStatus()) | |
| 650 | + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build())); | |
| 651 | + | |
| 652 | + ImGroupAdminRemoveReqVO reqVO = new ImGroupAdminRemoveReqVO(); | |
| 653 | + reqVO.setGroupId(10L); | |
| 654 | + reqVO.setUserIds(List.of(2L)); | |
| 655 | + | |
| 656 | + groupService.removeGroupAdmin(1L, reqVO); | |
| 657 | + | |
| 658 | + verify(groupMemberService, never()).updateGroupMemberRole(anyLong(), anyCollection(), anyInt()); | |
| 659 | + } | |
| 660 | + } | |
| 661 | + | |
| 662 | + // ========== transferGroupOwner ========== | |
| 663 | + | |
| 664 | + @Test | |
| 665 | + public void testTransferGroupOwner_success() { | |
| 666 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | |
| 667 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | |
| 668 | + .thenReturn(groupService); | |
| 669 | + | |
| 670 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | |
| 671 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | |
| 672 | + when(groupMapper.selectById(10L)).thenReturn(group); | |
| 673 | + when(groupMemberService.validateMemberInGroup(10L, 2L)).thenReturn( | |
| 674 | + ImGroupMemberDO.builder().groupId(10L).userId(2L) | |
| 675 | + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build()); | |
| 676 | + | |
| 677 | + ImGroupTransferOwnerReqVO reqVO = new ImGroupTransferOwnerReqVO(); | |
| 678 | + reqVO.setGroupId(10L); | |
| 679 | + reqVO.setNewOwnerUserId(2L); | |
| 680 | + | |
| 681 | + groupService.transferGroupOwner(1L, reqVO); | |
| 682 | + | |
| 683 | + // 群表 owner 切换 | |
| 684 | + ArgumentCaptor<ImGroupDO> groupCaptor = ArgumentCaptor.forClass(ImGroupDO.class); | |
| 685 | + verify(groupMapper).updateById(groupCaptor.capture()); | |
| 686 | + assertEquals(2L, groupCaptor.getValue().getOwnerUserId()); | |
| 687 | + // 旧群主 → MEMBER;新群主 → OWNER | |
| 688 | + verify(groupMemberService).updateGroupMemberRole(eq(10L), eq(Set.of(1L)), | |
| 689 | + eq(ImGroupMemberRoleEnum.NORMAL.getRole())); | |
| 690 | + verify(groupMemberService).updateGroupMemberRole(eq(10L), eq(Set.of(2L)), | |
| 691 | + eq(ImGroupMemberRoleEnum.OWNER.getRole())); | |
| 692 | + } | |
| 693 | + } | |
| 694 | + | |
| 695 | + @Test | |
| 696 | + public void testTransferGroupOwner_toSelf() { | |
| 697 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | |
| 698 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | |
| 699 | + .thenReturn(groupService); | |
| 700 | + | |
| 701 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | |
| 702 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | |
| 703 | + when(groupMapper.selectById(10L)).thenReturn(group); | |
| 704 | + | |
| 705 | + ImGroupTransferOwnerReqVO reqVO = new ImGroupTransferOwnerReqVO(); | |
| 706 | + reqVO.setGroupId(10L); | |
| 707 | + reqVO.setNewOwnerUserId(1L); | |
| 708 | + | |
| 709 | + ServiceException exception = assertThrows(ServiceException.class, | |
| 710 | + () -> groupService.transferGroupOwner(1L, reqVO)); | |
| 711 | + assertEquals(GROUP_TRANSFER_OWNER_TO_SELF.getCode(), exception.getCode()); | |
| 712 | + verify(groupMapper, never()).updateById(any(ImGroupDO.class)); | |
| 713 | + } | |
| 714 | + } | |
| 715 | + | |
| 716 | + @Test | |
| 717 | + public void testTransferGroupOwner_notOwner() { | |
| 718 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | |
| 719 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | |
| 720 | + .thenReturn(groupService); | |
| 721 | + | |
| 722 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) | |
| 723 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | |
| 724 | + when(groupMapper.selectById(10L)).thenReturn(group); | |
| 725 | + | |
| 726 | + ImGroupTransferOwnerReqVO reqVO = new ImGroupTransferOwnerReqVO(); | |
| 727 | + reqVO.setGroupId(10L); | |
| 728 | + reqVO.setNewOwnerUserId(2L); | |
| 729 | + | |
| 730 | + ServiceException exception = assertThrows(ServiceException.class, | |
| 731 | + () -> groupService.transferGroupOwner(1L, reqVO)); | |
| 732 | + assertEquals(GROUP_NOT_OWNER.getCode(), exception.getCode()); | |
| 733 | + } | |
| 734 | + } | |
| 735 | + | |
| 431 | 736 | // ========== validateGroupExists ========== |
| 432 | 737 | |
| 433 | 738 | @Test | ... | ... |
yudao-module-im/src/test/resources/sql/create_tables.sql
| ... | ... | @@ -62,9 +62,10 @@ CREATE TABLE IF NOT EXISTS "im_group_member" ( |
| 62 | 62 | "group_id" bigint NOT NULL COMMENT '群编号', |
| 63 | 63 | "user_id" bigint NOT NULL COMMENT '用户编号', |
| 64 | 64 | "display_user_name" varchar(64) DEFAULT NULL COMMENT '组内显示名', |
| 65 | - "display_group_name" varchar(64) DEFAULT NULL COMMENT '群显示备注', | |
| 65 | + "group_remark" varchar(64) DEFAULT NULL COMMENT '群备注', | |
| 66 | 66 | "muted" bit DEFAULT FALSE COMMENT '是否免打扰', |
| 67 | 67 | "status" tinyint NOT NULL COMMENT '成员状态', |
| 68 | + "role" tinyint NOT NULL DEFAULT 3 COMMENT '成员角色:1=群主 2=管理员 3=普通成员', | |
| 68 | 69 | "join_time" timestamp DEFAULT NULL COMMENT '入群时间', |
| 69 | 70 | "quit_time" timestamp DEFAULT NULL COMMENT '退群时间', |
| 70 | 71 | "creator" varchar(64) DEFAULT '', | ... | ... |