From 96ae9bbcb08a08dc95d1cf77dc076f65149b2f51 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 5 May 2026 14:44:23 +0800 Subject: [PATCH] feat(IM):群禁言功能(阶段二后端) --- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java | 23 +++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCancelMuteMemberReqVO.java | 19 +++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteAllReqVO.java | 20 ++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteMemberReqVO.java | 26 ++++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java | 3 +++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/member/ImGroupMemberRespVO.java | 3 +++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupDO.java | 4 ++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupMemberDO.java | 6 ++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java | 7 +++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java | 5 +++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java | 12 ++++++++---- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java | 9 +++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java | 12 ++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java | 29 +++++++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java | 23 +++++++++++++++++++++-- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java | 33 +++++++++++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupCancelMutedNotification.java | 13 +++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberCancelMutedNotification.java | 18 ++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberMutedNotification.java | 24 ++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMutedNotification.java | 13 +++++++++++++ yudao-module-im/src/test/resources/sql/create_tables.sql | 2 ++ 22 files changed, 380 insertions(+), 6 deletions(-) create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCancelMuteMemberReqVO.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteAllReqVO.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteMemberReqVO.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupCancelMutedNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberCancelMutedNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberMutedNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMutedNotification.java 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 33cc6e2..cc82205 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 @@ -145,6 +145,29 @@ public class ImGroupController { return success(true); } + // ==================== 群禁言 ==================== + + @PutMapping("/mute-all") + @Operation(summary = "全群禁言 / 取消(群主 / 管理员)") + public CommonResult muteAll(@Valid @RequestBody ImGroupMuteAllReqVO reqVO) { + groupService.muteAll(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/mute-member") + @Operation(summary = "禁言成员") + public CommonResult muteMember(@Valid @RequestBody ImGroupMuteMemberReqVO reqVO) { + groupService.muteMember(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/cancel-mute-member") + @Operation(summary = "取消成员禁言") + public CommonResult cancelMuteMember(@Valid @RequestBody ImGroupCancelMuteMemberReqVO reqVO) { + groupService.cancelMuteMember(getLoginUserId(), reqVO); + return success(true); + } + /** 单群转 VO + 关联回填 pinnedMessages(仅当登录用户是该群有效成员) */ private ImGroupRespVO buildGroupRespVO(ImGroupDO group, Long loginUserId) { if (group == null) { diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCancelMuteMemberReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCancelMuteMemberReqVO.java new file mode 100644 index 0000000..4781f82 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCancelMuteMemberReqVO.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 ImGroupCancelMuteMemberReqVO { + + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "群编号不能为空") + private Long groupId; + + @Schema(description = "被取消禁言的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "用户编号不能为空") + private Long userId; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteAllReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteAllReqVO.java new file mode 100644 index 0000000..faa42ac --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteAllReqVO.java @@ -0,0 +1,20 @@ +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 ImGroupMuteAllReqVO { + + // TODO @AI:改成 id 是不是更好? + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "群编号不能为空") + private Long groupId; + + @Schema(description = "是否全群禁言", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否全群禁言不能为空") + private Boolean mutedAll; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteMemberReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteMemberReqVO.java new file mode 100644 index 0000000..6b8eb18 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteMemberReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.im.controller.admin.group.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - 成员禁言 Request VO") +@Data +public class ImGroupMuteMemberReqVO { + + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "群编号不能为空") + private Long groupId; + + @Schema(description = "被禁言的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "用户编号不能为空") + private Long userId; + + // TODO @AI:是不是改成禁言结束时间更好?因为禁言时长可能会比较大,单位秒不太好理解。 + @Schema(description = "禁言时长(秒)", requiredMode = Schema.RequiredMode.REQUIRED, example = "600") + @NotNull(message = "禁言时长不能为空") + @Min(value = 1, message = "禁言时长至少 1 秒") + private Integer mutedSeconds; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java index 3f76444..0068ded 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java @@ -29,6 +29,9 @@ public class ImGroupRespVO { @Schema(description = "是否封禁") private Boolean banned; + @Schema(description = "是否全群禁言") + private Boolean mutedAll; + @Schema(description = "封禁时间") private LocalDateTime bannedTime; 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 9aee3c4..ce095e1 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 @@ -39,6 +39,9 @@ public class ImGroupMemberRespVO { @Schema(description = "退群时间") private LocalDateTime quitTime; + @Schema(description = "禁言到期时间") + private LocalDateTime muteEndTime; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupDO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupDO.java index 95a497e..875ae8d 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupDO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupDO.java @@ -74,6 +74,10 @@ public class ImGroupDO extends BaseDO { */ private LocalDateTime dissolvedTime; /** + * 是否全群禁言 + */ + private Boolean mutedAll; + /** * 群置顶消息编号列表 *

