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