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,6 +145,29 @@ public class ImGroupController { | ||
| 145 | return success(true); | 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 | /** 单群转 VO + 关联回填 pinnedMessages(仅当登录用户是该群有效成员) */ | 171 | /** 单群转 VO + 关联回填 pinnedMessages(仅当登录用户是该群有效成员) */ |
| 149 | private ImGroupRespVO buildGroupRespVO(ImGroupDO group, Long loginUserId) { | 172 | private ImGroupRespVO buildGroupRespVO(ImGroupDO group, Long loginUserId) { |
| 150 | if (group == null) { | 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,6 +29,9 @@ public class ImGroupRespVO { | ||
| 29 | @Schema(description = "是否封禁") | 29 | @Schema(description = "是否封禁") |
| 30 | private Boolean banned; | 30 | private Boolean banned; |
| 31 | 31 | ||
| 32 | + @Schema(description = "是否全群禁言") | ||
| 33 | + private Boolean mutedAll; | ||
| 34 | + | ||
| 32 | @Schema(description = "封禁时间") | 35 | @Schema(description = "封禁时间") |
| 33 | private LocalDateTime bannedTime; | 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,6 +39,9 @@ public class ImGroupMemberRespVO { | ||
| 39 | @Schema(description = "退群时间") | 39 | @Schema(description = "退群时间") |
| 40 | private LocalDateTime quitTime; | 40 | private LocalDateTime quitTime; |
| 41 | 41 | ||
| 42 | + @Schema(description = "禁言到期时间") | ||
| 43 | + private LocalDateTime muteEndTime; | ||
| 44 | + | ||
| 42 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) | 45 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
| 43 | private LocalDateTime createTime; | 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,6 +74,10 @@ public class ImGroupDO extends BaseDO { | ||
| 74 | */ | 74 | */ |
| 75 | private LocalDateTime dissolvedTime; | 75 | private LocalDateTime dissolvedTime; |
| 76 | /** | 76 | /** |
| 77 | + * 是否全群禁言 | ||
| 78 | + */ | ||
| 79 | + private Boolean mutedAll; | ||
| 80 | + /** | ||
| 77 | * 群置顶消息编号列表 | 81 | * 群置顶消息编号列表 |
| 78 | * <p> | 82 | * <p> |
| 79 | * 仅存 messageId,操作人 / 置顶时间从对应 PIN 事件的消息记录里反查 | 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,5 +74,11 @@ public class ImGroupMemberDO extends BaseDO { | ||
| 74 | * 退群时间 | 74 | * 退群时间 |
| 75 | */ | 75 | */ |
| 76 | private LocalDateTime quitTime; | 76 | private LocalDateTime quitTime; |
| 77 | + /** | ||
| 78 | + * 禁言到期时间 | ||
| 79 | + * <p> | ||
| 80 | + * null 表示未禁言;非 null 且在未来表示禁言中; | ||
| 81 | + */ | ||
| 82 | + private LocalDateTime muteEndTime; | ||
| 77 | 83 | ||
| 78 | } | 84 | } |
| 79 | \ No newline at end of file | 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,4 +117,11 @@ public interface ImGroupMemberMapper extends BaseMapperX<ImGroupMemberDO> { | ||
| 117 | return result; | 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 | \ No newline at end of file | 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,6 +41,11 @@ public interface ErrorCodeConstants { | ||
| 41 | ErrorCode GROUP_ADMIN_MAX_LIMIT = new ErrorCode(1_040_500_009, "群管理员数量不能超过 {} 人"); | 41 | ErrorCode GROUP_ADMIN_MAX_LIMIT = new ErrorCode(1_040_500_009, "群管理员数量不能超过 {} 人"); |
| 42 | ErrorCode GROUP_REMOVE_OWNER_DENIED = new ErrorCode(1_040_500_010, "群主无法被移出群聊"); | 42 | ErrorCode GROUP_REMOVE_OWNER_DENIED = new ErrorCode(1_040_500_010, "群主无法被移出群聊"); |
| 43 | ErrorCode GROUP_REMOVE_ADMIN_DENIED = new ErrorCode(1_040_500_011, "管理员无法移出其他管理员,请先由群主撤销其管理员身份"); | 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 | // ========== 好友 (1-040-600-000) ========== | 50 | // ========== 好友 (1-040-600-000) ========== |
| 46 | ErrorCode FRIEND_NOT_FRIEND = new ErrorCode(1_040_600_001, "对方不是您的好友"); | 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,22 +156,26 @@ public enum ImMessageTypeEnum implements ArrayValuable<Integer> { | ||
| 156 | GROUP_DISSOLVE(1511, "群解散", true, false), | 156 | GROUP_DISSOLVE(1511, "群解散", true, false), |
| 157 | /** | 157 | /** |
| 158 | * 对应 OpenIM:sdkws.GroupMemberMutedTips(GroupMemberMutedNotification 1512) | 158 | * 对应 OpenIM:sdkws.GroupMemberMutedTips(GroupMemberMutedNotification 1512) |
| 159 | - * TODO @AI 未实现:单成员禁言(管理员禁言某成员);本期发版要实现 | 159 | + * 对应自己的类:GroupMemberMutedNotification |
| 160 | + * 场景:群主 / 管理员禁言某成员,全员广播 | ||
| 160 | */ | 161 | */ |
| 161 | GROUP_MEMBER_MUTED(1512, "成员禁言", true, false), | 162 | GROUP_MEMBER_MUTED(1512, "成员禁言", true, false), |
| 162 | /** | 163 | /** |
| 163 | * 对应 OpenIM:sdkws.GroupMemberCancelMutedTips(GroupMemberCancelMutedNotification 1513) | 164 | * 对应 OpenIM:sdkws.GroupMemberCancelMutedTips(GroupMemberCancelMutedNotification 1513) |
| 164 | - * TODO @AI 未实现:单成员取消禁言;本期发版要实现 | 165 | + * 对应自己的类:GroupMemberCancelMutedNotification |
| 166 | + * 场景:群主 / 管理员取消某成员禁言,全员广播 | ||
| 165 | */ | 167 | */ |
| 166 | GROUP_MEMBER_CANCEL_MUTED(1513, "成员取消禁言", true, false), | 168 | GROUP_MEMBER_CANCEL_MUTED(1513, "成员取消禁言", true, false), |
| 167 | /** | 169 | /** |
| 168 | * 对应 OpenIM:sdkws.GroupMutedTips(GroupMutedNotification 1514) | 170 | * 对应 OpenIM:sdkws.GroupMutedTips(GroupMutedNotification 1514) |
| 169 | - * TODO @AI 未实现:全群禁言(管理员把整个群设为禁言状态,所有成员都不能发消息);本期发版要实现 | 171 | + * 对应自己的类:GroupMutedNotification |
| 172 | + * 场景:群主 / 管理员开启全群禁言,全员广播 | ||
| 170 | */ | 173 | */ |
| 171 | GROUP_MUTED(1514, "全群禁言", true, false), | 174 | GROUP_MUTED(1514, "全群禁言", true, false), |
| 172 | /** | 175 | /** |
| 173 | * 对应 OpenIM:sdkws.GroupCancelMutedTips(GroupCancelMutedNotification 1515) | 176 | * 对应 OpenIM:sdkws.GroupCancelMutedTips(GroupCancelMutedNotification 1515) |
| 174 | - * TODO @AI 未实现:全群取消禁言;本期发版要实现 | 177 | + * 对应自己的类:GroupCancelMutedNotification |
| 178 | + * 场景:群主 / 管理员取消全群禁言,全员广播 | ||
| 175 | */ | 179 | */ |
| 176 | GROUP_CANCEL_MUTED(1515, "全群取消禁言", true, false), | 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,4 +192,13 @@ public interface ImGroupMemberService { | ||
| 192 | */ | 192 | */ |
| 193 | Map<Long, Long> getActiveMemberCountMap(Collection<Long> groupIds); | 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,4 +266,16 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | ||
| 266 | return groupMemberMapper.selectCountMapByGroupIdsAndStatus(groupIds, CommonStatusEnum.ENABLE.getStatus()); | 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,7 +3,10 @@ package cn.iocoder.yudao.module.im.service.group; | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; | 4 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; |
| 5 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; | 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 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; | 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 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; | 10 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; |
| 8 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; | 11 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; |
| 9 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; | 12 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; |
| @@ -174,6 +177,32 @@ public interface ImGroupService { | @@ -174,6 +177,32 @@ public interface ImGroupService { | ||
| 174 | */ | 177 | */ |
| 175 | void unpinGroupMessage(Long userId, Long groupId, Long messageId); | 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,7 +12,10 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; | ||
| 12 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | 12 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 13 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; | 13 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; |
| 14 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; | 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 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; | 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 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; | 19 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; |
| 17 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; | 20 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; |
| 18 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; | 21 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; |
| @@ -527,6 +530,85 @@ public class ImGroupServiceImpl implements ImGroupService { | @@ -527,6 +530,85 @@ public class ImGroupServiceImpl implements ImGroupService { | ||
| 527 | groupMapper.updateById(new ImGroupDO().setId(id).setBanned(false)); | 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 | private ImGroupServiceImpl getSelf() { | 612 | private ImGroupServiceImpl getSelf() { |
| 531 | return SpringUtil.getBean(getClass()); | 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,10 +12,12 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | ||
| 12 | import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO; | 12 | import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO; |
| 13 | import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO; | 13 | import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO; |
| 14 | import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; | 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 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | 16 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; |
| 16 | import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; | 17 | import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; |
| 17 | import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; | 18 | import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; |
| 18 | import cn.iocoder.yudao.module.im.dal.redis.message.GroupMessageReadRedisDAO; | 19 | import cn.iocoder.yudao.module.im.dal.redis.message.GroupMessageReadRedisDAO; |
| 20 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; | ||
| 19 | import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; | 21 | import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; |
| 20 | import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | 22 | import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; |
| 21 | import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; | 23 | import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; |
| @@ -86,9 +88,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | @@ -86,9 +88,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | ||
| 86 | return existing; | 88 | return existing; |
| 87 | } | 89 | } |
| 88 | // 1.2 校验群存在、发送人仍在群中 | 90 | // 1.2 校验群存在、发送人仍在群中 |
| 89 | - groupService.validateGroupExists(reqVO.getGroupId()); | 91 | + ImGroupDO group = groupService.validateGroupExists(reqVO.getGroupId()); |
| 90 | ImGroupMemberDO senderMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), senderId); | 92 | ImGroupMemberDO senderMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), senderId); |
| 91 | - // 1.3 文本消息敏感词过滤 | 93 | + // 1.3 禁言校验 |
| 94 | + validateMuteStatus(group, senderMember); | ||
| 95 | + // 1.4 文本消息敏感词过滤 | ||
| 92 | if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) { | 96 | if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) { |
| 93 | sensitiveWordService.validateText(reqVO.getContent()); | 97 | sensitiveWordService.validateText(reqVO.getContent()); |
| 94 | } | 98 | } |
| @@ -604,4 +608,19 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | @@ -604,4 +608,19 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | ||
| 604 | return SpringUtil.getBean(getClass()); | 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 | \ No newline at end of file | 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,4 +174,37 @@ public class ImGroupMessageSendDTO { | ||
| 174 | .setType(ImMessageTypeEnum.GROUP_MESSAGE_UNPIN.getType()).setContent(notification); | 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,6 +48,7 @@ CREATE TABLE IF NOT EXISTS "im_group" ( | ||
| 48 | "banned_time" timestamp DEFAULT NULL COMMENT '封禁时间', | 48 | "banned_time" timestamp DEFAULT NULL COMMENT '封禁时间', |
| 49 | "status" tinyint NOT NULL COMMENT '群状态', | 49 | "status" tinyint NOT NULL COMMENT '群状态', |
| 50 | "dissolved_time" timestamp DEFAULT NULL COMMENT '解散时间', | 50 | "dissolved_time" timestamp DEFAULT NULL COMMENT '解散时间', |
| 51 | + "muted_all" bit DEFAULT FALSE COMMENT '是否全群禁言', | ||
| 51 | "pinned_message_ids" varchar(128) DEFAULT NULL COMMENT '群置顶消息编号列表,逗号分隔', | 52 | "pinned_message_ids" varchar(128) DEFAULT NULL COMMENT '群置顶消息编号列表,逗号分隔', |
| 52 | "creator" varchar(64) DEFAULT '', | 53 | "creator" varchar(64) DEFAULT '', |
| 53 | "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | 54 | "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| @@ -69,6 +70,7 @@ CREATE TABLE IF NOT EXISTS "im_group_member" ( | @@ -69,6 +70,7 @@ CREATE TABLE IF NOT EXISTS "im_group_member" ( | ||
| 69 | "role" tinyint NOT NULL DEFAULT 3 COMMENT '成员角色:1=群主 2=管理员 3=普通成员', | 70 | "role" tinyint NOT NULL DEFAULT 3 COMMENT '成员角色:1=群主 2=管理员 3=普通成员', |
| 70 | "join_time" timestamp DEFAULT NULL COMMENT '入群时间', | 71 | "join_time" timestamp DEFAULT NULL COMMENT '入群时间', |
| 71 | "quit_time" timestamp DEFAULT NULL COMMENT '退群时间', | 72 | "quit_time" timestamp DEFAULT NULL COMMENT '退群时间', |
| 73 | + "mute_end_time" timestamp DEFAULT NULL COMMENT '禁言到期时间', | ||
| 72 | "creator" varchar(64) DEFAULT '', | 74 | "creator" varchar(64) DEFAULT '', |
| 73 | "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | 75 | "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| 74 | "updater" varchar(64) DEFAULT '', | 76 | "updater" varchar(64) DEFAULT '', |