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,8 +2,11 @@ package cn.iocoder.yudao.module.im.controller.admin.group; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | 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 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; | 7 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; |
| 6 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupRespVO; | 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 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; | 10 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; |
| 8 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; | 11 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; |
| 9 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; | 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,7 +17,6 @@ import io.swagger.v3.oas.annotations.Parameter; | ||
| 14 | import io.swagger.v3.oas.annotations.tags.Tag; | 17 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 15 | import jakarta.annotation.Resource; | 18 | import jakarta.annotation.Resource; |
| 16 | import jakarta.validation.Valid; | 19 | import jakarta.validation.Valid; |
| 17 | -import org.springframework.security.access.prepost.PreAuthorize; | ||
| 18 | import org.springframework.validation.annotation.Validated; | 20 | import org.springframework.validation.annotation.Validated; |
| 19 | import org.springframework.web.bind.annotation.*; | 21 | import org.springframework.web.bind.annotation.*; |
| 20 | 22 | ||
| @@ -36,7 +38,6 @@ public class ImGroupController { | @@ -36,7 +38,6 @@ public class ImGroupController { | ||
| 36 | 38 | ||
| 37 | @PostMapping("/create") | 39 | @PostMapping("/create") |
| 38 | @Operation(summary = "创建群") | 40 | @Operation(summary = "创建群") |
| 39 | - @PreAuthorize("@ss.hasPermission('im:group:create')") | ||
| 40 | public CommonResult<ImGroupRespVO> createGroup(@Valid @RequestBody ImGroupCreateReqVO createReqVO) { | 41 | public CommonResult<ImGroupRespVO> createGroup(@Valid @RequestBody ImGroupCreateReqVO createReqVO) { |
| 41 | ImGroupDO group = groupService.createGroup(createReqVO, getLoginUserId()); | 42 | ImGroupDO group = groupService.createGroup(createReqVO, getLoginUserId()); |
| 42 | return success(BeanUtils.toBean(group, ImGroupRespVO.class)); | 43 | return success(BeanUtils.toBean(group, ImGroupRespVO.class)); |
| @@ -44,7 +45,6 @@ public class ImGroupController { | @@ -44,7 +45,6 @@ public class ImGroupController { | ||
| 44 | 45 | ||
| 45 | @PutMapping("/update") | 46 | @PutMapping("/update") |
| 46 | @Operation(summary = "更新群") | 47 | @Operation(summary = "更新群") |
| 47 | - @PreAuthorize("@ss.hasPermission('im:group:update')") | ||
| 48 | public CommonResult<ImGroupRespVO> updateGroup(@Valid @RequestBody ImGroupUpdateReqVO updateReqVO) { | 48 | public CommonResult<ImGroupRespVO> updateGroup(@Valid @RequestBody ImGroupUpdateReqVO updateReqVO) { |
| 49 | ImGroupDO group = groupService.updateGroup(updateReqVO, getLoginUserId()); | 49 | ImGroupDO group = groupService.updateGroup(updateReqVO, getLoginUserId()); |
| 50 | return success(BeanUtils.toBean(group, ImGroupRespVO.class)); | 50 | return success(BeanUtils.toBean(group, ImGroupRespVO.class)); |
| @@ -53,7 +53,6 @@ public class ImGroupController { | @@ -53,7 +53,6 @@ public class ImGroupController { | ||
| 53 | @DeleteMapping("/dissolve") | 53 | @DeleteMapping("/dissolve") |
| 54 | @Operation(summary = "解散群") | 54 | @Operation(summary = "解散群") |
| 55 | @Parameter(name = "id", description = "群编号", required = true) | 55 | @Parameter(name = "id", description = "群编号", required = true) |
| 56 | - @PreAuthorize("@ss.hasPermission('im:group:delete')") | ||
| 57 | public CommonResult<Boolean> dissolveGroup(@RequestParam("id") Long id) { | 56 | public CommonResult<Boolean> dissolveGroup(@RequestParam("id") Long id) { |
| 58 | groupService.dissolveGroup(id, getLoginUserId()); | 57 | groupService.dissolveGroup(id, getLoginUserId()); |
| 59 | return success(true); | 58 | return success(true); |
| @@ -64,7 +63,6 @@ public class ImGroupController { | @@ -64,7 +63,6 @@ public class ImGroupController { | ||
| 64 | @GetMapping("/get") | 63 | @GetMapping("/get") |
| 65 | @Operation(summary = "获得群") | 64 | @Operation(summary = "获得群") |
| 66 | @Parameter(name = "id", description = "编号", required = true, example = "1024") | 65 | @Parameter(name = "id", description = "编号", required = true, example = "1024") |
| 67 | - @PreAuthorize("@ss.hasPermission('im:group:query')") | ||
| 68 | public CommonResult<ImGroupRespVO> getGroup(@RequestParam("id") Long id) { | 66 | public CommonResult<ImGroupRespVO> getGroup(@RequestParam("id") Long id) { |
| 69 | ImGroupDO group = groupService.getGroup(id); | 67 | ImGroupDO group = groupService.getGroup(id); |
| 70 | return success(BeanUtils.toBean(group, ImGroupRespVO.class)); | 68 | return success(BeanUtils.toBean(group, ImGroupRespVO.class)); |
| @@ -81,7 +79,6 @@ public class ImGroupController { | @@ -81,7 +79,6 @@ public class ImGroupController { | ||
| 81 | 79 | ||
| 82 | @PostMapping("/invite") | 80 | @PostMapping("/invite") |
| 83 | @Operation(summary = "邀请用户加入群") | 81 | @Operation(summary = "邀请用户加入群") |
| 84 | - @PreAuthorize("@ss.hasPermission('im:group:update')") | ||
| 85 | public CommonResult<Boolean> inviteGroupMember(@Valid @RequestBody ImGroupMemberInviteReqVO inviteReqVO) { | 82 | public CommonResult<Boolean> inviteGroupMember(@Valid @RequestBody ImGroupMemberInviteReqVO inviteReqVO) { |
| 86 | groupService.inviteGroupMember(getLoginUserId(), inviteReqVO); | 83 | groupService.inviteGroupMember(getLoginUserId(), inviteReqVO); |
| 87 | return success(true); | 84 | return success(true); |
| @@ -90,7 +87,6 @@ public class ImGroupController { | @@ -90,7 +87,6 @@ public class ImGroupController { | ||
| 90 | @DeleteMapping("/quit") | 87 | @DeleteMapping("/quit") |
| 91 | @Operation(summary = "退出群") | 88 | @Operation(summary = "退出群") |
| 92 | @Parameter(name = "groupId", description = "群编号", required = true) | 89 | @Parameter(name = "groupId", description = "群编号", required = true) |
| 93 | - @PreAuthorize("@ss.hasPermission('im:group:update')") | ||
| 94 | public CommonResult<Boolean> quitGroup(@RequestParam("groupId") Long groupId) { | 90 | public CommonResult<Boolean> quitGroup(@RequestParam("groupId") Long groupId) { |
| 95 | groupService.quitGroup(groupId, getLoginUserId()); | 91 | groupService.quitGroup(groupId, getLoginUserId()); |
| 96 | return success(true); | 92 | return success(true); |
| @@ -98,10 +94,30 @@ public class ImGroupController { | @@ -98,10 +94,30 @@ public class ImGroupController { | ||
| 98 | 94 | ||
| 99 | @DeleteMapping("/kicking") | 95 | @DeleteMapping("/kicking") |
| 100 | @Operation(summary = "移除群成员") | 96 | @Operation(summary = "移除群成员") |
| 101 | - @PreAuthorize("@ss.hasPermission('im:group:update')") | ||
| 102 | public CommonResult<Boolean> removeGroupMember(@Valid @RequestBody ImGroupMemberRemoveReqVO removeReqVO) { | 97 | public CommonResult<Boolean> removeGroupMember(@Valid @RequestBody ImGroupMemberRemoveReqVO removeReqVO) { |
| 103 | groupService.removeGroupMember(getLoginUserId(), removeReqVO); | 98 | groupService.removeGroupMember(getLoginUserId(), removeReqVO); |
| 104 | return success(true); | 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,7 +17,6 @@ import io.swagger.v3.oas.annotations.Parameters; | ||
| 17 | import io.swagger.v3.oas.annotations.tags.Tag; | 17 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 18 | import jakarta.annotation.Resource; | 18 | import jakarta.annotation.Resource; |
| 19 | import jakarta.validation.Valid; | 19 | import jakarta.validation.Valid; |
| 20 | -import org.springframework.security.access.prepost.PreAuthorize; | ||
| 21 | import org.springframework.validation.annotation.Validated; | 20 | import org.springframework.validation.annotation.Validated; |
| 22 | import org.springframework.web.bind.annotation.*; | 21 | import org.springframework.web.bind.annotation.*; |
| 23 | 22 | ||
| @@ -44,7 +43,6 @@ public class ImGroupMemberController { | @@ -44,7 +43,6 @@ public class ImGroupMemberController { | ||
| 44 | 43 | ||
| 45 | @PutMapping("/update") | 44 | @PutMapping("/update") |
| 46 | @Operation(summary = "更新群成员") | 45 | @Operation(summary = "更新群成员") |
| 47 | - @PreAuthorize("@ss.hasPermission('im:group-member:update')") | ||
| 48 | public CommonResult<Boolean> updateGroupMember(@Valid @RequestBody ImGroupMemberUpdateReqVO updateReqVO) { | 46 | public CommonResult<Boolean> updateGroupMember(@Valid @RequestBody ImGroupMemberUpdateReqVO updateReqVO) { |
| 49 | groupMemberService.updateGroupMember(getLoginUserId(), updateReqVO); | 47 | groupMemberService.updateGroupMember(getLoginUserId(), updateReqVO); |
| 50 | return success(true); | 48 | return success(true); |
| @@ -57,7 +55,6 @@ public class ImGroupMemberController { | @@ -57,7 +55,6 @@ public class ImGroupMemberController { | ||
| 57 | @Parameter(name = "groupId", description = "群编号(与 userId 配合查)", example = "1"), | 55 | @Parameter(name = "groupId", description = "群编号(与 userId 配合查)", example = "1"), |
| 58 | @Parameter(name = "userId", description = "用户编号(与 groupId 配合查)", example = "100") | 56 | @Parameter(name = "userId", description = "用户编号(与 groupId 配合查)", example = "100") |
| 59 | }) | 57 | }) |
| 60 | - @PreAuthorize("@ss.hasPermission('im:group-member:query')") | ||
| 61 | public CommonResult<ImGroupMemberRespVO> getGroupMember(@RequestParam(value = "id", required = false) Long id, | 58 | public CommonResult<ImGroupMemberRespVO> getGroupMember(@RequestParam(value = "id", required = false) Long id, |
| 62 | @RequestParam(value = "groupId", required = false) Long groupId, | 59 | @RequestParam(value = "groupId", required = false) Long groupId, |
| 63 | @RequestParam(value = "userId", required = false) Long userId) { | 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,8 +21,8 @@ public class ImGroupMemberRespVO { | ||
| 21 | @Schema(description = "组内显示名", example = "芋艿") | 21 | @Schema(description = "组内显示名", example = "芋艿") |
| 22 | private String displayUserName; | 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 | @Schema(description = "是否免打扰") | 27 | @Schema(description = "是否免打扰") |
| 28 | private Boolean muted; | 28 | private Boolean muted; |
| @@ -30,6 +30,9 @@ public class ImGroupMemberRespVO { | @@ -30,6 +30,9 @@ public class ImGroupMemberRespVO { | ||
| 30 | @Schema(description = "成员状态", example = "0") | 30 | @Schema(description = "成员状态", example = "0") |
| 31 | private Integer status; | 31 | private Integer status; |
| 32 | 32 | ||
| 33 | + @Schema(description = "成员角色", example = "3") | ||
| 34 | + private Integer role; // 参见 ImGroupMemberRoleEnum 枚举类 | ||
| 35 | + | ||
| 33 | @Schema(description = "入群时间") | 36 | @Schema(description = "入群时间") |
| 34 | private LocalDateTime joinTime; | 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,8 +17,8 @@ public class ImGroupMemberUpdateReqVO { | ||
| 17 | @Schema(description = "群内昵称", example = "芋头") | 17 | @Schema(description = "群内昵称", example = "芋头") |
| 18 | private String displayUserName; | 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 | @Schema(description = "是否免打扰") | 23 | @Schema(description = "是否免打扰") |
| 24 | private Boolean muted; | 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,8 +21,8 @@ public class ImGroupMemberManagerRespVO { | ||
| 21 | @Schema(description = "组内显示名", example = "三哥") | 21 | @Schema(description = "组内显示名", example = "三哥") |
| 22 | private String displayUserName; | 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 | @Schema(description = "是否免打扰", example = "false") | 27 | @Schema(description = "是否免打扰", example = "false") |
| 28 | private Boolean muted; | 28 | private Boolean muted; |
| @@ -30,6 +30,9 @@ public class ImGroupMemberManagerRespVO { | @@ -30,6 +30,9 @@ public class ImGroupMemberManagerRespVO { | ||
| 30 | @Schema(description = "成员状态", example = "0") | 30 | @Schema(description = "成员状态", example = "0") |
| 31 | private Integer status; // 参见 CommonStatusEnum 枚举类 | 31 | private Integer status; // 参见 CommonStatusEnum 枚举类 |
| 32 | 32 | ||
| 33 | + @Schema(description = "成员角色;1=群主 2=管理员 3=普通成员", example = "3") | ||
| 34 | + private Integer role; // 参见 ImGroupMemberRoleEnum 枚举类 | ||
| 35 | + | ||
| 33 | @Schema(description = "入群时间") | 36 | @Schema(description = "入群时间") |
| 34 | private LocalDateTime joinTime; | 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,6 +2,7 @@ package cn.iocoder.yudao.module.im.dal.dataobject.group; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 5 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; | ||
| 5 | import com.baomidou.mybatisplus.annotation.KeySequence; | 6 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 6 | import com.baomidou.mybatisplus.annotation.TableId; | 7 | import com.baomidou.mybatisplus.annotation.TableId; |
| 7 | import com.baomidou.mybatisplus.annotation.TableName; | 8 | import com.baomidou.mybatisplus.annotation.TableName; |
| @@ -46,9 +47,9 @@ public class ImGroupMemberDO extends BaseDO { | @@ -46,9 +47,9 @@ public class ImGroupMemberDO extends BaseDO { | ||
| 46 | */ | 47 | */ |
| 47 | private String displayUserName; | 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,6 +61,12 @@ public class ImGroupMemberDO extends BaseDO { | ||
| 60 | */ | 61 | */ |
| 61 | private Integer status; | 62 | private Integer status; |
| 62 | /** | 63 | /** |
| 64 | + * 成员角色 | ||
| 65 | + * <p> | ||
| 66 | + * 枚举 {@link ImGroupMemberRoleEnum} | ||
| 67 | + */ | ||
| 68 | + private Integer role; | ||
| 69 | + /** | ||
| 63 | * 入群时间 | 70 | * 入群时间 |
| 64 | */ | 71 | */ |
| 65 | private LocalDateTime joinTime; | 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,6 +83,20 @@ public interface ImGroupMemberMapper extends BaseMapperX<ImGroupMemberDO> { | ||
| 83 | .eq(ImGroupMemberDO::getStatus, oldStatus)); | 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 | * 批量按 group_id 统计指定状态的成员数 | 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,6 +23,8 @@ public interface ErrorCodeConstants { | ||
| 23 | ErrorCode GROUP_BANNED = new ErrorCode(1_040_400_001, "群已被封禁"); | 23 | ErrorCode GROUP_BANNED = new ErrorCode(1_040_400_001, "群已被封禁"); |
| 24 | ErrorCode GROUP_DISSOLVED = new ErrorCode(1_040_400_002, "群已解散"); | 24 | ErrorCode GROUP_DISSOLVED = new ErrorCode(1_040_400_002, "群已解散"); |
| 25 | ErrorCode GROUP_NOT_OWNER = new ErrorCode(1_040_400_003, "仅群主可执行该操作"); | 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 | // ========== 群成员 (1-040-500-000) ========== | 29 | // ========== 群成员 (1-040-500-000) ========== |
| 28 | ErrorCode GROUP_MEMBER_NOT_IN_GROUP = new ErrorCode(1_040_500_001, "您已不在该群中"); | 30 | ErrorCode GROUP_MEMBER_NOT_IN_GROUP = new ErrorCode(1_040_500_001, "您已不在该群中"); |
| @@ -30,6 +32,11 @@ public interface ErrorCodeConstants { | @@ -30,6 +32,11 @@ public interface ErrorCodeConstants { | ||
| 30 | ErrorCode GROUP_CANNOT_REMOVE_SELF = new ErrorCode(1_040_500_004, "不能将自己移出群聊"); | 32 | ErrorCode GROUP_CANNOT_REMOVE_SELF = new ErrorCode(1_040_500_004, "不能将自己移出群聊"); |
| 31 | ErrorCode GROUP_MEMBER_EXCEED = new ErrorCode(1_040_500_005, "群聊人数不能超过 {} 人"); | 33 | ErrorCode GROUP_MEMBER_EXCEED = new ErrorCode(1_040_500_005, "群聊人数不能超过 {} 人"); |
| 32 | ErrorCode GROUP_INVITE_NOT_FRIEND = new ErrorCode(1_040_500_006, "'{}' 不是您的好友,邀请失败"); | 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 | // ========== 好友 (1-040-600-000) ========== | 41 | // ========== 好友 (1-040-600-000) ========== |
| 35 | ErrorCode FRIEND_NOT_FRIEND = new ErrorCode(1_040_600_001, "对方不是您的好友"); | 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,6 +18,16 @@ public interface ImCommonConstants { | ||
| 18 | int MAX_GROUP_MEMBER = 500; | 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 | long AT_USER_ID_ALL = -1L; | 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,6 +34,15 @@ public interface ImGroupMemberService { | ||
| 34 | ImGroupMemberDO getGroupMember(Long groupId, Long userId); | 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 | * 根据群组 id 查询群成员(包含所有状态) | 46 | * 根据群组 id 查询群成员(包含所有状态) |
| 38 | * | 47 | * |
| 39 | * @param groupId 群组id | 48 | * @param groupId 群组id |
| @@ -79,10 +88,7 @@ public interface ImGroupMemberService { | @@ -79,10 +88,7 @@ public interface ImGroupMemberService { | ||
| 79 | List<ImGroupMemberDO> getQuitGroupMemberListByUserId(Long userId, LocalDateTime minQuitTime); | 88 | List<ImGroupMemberDO> getQuitGroupMemberListByUserId(Long userId, LocalDateTime minQuitTime); |
| 80 | 89 | ||
| 81 | /** | 90 | /** |
| 82 | - * 添加群成员(入群) | ||
| 83 | - * <p> | ||
| 84 | - * 插入一条 ENABLE 状态的群成员记录,并设置 joinTime。 | ||
| 85 | - * 可用于创建群时添加群主、邀请入群等场景。 | 91 | + * 添加群成员(入群),角色默认 MEMBER |
| 86 | * | 92 | * |
| 87 | * @param groupId 群编号 | 93 | * @param groupId 群编号 |
| 88 | * @param userId 用户编号 | 94 | * @param userId 用户编号 |
| @@ -92,6 +98,19 @@ public interface ImGroupMemberService { | @@ -92,6 +98,19 @@ public interface ImGroupMemberService { | ||
| 92 | ImGroupMemberDO addGroupMember(Long groupId, Long userId); | 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 | * @param groupId 群编号 | 116 | * @param groupId 群编号 |
| @@ -119,6 +138,24 @@ public interface ImGroupMemberService { | @@ -119,6 +138,24 @@ public interface ImGroupMemberService { | ||
| 119 | void updateGroupMember(Long userId, @Valid ImGroupMemberUpdateReqVO updateReqVO); | 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 | * 移除指定群成员(设置为 DISABLE 状态) | 159 | * 移除指定群成员(设置为 DISABLE 状态) |
| 123 | * <p> | 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 +6,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | ||
| 6 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; | 6 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; |
| 7 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | 7 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; |
| 8 | import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; | 8 | import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; |
| 9 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; | ||
| 9 | import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; | 10 | import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; |
| 10 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; | 11 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; |
| 11 | import jakarta.annotation.Resource; | 12 | import jakarta.annotation.Resource; |
| @@ -51,6 +52,14 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | @@ -51,6 +52,14 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | ||
| 51 | } | 52 | } |
| 52 | 53 | ||
| 53 | @Override | 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 | public List<ImGroupMemberDO> getGroupMemberListByGroupId(Long groupId) { | 63 | public List<ImGroupMemberDO> getGroupMemberListByGroupId(Long groupId) { |
| 55 | return groupMemberMapper.selectListByGroupId(groupId); | 64 | return groupMemberMapper.selectListByGroupId(groupId); |
| 56 | } | 65 | } |
| @@ -88,27 +97,37 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | @@ -88,27 +97,37 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | ||
| 88 | return groupMemberMapper.selectQuitListByUserId(userId, minQuitTime); | 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 | * 并发安全:依靠 im_group_member 表的唯一索引 uk_im_group_member_group_user(group_id, user_id) 保证幂等, | 106 | * 并发安全:依靠 im_group_member 表的唯一索引 uk_im_group_member_group_user(group_id, user_id) 保证幂等, |
| 93 | * 当并发 insert 触发 {@link DuplicateKeyException} 时降级为 select + update。 | 107 | * 当并发 insert 触发 {@link DuplicateKeyException} 时降级为 select + update。 |
| 108 | + * <p> | ||
| 109 | + * 重置旧成员行时强制重置 role,避免离群期间残留管理员身份在重新入群后被复用。 | ||
| 94 | */ | 110 | */ |
| 95 | @Override | 111 | @Override |
| 96 | @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") | 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 | LocalDateTime now = LocalDateTime.now(); | 114 | LocalDateTime now = LocalDateTime.now(); |
| 99 | - // 情况一:已存在记录 → 复活或跳过 | 115 | + // 情况一:已存在记录 → 重置或跳过 |
| 100 | ImGroupMemberDO exists = groupMemberMapper.selectByGroupIdAndUserId(groupId, userId); | 116 | ImGroupMemberDO exists = groupMemberMapper.selectByGroupIdAndUserId(groupId, userId); |
| 101 | if (exists != null) { | 117 | if (exists != null) { |
| 102 | if (CommonStatusEnum.isDisable(exists.getStatus())) { | 118 | if (CommonStatusEnum.isDisable(exists.getStatus())) { |
| 103 | groupMemberMapper.updateById(new ImGroupMemberDO().setId(exists.getId()) | 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 | ImGroupMemberDO member = new ImGroupMemberDO() | 127 | ImGroupMemberDO member = new ImGroupMemberDO() |
| 110 | .setGroupId(groupId).setUserId(userId) | 128 | .setGroupId(groupId).setUserId(userId) |
| 111 | - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now); | 129 | + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now) |
| 130 | + .setRole(role); | ||
| 112 | try { | 131 | try { |
| 113 | groupMemberMapper.insert(member); | 132 | groupMemberMapper.insert(member); |
| 114 | return member; | 133 | return member; |
| @@ -123,6 +142,7 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | @@ -123,6 +142,7 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | ||
| 123 | @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") | 142 | @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") |
| 124 | public void addGroupMembers(Long groupId, Collection<Long> userIds) { | 143 | public void addGroupMembers(Long groupId, Collection<Long> userIds) { |
| 125 | LocalDateTime now = LocalDateTime.now(); | 144 | LocalDateTime now = LocalDateTime.now(); |
| 145 | + Integer role = ImGroupMemberRoleEnum.NORMAL.getRole(); | ||
| 126 | // 1.1 查询已有记录(含已退群的 DISABLE 记录) | 146 | // 1.1 查询已有记录(含已退群的 DISABLE 记录) |
| 127 | List<ImGroupMemberDO> existMembers = groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds); | 147 | List<ImGroupMemberDO> existMembers = groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds); |
| 128 | Map<Long, ImGroupMemberDO> existMap = convertMap(existMembers, ImGroupMemberDO::getUserId); | 148 | Map<Long, ImGroupMemberDO> existMap = convertMap(existMembers, ImGroupMemberDO::getUserId); |
| @@ -133,10 +153,10 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | @@ -133,10 +153,10 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | ||
| 133 | ImGroupMemberDO exist = existMap.get(userId); | 153 | ImGroupMemberDO exist = existMap.get(userId); |
| 134 | if (exist == null) { | 154 | if (exist == null) { |
| 135 | inserts.add(new ImGroupMemberDO().setGroupId(groupId).setUserId(userId) | 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 | } else if (CommonStatusEnum.DISABLE.getStatus().equals(exist.getStatus())) { | 157 | } else if (CommonStatusEnum.DISABLE.getStatus().equals(exist.getStatus())) { |
| 138 | updates.add(new ImGroupMemberDO().setId(exist.getId()) | 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,6 +188,20 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | ||
| 168 | } | 188 | } |
| 169 | 189 | ||
| 170 | @Override | 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 | public void updateGroupMember(Long userId, ImGroupMemberUpdateReqVO updateReqVO) { | 205 | public void updateGroupMember(Long userId, ImGroupMemberUpdateReqVO updateReqVO) { |
| 172 | // 1. 校验是群的有效成员 | 206 | // 1. 校验是群的有效成员 |
| 173 | ImGroupMemberDO member = validateMemberInGroup(updateReqVO.getGroupId(), userId); | 207 | ImGroupMemberDO member = validateMemberInGroup(updateReqVO.getGroupId(), userId); |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java
| 1 | package cn.iocoder.yudao.module.im.service.group; | 1 | package cn.iocoder.yudao.module.im.service.group; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 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 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; | 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 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; | 8 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; |
| 6 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; | 9 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; |
| 7 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; | 10 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; |
| @@ -80,16 +83,6 @@ public interface ImGroupService { | @@ -80,16 +83,6 @@ public interface ImGroupService { | ||
| 80 | ImGroupDO validateGroupExists(Long groupId); | 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 | * <p> | 87 | * <p> |
| 95 | * 返回用户当前仍有效的群,以及最近 {@link cn.iocoder.yudao.module.im.enums.ImCommonConstants#MESSAGE_GROUP_PULL_MAX_DAYS} | 88 | * 返回用户当前仍有效的群,以及最近 {@link cn.iocoder.yudao.module.im.enums.ImCommonConstants#MESSAGE_GROUP_PULL_MAX_DAYS} |
| @@ -128,13 +121,39 @@ public interface ImGroupService { | @@ -128,13 +121,39 @@ public interface ImGroupService { | ||
| 128 | /** | 121 | /** |
| 129 | * 移除群成员(踢人) | 122 | * 移除群成员(踢人) |
| 130 | * <p> | 123 | * <p> |
| 131 | - * 仅群主可执行,且不能移除自己,支持批量 | 124 | + * 群主可踢管理员和普通成员;管理员仅能踢普通成员;群主不可被踢。 |
| 132 | * | 125 | * |
| 133 | - * @param userId 当前登录用户编号(群主) | 126 | + * @param userId 当前登录用户编号 |
| 134 | * @param removeReqVO 移除信息 | 127 | * @param removeReqVO 移除信息 |
| 135 | */ | 128 | */ |
| 136 | void removeGroupMember(Long userId, @Valid ImGroupMemberRemoveReqVO removeReqVO); | 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,6 +12,9 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | ||
| 12 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; | 12 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; |
| 13 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; | 13 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; |
| 14 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; | 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 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; | 18 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; |
| 16 | import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO; | 19 | import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO; |
| 17 | import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO; | 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,6 +22,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; | ||
| 19 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; | 22 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; |
| 20 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | 23 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; |
| 21 | import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; | 24 | import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; |
| 25 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; | ||
| 22 | import cn.iocoder.yudao.module.im.service.friend.ImFriendService; | 26 | import cn.iocoder.yudao.module.im.service.friend.ImFriendService; |
| 23 | import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; | 27 | import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; |
| 24 | import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; | 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,7 +48,6 @@ import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP; | ||
| 44 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; | 48 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; |
| 45 | import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.*; | 49 | import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.*; |
| 46 | 50 | ||
| 47 | -// TODO @芋艿:群主的调整,暂时不考虑。后续在弄; | ||
| 48 | /** | 51 | /** |
| 49 | * 群 Service 实现类 | 52 | * 群 Service 实现类 |
| 50 | * | 53 | * |
| @@ -82,8 +85,8 @@ public class ImGroupServiceImpl implements ImGroupService { | @@ -82,8 +85,8 @@ public class ImGroupServiceImpl implements ImGroupService { | ||
| 82 | .setOwnerUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); | 85 | .setOwnerUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); |
| 83 | groupMapper.insert(group); | 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 | // 3. 推送群创建事件给群主(多端同步) | 91 | // 3. 推送群创建事件给群主(多端同步) |
| 89 | webSocketService.sendGroupMessageAsync(userId, | 92 | webSocketService.sendGroupMessageAsync(userId, |
| @@ -207,26 +210,126 @@ public class ImGroupServiceImpl implements ImGroupService { | @@ -207,26 +210,126 @@ public class ImGroupServiceImpl implements ImGroupService { | ||
| 207 | @Override | 210 | @Override |
| 208 | public void removeGroupMember(Long userId, ImGroupMemberRemoveReqVO removeReqVO) { | 211 | public void removeGroupMember(Long userId, ImGroupMemberRemoveReqVO removeReqVO) { |
| 209 | Long groupId = removeReqVO.getGroupId(); | 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 | throw exception(GROUP_CANNOT_REMOVE_SELF); | 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 | ImGroupMessageDTO.ofGroupDelete(userId, groupId)); | 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 | @Override | 335 | @Override |
| @@ -244,8 +347,11 @@ public class ImGroupServiceImpl implements ImGroupService { | @@ -244,8 +347,11 @@ public class ImGroupServiceImpl implements ImGroupService { | ||
| 244 | return group; | 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 | ImGroupDO group = validateGroupExists(groupId); | 355 | ImGroupDO group = validateGroupExists(groupId); |
| 250 | if (ObjUtil.notEqual(group.getOwnerUserId(), userId)) { | 356 | if (ObjUtil.notEqual(group.getOwnerUserId(), userId)) { |
| 251 | throw exception(GROUP_NOT_OWNER); | 357 | throw exception(GROUP_NOT_OWNER); |
| @@ -253,6 +359,18 @@ public class ImGroupServiceImpl implements ImGroupService { | @@ -253,6 +359,18 @@ public class ImGroupServiceImpl implements ImGroupService { | ||
| 253 | return group; | 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 | @Override | 374 | @Override |
| 257 | @Cacheable(cacheNames = GROUP, key = "#id", unless = "#result == null") | 375 | @Cacheable(cacheNames = GROUP, key = "#id", unless = "#result == null") |
| 258 | public ImGroupDO getGroup(Long id) { | 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 +6,7 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | ||
| 6 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; | 6 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; |
| 7 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | 7 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; |
| 8 | import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; | 8 | import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; |
| 9 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; | ||
| 9 | import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; | 10 | import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; |
| 10 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; | 11 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; |
| 11 | import org.junit.jupiter.api.Test; | 12 | import org.junit.jupiter.api.Test; |
| @@ -61,7 +62,6 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | @@ -61,7 +62,6 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | ||
| 61 | ImGroupMemberDO exists = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) | 62 | ImGroupMemberDO exists = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) |
| 62 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); | 63 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); |
| 63 | when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(exists); | 64 | when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(exists); |
| 64 | - when(groupMemberMapper.selectById(50L)).thenReturn(exists); | ||
| 65 | 65 | ||
| 66 | ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); | 66 | ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); |
| 67 | 67 | ||
| @@ -72,13 +72,11 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | @@ -72,13 +72,11 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | ||
| 72 | 72 | ||
| 73 | @Test | 73 | @Test |
| 74 | public void testAddGroupMember_existingDisabledRecovers() { | 74 | public void testAddGroupMember_existingDisabledRecovers() { |
| 75 | - // 准备:已存在且 DISABLE,应更新为 ENABLE | 75 | + // 准备:已存在且 DISABLE,应重置为 ENABLE 并重置 role 为 MEMBER |
| 76 | ImGroupMemberDO exists = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) | 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 | when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(exists); | 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 | ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); | 81 | ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); |
| 84 | 82 | ||
| @@ -86,8 +84,10 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | @@ -86,8 +84,10 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | ||
| 86 | verify(groupMemberMapper).updateById(captor.capture()); | 84 | verify(groupMemberMapper).updateById(captor.capture()); |
| 87 | assertEquals(50L, captor.getValue().getId()); | 85 | assertEquals(50L, captor.getValue().getId()); |
| 88 | assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); | 86 | assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); |
| 87 | + assertEquals(ImGroupMemberRoleEnum.NORMAL.getRole(), captor.getValue().getRole()); | ||
| 89 | assertNotNull(captor.getValue().getJoinTime()); | 88 | assertNotNull(captor.getValue().getJoinTime()); |
| 90 | assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); | 89 | assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); |
| 90 | + assertEquals(ImGroupMemberRoleEnum.NORMAL.getRole(), result.getRole()); | ||
| 91 | } | 91 | } |
| 92 | 92 | ||
| 93 | @Test | 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,7 +4,10 @@ import cn.hutool.extra.spring.SpringUtil; | ||
| 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 5 | import cn.iocoder.yudao.framework.common.exception.ServiceException; | 5 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 6 | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | 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 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; | 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 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; | 11 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; |
| 9 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; | 12 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; |
| 10 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; | 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,6 +15,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; | ||
| 12 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; | 15 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; |
| 13 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | 16 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; |
| 14 | 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.ImGroupMemberRoleEnum; | ||
| 15 | import cn.iocoder.yudao.module.im.service.friend.ImFriendService; | 19 | import cn.iocoder.yudao.module.im.service.friend.ImFriendService; |
| 16 | import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; | 20 | import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; |
| 17 | import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; | 21 | import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; |
| @@ -29,6 +33,7 @@ import java.util.ArrayList; | @@ -29,6 +33,7 @@ import java.util.ArrayList; | ||
| 29 | import java.util.HashMap; | 33 | import java.util.HashMap; |
| 30 | import java.util.List; | 34 | import java.util.List; |
| 31 | import java.util.Map; | 35 | import java.util.Map; |
| 36 | +import java.util.Set; | ||
| 32 | 37 | ||
| 33 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; | 38 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; |
| 34 | import static org.junit.jupiter.api.Assertions.*; | 39 | import static org.junit.jupiter.api.Assertions.*; |
| @@ -78,8 +83,8 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { | @@ -78,8 +83,8 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { | ||
| 78 | assertEquals(100L, result.getId()); | 83 | assertEquals(100L, result.getId()); |
| 79 | assertEquals(1L, result.getOwnerUserId()); | 84 | assertEquals(1L, result.getOwnerUserId()); |
| 80 | assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); | 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 | verify(webSocketService).sendGroupMessageAsync(eq(1L), any(ImGroupMessageDTO.class)); | 88 | verify(webSocketService).sendGroupMessageAsync(eq(1L), any(ImGroupMessageDTO.class)); |
| 84 | } | 89 | } |
| 85 | 90 | ||
| @@ -394,6 +399,9 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { | @@ -394,6 +399,9 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { | ||
| 394 | ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | 399 | ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) |
| 395 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); | 400 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); |
| 396 | when(groupMapper.selectById(10L)).thenReturn(group); | 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 | ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); | 406 | ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); |
| 399 | reqVO.setGroupId(10L); | 407 | reqVO.setGroupId(10L); |
| @@ -414,6 +422,16 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { | @@ -414,6 +422,16 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { | ||
| 414 | ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | 422 | ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) |
| 415 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); | 423 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); |
| 416 | when(groupMapper.selectById(10L)).thenReturn(group); | 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 | ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); | 436 | ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); |
| 419 | reqVO.setGroupId(10L); | 437 | reqVO.setGroupId(10L); |
| @@ -428,6 +446,293 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { | @@ -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 | // ========== validateGroupExists ========== | 736 | // ========== validateGroupExists ========== |
| 432 | 737 | ||
| 433 | @Test | 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,9 +62,10 @@ CREATE TABLE IF NOT EXISTS "im_group_member" ( | ||
| 62 | "group_id" bigint NOT NULL COMMENT '群编号', | 62 | "group_id" bigint NOT NULL COMMENT '群编号', |
| 63 | "user_id" bigint NOT NULL COMMENT '用户编号', | 63 | "user_id" bigint NOT NULL COMMENT '用户编号', |
| 64 | "display_user_name" varchar(64) DEFAULT NULL COMMENT '组内显示名', | 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 | "muted" bit DEFAULT FALSE COMMENT '是否免打扰', | 66 | "muted" bit DEFAULT FALSE COMMENT '是否免打扰', |
| 67 | "status" tinyint NOT NULL COMMENT '成员状态', | 67 | "status" tinyint NOT NULL COMMENT '成员状态', |
| 68 | + "role" tinyint NOT NULL DEFAULT 3 COMMENT '成员角色:1=群主 2=管理员 3=普通成员', | ||
| 68 | "join_time" timestamp DEFAULT NULL COMMENT '入群时间', | 69 | "join_time" timestamp DEFAULT NULL COMMENT '入群时间', |
| 69 | "quit_time" timestamp DEFAULT NULL COMMENT '退群时间', | 70 | "quit_time" timestamp DEFAULT NULL COMMENT '退群时间', |
| 70 | "creator" varchar(64) DEFAULT '', | 71 | "creator" varchar(64) DEFAULT '', |