Commit 96ae9bbcb08a08dc95d1cf77dc076f65149b2f51

Authored by YunaiV
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
... ... @@ -29,6 +29,9 @@ public class ImGroupRespVO {
29 29 @Schema(description = "是否封禁")
30 30 private Boolean banned;
31 31  
  32 + @Schema(description = "是否全群禁言")
  33 + private Boolean mutedAll;
  34 +
32 35 @Schema(description = "封禁时间")
33 36 private LocalDateTime bannedTime;
34 37  
... ...
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
... ... @@ -74,6 +74,10 @@ public class ImGroupDO extends BaseDO {
74 74 */
75 75 private LocalDateTime dissolvedTime;
76 76 /**
  77 + * 是否全群禁言
  78 + */
  79 + private Boolean mutedAll;
  80 + /**
77 81 * 群置顶消息编号列表
78 82 * <p>
79 83 * 仅存 messageId,操作人 / 置顶时间从对应 PIN 事件的消息记录里反查
... ...
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&lt;ImGroupMemberDO&gt; {
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&lt;Integer&gt; {
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 &quot;im_group&quot; (
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 &quot;im_group_member&quot; (
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 '',
... ...