Commit 1c29398da594acd50c987b11b11c9d69e4c6e80f

Authored by YunaiV
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&lt;ImGroupMemberDO&gt; { @@ -83,6 +83,20 @@ public interface ImGroupMemberMapper extends BaseMapperX&lt;ImGroupMemberDO&gt; {
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 &quot;im_group_member&quot; ( @@ -62,9 +62,10 @@ CREATE TABLE IF NOT EXISTS &quot;im_group_member&quot; (
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 '',