diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java index 4867775..7063f6b 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java +++ b/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; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupRespVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; @@ -14,7 +17,6 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -36,7 +38,6 @@ public class ImGroupController { @PostMapping("/create") @Operation(summary = "创建群") - @PreAuthorize("@ss.hasPermission('im:group:create')") public CommonResult createGroup(@Valid @RequestBody ImGroupCreateReqVO createReqVO) { ImGroupDO group = groupService.createGroup(createReqVO, getLoginUserId()); return success(BeanUtils.toBean(group, ImGroupRespVO.class)); @@ -44,7 +45,6 @@ public class ImGroupController { @PutMapping("/update") @Operation(summary = "更新群") - @PreAuthorize("@ss.hasPermission('im:group:update')") public CommonResult updateGroup(@Valid @RequestBody ImGroupUpdateReqVO updateReqVO) { ImGroupDO group = groupService.updateGroup(updateReqVO, getLoginUserId()); return success(BeanUtils.toBean(group, ImGroupRespVO.class)); @@ -53,7 +53,6 @@ public class ImGroupController { @DeleteMapping("/dissolve") @Operation(summary = "解散群") @Parameter(name = "id", description = "群编号", required = true) - @PreAuthorize("@ss.hasPermission('im:group:delete')") public CommonResult dissolveGroup(@RequestParam("id") Long id) { groupService.dissolveGroup(id, getLoginUserId()); return success(true); @@ -64,7 +63,6 @@ public class ImGroupController { @GetMapping("/get") @Operation(summary = "获得群") @Parameter(name = "id", description = "编号", required = true, example = "1024") - @PreAuthorize("@ss.hasPermission('im:group:query')") public CommonResult getGroup(@RequestParam("id") Long id) { ImGroupDO group = groupService.getGroup(id); return success(BeanUtils.toBean(group, ImGroupRespVO.class)); @@ -81,7 +79,6 @@ public class ImGroupController { @PostMapping("/invite") @Operation(summary = "邀请用户加入群") - @PreAuthorize("@ss.hasPermission('im:group:update')") public CommonResult inviteGroupMember(@Valid @RequestBody ImGroupMemberInviteReqVO inviteReqVO) { groupService.inviteGroupMember(getLoginUserId(), inviteReqVO); return success(true); @@ -90,7 +87,6 @@ public class ImGroupController { @DeleteMapping("/quit") @Operation(summary = "退出群") @Parameter(name = "groupId", description = "群编号", required = true) - @PreAuthorize("@ss.hasPermission('im:group:update')") public CommonResult quitGroup(@RequestParam("groupId") Long groupId) { groupService.quitGroup(groupId, getLoginUserId()); return success(true); @@ -98,10 +94,30 @@ public class ImGroupController { @DeleteMapping("/kicking") @Operation(summary = "移除群成员") - @PreAuthorize("@ss.hasPermission('im:group:update')") public CommonResult removeGroupMember(@Valid @RequestBody ImGroupMemberRemoveReqVO removeReqVO) { groupService.removeGroupMember(getLoginUserId(), removeReqVO); return success(true); } + @PutMapping("/add-admin") + @Operation(summary = "添加群管理员") + public CommonResult addGroupAdmin(@Valid @RequestBody ImGroupAdminAddReqVO reqVO) { + groupService.addGroupAdmin(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/remove-admin") + @Operation(summary = "撤销群管理员") + public CommonResult removeGroupAdmin(@Valid @RequestBody ImGroupAdminRemoveReqVO reqVO) { + groupService.removeGroupAdmin(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/transfer-owner") + @Operation(summary = "转让群主") + public CommonResult transferGroupOwner(@Valid @RequestBody ImGroupTransferOwnerReqVO transferReqVO) { + groupService.transferGroupOwner(getLoginUserId(), transferReqVO); + return success(true); + } + } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java index 22d879d..6dbab64 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java +++ b/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; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -44,7 +43,6 @@ public class ImGroupMemberController { @PutMapping("/update") @Operation(summary = "更新群成员") - @PreAuthorize("@ss.hasPermission('im:group-member:update')") public CommonResult updateGroupMember(@Valid @RequestBody ImGroupMemberUpdateReqVO updateReqVO) { groupMemberService.updateGroupMember(getLoginUserId(), updateReqVO); return success(true); @@ -57,7 +55,6 @@ public class ImGroupMemberController { @Parameter(name = "groupId", description = "群编号(与 userId 配合查)", example = "1"), @Parameter(name = "userId", description = "用户编号(与 groupId 配合查)", example = "100") }) - @PreAuthorize("@ss.hasPermission('im:group-member:query')") public CommonResult getGroupMember(@RequestParam(value = "id", required = false) Long id, @RequestParam(value = "groupId", required = false) Long groupId, @RequestParam(value = "userId", required = false) Long userId) { diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminAddReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminAddReqVO.java new file mode 100644 index 0000000..72b518c --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminAddReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.im.controller.admin.group.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 添加群管理员 Request VO") +@Data +public class ImGroupAdminAddReqVO { + + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279") + @NotNull(message = "群编号不能为空") + private Long groupId; + + @Schema(description = "目标用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[101, 102]") + @NotEmpty(message = "目标用户编号列表不能为空") + private List userIds; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminRemoveReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminRemoveReqVO.java new file mode 100644 index 0000000..3444128 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupAdminRemoveReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.im.controller.admin.group.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 撤销群管理员 Request VO") +@Data +public class ImGroupAdminRemoveReqVO { + + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279") + @NotNull(message = "群编号不能为空") + private Long groupId; + + @Schema(description = "目标用户编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[101, 102]") + @NotEmpty(message = "目标用户编号列表不能为空") + private List userIds; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupTransferOwnerReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupTransferOwnerReqVO.java new file mode 100644 index 0000000..9879099 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupTransferOwnerReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.im.controller.admin.group.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 群主转让 Request VO") +@Data +public class ImGroupTransferOwnerReqVO { + + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279") + @NotNull(message = "群编号不能为空") + private Long groupId; + + @Schema(description = "新群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "202") + @NotNull(message = "新群主用户编号不能为空") + private Long newOwnerUserId; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberRespVO.java index 1827e29..087bf6c 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberRespVO.java +++ b/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 { @Schema(description = "组内显示名", example = "芋艿") private String displayUserName; - @Schema(description = "群显示备注", example = "核心群") - private String displayGroupName; + @Schema(description = "群备注", example = "核心群") + private String groupRemark; @Schema(description = "是否免打扰") private Boolean muted; @@ -30,6 +30,9 @@ public class ImGroupMemberRespVO { @Schema(description = "成员状态", example = "0") private Integer status; + @Schema(description = "成员角色", example = "3") + private Integer role; // 参见 ImGroupMemberRoleEnum 枚举类 + @Schema(description = "入群时间") private LocalDateTime joinTime; diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberUpdateReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberUpdateReqVO.java index 3e31cb9..62c34fe 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberUpdateReqVO.java +++ b/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 { @Schema(description = "群内昵称", example = "芋头") private String displayUserName; - @Schema(description = "群名备注", example = "公司群") - private String displayGroupName; + @Schema(description = "群备注", example = "公司群") + private String groupRemark; @Schema(description = "是否免打扰") private Boolean muted; diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/member/ImGroupMemberManagerRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/member/ImGroupMemberManagerRespVO.java index 2618ffd..5a6f56a 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/vo/member/ImGroupMemberManagerRespVO.java +++ b/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 { @Schema(description = "组内显示名", example = "三哥") private String displayUserName; - @Schema(description = "群显示备注", example = "技术交流群") - private String displayGroupName; + @Schema(description = "群备注", example = "技术交流群") + private String groupRemark; @Schema(description = "是否免打扰", example = "false") private Boolean muted; @@ -30,6 +30,9 @@ public class ImGroupMemberManagerRespVO { @Schema(description = "成员状态", example = "0") private Integer status; // 参见 CommonStatusEnum 枚举类 + @Schema(description = "成员角色;1=群主 2=管理员 3=普通成员", example = "3") + private Integer role; // 参见 ImGroupMemberRoleEnum 枚举类 + @Schema(description = "入群时间") private LocalDateTime joinTime; diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupMemberDO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupMemberDO.java index cd275f9..7af3412 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupMemberDO.java +++ b/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; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -46,9 +47,9 @@ public class ImGroupMemberDO extends BaseDO { */ private String displayUserName; /** - * 群显示备注 + * 群备注 */ - private String displayGroupName; + private String groupRemark; /** * 是否免打扰 */ @@ -60,6 +61,12 @@ public class ImGroupMemberDO extends BaseDO { */ private Integer status; /** + * 成员角色 + *

+ * 枚举 {@link ImGroupMemberRoleEnum} + */ + private Integer role; + /** * 入群时间 */ private LocalDateTime joinTime; diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java index afffef6..2d9f6dd 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java +++ b/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 { .eq(ImGroupMemberDO::getStatus, oldStatus)); } + @SuppressWarnings("UnusedReturnValue") + default int updateListByGroupIdAndUserIds(Long groupId, Collection userIds, ImGroupMemberDO updateObj) { + return update(updateObj, new LambdaQueryWrapperX() + .eq(ImGroupMemberDO::getGroupId, groupId) + .in(ImGroupMemberDO::getUserId, userIds)); + } + + default Long selectCountByGroupIdAndRoleAndStatus(Long groupId, Integer role, Integer status) { + return selectCount(new LambdaQueryWrapperX() + .eq(ImGroupMemberDO::getGroupId, groupId) + .eq(ImGroupMemberDO::getRole, role) + .eq(ImGroupMemberDO::getStatus, status)); + } + /** * 批量按 group_id 统计指定状态的成员数 */ diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java index 32cfbf4..b621d0f 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java @@ -23,6 +23,8 @@ public interface ErrorCodeConstants { ErrorCode GROUP_BANNED = new ErrorCode(1_040_400_001, "群已被封禁"); ErrorCode GROUP_DISSOLVED = new ErrorCode(1_040_400_002, "群已解散"); ErrorCode GROUP_NOT_OWNER = new ErrorCode(1_040_400_003, "仅群主可执行该操作"); + ErrorCode GROUP_NOT_OWNER_OR_ADMIN = new ErrorCode(1_040_400_004, "仅群主或管理员可执行该操作"); + ErrorCode GROUP_TRANSFER_OWNER_TO_SELF = new ErrorCode(1_040_400_005, "不能将群主转让给自己"); // ========== 群成员 (1-040-500-000) ========== ErrorCode GROUP_MEMBER_NOT_IN_GROUP = new ErrorCode(1_040_500_001, "您已不在该群中"); @@ -30,6 +32,11 @@ public interface ErrorCodeConstants { ErrorCode GROUP_CANNOT_REMOVE_SELF = new ErrorCode(1_040_500_004, "不能将自己移出群聊"); ErrorCode GROUP_MEMBER_EXCEED = new ErrorCode(1_040_500_005, "群聊人数不能超过 {} 人"); ErrorCode GROUP_INVITE_NOT_FRIEND = new ErrorCode(1_040_500_006, "'{}' 不是您的好友,邀请失败"); + ErrorCode GROUP_ADMIN_TARGET_NOT_IN_GROUP = new ErrorCode(1_040_500_007, "目标用户已不在该群中"); + ErrorCode GROUP_ADMIN_TARGET_IS_OWNER = new ErrorCode(1_040_500_008, "群主无法被设为或撤销管理员"); + ErrorCode GROUP_ADMIN_MAX_LIMIT = new ErrorCode(1_040_500_009, "群管理员数量不能超过 {} 人"); + ErrorCode GROUP_REMOVE_OWNER_DENIED = new ErrorCode(1_040_500_010, "群主无法被移出群聊"); + ErrorCode GROUP_REMOVE_ADMIN_DENIED = new ErrorCode(1_040_500_011, "管理员无法移出其他管理员,请先由群主撤销其管理员身份"); // ========== 好友 (1-040-600-000) ========== ErrorCode FRIEND_NOT_FRIEND = new ErrorCode(1_040_600_001, "对方不是您的好友"); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java index 4bde0aa..e4f645d 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java @@ -18,6 +18,16 @@ public interface ImCommonConstants { int MAX_GROUP_MEMBER = 500; /** + * 单群管理员人数上限 + */ + int GROUP_ADMIN_MAX_COUNT = 3; + + /** + * 单群置顶消息条数上限 + */ + int GROUP_PIN_MAX_COUNT = 5; + + /** * 群消息 @ 所有人的特殊用户编号 */ long AT_USER_ID_ALL = -1L; diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/group/ImGroupMemberRoleEnum.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/group/ImGroupMemberRoleEnum.java new file mode 100644 index 0000000..48a4e79 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/group/ImGroupMemberRoleEnum.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.im.enums.group; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Objects; + +/** + * IM 群成员角色枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum ImGroupMemberRoleEnum implements ArrayValuable { + + OWNER(1, "群主"), + ADMIN(2, "管理员"), + NORMAL(3, "普通成员"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImGroupMemberRoleEnum::getRole).toArray(Integer[]::new); + + /** + * 角色 + */ + private final Integer role; + /** + * 名字 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static boolean isOwner(Integer role) { + return Objects.equals(OWNER.role, role); + } + + public static boolean isAdmin(Integer role) { + return Objects.equals(ADMIN.role, role); + } + + public static boolean isOwnerOrAdmin(Integer role) { + return isOwner(role) || isAdmin(role); + } + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java index cfb0a4d..9cdebfd 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java @@ -34,6 +34,15 @@ public interface ImGroupMemberService { ImGroupMemberDO getGroupMember(Long groupId, Long userId); /** + * 批量查询群成员(包含所有状态) + * + * @param groupId 群编号 + * @param userIds 用户编号集合 + * @return 群成员列表 + */ + List getGroupMembers(Long groupId, Collection userIds); + + /** * 根据群组 id 查询群成员(包含所有状态) * * @param groupId 群组id @@ -79,10 +88,7 @@ public interface ImGroupMemberService { List getQuitGroupMemberListByUserId(Long userId, LocalDateTime minQuitTime); /** - * 添加群成员(入群) - *

- * 插入一条 ENABLE 状态的群成员记录,并设置 joinTime。 - * 可用于创建群时添加群主、邀请入群等场景。 + * 添加群成员(入群),角色默认 MEMBER * * @param groupId 群编号 * @param userId 用户编号 @@ -92,6 +98,19 @@ public interface ImGroupMemberService { ImGroupMemberDO addGroupMember(Long groupId, Long userId); /** + * 添加群成员(入群),并指定角色 + *

+ * 重置旧成员行也会强制重置 role,避免离群期间残留管理员身份被复用。 + * + * @param groupId 群编号 + * @param userId 用户编号 + * @param role 成员角色,见 {@link cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum} + * @return 群成员记录 + */ + @SuppressWarnings("UnusedReturnValue") + ImGroupMemberDO addGroupMember(Long groupId, Long userId, Integer role); + + /** * 批量添加群成员(入群) * * @param groupId 群编号 @@ -119,6 +138,24 @@ public interface ImGroupMemberService { void updateGroupMember(Long userId, @Valid ImGroupMemberUpdateReqVO updateReqVO); /** + * 批量更新群成员角色 + * + * @param groupId 群编号 + * @param userIds 用户编号集合 + * @param role 新角色,见 {@link cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum} + */ + void updateGroupMemberRole(Long groupId, Collection userIds, Integer role); + + /** + * 统计群内指定 role 的活跃成员数量 + * + * @param groupId 群编号 + * @param role 成员角色 + * @return 数量 + */ + Long getGroupMemberCountByRole(Long groupId, Integer role); + + /** * 移除指定群成员(设置为 DISABLE 状态) *

* 用于退群、踢出场景 diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java index b4697e6..99213ea 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java +++ b/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; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; import jakarta.annotation.Resource; @@ -51,6 +52,14 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { } @Override + public List getGroupMembers(Long groupId, Collection userIds) { + if (CollUtil.isEmpty(userIds)) { + return Collections.emptyList(); + } + return groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds); + } + + @Override public List getGroupMemberListByGroupId(Long groupId) { return groupMemberMapper.selectListByGroupId(groupId); } @@ -88,27 +97,37 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { return groupMemberMapper.selectQuitListByUserId(userId, minQuitTime); } + @Override + public ImGroupMemberDO addGroupMember(Long groupId, Long userId) { + return addGroupMember(groupId, userId, ImGroupMemberRoleEnum.NORMAL.getRole()); + } + /** * 并发安全:依靠 im_group_member 表的唯一索引 uk_im_group_member_group_user(group_id, user_id) 保证幂等, * 当并发 insert 触发 {@link DuplicateKeyException} 时降级为 select + update。 + *

+ * 重置旧成员行时强制重置 role,避免离群期间残留管理员身份在重新入群后被复用。 */ @Override @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") - public ImGroupMemberDO addGroupMember(Long groupId, Long userId) { + public ImGroupMemberDO addGroupMember(Long groupId, Long userId, Integer role) { LocalDateTime now = LocalDateTime.now(); - // 情况一:已存在记录 → 复活或跳过 + // 情况一:已存在记录 → 重置或跳过 ImGroupMemberDO exists = groupMemberMapper.selectByGroupIdAndUserId(groupId, userId); if (exists != null) { if (CommonStatusEnum.isDisable(exists.getStatus())) { groupMemberMapper.updateById(new ImGroupMemberDO().setId(exists.getId()) - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now)); + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now) + .setRole(role)); + exists.setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now).setRole(role); } - return groupMemberMapper.selectById(exists.getId()); + return exists; } // 情况二:新增成员 ImGroupMemberDO member = new ImGroupMemberDO() .setGroupId(groupId).setUserId(userId) - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now); + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now) + .setRole(role); try { groupMemberMapper.insert(member); return member; @@ -123,6 +142,7 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") public void addGroupMembers(Long groupId, Collection userIds) { LocalDateTime now = LocalDateTime.now(); + Integer role = ImGroupMemberRoleEnum.NORMAL.getRole(); // 1.1 查询已有记录(含已退群的 DISABLE 记录) List existMembers = groupMemberMapper.selectListByGroupIdAndUserIds(groupId, userIds); Map existMap = convertMap(existMembers, ImGroupMemberDO::getUserId); @@ -133,10 +153,10 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { ImGroupMemberDO exist = existMap.get(userId); if (exist == null) { inserts.add(new ImGroupMemberDO().setGroupId(groupId).setUserId(userId) - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now)); + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setRole(role).setJoinTime(now)); } else if (CommonStatusEnum.DISABLE.getStatus().equals(exist.getStatus())) { updates.add(new ImGroupMemberDO().setId(exist.getId()) - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now)); + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setRole(role).setJoinTime(now)); } } @@ -168,6 +188,20 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { } @Override + public void updateGroupMemberRole(Long groupId, Collection userIds, Integer role) { + if (CollUtil.isEmpty(userIds)) { + return; + } + groupMemberMapper.updateListByGroupIdAndUserIds(groupId, userIds, new ImGroupMemberDO().setRole(role)); + } + + @Override + public Long getGroupMemberCountByRole(Long groupId, Integer role) { + return groupMemberMapper.selectCountByGroupIdAndRoleAndStatus( + groupId, role, CommonStatusEnum.ENABLE.getStatus()); + } + + @Override public void updateGroupMember(Long userId, ImGroupMemberUpdateReqVO updateReqVO) { // 1. 校验是群的有效成员 ImGroupMemberDO member = validateMemberInGroup(updateReqVO.getGroupId(), userId); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java index 281cb64..dcffda1 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java @@ -1,7 +1,10 @@ package cn.iocoder.yudao.module.im.service.group; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; @@ -80,16 +83,6 @@ public interface ImGroupService { ImGroupDO validateGroupExists(Long groupId); /** - * 校验当前用户是否为群主 - * - * @param groupId 群编号 - * @param userId 用户编号 - * @return 群信息 - */ - @SuppressWarnings("UnusedReturnValue") - ImGroupDO validateGroupOwner(Long groupId, Long userId); - - /** * 获取指定用户的群列表 *

* 返回用户当前仍有效的群,以及最近 {@link cn.iocoder.yudao.module.im.enums.ImCommonConstants#MESSAGE_GROUP_PULL_MAX_DAYS} @@ -128,13 +121,39 @@ public interface ImGroupService { /** * 移除群成员(踢人) *

- * 仅群主可执行,且不能移除自己,支持批量 + * 群主可踢管理员和普通成员;管理员仅能踢普通成员;群主不可被踢。 * - * @param userId 当前登录用户编号(群主) + * @param userId 当前登录用户编号 * @param removeReqVO 移除信息 */ void removeGroupMember(Long userId, @Valid ImGroupMemberRemoveReqVO removeReqVO); + /** + * 添加群管理员(仅群主可执行) + * + * @param userId 当前登录用户编号(群主) + * @param reqVO 添加信息(含群编号、目标用户编号列表) + */ + void addGroupAdmin(Long userId, @Valid ImGroupAdminAddReqVO reqVO); + + /** + * 撤销群管理员(仅群主可执行) + * + * @param userId 当前登录用户编号(群主) + * @param reqVO 撤销信息(含群编号、目标用户编号列表) + */ + void removeGroupAdmin(Long userId, @Valid ImGroupAdminRemoveReqVO reqVO); + + /** + * 转让群主(仅老群主可执行) + *

+ * 转让后:旧群主 role 降为 MEMBER,新群主 role 升为 OWNER + * + * @param userId 当前登录用户编号(旧群主) + * @param transferReqVO 转让信息 + */ + void transferGroupOwner(Long userId, @Valid ImGroupTransferOwnerReqVO transferReqVO); + // ==================== 管理后台 ==================== /** diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java index ea8e0e1..175fdbc 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java +++ b/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; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO; 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; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; import cn.iocoder.yudao.module.im.service.friend.ImFriendService; import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; @@ -44,7 +48,6 @@ import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.*; -// TODO @芋艿:群主的调整,暂时不考虑。后续在弄; /** * 群 Service 实现类 * @@ -82,8 +85,8 @@ public class ImGroupServiceImpl implements ImGroupService { .setOwnerUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); groupMapper.insert(group); - // 2. 将群主插入为群成员 - groupMemberService.addGroupMember(group.getId(), userId); + // 2. 将群主插入为群成员;显式 setRole(OWNER) 避免依赖 DDL DEFAULT 把群主行误认成 MEMBER + groupMemberService.addGroupMember(group.getId(), userId, ImGroupMemberRoleEnum.OWNER.getRole()); // 3. 推送群创建事件给群主(多端同步) webSocketService.sendGroupMessageAsync(userId, @@ -207,26 +210,126 @@ public class ImGroupServiceImpl implements ImGroupService { @Override public void removeGroupMember(Long userId, ImGroupMemberRemoveReqVO removeReqVO) { Long groupId = removeReqVO.getGroupId(); - // 1. 校验群存在 + 当前用户是群主 - validateGroupOwner(groupId, userId); - // 2. 不能移除自己 - if (removeReqVO.getMemberUserIds().contains(userId)) { + Set targetUserIds = new HashSet<>(removeReqVO.getMemberUserIds()); + // 1.1 校验群存在 + 操作者是群主或管理员 + ImGroupMemberDO operator = validateGroupOwnerOrAdmin(groupId, userId); + // 1.2 不能移除自己 + if (targetUserIds.contains(userId)) { throw exception(GROUP_CANNOT_REMOVE_SELF); } + // 1.3 三档权限校验:群主不可被移出;管理员不能移出管理员 + List targets = groupMemberService.getGroupMembers(groupId, targetUserIds); + boolean operatorIsAdmin = ImGroupMemberRoleEnum.isAdmin(operator.getRole()); + for (ImGroupMemberDO target : targets) { + if (ImGroupMemberRoleEnum.isOwner(target.getRole())) { + throw exception(GROUP_REMOVE_OWNER_DENIED); + } + if (operatorIsAdmin && ImGroupMemberRoleEnum.isAdmin(target.getRole())) { + throw exception(GROUP_REMOVE_ADMIN_DENIED); + } + } - // 3.1 批量移除群成员 - Set memberUserIds = new HashSet<>(removeReqVO.getMemberUserIds()); - groupMemberService.removeGroupMembers(groupId, memberUserIds); - // 3.2 批量清理已读缓存 - groupMessageService.deleteReadMaxMessageIds(groupId, memberUserIds); + // 2.1 批量移除群成员 + groupMemberService.removeGroupMembers(groupId, targetUserIds); + // 2.2 批量清理已读缓存 + groupMessageService.deleteReadMaxMessageIds(groupId, targetUserIds); - // 4.1 发送踢出提示消息(TIP_TEXT) - groupMessageService.sendTipGroupMessage(userId, groupId, memberUserIds, GROUP_REMOVE_TIP_MESSAGE); - // 4.2 推送群删除事件 - webSocketService.sendGroupMessageAsync(memberUserIds, + // 3.1 发送移出提示消息(TIP_TEXT) + groupMessageService.sendTipGroupMessage(userId, groupId, targetUserIds, GROUP_REMOVE_TIP_MESSAGE); + // 3.2 推送群删除事件 + webSocketService.sendGroupMessageAsync(targetUserIds, ImGroupMessageDTO.ofGroupDelete(userId, groupId)); } + @Override + @Transactional(rollbackFor = Exception.class) + public void addGroupAdmin(Long userId, ImGroupAdminAddReqVO reqVO) { + Long groupId = reqVO.getGroupId(); + Set targetUserIds = new HashSet<>(reqVO.getUserIds()); + // 1.1 仅群主可操作 + validateGroupOwner(groupId, userId); + // 1.2 校验目标都是有效成员且非群主 + Map targetMap = convertMap( + groupMemberService.getGroupMembers(groupId, targetUserIds), ImGroupMemberDO::getUserId); + validateAdminTargets(targetUserIds, targetMap); + // 1.3 幂等过滤:跳过已是 ADMIN + Set changedUserIds = convertSet(targetUserIds, + id -> id, + id -> !ImGroupMemberRoleEnum.isAdmin(targetMap.get(id).getRole())); + if (CollUtil.isEmpty(changedUserIds)) { + return; + } + // 1.4 校验上限 + Long existAdminCount = groupMemberService.getGroupMemberCountByRole( + groupId, ImGroupMemberRoleEnum.ADMIN.getRole()); + if (existAdminCount + changedUserIds.size() > GROUP_ADMIN_MAX_COUNT) { + throw exception(GROUP_ADMIN_MAX_LIMIT, GROUP_ADMIN_MAX_COUNT); + } + + // 2. 批量更新角色 + groupMemberService.updateGroupMemberRole(groupId, changedUserIds, ImGroupMemberRoleEnum.ADMIN.getRole()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeGroupAdmin(Long userId, ImGroupAdminRemoveReqVO reqVO) { + Long groupId = reqVO.getGroupId(); + Set targetUserIds = new HashSet<>(reqVO.getUserIds()); + // 1.1 仅群主可操作 + validateGroupOwner(groupId, userId); + // 1.2 校验目标都是有效成员且非群主 + Map targetMap = convertMap( + groupMemberService.getGroupMembers(groupId, targetUserIds), ImGroupMemberDO::getUserId); + validateAdminTargets(targetUserIds, targetMap); + // 1.3 幂等过滤:跳过已是 MEMBER + Set changedUserIds = convertSet(targetUserIds, + id -> id, + id -> ImGroupMemberRoleEnum.isAdmin(targetMap.get(id).getRole())); + if (CollUtil.isEmpty(changedUserIds)) { + return; + } + + // 2. 批量更新角色 + groupMemberService.updateGroupMemberRole(groupId, changedUserIds, ImGroupMemberRoleEnum.NORMAL.getRole()); + } + + /** + * 校验管理员变更目标都是当前群的有效成员(status=ENABLE)且非群主 + */ + private void validateAdminTargets(Set targetUserIds, Map targetMap) { + for (Long targetUserId : targetUserIds) { + ImGroupMemberDO target = targetMap.get(targetUserId); + if (target == null || CommonStatusEnum.DISABLE.getStatus().equals(target.getStatus())) { + throw exception(GROUP_ADMIN_TARGET_NOT_IN_GROUP); + } + if (ImGroupMemberRoleEnum.isOwner(target.getRole())) { + throw exception(GROUP_ADMIN_TARGET_IS_OWNER); + } + } + } + + @Override + @CacheEvict(cacheNames = GROUP, key = "#transferReqVO.groupId") + @Transactional(rollbackFor = Exception.class) + public void transferGroupOwner(Long userId, ImGroupTransferOwnerReqVO transferReqVO) { + Long groupId = transferReqVO.getGroupId(); + Long newOwnerUserId = transferReqVO.getNewOwnerUserId(); + // 1.1 仅老群主可执行 + validateGroupOwner(groupId, userId); + // 1.2 不能转让给自己 + if (ObjUtil.equal(userId, newOwnerUserId)) { + throw exception(GROUP_TRANSFER_OWNER_TO_SELF); + } + // 1.3 新群主必须是群的有效成员 + ImGroupMemberDO newOwner = groupMemberService.validateMemberInGroup(groupId, newOwnerUserId); + + // 2.1 更新群表 owner_user_id + groupMapper.updateById(new ImGroupDO().setId(groupId).setOwnerUserId(newOwnerUserId)); + // 2.2 旧群主 role → MEMBER;新群主 role → OWNER + groupMemberService.updateGroupMemberRole(groupId, Set.of(userId), ImGroupMemberRoleEnum.NORMAL.getRole()); + groupMemberService.updateGroupMemberRole(groupId, Set.of(newOwner.getUserId()), ImGroupMemberRoleEnum.OWNER.getRole()); + } + // ==================== 群的读操作 ==================== @Override @@ -244,8 +347,11 @@ public class ImGroupServiceImpl implements ImGroupService { return group; } - @Override - public ImGroupDO validateGroupOwner(Long groupId, Long userId) { + /** + * 校验当前用户是群主,否则抛 GROUP_NOT_OWNER + */ + @SuppressWarnings("UnusedReturnValue") + private ImGroupDO validateGroupOwner(Long groupId, Long userId) { ImGroupDO group = validateGroupExists(groupId); if (ObjUtil.notEqual(group.getOwnerUserId(), userId)) { throw exception(GROUP_NOT_OWNER); @@ -253,6 +359,18 @@ public class ImGroupServiceImpl implements ImGroupService { return group; } + /** + * 校验当前用户是群主或管理员,否则抛 GROUP_NOT_OWNER_OR_ADMIN + */ + private ImGroupMemberDO validateGroupOwnerOrAdmin(Long groupId, Long userId) { + validateGroupExists(groupId); + ImGroupMemberDO member = groupMemberService.validateMemberInGroup(groupId, userId); + if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())) { + throw exception(GROUP_NOT_OWNER_OR_ADMIN); + } + return member; + } + @Override @Cacheable(cacheNames = GROUP, key = "#id", unless = "#result == null") public ImGroupDO getGroup(Long id) { diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImplTest.java index 37a7250..82da567 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImplTest.java +++ b/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; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; import org.junit.jupiter.api.Test; @@ -61,7 +62,6 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { ImGroupMemberDO exists = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(exists); - when(groupMemberMapper.selectById(50L)).thenReturn(exists); ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); @@ -72,13 +72,11 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { @Test public void testAddGroupMember_existingDisabledRecovers() { - // 准备:已存在且 DISABLE,应更新为 ENABLE + // 准备:已存在且 DISABLE,应重置为 ENABLE 并重置 role 为 MEMBER ImGroupMemberDO exists = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) - .status(CommonStatusEnum.DISABLE.getStatus()).build(); + .status(CommonStatusEnum.DISABLE.getStatus()) + .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build(); when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(exists); - ImGroupMemberDO recovered = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) - .status(CommonStatusEnum.ENABLE.getStatus()).build(); - when(groupMemberMapper.selectById(50L)).thenReturn(recovered); ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); @@ -86,8 +84,10 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { verify(groupMemberMapper).updateById(captor.capture()); assertEquals(50L, captor.getValue().getId()); assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); + assertEquals(ImGroupMemberRoleEnum.NORMAL.getRole(), captor.getValue().getRole()); assertNotNull(captor.getValue().getJoinTime()); assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); + assertEquals(ImGroupMemberRoleEnum.NORMAL.getRole(), result.getRole()); } @Test diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java index a68b897..9e8bef7 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java +++ b/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; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; 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; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; import cn.iocoder.yudao.module.im.service.friend.ImFriendService; import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; @@ -29,6 +33,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; @@ -78,8 +83,8 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { assertEquals(100L, result.getId()); assertEquals(1L, result.getOwnerUserId()); assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); - // 验证:群主加入群 + 推送群创建事件 - verify(groupMemberService).addGroupMember(100L, 1L); + // 验证:群主加入群(带 OWNER role)+ 推送群创建事件 + verify(groupMemberService).addGroupMember(100L, 1L, ImGroupMemberRoleEnum.OWNER.getRole()); verify(webSocketService).sendGroupMessageAsync(eq(1L), any(ImGroupMessageDTO.class)); } @@ -394,6 +399,9 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupMapper.selectById(10L)).thenReturn(group); + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( + ImGroupMemberDO.builder().groupId(10L).userId(1L) + .role(ImGroupMemberRoleEnum.OWNER.getRole()).build()); ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); reqVO.setGroupId(10L); @@ -414,6 +422,16 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupMapper.selectById(10L)).thenReturn(group); + // 操作者:群主 + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( + ImGroupMemberDO.builder().groupId(10L).userId(1L) + .role(ImGroupMemberRoleEnum.OWNER.getRole()).build()); + // 目标:两个普通成员 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( + ImGroupMemberDO.builder().groupId(10L).userId(2L) + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build(), + ImGroupMemberDO.builder().groupId(10L).userId(3L) + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build())); ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); reqVO.setGroupId(10L); @@ -428,6 +446,293 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { } } + @Test + public void testRemoveGroupMember_adminCannotRemoveAdmin() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) + .thenReturn(groupService); + + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupMapper.selectById(10L)).thenReturn(group); + // 操作者:管理员 + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( + ImGroupMemberDO.builder().groupId(10L).userId(1L) + .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build()); + // 目标:另一个管理员 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( + ImGroupMemberDO.builder().groupId(10L).userId(2L) + .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build())); + + ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); + reqVO.setGroupId(10L); + reqVO.setMemberUserIds(List.of(2L)); + + ServiceException exception = assertThrows(ServiceException.class, + () -> groupService.removeGroupMember(1L, reqVO)); + assertEquals(GROUP_REMOVE_ADMIN_DENIED.getCode(), exception.getCode()); + verify(groupMemberService, never()).removeGroupMembers(anyLong(), anyCollection()); + } + } + + @Test + public void testRemoveGroupMember_ownerCannotBeRemoved() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) + .thenReturn(groupService); + + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupMapper.selectById(10L)).thenReturn(group); + // 操作者:群主(不能踢自己;这里换成另一个 userId 的群主语义 — 用 ADMIN 操作群主) + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( + ImGroupMemberDO.builder().groupId(10L).userId(1L) + .role(ImGroupMemberRoleEnum.OWNER.getRole()).build()); + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( + ImGroupMemberDO.builder().groupId(10L).userId(99L) + .role(ImGroupMemberRoleEnum.OWNER.getRole()).build())); + + ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); + reqVO.setGroupId(10L); + reqVO.setMemberUserIds(List.of(99L)); + + ServiceException exception = assertThrows(ServiceException.class, + () -> groupService.removeGroupMember(1L, reqVO)); + assertEquals(GROUP_REMOVE_OWNER_DENIED.getCode(), exception.getCode()); + } + } + + // ========== addGroupAdmin ========== + + @Test + public void testAddGroupAdmin_success() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) + .thenReturn(groupService); + + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupMapper.selectById(10L)).thenReturn(group); + // 目标 3 是普通成员 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( + ImGroupMemberDO.builder().userId(3L).status(CommonStatusEnum.ENABLE.getStatus()) + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build())); + // 群里已有 1 个 ADMIN,1 + 1 ≤ 3 不超上限 + when(groupMemberService.getGroupMemberCountByRole(10L, ImGroupMemberRoleEnum.ADMIN.getRole())) + .thenReturn(1L); + + ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO(); + reqVO.setGroupId(10L); + reqVO.setUserIds(List.of(3L)); + + groupService.addGroupAdmin(1L, reqVO); + + verify(groupMemberService).updateGroupMemberRole(eq(10L), argThat((Set ids) -> + ids.size() == 1 && ids.contains(3L)), + eq(ImGroupMemberRoleEnum.ADMIN.getRole())); + } + } + + @Test + public void testAddGroupAdmin_exceedsLimit() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) + .thenReturn(groupService); + + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupMapper.selectById(10L)).thenReturn(group); + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( + ImGroupMemberDO.builder().userId(5L).status(CommonStatusEnum.ENABLE.getStatus()) + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build())); + // 群里已有 3 个 ADMIN(达到上限),再加 1 会超 + when(groupMemberService.getGroupMemberCountByRole(10L, ImGroupMemberRoleEnum.ADMIN.getRole())) + .thenReturn(3L); + + ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO(); + reqVO.setGroupId(10L); + reqVO.setUserIds(List.of(5L)); + + ServiceException exception = assertThrows(ServiceException.class, + () -> groupService.addGroupAdmin(1L, reqVO)); + assertEquals(GROUP_ADMIN_MAX_LIMIT.getCode(), exception.getCode()); + verify(groupMemberService, never()).updateGroupMemberRole(anyLong(), anyCollection(), anyInt()); + } + } + + @Test + public void testAddGroupAdmin_targetIsOwner() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) + .thenReturn(groupService); + + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupMapper.selectById(10L)).thenReturn(group); + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( + ImGroupMemberDO.builder().userId(1L).status(CommonStatusEnum.ENABLE.getStatus()) + .role(ImGroupMemberRoleEnum.OWNER.getRole()).build())); + + ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO(); + reqVO.setGroupId(10L); + reqVO.setUserIds(List.of(1L)); + + ServiceException exception = assertThrows(ServiceException.class, + () -> groupService.addGroupAdmin(1L, reqVO)); + assertEquals(GROUP_ADMIN_TARGET_IS_OWNER.getCode(), exception.getCode()); + } + } + + @Test + public void testAddGroupAdmin_idempotentSkipWhenAlreadyAdmin() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) + .thenReturn(groupService); + + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupMapper.selectById(10L)).thenReturn(group); + // 目标已是 ADMIN:再加无需操作 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( + ImGroupMemberDO.builder().userId(2L).status(CommonStatusEnum.ENABLE.getStatus()) + .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build())); + + ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO(); + reqVO.setGroupId(10L); + reqVO.setUserIds(List.of(2L)); + + groupService.addGroupAdmin(1L, reqVO); + + verify(groupMemberService, never()).updateGroupMemberRole(anyLong(), anyCollection(), anyInt()); + verify(groupMemberService, never()).getGroupMemberCountByRole(anyLong(), anyInt()); + } + } + + // ========== removeGroupAdmin ========== + + @Test + public void testRemoveGroupAdmin_success() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) + .thenReturn(groupService); + + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupMapper.selectById(10L)).thenReturn(group); + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( + ImGroupMemberDO.builder().userId(2L).status(CommonStatusEnum.ENABLE.getStatus()) + .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build())); + + ImGroupAdminRemoveReqVO reqVO = new ImGroupAdminRemoveReqVO(); + reqVO.setGroupId(10L); + reqVO.setUserIds(List.of(2L)); + + groupService.removeGroupAdmin(1L, reqVO); + + verify(groupMemberService).updateGroupMemberRole(eq(10L), argThat((Set ids) -> + ids.size() == 1 && ids.contains(2L)), + eq(ImGroupMemberRoleEnum.NORMAL.getRole())); + } + } + + @Test + public void testRemoveGroupAdmin_idempotentSkipWhenAlreadyMember() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) + .thenReturn(groupService); + + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupMapper.selectById(10L)).thenReturn(group); + // 目标已是 MEMBER:撤销无需操作 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of( + ImGroupMemberDO.builder().userId(2L).status(CommonStatusEnum.ENABLE.getStatus()) + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build())); + + ImGroupAdminRemoveReqVO reqVO = new ImGroupAdminRemoveReqVO(); + reqVO.setGroupId(10L); + reqVO.setUserIds(List.of(2L)); + + groupService.removeGroupAdmin(1L, reqVO); + + verify(groupMemberService, never()).updateGroupMemberRole(anyLong(), anyCollection(), anyInt()); + } + } + + // ========== transferGroupOwner ========== + + @Test + public void testTransferGroupOwner_success() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) + .thenReturn(groupService); + + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupMapper.selectById(10L)).thenReturn(group); + when(groupMemberService.validateMemberInGroup(10L, 2L)).thenReturn( + ImGroupMemberDO.builder().groupId(10L).userId(2L) + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build()); + + ImGroupTransferOwnerReqVO reqVO = new ImGroupTransferOwnerReqVO(); + reqVO.setGroupId(10L); + reqVO.setNewOwnerUserId(2L); + + groupService.transferGroupOwner(1L, reqVO); + + // 群表 owner 切换 + ArgumentCaptor groupCaptor = ArgumentCaptor.forClass(ImGroupDO.class); + verify(groupMapper).updateById(groupCaptor.capture()); + assertEquals(2L, groupCaptor.getValue().getOwnerUserId()); + // 旧群主 → MEMBER;新群主 → OWNER + verify(groupMemberService).updateGroupMemberRole(eq(10L), eq(Set.of(1L)), + eq(ImGroupMemberRoleEnum.NORMAL.getRole())); + verify(groupMemberService).updateGroupMemberRole(eq(10L), eq(Set.of(2L)), + eq(ImGroupMemberRoleEnum.OWNER.getRole())); + } + } + + @Test + public void testTransferGroupOwner_toSelf() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) + .thenReturn(groupService); + + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupMapper.selectById(10L)).thenReturn(group); + + ImGroupTransferOwnerReqVO reqVO = new ImGroupTransferOwnerReqVO(); + reqVO.setGroupId(10L); + reqVO.setNewOwnerUserId(1L); + + ServiceException exception = assertThrows(ServiceException.class, + () -> groupService.transferGroupOwner(1L, reqVO)); + assertEquals(GROUP_TRANSFER_OWNER_TO_SELF.getCode(), exception.getCode()); + verify(groupMapper, never()).updateById(any(ImGroupDO.class)); + } + } + + @Test + public void testTransferGroupOwner_notOwner() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) + .thenReturn(groupService); + + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(); + when(groupMapper.selectById(10L)).thenReturn(group); + + ImGroupTransferOwnerReqVO reqVO = new ImGroupTransferOwnerReqVO(); + reqVO.setGroupId(10L); + reqVO.setNewOwnerUserId(2L); + + ServiceException exception = assertThrows(ServiceException.class, + () -> groupService.transferGroupOwner(1L, reqVO)); + assertEquals(GROUP_NOT_OWNER.getCode(), exception.getCode()); + } + } + // ========== validateGroupExists ========== @Test diff --git a/yudao-module-im/src/test/resources/sql/create_tables.sql b/yudao-module-im/src/test/resources/sql/create_tables.sql index 95dc219..518447a 100644 --- a/yudao-module-im/src/test/resources/sql/create_tables.sql +++ b/yudao-module-im/src/test/resources/sql/create_tables.sql @@ -62,9 +62,10 @@ CREATE TABLE IF NOT EXISTS "im_group_member" ( "group_id" bigint NOT NULL COMMENT '群编号', "user_id" bigint NOT NULL COMMENT '用户编号', "display_user_name" varchar(64) DEFAULT NULL COMMENT '组内显示名', - "display_group_name" varchar(64) DEFAULT NULL COMMENT '群显示备注', + "group_remark" varchar(64) DEFAULT NULL COMMENT '群备注', "muted" bit DEFAULT FALSE COMMENT '是否免打扰', "status" tinyint NOT NULL COMMENT '成员状态', + "role" tinyint NOT NULL DEFAULT 3 COMMENT '成员角色:1=群主 2=管理员 3=普通成员', "join_time" timestamp DEFAULT NULL COMMENT '入群时间', "quit_time" timestamp DEFAULT NULL COMMENT '退群时间', "creator" varchar(64) DEFAULT '',