* 仅存 messageId,操作人 / 置顶时间从对应 PIN 事件的消息记录里反查 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 dd8816f..269517e 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 @@ -74,5 +74,11 @@ public class ImGroupMemberDO extends BaseDO { * 退群时间 */ private LocalDateTime quitTime; + /** + * 禁言到期时间 + *

+ * null 表示未禁言;非 null 且在未来表示禁言中; + */ + private LocalDateTime muteEndTime; } \ No newline at end of file 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 2d9f6dd..fcd198f 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 @@ -117,4 +117,11 @@ public interface ImGroupMemberMapper extends BaseMapperX { return result; } + @SuppressWarnings("UnusedReturnValue") + default int updateMuteEndTimeNull(Long id) { + return update(null, Wrappers.lambdaUpdate() + .eq(ImGroupMemberDO::getId, id) + .set(ImGroupMemberDO::getMuteEndTime, null)); + } + } \ No newline at end of file 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 a367109..4a67f11 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 @@ -41,6 +41,11 @@ public interface ErrorCodeConstants { 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, "管理员无法移出其他管理员,请先由群主撤销其管理员身份"); + ErrorCode GROUP_MUTED_CANNOT_SEND = new ErrorCode(1_040_500_012, "群已全局禁言,仅群主和管理员可发送消息"); + ErrorCode GROUP_MEMBER_MUTED_CANNOT_SEND = new ErrorCode(1_040_500_013, "您已被禁言,解除时间:{}"); + ErrorCode GROUP_MUTE_MEMBER_SELF = new ErrorCode(1_040_500_014, "不能禁言自己"); + ErrorCode GROUP_MUTE_OWNER_DENIED = new ErrorCode(1_040_500_015, "群主无法被禁言"); + ErrorCode GROUP_MUTE_ADMIN_DENIED = new ErrorCode(1_040_500_016, "管理员无法禁言其他管理员"); // ========== 好友 (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/message/ImMessageTypeEnum.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java index f65ca10..b14eb4a 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java @@ -156,22 +156,26 @@ public enum ImMessageTypeEnum implements ArrayValuable { GROUP_DISSOLVE(1511, "群解散", true, false), /** * 对应 OpenIM:sdkws.GroupMemberMutedTips(GroupMemberMutedNotification 1512) - * TODO @AI 未实现:单成员禁言(管理员禁言某成员);本期发版要实现 + * 对应自己的类:GroupMemberMutedNotification + * 场景:群主 / 管理员禁言某成员,全员广播 */ GROUP_MEMBER_MUTED(1512, "成员禁言", true, false), /** * 对应 OpenIM:sdkws.GroupMemberCancelMutedTips(GroupMemberCancelMutedNotification 1513) - * TODO @AI 未实现:单成员取消禁言;本期发版要实现 + * 对应自己的类:GroupMemberCancelMutedNotification + * 场景:群主 / 管理员取消某成员禁言,全员广播 */ GROUP_MEMBER_CANCEL_MUTED(1513, "成员取消禁言", true, false), /** * 对应 OpenIM:sdkws.GroupMutedTips(GroupMutedNotification 1514) - * TODO @AI 未实现:全群禁言(管理员把整个群设为禁言状态,所有成员都不能发消息);本期发版要实现 + * 对应自己的类:GroupMutedNotification + * 场景:群主 / 管理员开启全群禁言,全员广播 */ GROUP_MUTED(1514, "全群禁言", true, false), /** * 对应 OpenIM:sdkws.GroupCancelMutedTips(GroupCancelMutedNotification 1515) - * TODO @AI 未实现:全群取消禁言;本期发版要实现 + * 对应自己的类:GroupCancelMutedNotification + * 场景:群主 / 管理员取消全群禁言,全员广播 */ GROUP_CANCEL_MUTED(1515, "全群取消禁言", true, false), /** 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 9cdebfd..b9dd6a7 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 @@ -192,4 +192,13 @@ public interface ImGroupMemberService { */ Map getActiveMemberCountMap(Collection groupIds); + /** + * 更新成员禁言到期时间 + * + * @param groupId 群编号 + * @param userId 用户编号 + * @param muteEndTime 禁言到期时间;null 表示取消禁言 + */ + void updateGroupMemberMuteEndTime(Long groupId, Long userId, LocalDateTime muteEndTime); + } 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 8320ea5..cf6a0de 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 @@ -266,4 +266,16 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { return groupMemberMapper.selectCountMapByGroupIdsAndStatus(groupIds, CommonStatusEnum.ENABLE.getStatus()); } + @Override + public void updateGroupMemberMuteEndTime(Long groupId, Long userId, LocalDateTime muteEndTime) { + ImGroupMemberDO member = validateMemberInGroup(groupId, userId); + if (muteEndTime != null) { + // 禁言:直接更新到期时间 + groupMemberMapper.updateById(new ImGroupMemberDO().setId(member.getId()).setMuteEndTime(muteEndTime)); + } else { + // 取消禁言 + groupMemberMapper.updateMuteEndTimeNull(member.getId()); + } + } + } 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 25a80a8..6c5fed2 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 @@ -3,7 +3,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.ImGroupCancelMuteMemberReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupMuteAllReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupMuteMemberReqVO; 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; @@ -174,6 +177,32 @@ public interface ImGroupService { */ void unpinGroupMessage(Long userId, Long groupId, Long messageId); + // ==================== 群禁言 ==================== + + /** + * 全群禁言 / 取消(仅群主或管理员可执行) + * + * @param userId 当前登录用户编号 + * @param reqVO 禁言信息 + */ + void muteAll(Long userId, @Valid ImGroupMuteAllReqVO reqVO); + + /** + * 禁言单个成员(三档分层权限) + * + * @param userId 当前登录用户编号 + * @param reqVO 禁言信息 + */ + void muteMember(Long userId, @Valid ImGroupMuteMemberReqVO reqVO); + + /** + * 取消成员禁言(三档分层权限) + * + * @param userId 当前登录用户编号 + * @param reqVO 取消禁言信息 + */ + void cancelMuteMember(Long userId, @Valid ImGroupCancelMuteMemberReqVO reqVO); + // ==================== 管理后台 ==================== /** 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 b27edb0..7db533d 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,7 +12,10 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; 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.ImGroupCancelMuteMemberReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupMuteAllReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupMuteMemberReqVO; 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; @@ -527,6 +530,85 @@ public class ImGroupServiceImpl implements ImGroupService { groupMapper.updateById(new ImGroupDO().setId(id).setBanned(false)); } + // ==================== 群禁言 ==================== + + @Override + @CacheEvict(cacheNames = GROUP, key = "#reqVO.groupId") + public void muteAll(Long userId, ImGroupMuteAllReqVO reqVO) { + // 1. 校验群主或管理员 + validateGroupOwnerOrAdmin(reqVO.getGroupId(), userId); + + // 2. 更新 mutedAll + groupMapper.updateById(new ImGroupDO().setId(reqVO.getGroupId()).setMutedAll(reqVO.getMutedAll())); + + // 3. 广播通知 + // TODO @AI:改成 messageSendDTO;其它也是;更好理解一点; + ImGroupMessageSendDTO dto = Boolean.TRUE.equals(reqVO.getMutedAll()) + ? ImGroupMessageSendDTO.ofGroupMuted(reqVO.getGroupId(), userId) + : ImGroupMessageSendDTO.ofGroupCancelMuted(reqVO.getGroupId(), userId); + groupMessageService.sendGroupMessage(userId, dto); + } + + @Override + public void muteMember(Long userId, ImGroupMuteMemberReqVO reqVO) { + // 1.1 不能禁言自己 + if (ObjUtil.equal(userId, reqVO.getUserId())) { + throw exception(GROUP_MUTE_MEMBER_SELF); + } + // 1.2 校验群存在且未封禁 + validateGroupExists(reqVO.getGroupId()); + // 1.3 校验操作人和目标都在群中 + ImGroupMemberDO operatorMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), userId); + ImGroupMemberDO targetMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), reqVO.getUserId()); + // 1.4 三档权限校验 + validateMutePermission(operatorMember, targetMember); + + // 2. 设置 muteEndTime + LocalDateTime muteEndTime = LocalDateTime.now().plusSeconds(reqVO.getMutedSeconds()); + groupMemberService.updateGroupMemberMuteEndTime(reqVO.getGroupId(), reqVO.getUserId(), muteEndTime); + + // 3. 广播通知 + groupMessageService.sendGroupMessage(userId, + ImGroupMessageSendDTO.ofGroupMemberMuted(reqVO.getGroupId(), userId, + reqVO.getUserId(), muteEndTime)); + } + + @Override + public void cancelMuteMember(Long userId, ImGroupCancelMuteMemberReqVO reqVO) { + // 1.1 校验群存在且未封禁 + validateGroupExists(reqVO.getGroupId()); + // 1.2 校验操作人和目标都在群中 + ImGroupMemberDO operatorMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), userId); + ImGroupMemberDO targetMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), reqVO.getUserId()); + // 1.3 三档权限校验 + validateMutePermission(operatorMember, targetMember); + + // 2. 取消禁言(清空 muteEndTime) + groupMemberService.updateGroupMemberMuteEndTime(reqVO.getGroupId(), reqVO.getUserId(), null); + + // 3. 广播通知 + groupMessageService.sendGroupMessage(userId, + ImGroupMessageSendDTO.ofGroupMemberCancelMuted(reqVO.getGroupId(), userId, reqVO.getUserId())); + } + + /** + * 三档分层禁言权限校验 + */ + private void validateMutePermission(ImGroupMemberDO operator, ImGroupMemberDO target) { + // 群主不可被禁言 + if (ImGroupMemberRoleEnum.isOwner(target.getRole())) { + throw exception(GROUP_MUTE_OWNER_DENIED); + } + // 管理员不能禁言其他管理员 + if (ImGroupMemberRoleEnum.isAdmin(target.getRole()) && !ImGroupMemberRoleEnum.isOwner(operator.getRole())) { + throw exception(GROUP_MUTE_ADMIN_DENIED); + } + // 普通成员不能禁言任何人 + if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(operator.getRole())) { + throw exception(GROUP_NOT_OWNER_OR_ADMIN); + } + } + private ImGroupServiceImpl getSelf() { return SpringUtil.getBean(getClass()); } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java index 0ffd12d..5483d9b 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java @@ -12,10 +12,12 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO; import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO; import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; +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.dataobject.message.ImGroupMessageDO; import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; import cn.iocoder.yudao.module.im.dal.redis.message.GroupMessageReadRedisDAO; +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; @@ -86,9 +88,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { return existing; } // 1.2 校验群存在、发送人仍在群中 - groupService.validateGroupExists(reqVO.getGroupId()); + ImGroupDO group = groupService.validateGroupExists(reqVO.getGroupId()); ImGroupMemberDO senderMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), senderId); - // 1.3 文本消息敏感词过滤 + // 1.3 禁言校验 + validateMuteStatus(group, senderMember); + // 1.4 文本消息敏感词过滤 if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) { sensitiveWordService.validateText(reqVO.getContent()); } @@ -604,4 +608,19 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { return SpringUtil.getBean(getClass()); } + /** + * 禁言状态校验:全群禁言 → 成员禁言;群主 / 管理员豁免全群禁言 + */ + private void validateMuteStatus(ImGroupDO group, ImGroupMemberDO senderMember) { + boolean isOwnerOrAdmin = ImGroupMemberRoleEnum.isOwnerOrAdmin(senderMember.getRole()); + // 1. 全群禁言:群主 / 管理员豁免 + if (Boolean.TRUE.equals(group.getMutedAll()) && !isOwnerOrAdmin) { + throw exception(GROUP_MUTED_CANNOT_SEND); + } + // 2. 成员禁言 + if (senderMember.getMuteEndTime() != null && senderMember.getMuteEndTime().isAfter(LocalDateTime.now())) { + throw exception(GROUP_MEMBER_MUTED_CANNOT_SEND, senderMember.getMuteEndTime()); + } + } + } \ No newline at end of file diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java index d4f28db..95f4820 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java @@ -174,4 +174,37 @@ public class ImGroupMessageSendDTO { .setType(ImMessageTypeEnum.GROUP_MESSAGE_UNPIN.getType()).setContent(notification); } + public static ImGroupMessageSendDTO ofGroupMemberMuted(Long groupId, Long operatorUserId, + Long mutedUserId, + java.time.LocalDateTime muteEndTime) { + GroupMemberMutedNotification notification = new GroupMemberMutedNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setMutedUserId(mutedUserId); + notification.setMuteEndTime(muteEndTime); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_MEMBER_MUTED.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupMemberCancelMuted(Long groupId, Long operatorUserId, Long mutedUserId) { + GroupMemberCancelMutedNotification notification = new GroupMemberCancelMutedNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setMutedUserId(mutedUserId); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_MEMBER_CANCEL_MUTED.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupMuted(Long groupId, Long operatorUserId) { + GroupMutedNotification notification = new GroupMutedNotification(); + notification.setOperatorUserId(operatorUserId); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_MUTED.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupCancelMuted(Long groupId, Long operatorUserId) { + GroupCancelMutedNotification notification = new GroupCancelMutedNotification(); + notification.setOperatorUserId(operatorUserId); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_CANCEL_MUTED.getType()).setContent(notification); + } + } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupCancelMutedNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupCancelMutedNotification.java new file mode 100644 index 0000000..bddf744 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupCancelMutedNotification.java @@ -0,0 +1,13 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 全群取消禁言通知 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class GroupCancelMutedNotification extends BaseGroupNotification { + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberCancelMutedNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberCancelMutedNotification.java new file mode 100644 index 0000000..48df8fc --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberCancelMutedNotification.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 取消成员禁言通知 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class GroupMemberCancelMutedNotification extends BaseGroupNotification { + + /** + * 被取消禁言的用户编号 + */ + private Long mutedUserId; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberMutedNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberMutedNotification.java new file mode 100644 index 0000000..557d901 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberMutedNotification.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 成员禁言通知 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class GroupMemberMutedNotification extends BaseGroupNotification { + + /** + * 被禁言的用户编号 + */ + private Long mutedUserId; + /** + * 禁言到期时间 + */ + private LocalDateTime muteEndTime; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMutedNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMutedNotification.java new file mode 100644 index 0000000..86fa3e6 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMutedNotification.java @@ -0,0 +1,13 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 全群禁言通知 + */ +@Data +@EqualsAndHashCode(callSuper = true) // TODO @AI:不需要添加这个,全局已经默认了; +public class GroupMutedNotification extends BaseGroupNotification { + +} 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 1ffe858..99f49b8 100644 --- a/yudao-module-im/src/test/resources/sql/create_tables.sql +++ b/yudao-module-im/src/test/resources/sql/create_tables.sql @@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS "im_group" ( "banned_time" timestamp DEFAULT NULL COMMENT '封禁时间', "status" tinyint NOT NULL COMMENT '群状态', "dissolved_time" timestamp DEFAULT NULL COMMENT '解散时间', + "muted_all" bit DEFAULT FALSE COMMENT '是否全群禁言', "pinned_message_ids" varchar(128) DEFAULT NULL COMMENT '群置顶消息编号列表,逗号分隔', "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -69,6 +70,7 @@ CREATE TABLE IF NOT EXISTS "im_group_member" ( "role" tinyint NOT NULL DEFAULT 3 COMMENT '成员角色:1=群主 2=管理员 3=普通成员', "join_time" timestamp DEFAULT NULL COMMENT '入群时间', "quit_time" timestamp DEFAULT NULL COMMENT '退群时间', + "mute_end_time" timestamp DEFAULT NULL COMMENT '禁言到期时间', "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '', -- libgit2 0.21.4