Commit 96ae9bbcb08a08dc95d1cf77dc076f65149b2f51
1 parent
14090be6
feat(IM):群禁言功能(阶段二后端)
1. im_group 新增 muted_all 字段,支持全群禁言 / 取消 2. im_group_member 新增 mute_end_time 字段,支持单成员定时禁言 3. 新增 3 个 API:mute-all、mute-member、cancel-mute-member 4. sendGroupMessage 新增 validateMuteStatus 拦截:全群禁言(群主/管理员豁免)+ 成员禁言 5. 三档分层权限校验:群主可禁言管理员和普通成员,管理员仅可禁言普通成员 6. 新增 4 个 WebSocket 通知类(1512-1515)及对应 DTO 工厂方法 7. Mapper 新增 updateMuteEndTimeNull 方法,解决 MyBatis-Plus 默认跳过 null 字段的问题 8. 新增 5 个错误码(GROUP_MUTED_CANNOT_SEND 等)
Showing
22 changed files
with
380 additions
and
6 deletions
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java
| ... | ... | @@ -145,6 +145,29 @@ public class ImGroupController { |
| 145 | 145 | return success(true); |
| 146 | 146 | } |
| 147 | 147 | |
| 148 | + // ==================== 群禁言 ==================== | |
| 149 | + | |
| 150 | + @PutMapping("/mute-all") | |
| 151 | + @Operation(summary = "全群禁言 / 取消(群主 / 管理员)") | |
| 152 | + public CommonResult<Boolean> muteAll(@Valid @RequestBody ImGroupMuteAllReqVO reqVO) { | |
| 153 | + groupService.muteAll(getLoginUserId(), reqVO); | |
| 154 | + return success(true); | |
| 155 | + } | |
| 156 | + | |
| 157 | + @PutMapping("/mute-member") | |
| 158 | + @Operation(summary = "禁言成员") | |
| 159 | + public CommonResult<Boolean> muteMember(@Valid @RequestBody ImGroupMuteMemberReqVO reqVO) { | |
| 160 | + groupService.muteMember(getLoginUserId(), reqVO); | |
| 161 | + return success(true); | |
| 162 | + } | |
| 163 | + | |
| 164 | + @PutMapping("/cancel-mute-member") | |
| 165 | + @Operation(summary = "取消成员禁言") | |
| 166 | + public CommonResult<Boolean> cancelMuteMember(@Valid @RequestBody ImGroupCancelMuteMemberReqVO reqVO) { | |
| 167 | + groupService.cancelMuteMember(getLoginUserId(), reqVO); | |
| 168 | + return success(true); | |
| 169 | + } | |
| 170 | + | |
| 148 | 171 | /** 单群转 VO + 关联回填 pinnedMessages(仅当登录用户是该群有效成员) */ |
| 149 | 172 | private ImGroupRespVO buildGroupRespVO(ImGroupDO group, Long loginUserId) { |
| 150 | 173 | if (group == null) { | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCancelMuteMemberReqVO.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 ImGroupCancelMuteMemberReqVO { | |
| 10 | + | |
| 11 | + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") | |
| 12 | + @NotNull(message = "群编号不能为空") | |
| 13 | + private Long groupId; | |
| 14 | + | |
| 15 | + @Schema(description = "被取消禁言的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") | |
| 16 | + @NotNull(message = "用户编号不能为空") | |
| 17 | + private Long userId; | |
| 18 | + | |
| 19 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteAllReqVO.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 ImGroupMuteAllReqVO { | |
| 10 | + | |
| 11 | + // TODO @AI:改成 id 是不是更好? | |
| 12 | + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") | |
| 13 | + @NotNull(message = "群编号不能为空") | |
| 14 | + private Long groupId; | |
| 15 | + | |
| 16 | + @Schema(description = "是否全群禁言", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") | |
| 17 | + @NotNull(message = "是否全群禁言不能为空") | |
| 18 | + private Boolean mutedAll; | |
| 19 | + | |
| 20 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMuteMemberReqVO.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.Min; | |
| 5 | +import jakarta.validation.constraints.NotNull; | |
| 6 | +import lombok.Data; | |
| 7 | + | |
| 8 | +@Schema(description = "管理后台 - 成员禁言 Request VO") | |
| 9 | +@Data | |
| 10 | +public class ImGroupMuteMemberReqVO { | |
| 11 | + | |
| 12 | + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") | |
| 13 | + @NotNull(message = "群编号不能为空") | |
| 14 | + private Long groupId; | |
| 15 | + | |
| 16 | + @Schema(description = "被禁言的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") | |
| 17 | + @NotNull(message = "用户编号不能为空") | |
| 18 | + private Long userId; | |
| 19 | + | |
| 20 | + // TODO @AI:是不是改成禁言结束时间更好?因为禁言时长可能会比较大,单位秒不太好理解。 | |
| 21 | + @Schema(description = "禁言时长(秒)", requiredMode = Schema.RequiredMode.REQUIRED, example = "600") | |
| 22 | + @NotNull(message = "禁言时长不能为空") | |
| 23 | + @Min(value = 1, message = "禁言时长至少 1 秒") | |
| 24 | + private Integer mutedSeconds; | |
| 25 | + | |
| 26 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java
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 { |
| 39 | 39 | @Schema(description = "退群时间") |
| 40 | 40 | private LocalDateTime quitTime; |
| 41 | 41 | |
| 42 | + @Schema(description = "禁言到期时间") | |
| 43 | + private LocalDateTime muteEndTime; | |
| 44 | + | |
| 42 | 45 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
| 43 | 46 | private LocalDateTime createTime; |
| 44 | 47 | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupDO.java
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 { |
| 74 | 74 | * 退群时间 |
| 75 | 75 | */ |
| 76 | 76 | private LocalDateTime quitTime; |
| 77 | + /** | |
| 78 | + * 禁言到期时间 | |
| 79 | + * <p> | |
| 80 | + * null 表示未禁言;非 null 且在未来表示禁言中; | |
| 81 | + */ | |
| 82 | + private LocalDateTime muteEndTime; | |
| 77 | 83 | |
| 78 | 84 | } |
| 79 | 85 | \ No newline at end of file | ... | ... |
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<ImGroupMemberDO> { |
| 117 | 117 | return result; |
| 118 | 118 | } |
| 119 | 119 | |
| 120 | + @SuppressWarnings("UnusedReturnValue") | |
| 121 | + default int updateMuteEndTimeNull(Long id) { | |
| 122 | + return update(null, Wrappers.<ImGroupMemberDO>lambdaUpdate() | |
| 123 | + .eq(ImGroupMemberDO::getId, id) | |
| 124 | + .set(ImGroupMemberDO::getMuteEndTime, null)); | |
| 125 | + } | |
| 126 | + | |
| 120 | 127 | } |
| 121 | 128 | \ No newline at end of file | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java
| ... | ... | @@ -41,6 +41,11 @@ public interface ErrorCodeConstants { |
| 41 | 41 | ErrorCode GROUP_ADMIN_MAX_LIMIT = new ErrorCode(1_040_500_009, "群管理员数量不能超过 {} 人"); |
| 42 | 42 | ErrorCode GROUP_REMOVE_OWNER_DENIED = new ErrorCode(1_040_500_010, "群主无法被移出群聊"); |
| 43 | 43 | ErrorCode GROUP_REMOVE_ADMIN_DENIED = new ErrorCode(1_040_500_011, "管理员无法移出其他管理员,请先由群主撤销其管理员身份"); |
| 44 | + ErrorCode GROUP_MUTED_CANNOT_SEND = new ErrorCode(1_040_500_012, "群已全局禁言,仅群主和管理员可发送消息"); | |
| 45 | + ErrorCode GROUP_MEMBER_MUTED_CANNOT_SEND = new ErrorCode(1_040_500_013, "您已被禁言,解除时间:{}"); | |
| 46 | + ErrorCode GROUP_MUTE_MEMBER_SELF = new ErrorCode(1_040_500_014, "不能禁言自己"); | |
| 47 | + ErrorCode GROUP_MUTE_OWNER_DENIED = new ErrorCode(1_040_500_015, "群主无法被禁言"); | |
| 48 | + ErrorCode GROUP_MUTE_ADMIN_DENIED = new ErrorCode(1_040_500_016, "管理员无法禁言其他管理员"); | |
| 44 | 49 | |
| 45 | 50 | // ========== 好友 (1-040-600-000) ========== |
| 46 | 51 | ErrorCode FRIEND_NOT_FRIEND = new ErrorCode(1_040_600_001, "对方不是您的好友"); | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java
| ... | ... | @@ -156,22 +156,26 @@ public enum ImMessageTypeEnum implements ArrayValuable<Integer> { |
| 156 | 156 | GROUP_DISSOLVE(1511, "群解散", true, false), |
| 157 | 157 | /** |
| 158 | 158 | * 对应 OpenIM:sdkws.GroupMemberMutedTips(GroupMemberMutedNotification 1512) |
| 159 | - * TODO @AI 未实现:单成员禁言(管理员禁言某成员);本期发版要实现 | |
| 159 | + * 对应自己的类:GroupMemberMutedNotification | |
| 160 | + * 场景:群主 / 管理员禁言某成员,全员广播 | |
| 160 | 161 | */ |
| 161 | 162 | GROUP_MEMBER_MUTED(1512, "成员禁言", true, false), |
| 162 | 163 | /** |
| 163 | 164 | * 对应 OpenIM:sdkws.GroupMemberCancelMutedTips(GroupMemberCancelMutedNotification 1513) |
| 164 | - * TODO @AI 未实现:单成员取消禁言;本期发版要实现 | |
| 165 | + * 对应自己的类:GroupMemberCancelMutedNotification | |
| 166 | + * 场景:群主 / 管理员取消某成员禁言,全员广播 | |
| 165 | 167 | */ |
| 166 | 168 | GROUP_MEMBER_CANCEL_MUTED(1513, "成员取消禁言", true, false), |
| 167 | 169 | /** |
| 168 | 170 | * 对应 OpenIM:sdkws.GroupMutedTips(GroupMutedNotification 1514) |
| 169 | - * TODO @AI 未实现:全群禁言(管理员把整个群设为禁言状态,所有成员都不能发消息);本期发版要实现 | |
| 171 | + * 对应自己的类:GroupMutedNotification | |
| 172 | + * 场景:群主 / 管理员开启全群禁言,全员广播 | |
| 170 | 173 | */ |
| 171 | 174 | GROUP_MUTED(1514, "全群禁言", true, false), |
| 172 | 175 | /** |
| 173 | 176 | * 对应 OpenIM:sdkws.GroupCancelMutedTips(GroupCancelMutedNotification 1515) |
| 174 | - * TODO @AI 未实现:全群取消禁言;本期发版要实现 | |
| 177 | + * 对应自己的类:GroupCancelMutedNotification | |
| 178 | + * 场景:群主 / 管理员取消全群禁言,全员广播 | |
| 175 | 179 | */ |
| 176 | 180 | GROUP_CANCEL_MUTED(1515, "全群取消禁言", true, false), |
| 177 | 181 | /** | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java
| ... | ... | @@ -192,4 +192,13 @@ public interface ImGroupMemberService { |
| 192 | 192 | */ |
| 193 | 193 | Map<Long, Long> getActiveMemberCountMap(Collection<Long> groupIds); |
| 194 | 194 | |
| 195 | + /** | |
| 196 | + * 更新成员禁言到期时间 | |
| 197 | + * | |
| 198 | + * @param groupId 群编号 | |
| 199 | + * @param userId 用户编号 | |
| 200 | + * @param muteEndTime 禁言到期时间;null 表示取消禁言 | |
| 201 | + */ | |
| 202 | + void updateGroupMemberMuteEndTime(Long groupId, Long userId, LocalDateTime muteEndTime); | |
| 203 | + | |
| 195 | 204 | } | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java
| ... | ... | @@ -266,4 +266,16 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { |
| 266 | 266 | return groupMemberMapper.selectCountMapByGroupIdsAndStatus(groupIds, CommonStatusEnum.ENABLE.getStatus()); |
| 267 | 267 | } |
| 268 | 268 | |
| 269 | + @Override | |
| 270 | + public void updateGroupMemberMuteEndTime(Long groupId, Long userId, LocalDateTime muteEndTime) { | |
| 271 | + ImGroupMemberDO member = validateMemberInGroup(groupId, userId); | |
| 272 | + if (muteEndTime != null) { | |
| 273 | + // 禁言:直接更新到期时间 | |
| 274 | + groupMemberMapper.updateById(new ImGroupMemberDO().setId(member.getId()).setMuteEndTime(muteEndTime)); | |
| 275 | + } else { | |
| 276 | + // 取消禁言 | |
| 277 | + groupMemberMapper.updateMuteEndTimeNull(member.getId()); | |
| 278 | + } | |
| 279 | + } | |
| 280 | + | |
| 269 | 281 | } | ... | ... |
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; |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 4 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; |
| 5 | 5 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; |
| 6 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCancelMuteMemberReqVO; | |
| 6 | 7 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; |
| 8 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupMuteAllReqVO; | |
| 9 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupMuteMemberReqVO; | |
| 7 | 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; |
| ... | ... | @@ -174,6 +177,32 @@ public interface ImGroupService { |
| 174 | 177 | */ |
| 175 | 178 | void unpinGroupMessage(Long userId, Long groupId, Long messageId); |
| 176 | 179 | |
| 180 | + // ==================== 群禁言 ==================== | |
| 181 | + | |
| 182 | + /** | |
| 183 | + * 全群禁言 / 取消(仅群主或管理员可执行) | |
| 184 | + * | |
| 185 | + * @param userId 当前登录用户编号 | |
| 186 | + * @param reqVO 禁言信息 | |
| 187 | + */ | |
| 188 | + void muteAll(Long userId, @Valid ImGroupMuteAllReqVO reqVO); | |
| 189 | + | |
| 190 | + /** | |
| 191 | + * 禁言单个成员(三档分层权限) | |
| 192 | + * | |
| 193 | + * @param userId 当前登录用户编号 | |
| 194 | + * @param reqVO 禁言信息 | |
| 195 | + */ | |
| 196 | + void muteMember(Long userId, @Valid ImGroupMuteMemberReqVO reqVO); | |
| 197 | + | |
| 198 | + /** | |
| 199 | + * 取消成员禁言(三档分层权限) | |
| 200 | + * | |
| 201 | + * @param userId 当前登录用户编号 | |
| 202 | + * @param reqVO 取消禁言信息 | |
| 203 | + */ | |
| 204 | + void cancelMuteMember(Long userId, @Valid ImGroupCancelMuteMemberReqVO reqVO); | |
| 205 | + | |
| 177 | 206 | // ==================== 管理后台 ==================== |
| 178 | 207 | |
| 179 | 208 | /** | ... | ... |
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; |
| 12 | 12 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 13 | 13 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; |
| 14 | 14 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; |
| 15 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCancelMuteMemberReqVO; | |
| 15 | 16 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; |
| 17 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupMuteAllReqVO; | |
| 18 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupMuteMemberReqVO; | |
| 16 | 19 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; |
| 17 | 20 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; |
| 18 | 21 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; |
| ... | ... | @@ -527,6 +530,85 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 527 | 530 | groupMapper.updateById(new ImGroupDO().setId(id).setBanned(false)); |
| 528 | 531 | } |
| 529 | 532 | |
| 533 | + // ==================== 群禁言 ==================== | |
| 534 | + | |
| 535 | + @Override | |
| 536 | + @CacheEvict(cacheNames = GROUP, key = "#reqVO.groupId") | |
| 537 | + public void muteAll(Long userId, ImGroupMuteAllReqVO reqVO) { | |
| 538 | + // 1. 校验群主或管理员 | |
| 539 | + validateGroupOwnerOrAdmin(reqVO.getGroupId(), userId); | |
| 540 | + | |
| 541 | + // 2. 更新 mutedAll | |
| 542 | + groupMapper.updateById(new ImGroupDO().setId(reqVO.getGroupId()).setMutedAll(reqVO.getMutedAll())); | |
| 543 | + | |
| 544 | + // 3. 广播通知 | |
| 545 | + // TODO @AI:改成 messageSendDTO;其它也是;更好理解一点; | |
| 546 | + ImGroupMessageSendDTO dto = Boolean.TRUE.equals(reqVO.getMutedAll()) | |
| 547 | + ? ImGroupMessageSendDTO.ofGroupMuted(reqVO.getGroupId(), userId) | |
| 548 | + : ImGroupMessageSendDTO.ofGroupCancelMuted(reqVO.getGroupId(), userId); | |
| 549 | + groupMessageService.sendGroupMessage(userId, dto); | |
| 550 | + } | |
| 551 | + | |
| 552 | + @Override | |
| 553 | + public void muteMember(Long userId, ImGroupMuteMemberReqVO reqVO) { | |
| 554 | + // 1.1 不能禁言自己 | |
| 555 | + if (ObjUtil.equal(userId, reqVO.getUserId())) { | |
| 556 | + throw exception(GROUP_MUTE_MEMBER_SELF); | |
| 557 | + } | |
| 558 | + // 1.2 校验群存在且未封禁 | |
| 559 | + validateGroupExists(reqVO.getGroupId()); | |
| 560 | + // 1.3 校验操作人和目标都在群中 | |
| 561 | + ImGroupMemberDO operatorMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), userId); | |
| 562 | + ImGroupMemberDO targetMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), reqVO.getUserId()); | |
| 563 | + // 1.4 三档权限校验 | |
| 564 | + validateMutePermission(operatorMember, targetMember); | |
| 565 | + | |
| 566 | + // 2. 设置 muteEndTime | |
| 567 | + LocalDateTime muteEndTime = LocalDateTime.now().plusSeconds(reqVO.getMutedSeconds()); | |
| 568 | + groupMemberService.updateGroupMemberMuteEndTime(reqVO.getGroupId(), reqVO.getUserId(), muteEndTime); | |
| 569 | + | |
| 570 | + // 3. 广播通知 | |
| 571 | + groupMessageService.sendGroupMessage(userId, | |
| 572 | + ImGroupMessageSendDTO.ofGroupMemberMuted(reqVO.getGroupId(), userId, | |
| 573 | + reqVO.getUserId(), muteEndTime)); | |
| 574 | + } | |
| 575 | + | |
| 576 | + @Override | |
| 577 | + public void cancelMuteMember(Long userId, ImGroupCancelMuteMemberReqVO reqVO) { | |
| 578 | + // 1.1 校验群存在且未封禁 | |
| 579 | + validateGroupExists(reqVO.getGroupId()); | |
| 580 | + // 1.2 校验操作人和目标都在群中 | |
| 581 | + ImGroupMemberDO operatorMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), userId); | |
| 582 | + ImGroupMemberDO targetMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), reqVO.getUserId()); | |
| 583 | + // 1.3 三档权限校验 | |
| 584 | + validateMutePermission(operatorMember, targetMember); | |
| 585 | + | |
| 586 | + // 2. 取消禁言(清空 muteEndTime) | |
| 587 | + groupMemberService.updateGroupMemberMuteEndTime(reqVO.getGroupId(), reqVO.getUserId(), null); | |
| 588 | + | |
| 589 | + // 3. 广播通知 | |
| 590 | + groupMessageService.sendGroupMessage(userId, | |
| 591 | + ImGroupMessageSendDTO.ofGroupMemberCancelMuted(reqVO.getGroupId(), userId, reqVO.getUserId())); | |
| 592 | + } | |
| 593 | + | |
| 594 | + /** | |
| 595 | + * 三档分层禁言权限校验 | |
| 596 | + */ | |
| 597 | + private void validateMutePermission(ImGroupMemberDO operator, ImGroupMemberDO target) { | |
| 598 | + // 群主不可被禁言 | |
| 599 | + if (ImGroupMemberRoleEnum.isOwner(target.getRole())) { | |
| 600 | + throw exception(GROUP_MUTE_OWNER_DENIED); | |
| 601 | + } | |
| 602 | + // 管理员不能禁言其他管理员 | |
| 603 | + if (ImGroupMemberRoleEnum.isAdmin(target.getRole()) && !ImGroupMemberRoleEnum.isOwner(operator.getRole())) { | |
| 604 | + throw exception(GROUP_MUTE_ADMIN_DENIED); | |
| 605 | + } | |
| 606 | + // 普通成员不能禁言任何人 | |
| 607 | + if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(operator.getRole())) { | |
| 608 | + throw exception(GROUP_NOT_OWNER_OR_ADMIN); | |
| 609 | + } | |
| 610 | + } | |
| 611 | + | |
| 530 | 612 | private ImGroupServiceImpl getSelf() { |
| 531 | 613 | return SpringUtil.getBean(getClass()); |
| 532 | 614 | } | ... | ... |
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; |
| 12 | 12 | import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO; |
| 13 | 13 | import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO; |
| 14 | 14 | import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; |
| 15 | +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; | |
| 15 | 16 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; |
| 16 | 17 | import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; |
| 17 | 18 | import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; |
| 18 | 19 | import cn.iocoder.yudao.module.im.dal.redis.message.GroupMessageReadRedisDAO; |
| 20 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; | |
| 19 | 21 | import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; |
| 20 | 22 | import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; |
| 21 | 23 | import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; |
| ... | ... | @@ -86,9 +88,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { |
| 86 | 88 | return existing; |
| 87 | 89 | } |
| 88 | 90 | // 1.2 校验群存在、发送人仍在群中 |
| 89 | - groupService.validateGroupExists(reqVO.getGroupId()); | |
| 91 | + ImGroupDO group = groupService.validateGroupExists(reqVO.getGroupId()); | |
| 90 | 92 | ImGroupMemberDO senderMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), senderId); |
| 91 | - // 1.3 文本消息敏感词过滤 | |
| 93 | + // 1.3 禁言校验 | |
| 94 | + validateMuteStatus(group, senderMember); | |
| 95 | + // 1.4 文本消息敏感词过滤 | |
| 92 | 96 | if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) { |
| 93 | 97 | sensitiveWordService.validateText(reqVO.getContent()); |
| 94 | 98 | } |
| ... | ... | @@ -604,4 +608,19 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { |
| 604 | 608 | return SpringUtil.getBean(getClass()); |
| 605 | 609 | } |
| 606 | 610 | |
| 611 | + /** | |
| 612 | + * 禁言状态校验:全群禁言 → 成员禁言;群主 / 管理员豁免全群禁言 | |
| 613 | + */ | |
| 614 | + private void validateMuteStatus(ImGroupDO group, ImGroupMemberDO senderMember) { | |
| 615 | + boolean isOwnerOrAdmin = ImGroupMemberRoleEnum.isOwnerOrAdmin(senderMember.getRole()); | |
| 616 | + // 1. 全群禁言:群主 / 管理员豁免 | |
| 617 | + if (Boolean.TRUE.equals(group.getMutedAll()) && !isOwnerOrAdmin) { | |
| 618 | + throw exception(GROUP_MUTED_CANNOT_SEND); | |
| 619 | + } | |
| 620 | + // 2. 成员禁言 | |
| 621 | + if (senderMember.getMuteEndTime() != null && senderMember.getMuteEndTime().isAfter(LocalDateTime.now())) { | |
| 622 | + throw exception(GROUP_MEMBER_MUTED_CANNOT_SEND, senderMember.getMuteEndTime()); | |
| 623 | + } | |
| 624 | + } | |
| 625 | + | |
| 607 | 626 | } |
| 608 | 627 | \ No newline at end of file | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java
| ... | ... | @@ -174,4 +174,37 @@ public class ImGroupMessageSendDTO { |
| 174 | 174 | .setType(ImMessageTypeEnum.GROUP_MESSAGE_UNPIN.getType()).setContent(notification); |
| 175 | 175 | } |
| 176 | 176 | |
| 177 | + public static ImGroupMessageSendDTO ofGroupMemberMuted(Long groupId, Long operatorUserId, | |
| 178 | + Long mutedUserId, | |
| 179 | + java.time.LocalDateTime muteEndTime) { | |
| 180 | + GroupMemberMutedNotification notification = new GroupMemberMutedNotification(); | |
| 181 | + notification.setOperatorUserId(operatorUserId); | |
| 182 | + notification.setMutedUserId(mutedUserId); | |
| 183 | + notification.setMuteEndTime(muteEndTime); | |
| 184 | + return new ImGroupMessageSendDTO().setGroupId(groupId) | |
| 185 | + .setType(ImMessageTypeEnum.GROUP_MEMBER_MUTED.getType()).setContent(notification); | |
| 186 | + } | |
| 187 | + | |
| 188 | + public static ImGroupMessageSendDTO ofGroupMemberCancelMuted(Long groupId, Long operatorUserId, Long mutedUserId) { | |
| 189 | + GroupMemberCancelMutedNotification notification = new GroupMemberCancelMutedNotification(); | |
| 190 | + notification.setOperatorUserId(operatorUserId); | |
| 191 | + notification.setMutedUserId(mutedUserId); | |
| 192 | + return new ImGroupMessageSendDTO().setGroupId(groupId) | |
| 193 | + .setType(ImMessageTypeEnum.GROUP_MEMBER_CANCEL_MUTED.getType()).setContent(notification); | |
| 194 | + } | |
| 195 | + | |
| 196 | + public static ImGroupMessageSendDTO ofGroupMuted(Long groupId, Long operatorUserId) { | |
| 197 | + GroupMutedNotification notification = new GroupMutedNotification(); | |
| 198 | + notification.setOperatorUserId(operatorUserId); | |
| 199 | + return new ImGroupMessageSendDTO().setGroupId(groupId) | |
| 200 | + .setType(ImMessageTypeEnum.GROUP_MUTED.getType()).setContent(notification); | |
| 201 | + } | |
| 202 | + | |
| 203 | + public static ImGroupMessageSendDTO ofGroupCancelMuted(Long groupId, Long operatorUserId) { | |
| 204 | + GroupCancelMutedNotification notification = new GroupCancelMutedNotification(); | |
| 205 | + notification.setOperatorUserId(operatorUserId); | |
| 206 | + return new ImGroupMessageSendDTO().setGroupId(groupId) | |
| 207 | + .setType(ImMessageTypeEnum.GROUP_CANCEL_MUTED.getType()).setContent(notification); | |
| 208 | + } | |
| 209 | + | |
| 177 | 210 | } | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupCancelMutedNotification.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | +import lombok.EqualsAndHashCode; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * 全群取消禁言通知 | |
| 8 | + */ | |
| 9 | +@Data | |
| 10 | +@EqualsAndHashCode(callSuper = true) | |
| 11 | +public class GroupCancelMutedNotification extends BaseGroupNotification { | |
| 12 | + | |
| 13 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberCancelMutedNotification.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | +import lombok.EqualsAndHashCode; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * 取消成员禁言通知 | |
| 8 | + */ | |
| 9 | +@Data | |
| 10 | +@EqualsAndHashCode(callSuper = true) | |
| 11 | +public class GroupMemberCancelMutedNotification extends BaseGroupNotification { | |
| 12 | + | |
| 13 | + /** | |
| 14 | + * 被取消禁言的用户编号 | |
| 15 | + */ | |
| 16 | + private Long mutedUserId; | |
| 17 | + | |
| 18 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberMutedNotification.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | +import lombok.EqualsAndHashCode; | |
| 5 | + | |
| 6 | +import java.time.LocalDateTime; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * 成员禁言通知 | |
| 10 | + */ | |
| 11 | +@Data | |
| 12 | +@EqualsAndHashCode(callSuper = true) | |
| 13 | +public class GroupMemberMutedNotification extends BaseGroupNotification { | |
| 14 | + | |
| 15 | + /** | |
| 16 | + * 被禁言的用户编号 | |
| 17 | + */ | |
| 18 | + private Long mutedUserId; | |
| 19 | + /** | |
| 20 | + * 禁言到期时间 | |
| 21 | + */ | |
| 22 | + private LocalDateTime muteEndTime; | |
| 23 | + | |
| 24 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMutedNotification.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | +import lombok.EqualsAndHashCode; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * 全群禁言通知 | |
| 8 | + */ | |
| 9 | +@Data | |
| 10 | +@EqualsAndHashCode(callSuper = true) // TODO @AI:不需要添加这个,全局已经默认了; | |
| 11 | +public class GroupMutedNotification extends BaseGroupNotification { | |
| 12 | + | |
| 13 | +} | ... | ... |
yudao-module-im/src/test/resources/sql/create_tables.sql
| ... | ... | @@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS "im_group" ( |
| 48 | 48 | "banned_time" timestamp DEFAULT NULL COMMENT '封禁时间', |
| 49 | 49 | "status" tinyint NOT NULL COMMENT '群状态', |
| 50 | 50 | "dissolved_time" timestamp DEFAULT NULL COMMENT '解散时间', |
| 51 | + "muted_all" bit DEFAULT FALSE COMMENT '是否全群禁言', | |
| 51 | 52 | "pinned_message_ids" varchar(128) DEFAULT NULL COMMENT '群置顶消息编号列表,逗号分隔', |
| 52 | 53 | "creator" varchar(64) DEFAULT '', |
| 53 | 54 | "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| ... | ... | @@ -69,6 +70,7 @@ CREATE TABLE IF NOT EXISTS "im_group_member" ( |
| 69 | 70 | "role" tinyint NOT NULL DEFAULT 3 COMMENT '成员角色:1=群主 2=管理员 3=普通成员', |
| 70 | 71 | "join_time" timestamp DEFAULT NULL COMMENT '入群时间', |
| 71 | 72 | "quit_time" timestamp DEFAULT NULL COMMENT '退群时间', |
| 73 | + "mute_end_time" timestamp DEFAULT NULL COMMENT '禁言到期时间', | |
| 72 | 74 | "creator" varchar(64) DEFAULT '', |
| 73 | 75 | "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| 74 | 76 | "updater" varchar(64) DEFAULT '', | ... | ... |