Commit 37f0a8a01d727f38ca7576a33ee0a3bf5b13acab

Authored by YunaiV
1 parent d0fbab8f

✨ feat(im): 增加 friend、group、message、sensitiveword 的管理接口

Showing 37 changed files with 1292 additions and 45 deletions
yudao-module-im/pom.xml
@@ -62,6 +62,14 @@ @@ -62,6 +62,14 @@
62 <artifactId>yudao-spring-boot-starter-excel</artifactId> 62 <artifactId>yudao-spring-boot-starter-excel</artifactId>
63 </dependency> 63 </dependency>
64 64
  65 + <!-- 敏感词检测:houbb sensitive-word(trie 树高效匹配,支持忽略大小写 / 全半角 / 数字风格) -->
  66 + <!-- TODO @AI:放到基础 pom 里管理。 -->
  67 + <dependency>
  68 + <groupId>com.github.houbb</groupId>
  69 + <artifactId>sensitive-word</artifactId>
  70 + <version>0.29.5</version>
  71 + </dependency>
  72 +
65 <!-- Test 测试相关 --> 73 <!-- Test 测试相关 -->
66 <dependency> 74 <dependency>
67 <groupId>cn.iocoder.boot</groupId> 75 <groupId>cn.iocoder.boot</groupId>
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/ImFriendManagerController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.friend;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerPageReqVO;
  6 +import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerRespVO;
  7 +import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
  8 +import io.swagger.v3.oas.annotations.Operation;
  9 +import io.swagger.v3.oas.annotations.tags.Tag;
  10 +import jakarta.annotation.Resource;
  11 +import jakarta.validation.Valid;
  12 +import org.springframework.security.access.prepost.PreAuthorize;
  13 +import org.springframework.validation.annotation.Validated;
  14 +import org.springframework.web.bind.annotation.GetMapping;
  15 +import org.springframework.web.bind.annotation.RequestMapping;
  16 +import org.springframework.web.bind.annotation.RestController;
  17 +
  18 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  19 +
  20 +@Tag(name = "管理后台 - IM 好友管理")
  21 +@RestController
  22 +@RequestMapping("/im/manager/friend")
  23 +@Validated
  24 +public class ImFriendManagerController {
  25 +
  26 + @Resource
  27 + private ImFriendService friendService;
  28 +
  29 + @GetMapping("/page")
  30 + @Operation(summary = "获得好友关系分页")
  31 + @PreAuthorize("@ss.hasPermission('im:manager:friend:query')")
  32 + public CommonResult<PageResult<ImFriendManagerRespVO>> getFriendPage(
  33 + @Valid ImFriendManagerPageReqVO pageReqVO) {
  34 + return success(friendService.getFriendPage(pageReqVO));
  35 + }
  36 +
  37 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendManagerPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +
  9 +import java.time.LocalDateTime;
  10 +
  11 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  12 +
  13 +@Schema(description = "管理后台 - IM 好友关系分页 Request VO")
  14 +@Data
  15 +@EqualsAndHashCode(callSuper = true)
  16 +public class ImFriendManagerPageReqVO extends PageParam {
  17 +
  18 + @Schema(description = "用户编号", example = "1024")
  19 + private Long userId;
  20 +
  21 + @Schema(description = "好友用户编号", example = "2048")
  22 + private Long friendUserId;
  23 +
  24 + @Schema(description = "好友状态", example = "0")
  25 + private Integer status; // 参见 CommonStatusEnum 枚举类
  26 +
  27 + @Schema(description = "是否免打扰", example = "false")
  28 + private Boolean muted;
  29 +
  30 + @Schema(description = "添加好友时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
  31 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  32 + private LocalDateTime[] addTime;
  33 +
  34 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/friend/vo/ImFriendManagerRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - IM 好友关系 Response VO")
  9 +@Data
  10 +public class ImFriendManagerRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  16 + private Long userId;
  17 +
  18 + @Schema(description = "用户昵称", example = "张三")
  19 + private String userNickname;
  20 +
  21 + @Schema(description = "好友用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
  22 + private Long friendUserId;
  23 +
  24 + @Schema(description = "好友昵称", example = "李四")
  25 + private String friendNickname;
  26 +
  27 + @Schema(description = "好友展示备注")
  28 + private String displayName;
  29 +
  30 + @Schema(description = "是否免打扰", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
  31 + private Boolean muted;
  32 +
  33 + @Schema(description = "好友状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  34 + private Integer status; // 参见 CommonStatusEnum 枚举类
  35 +
  36 + @Schema(description = "添加好友时间")
  37 + private LocalDateTime addTime;
  38 +
  39 + @Schema(description = "删除好友时间")
  40 + private LocalDateTime deleteTime;
  41 +
  42 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  43 + private LocalDateTime createTime;
  44 +
  45 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/ImGroupManagerController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.group;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO;
  7 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO;
  8 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerRespVO;
  9 +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
  10 +import cn.iocoder.yudao.module.im.service.group.ImGroupService;
  11 +import io.swagger.v3.oas.annotations.Operation;
  12 +import io.swagger.v3.oas.annotations.Parameter;
  13 +import io.swagger.v3.oas.annotations.tags.Tag;
  14 +import jakarta.annotation.Resource;
  15 +import jakarta.validation.Valid;
  16 +import org.springframework.security.access.prepost.PreAuthorize;
  17 +import org.springframework.validation.annotation.Validated;
  18 +import org.springframework.web.bind.annotation.*;
  19 +
  20 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  21 +
  22 +@Tag(name = "管理后台 - IM 群聊管理")
  23 +@RestController
  24 +@RequestMapping("/im/manager/group")
  25 +@Validated
  26 +public class ImGroupManagerController {
  27 +
  28 + @Resource
  29 + private ImGroupService groupService;
  30 +
  31 + @GetMapping("/page")
  32 + @Operation(summary = "获得群分页")
  33 + @PreAuthorize("@ss.hasPermission('im:manager:group:query')")
  34 + public CommonResult<PageResult<ImGroupManagerRespVO>> getGroupPage(@Valid ImGroupManagerPageReqVO pageReqVO) {
  35 + return success(groupService.getGroupPage(pageReqVO));
  36 + }
  37 +
  38 + @GetMapping("/get")
  39 + @Operation(summary = "获得群详情")
  40 + @Parameter(name = "id", description = "群编号", required = true, example = "1024")
  41 + @PreAuthorize("@ss.hasPermission('im:manager:group:query')")
  42 + public CommonResult<ImGroupManagerRespVO> getGroup(@RequestParam("id") Long id) {
  43 + ImGroupDO group = groupService.getGroup(id);
  44 + return success(BeanUtils.toBean(group, ImGroupManagerRespVO.class));
  45 + }
  46 +
  47 + @PutMapping("/ban")
  48 + @Operation(summary = "封禁群")
  49 + @PreAuthorize("@ss.hasPermission('im:manager:group:ban')")
  50 + public CommonResult<Boolean> banGroup(@Valid @RequestBody ImGroupManagerBanReqVO reqVO) {
  51 + groupService.banGroup(reqVO);
  52 + return success(true);
  53 + }
  54 +
  55 + @PutMapping("/unban")
  56 + @Operation(summary = "解封群")
  57 + @Parameter(name = "id", description = "群编号", required = true, example = "1024")
  58 + @PreAuthorize("@ss.hasPermission('im:manager:group:ban')")
  59 + public CommonResult<Boolean> unbanGroup(@RequestParam("id") Long id) {
  60 + groupService.unbanGroup(id);
  61 + return success(true);
  62 + }
  63 +
  64 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/group/ImGroupMemberManagerController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.group;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.member.ImGroupMemberManagerRespVO;
  5 +import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
  6 +import io.swagger.v3.oas.annotations.Operation;
  7 +import io.swagger.v3.oas.annotations.Parameter;
  8 +import io.swagger.v3.oas.annotations.tags.Tag;
  9 +import jakarta.annotation.Resource;
  10 +import org.springframework.security.access.prepost.PreAuthorize;
  11 +import org.springframework.validation.annotation.Validated;
  12 +import org.springframework.web.bind.annotation.GetMapping;
  13 +import org.springframework.web.bind.annotation.RequestMapping;
  14 +import org.springframework.web.bind.annotation.RequestParam;
  15 +import org.springframework.web.bind.annotation.RestController;
  16 +
  17 +import java.util.List;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  20 +
  21 +@Tag(name = "管理后台 - IM 群成员管理")
  22 +@RestController
  23 +@RequestMapping("/im/manager/group/member")
  24 +@Validated
  25 +public class ImGroupMemberManagerController {
  26 +
  27 + @Resource
  28 + private ImGroupMemberService groupMemberService;
  29 +
  30 + @GetMapping("/list")
  31 + @Operation(summary = "获得群成员列表")
  32 + @Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
  33 + @PreAuthorize("@ss.hasPermission('im:manager:group:query')")
  34 + public CommonResult<List<ImGroupMemberManagerRespVO>> getGroupMemberList(@RequestParam("groupId") Long groupId) {
  35 + return success(groupMemberService.getGroupMemberList(groupId));
  36 + }
  37 +
  38 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImGroupMessageManagerController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.message;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO;
  7 +import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerRespVO;
  8 +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
  9 +import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
  10 +import io.swagger.v3.oas.annotations.Operation;
  11 +import io.swagger.v3.oas.annotations.Parameter;
  12 +import io.swagger.v3.oas.annotations.tags.Tag;
  13 +import jakarta.annotation.Resource;
  14 +import jakarta.validation.Valid;
  15 +import org.springframework.security.access.prepost.PreAuthorize;
  16 +import org.springframework.validation.annotation.Validated;
  17 +import org.springframework.web.bind.annotation.*;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  20 +
  21 +@Tag(name = "管理后台 - IM 群聊消息")
  22 +@RestController
  23 +@RequestMapping("/im/manager/message/group")
  24 +@Validated
  25 +public class ImGroupMessageManagerController {
  26 +
  27 + @Resource
  28 + private ImGroupMessageService groupMessageService;
  29 +
  30 + @GetMapping("/page")
  31 + @Operation(summary = "获得群聊消息分页")
  32 + @PreAuthorize("@ss.hasPermission('im:manager:message:query')")
  33 + public CommonResult<PageResult<ImGroupMessageManagerRespVO>> getGroupMessagePage(
  34 + @Valid ImGroupMessageManagerPageReqVO pageReqVO) {
  35 + return success(groupMessageService.getGroupMessagePage(pageReqVO));
  36 + }
  37 +
  38 + @GetMapping("/get")
  39 + @Operation(summary = "获得群聊消息详情")
  40 + @Parameter(name = "id", description = "消息编号", required = true, example = "1024")
  41 + @PreAuthorize("@ss.hasPermission('im:manager:message:query')")
  42 + public CommonResult<ImGroupMessageManagerRespVO> getGroupMessage(@RequestParam("id") Long id) {
  43 + ImGroupMessageDO message = groupMessageService.getGroupMessage(id);
  44 + return success(BeanUtils.toBean(message, ImGroupMessageManagerRespVO.class));
  45 + }
  46 +
  47 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/ImPrivateMessageManagerController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.message;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerPageReqVO;
  7 +import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerRespVO;
  8 +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
  9 +import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
  10 +import io.swagger.v3.oas.annotations.Operation;
  11 +import io.swagger.v3.oas.annotations.Parameter;
  12 +import io.swagger.v3.oas.annotations.tags.Tag;
  13 +import jakarta.annotation.Resource;
  14 +import jakarta.validation.Valid;
  15 +import org.springframework.security.access.prepost.PreAuthorize;
  16 +import org.springframework.validation.annotation.Validated;
  17 +import org.springframework.web.bind.annotation.*;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  20 +
  21 +@Tag(name = "管理后台 - IM 私聊消息")
  22 +@RestController
  23 +@RequestMapping("/im/manager/message/private")
  24 +@Validated
  25 +public class ImPrivateMessageManagerController {
  26 +
  27 + @Resource
  28 + private ImPrivateMessageService privateMessageService;
  29 +
  30 + @GetMapping("/page")
  31 + @Operation(summary = "获得私聊消息分页")
  32 + @PreAuthorize("@ss.hasPermission('im:manager:message:query')")
  33 + public CommonResult<PageResult<ImPrivateMessageManagerRespVO>> getPrivateMessagePage(
  34 + @Valid ImPrivateMessageManagerPageReqVO pageReqVO) {
  35 + return success(privateMessageService.getPrivateMessagePage(pageReqVO));
  36 + }
  37 +
  38 + @GetMapping("/get")
  39 + @Operation(summary = "获得私聊消息详情")
  40 + @Parameter(name = "id", description = "消息编号", required = true, example = "1024")
  41 + @PreAuthorize("@ss.hasPermission('im:manager:message:query')")
  42 + public CommonResult<ImPrivateMessageManagerRespVO> getPrivateMessage(@RequestParam("id") Long id) {
  43 + ImPrivateMessageDO message = privateMessageService.getPrivateMessage(id);
  44 + return success(BeanUtils.toBean(message, ImPrivateMessageManagerRespVO.class));
  45 + }
  46 +
  47 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/group/ImGroupMessageManagerPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +
  9 +import java.time.LocalDateTime;
  10 +
  11 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  12 +
  13 +@Schema(description = "管理后台 - IM 群聊消息分页 Request VO")
  14 +@Data
  15 +@EqualsAndHashCode(callSuper = true)
  16 +public class ImGroupMessageManagerPageReqVO extends PageParam {
  17 +
  18 + @Schema(description = "群编号", example = "1024")
  19 + private Long groupId;
  20 +
  21 + @Schema(description = "发送人编号", example = "1024")
  22 + private Long senderId;
  23 +
  24 + // TODO @AI:参见 ImMessageTypeEnum 枚举类,都放到 // 注释后面。注意加到我的 memory 里。其它类似也处理下。im 的。
  25 + @Schema(description = "消息类型,参见 ImMessageTypeEnum 枚举类", example = "1")
  26 + private Integer type;
  27 +
  28 + @Schema(description = "消息状态,参见 ImMessageStatusEnum 枚举类", example = "0")
  29 + private Integer status;
  30 +
  31 + @Schema(description = "发送时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
  32 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  33 + private LocalDateTime[] sendTime;
  34 +
  35 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/group/ImGroupMessageManagerRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +import java.util.List;
  8 +
  9 +@Schema(description = "管理后台 - IM 群聊消息 Response VO")
  10 +@Data
  11 +public class ImGroupMessageManagerRespVO {
  12 +
  13 + @Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  14 + private Long id;
  15 +
  16 + @Schema(description = "客户端消息编号", example = "c-uuid-xxx")
  17 + private String clientMessageId;
  18 +
  19 + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  20 + private Long groupId;
  21 +
  22 + @Schema(description = "群名称", example = "技术交流群")
  23 + private String groupName;
  24 +
  25 + @Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  26 + private Long senderId;
  27 +
  28 + @Schema(description = "发送人昵称", example = "张三")
  29 + private String senderNickname;
  30 +
  31 + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  32 + private Integer type; // 参见 ImMessageTypeEnum 枚举类
  33 +
  34 + @Schema(description = "消息内容(JSON 格式)", requiredMode = Schema.RequiredMode.REQUIRED)
  35 + private String content;
  36 +
  37 + @Schema(description = "消息状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  38 + private Integer status; // 参见 ImMessageStatusEnum 枚举类
  39 +
  40 + @Schema(description = "@ 目标用户编号列表")
  41 + private List<Long> atUserIds;
  42 +
  43 + @Schema(description = "回执状态", example = "0")
  44 + private Integer receiptStatus; // 参见 ImGroupMessageReceiptStatusEnum 枚举类
  45 +
  46 + @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
  47 + private LocalDateTime sendTime;
  48 +
  49 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  50 + private LocalDateTime createTime;
  51 +
  52 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/privates/ImPrivateMessageManagerPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +
  9 +import java.time.LocalDateTime;
  10 +
  11 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  12 +
  13 +@Schema(description = "管理后台 - IM 私聊消息分页 Request VO")
  14 +@Data
  15 +@EqualsAndHashCode(callSuper = true)
  16 +public class ImPrivateMessageManagerPageReqVO extends PageParam {
  17 +
  18 + @Schema(description = "发送人编号", example = "1024")
  19 + private Long senderId;
  20 +
  21 + @Schema(description = "接收人编号", example = "2048")
  22 + private Long receiverId;
  23 +
  24 + @Schema(description = "消息类型,参见 ImMessageTypeEnum 枚举类", example = "1")
  25 + private Integer type;
  26 +
  27 + @Schema(description = "消息状态,参见 ImMessageStatusEnum 枚举类", example = "0")
  28 + private Integer status;
  29 +
  30 + @Schema(description = "发送时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
  31 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  32 + private LocalDateTime[] sendTime;
  33 +
  34 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/privates/ImPrivateMessageManagerRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - IM 私聊消息 Response VO")
  9 +@Data
  10 +public class ImPrivateMessageManagerRespVO {
  11 +
  12 + @Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "客户端消息编号", example = "c-uuid-xxx")
  16 + private String clientMessageId;
  17 +
  18 + @Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  19 + private Long senderId;
  20 +
  21 + @Schema(description = "发送人昵称", example = "张三")
  22 + private String senderNickname;
  23 +
  24 + @Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
  25 + private Long receiverId;
  26 +
  27 + @Schema(description = "接收人昵称", example = "李四")
  28 + private String receiverNickname;
  29 +
  30 + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  31 + private Integer type; // 参见 ImMessageTypeEnum 枚举类
  32 +
  33 + @Schema(description = "消息内容(JSON 格式)", requiredMode = Schema.RequiredMode.REQUIRED)
  34 + private String content;
  35 +
  36 + @Schema(description = "消息状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  37 + private Integer status; // 参见 ImMessageStatusEnum 枚举类
  38 +
  39 + @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
  40 + private LocalDateTime sendTime;
  41 +
  42 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  43 + private LocalDateTime createTime;
  44 +
  45 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/ImSensitiveWordManagerController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordPageReqVO;
  7 +import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordRespVO;
  8 +import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordSaveReqVO;
  9 +import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
  10 +import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
  11 +import io.swagger.v3.oas.annotations.Operation;
  12 +import io.swagger.v3.oas.annotations.Parameter;
  13 +import io.swagger.v3.oas.annotations.tags.Tag;
  14 +import jakarta.annotation.Resource;
  15 +import jakarta.validation.Valid;
  16 +import org.springframework.security.access.prepost.PreAuthorize;
  17 +import org.springframework.validation.annotation.Validated;
  18 +import org.springframework.web.bind.annotation.*;
  19 +
  20 +import java.util.List;
  21 +
  22 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  23 +
  24 +@Tag(name = "管理后台 - IM 敏感词")
  25 +@RestController
  26 +@RequestMapping("/im/manager/sensitive-word")
  27 +@Validated
  28 +public class ImSensitiveWordManagerController {
  29 +
  30 + @Resource
  31 + private ImSensitiveWordService sensitiveWordService;
  32 +
  33 + @PostMapping("/create")
  34 + @Operation(summary = "新增敏感词")
  35 + @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:create')")
  36 + public CommonResult<Long> createSensitiveWord(@Valid @RequestBody ImSensitiveWordSaveReqVO reqVO) {
  37 + return success(sensitiveWordService.createSensitiveWord(reqVO));
  38 + }
  39 +
  40 + @PutMapping("/update")
  41 + @Operation(summary = "修改敏感词")
  42 + @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:update')")
  43 + public CommonResult<Boolean> updateSensitiveWord(@Valid @RequestBody ImSensitiveWordSaveReqVO reqVO) {
  44 + sensitiveWordService.updateSensitiveWord(reqVO);
  45 + return success(true);
  46 + }
  47 +
  48 + @DeleteMapping("/delete")
  49 + @Operation(summary = "删除敏感词")
  50 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  51 + @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:delete')")
  52 + public CommonResult<Boolean> deleteSensitiveWord(@RequestParam("id") Long id) {
  53 + sensitiveWordService.deleteSensitiveWord(id);
  54 + return success(true);
  55 + }
  56 +
  57 + @DeleteMapping("/delete-list")
  58 + @Operation(summary = "批量删除敏感词")
  59 + @Parameter(name = "ids", description = "编号列表", required = true)
  60 + @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:delete')")
  61 + public CommonResult<Boolean> deleteSensitiveWordList(@RequestParam("ids") List<Long> ids) {
  62 + sensitiveWordService.deleteSensitiveWordList(ids);
  63 + return success(true);
  64 + }
  65 +
  66 + @GetMapping("/page")
  67 + @Operation(summary = "获得敏感词分页")
  68 + @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:query')")
  69 + public CommonResult<PageResult<ImSensitiveWordRespVO>> getSensitiveWordPage(
  70 + @Valid ImSensitiveWordPageReqVO pageReqVO) {
  71 + PageResult<ImSensitiveWordDO> pageResult = sensitiveWordService.getSensitiveWordPage(pageReqVO);
  72 + return success(BeanUtils.toBean(pageResult, ImSensitiveWordRespVO.class));
  73 + }
  74 +
  75 + @GetMapping("/get")
  76 + @Operation(summary = "获得敏感词详情")
  77 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  78 + @PreAuthorize("@ss.hasPermission('im:manager:sensitive-word:query')")
  79 + public CommonResult<ImSensitiveWordRespVO> getSensitiveWord(@RequestParam("id") Long id) {
  80 + ImSensitiveWordDO word = sensitiveWordService.getSensitiveWord(id);
  81 + return success(BeanUtils.toBean(word, ImSensitiveWordRespVO.class));
  82 + }
  83 +
  84 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +
  9 +import java.time.LocalDateTime;
  10 +
  11 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  12 +
  13 +@Schema(description = "管理后台 - IM 敏感词分页 Request VO")
  14 +@Data
  15 +@EqualsAndHashCode(callSuper = true)
  16 +public class ImSensitiveWordPageReqVO extends PageParam {
  17 +
  18 + @Schema(description = "敏感词,模糊匹配", example = "敏感")
  19 + private String word;
  20 +
  21 + @Schema(description = "状态,参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)", example = "0")
  22 + private Integer status;
  23 +
  24 + @Schema(description = "创建时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
  25 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  26 + private LocalDateTime[] createTime;
  27 +
  28 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - IM 敏感词 Response VO")
  9 +@Data
  10 +public class ImSensitiveWordRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "敏感词", requiredMode = Schema.RequiredMode.REQUIRED, example = "敏感词内容")
  16 + private String word;
  17 +
  18 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  19 + private Integer status; // 参见 CommonStatusEnum 枚举类
  20 +
  21 + @Schema(description = "创建人")
  22 + private String creator;
  23 +
  24 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  25 + private LocalDateTime createTime;
  26 +
  27 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/sensitiveword/vo/ImSensitiveWordSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotBlank;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import jakarta.validation.constraints.Size;
  7 +import lombok.Data;
  8 +
  9 +@Schema(description = "管理后台 - IM 敏感词新增 / 修改 Request VO")
  10 +@Data
  11 +public class ImSensitiveWordSaveReqVO {
  12 +
  13 + @Schema(description = "编号(修改时必填)", example = "1024")
  14 + private Long id;
  15 +
  16 + @Schema(description = "敏感词", requiredMode = Schema.RequiredMode.REQUIRED, example = "敏感词内容")
  17 + @NotBlank(message = "敏感词不能为空")
  18 + @Size(max = 64, message = "敏感词长度不能超过 64")
  19 + private String word;
  20 +
  21 + @Schema(description = "状态,参见 CommonStatusEnum 枚举类(0 启用 / 1 禁用)",
  22 + requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  23 + @NotNull(message = "状态不能为空")
  24 + private Integer status;
  25 +
  26 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java
1 package cn.iocoder.yudao.module.im.dal.mysql.friend; 1 package cn.iocoder.yudao.module.im.dal.mysql.friend;
2 2
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerPageReqVO;
5 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; 7 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
6 import org.apache.ibatis.annotations.Mapper; 8 import org.apache.ibatis.annotations.Mapper;
7 9
@@ -43,4 +45,14 @@ public interface ImFriendMapper extends BaseMapperX&lt;ImFriendDO&gt; { @@ -43,4 +45,14 @@ public interface ImFriendMapper extends BaseMapperX&lt;ImFriendDO&gt; {
43 .eq(ImFriendDO::getStatus, status)); 45 .eq(ImFriendDO::getStatus, status));
44 } 46 }
45 47
  48 + default PageResult<ImFriendDO> selectPage(ImFriendManagerPageReqVO reqVO) {
  49 + return selectPage(reqVO, new LambdaQueryWrapperX<ImFriendDO>()
  50 + .eqIfPresent(ImFriendDO::getUserId, reqVO.getUserId())
  51 + .eqIfPresent(ImFriendDO::getFriendUserId, reqVO.getFriendUserId())
  52 + .eqIfPresent(ImFriendDO::getStatus, reqVO.getStatus())
  53 + .eqIfPresent(ImFriendDO::getMuted, reqVO.getMuted())
  54 + .betweenIfPresent(ImFriendDO::getAddTime, reqVO.getAddTime())
  55 + .orderByDesc(ImFriendDO::getId));
  56 + }
  57 +
46 } 58 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMapper.java
1 package cn.iocoder.yudao.module.im.dal.mysql.group; 1 package cn.iocoder.yudao.module.im.dal.mysql.group;
2 2
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO;
4 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; 7 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
5 import org.apache.ibatis.annotations.Mapper; 8 import org.apache.ibatis.annotations.Mapper;
6 9
@@ -11,4 +14,15 @@ import org.apache.ibatis.annotations.Mapper; @@ -11,4 +14,15 @@ import org.apache.ibatis.annotations.Mapper;
11 */ 14 */
12 @Mapper 15 @Mapper
13 public interface ImGroupMapper extends BaseMapperX<ImGroupDO> { 16 public interface ImGroupMapper extends BaseMapperX<ImGroupDO> {
  17 +
  18 + default PageResult<ImGroupDO> selectPage(ImGroupManagerPageReqVO reqVO) {
  19 + return selectPage(reqVO, new LambdaQueryWrapperX<ImGroupDO>()
  20 + .likeIfPresent(ImGroupDO::getName, reqVO.getName())
  21 + .eqIfPresent(ImGroupDO::getOwnerUserId, reqVO.getOwnerUserId())
  22 + .eqIfPresent(ImGroupDO::getStatus, reqVO.getStatus())
  23 + .eqIfPresent(ImGroupDO::getBanned, reqVO.getBanned())
  24 + .betweenIfPresent(ImGroupDO::getCreateTime, reqVO.getCreateTime())
  25 + .orderByDesc(ImGroupDO::getId));
  26 + }
  27 +
14 } 28 }
15 \ No newline at end of file 29 \ No newline at end of file
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java
1 package cn.iocoder.yudao.module.im.dal.mysql.group; 1 package cn.iocoder.yudao.module.im.dal.mysql.group;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 7 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
  8 +import com.baomidou.mybatisplus.core.toolkit.Wrappers;
7 import org.apache.ibatis.annotations.Mapper; 9 import org.apache.ibatis.annotations.Mapper;
8 10
9 import java.time.LocalDateTime; 11 import java.time.LocalDateTime;
10 import java.util.Collection; 12 import java.util.Collection;
  13 +import java.util.Collections;
  14 +import java.util.HashMap;
11 import java.util.List; 15 import java.util.List;
  16 +import java.util.Map;
12 17
13 /** 18 /**
14 * IM 群成员 Mapper 19 * IM 群成员 Mapper
@@ -78,4 +83,25 @@ public interface ImGroupMemberMapper extends BaseMapperX&lt;ImGroupMemberDO&gt; { @@ -78,4 +83,25 @@ public interface ImGroupMemberMapper extends BaseMapperX&lt;ImGroupMemberDO&gt; {
78 .eq(ImGroupMemberDO::getStatus, oldStatus)); 83 .eq(ImGroupMemberDO::getStatus, oldStatus));
79 } 84 }
80 85
  86 + /**
  87 + * 批量按 group_id 统计指定状态的成员数
  88 + */
  89 + default Map<Long, Long> selectCountMapByGroupIdsAndStatus(Collection<Long> groupIds, Integer status) {
  90 + if (CollUtil.isEmpty(groupIds)) {
  91 + return Collections.emptyMap();
  92 + }
  93 + // TODO @AI:使用 mybatis plus join,可以简化这个操作么?
  94 + List<Map<String, Object>> rows = selectMaps(Wrappers.<ImGroupMemberDO>query()
  95 + .select("group_id AS groupId", "COUNT(*) AS cnt")
  96 + .in("group_id", groupIds)
  97 + .eq("status", status)
  98 + .groupBy("group_id"));
  99 + // 转换成 Map<Long, Long>
  100 + Map<Long, Long> result = new HashMap<>(rows.size());
  101 + rows.forEach(row -> result.put(
  102 + ((Number) row.get("groupId")).longValue(),
  103 + ((Number) row.get("cnt")).longValue()));
  104 + return result;
  105 + }
  106 +
81 } 107 }
82 \ No newline at end of file 108 \ No newline at end of file
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapper.java
1 package cn.iocoder.yudao.module.im.dal.mysql.message; 1 package cn.iocoder.yudao.module.im.dal.mysql.message;
2 2
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; 6 import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
  7 +import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO;
6 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; 8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
7 import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; 9 import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum;
8 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 10 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
@@ -113,4 +115,14 @@ public interface ImGroupMessageMapper extends BaseMapperX&lt;ImGroupMessageDO&gt; { @@ -113,4 +115,14 @@ public interface ImGroupMessageMapper extends BaseMapperX&lt;ImGroupMessageDO&gt; {
113 .ne(ImGroupMessageDO::getStatus, ImMessageStatusEnum.RECALL.getStatus())); 115 .ne(ImGroupMessageDO::getStatus, ImMessageStatusEnum.RECALL.getStatus()));
114 } 116 }
115 117
  118 + default PageResult<ImGroupMessageDO> selectPage(ImGroupMessageManagerPageReqVO reqVO) {
  119 + return selectPage(reqVO, new LambdaQueryWrapperX<ImGroupMessageDO>()
  120 + .eqIfPresent(ImGroupMessageDO::getGroupId, reqVO.getGroupId())
  121 + .eqIfPresent(ImGroupMessageDO::getSenderId, reqVO.getSenderId())
  122 + .eqIfPresent(ImGroupMessageDO::getType, reqVO.getType())
  123 + .eqIfPresent(ImGroupMessageDO::getStatus, reqVO.getStatus())
  124 + .betweenIfPresent(ImGroupMessageDO::getSendTime, reqVO.getSendTime())
  125 + .orderByDesc(ImGroupMessageDO::getId));
  126 + }
  127 +
116 } 128 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapper.java
1 package cn.iocoder.yudao.module.im.dal.mysql.message; 1 package cn.iocoder.yudao.module.im.dal.mysql.message;
2 2
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; 6 import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
  7 +import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerPageReqVO;
6 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
7 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 9 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
8 import org.apache.ibatis.annotations.Mapper; 10 import org.apache.ibatis.annotations.Mapper;
@@ -86,4 +88,14 @@ public interface ImPrivateMessageMapper extends BaseMapperX&lt;ImPrivateMessageDO&gt; @@ -86,4 +88,14 @@ public interface ImPrivateMessageMapper extends BaseMapperX&lt;ImPrivateMessageDO&gt;
86 .eq(ImPrivateMessageDO::getStatus, whereStatus)); 88 .eq(ImPrivateMessageDO::getStatus, whereStatus));
87 } 89 }
88 90
  91 + default PageResult<ImPrivateMessageDO> selectPage(ImPrivateMessageManagerPageReqVO reqVO) {
  92 + return selectPage(reqVO, new LambdaQueryWrapperX<ImPrivateMessageDO>()
  93 + .eqIfPresent(ImPrivateMessageDO::getSenderId, reqVO.getSenderId())
  94 + .eqIfPresent(ImPrivateMessageDO::getReceiverId, reqVO.getReceiverId())
  95 + .eqIfPresent(ImPrivateMessageDO::getType, reqVO.getType())
  96 + .eqIfPresent(ImPrivateMessageDO::getStatus, reqVO.getStatus())
  97 + .betweenIfPresent(ImPrivateMessageDO::getSendTime, reqVO.getSendTime())
  98 + .orderByDesc(ImPrivateMessageDO::getId));
  99 + }
  100 +
89 } 101 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/sensitiveword/ImSensitiveWordMapper.java
1 package cn.iocoder.yudao.module.im.dal.mysql.sensitiveword; 1 package cn.iocoder.yudao.module.im.dal.mysql.sensitiveword;
2 2
3 -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordPageReqVO;
6 import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO; 7 import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
7 import org.apache.ibatis.annotations.Mapper; 8 import org.apache.ibatis.annotations.Mapper;
8 9
@@ -16,10 +17,22 @@ import java.util.List; @@ -16,10 +17,22 @@ import java.util.List;
16 @Mapper 17 @Mapper
17 public interface ImSensitiveWordMapper extends BaseMapperX<ImSensitiveWordDO> { 18 public interface ImSensitiveWordMapper extends BaseMapperX<ImSensitiveWordDO> {
18 19
19 - // DONE @AI:已改名为 selectListByStatus,status 作为参数  
20 default List<ImSensitiveWordDO> selectListByStatus(Integer status) { 20 default List<ImSensitiveWordDO> selectListByStatus(Integer status) {
21 return selectList(new LambdaQueryWrapperX<ImSensitiveWordDO>() 21 return selectList(new LambdaQueryWrapperX<ImSensitiveWordDO>()
22 .eq(ImSensitiveWordDO::getStatus, status)); 22 .eq(ImSensitiveWordDO::getStatus, status));
23 } 23 }
24 24
  25 + default ImSensitiveWordDO selectByWord(String word) {
  26 + return selectOne(new LambdaQueryWrapperX<ImSensitiveWordDO>()
  27 + .eq(ImSensitiveWordDO::getWord, word));
  28 + }
  29 +
  30 + default PageResult<ImSensitiveWordDO> selectPage(ImSensitiveWordPageReqVO reqVO) {
  31 + return selectPage(reqVO, new LambdaQueryWrapperX<ImSensitiveWordDO>()
  32 + .likeIfPresent(ImSensitiveWordDO::getWord, reqVO.getWord())
  33 + .eqIfPresent(ImSensitiveWordDO::getStatus, reqVO.getStatus())
  34 + .betweenIfPresent(ImSensitiveWordDO::getCreateTime, reqVO.getCreateTime())
  35 + .orderByDesc(ImSensitiveWordDO::getId));
  36 + }
  37 +
25 } 38 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java
@@ -34,4 +34,8 @@ public interface ErrorCodeConstants { @@ -34,4 +34,8 @@ public interface ErrorCodeConstants {
34 ErrorCode FRIEND_NOT_FRIEND = new ErrorCode(1_040_600_001, "对方不是您的好友"); 34 ErrorCode FRIEND_NOT_FRIEND = new ErrorCode(1_040_600_001, "对方不是您的好友");
35 ErrorCode FRIEND_ADD_SELF = new ErrorCode(1_040_600_002, "不允许添加自己为好友"); 35 ErrorCode FRIEND_ADD_SELF = new ErrorCode(1_040_600_002, "不允许添加自己为好友");
36 36
  37 + // ========== 敏感词 (1-040-700-000) ==========
  38 + ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1_040_700_000, "敏感词不存在");
  39 + ErrorCode SENSITIVE_WORD_DUPLICATED = new ErrorCode(1_040_700_001, "敏感词 '{}' 已存在");
  40 +
37 } 41 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/sensitiveword/ImSensitiveWordConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.framework.sensitiveword;
  2 +
  3 +import com.github.houbb.sensitive.word.bs.SensitiveWordBs;
  4 +import org.springframework.context.annotation.Bean;
  5 +import org.springframework.context.annotation.Configuration;
  6 +
  7 +/**
  8 + * IM 敏感词检测配置
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Configuration
  13 +public class ImSensitiveWordConfiguration {
  14 +
  15 + // TODO @AI:可能需要按需创建;因为多租户。
  16 + /**
  17 + * 装配 {@link SensitiveWordBs}:
  18 + * 词库来源 = {@link ImSensitiveWordDeny}(DB 里启用的敏感词),并开启常用规范化能力。
  19 + */
  20 + @Bean
  21 + public SensitiveWordBs sensitiveWordBs(ImSensitiveWordDeny imSensitiveWordDeny) {
  22 + return SensitiveWordBs.newInstance()
  23 + .wordDeny(imSensitiveWordDeny)
  24 + .ignoreCase(true)
  25 + .ignoreWidth(true) // 忽略全/半角
  26 + .ignoreNumStyle(true) // 忽略数字风格(中文/阿拉伯)
  27 + .ignoreChineseStyle(true) // 忽略繁简体
  28 + .enableWordCheck(true)
  29 + .init();
  30 + }
  31 +
  32 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/framework/sensitiveword/ImSensitiveWordDeny.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.framework.sensitiveword;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
  5 +import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper;
  6 +import com.github.houbb.sensitive.word.api.IWordDeny;
  7 +import jakarta.annotation.Resource;
  8 +import org.springframework.stereotype.Component;
  9 +
  10 +import java.util.List;
  11 +
  12 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  13 +
  14 +// TODO @AI:是不是可以,直接塞入一个数组。而不是这里查询???
  15 +/**
  16 + * 自定义敏感词数据来源:从 im_sensitive_word 表加载启用状态的敏感词
  17 + * <p>
  18 + * 由 {@link com.github.houbb.sensitive.word.bs.SensitiveWordBs} 在 init() 时回调,
  19 + * 因此 CRUD 完成后调用 sensitiveWordBs.init() 即可实现热更新。
  20 + *
  21 + * @author 芋道源码
  22 + */
  23 +@Component
  24 +public class ImSensitiveWordDeny implements IWordDeny {
  25 +
  26 + @Resource
  27 + private ImSensitiveWordMapper sensitiveWordMapper;
  28 +
  29 + @Override
  30 + public List<String> deny() {
  31 + return convertList(
  32 + sensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()),
  33 + ImSensitiveWordDO::getWord);
  34 + }
  35 +
  36 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java
1 package cn.iocoder.yudao.module.im.service.friend; 1 package cn.iocoder.yudao.module.im.service.friend;
2 2
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
3 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO; 4 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
  5 +import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerPageReqVO;
  6 +import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerRespVO;
4 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; 7 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
5 8
6 import java.util.Collection; 9 import java.util.Collection;
@@ -84,5 +87,11 @@ public interface ImFriendService { @@ -84,5 +87,11 @@ public interface ImFriendService {
84 */ 87 */
85 void updateFriend(Long userId, ImFriendUpdateReqVO reqVO); 88 void updateFriend(Long userId, ImFriendUpdateReqVO reqVO);
86 89
  90 + // ==================== 管理后台 ====================
  91 +
  92 + /**
  93 + * 【管理后台】分页查询好友关系(已填充用户 / 好友昵称)
  94 + */
  95 + PageResult<ImFriendManagerRespVO> getFriendPage(ImFriendManagerPageReqVO reqVO);
87 96
88 } 97 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
1 package cn.iocoder.yudao.module.im.service.friend; 1 package cn.iocoder.yudao.module.im.service.friend;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
3 import cn.hutool.extra.spring.SpringUtil; 4 import cn.hutool.extra.spring.SpringUtil;
4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  8 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
5 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO; 9 import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO;
  10 +import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerPageReqVO;
  11 +import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerRespVO;
6 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; 12 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
7 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper; 13 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
8 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService; 14 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
9 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 15 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
10 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; 16 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
11 import cn.iocoder.yudao.module.system.api.user.AdminUserApi; 17 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  18 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
12 import jakarta.annotation.Resource; 19 import jakarta.annotation.Resource;
13 import lombok.extern.slf4j.Slf4j; 20 import lombok.extern.slf4j.Slf4j;
14 import org.springframework.cache.annotation.CacheEvict; 21 import org.springframework.cache.annotation.CacheEvict;
@@ -20,11 +27,10 @@ import org.springframework.transaction.annotation.Transactional; @@ -20,11 +27,10 @@ import org.springframework.transaction.annotation.Transactional;
20 import org.springframework.validation.annotation.Validated; 27 import org.springframework.validation.annotation.Validated;
21 28
22 import java.time.LocalDateTime; 29 import java.time.LocalDateTime;
23 -import java.util.Collection;  
24 -import java.util.List;  
25 -import java.util.Objects; 30 +import java.util.*;
26 31
27 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 32 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  33 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
28 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND; 34 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.FRIEND;
29 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_ADD_SELF; 35 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_ADD_SELF;
30 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; 36 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND;
@@ -181,4 +187,28 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -181,4 +187,28 @@ public class ImFriendServiceImpl implements ImFriendService {
181 return SpringUtil.getBean(getClass()); 187 return SpringUtil.getBean(getClass());
182 } 188 }
183 189
  190 + // ==================== 管理后台 ====================
  191 +
  192 + // TODO @AI:拿到 controller 拼接数据,简化 service;
  193 + @Override
  194 + public PageResult<ImFriendManagerRespVO> getFriendPage(ImFriendManagerPageReqVO reqVO) {
  195 + // 1. 分页查询
  196 + PageResult<ImFriendDO> pageResult = imFriendMapper.selectPage(reqVO);
  197 + if (CollUtil.isEmpty(pageResult.getList())) {
  198 + return PageResult.empty(pageResult.getTotal());
  199 + }
  200 +
  201 + // 2. 一次性批量查询用户 + 好友的昵称
  202 + Set<Long> userIds = new HashSet<>();
  203 + userIds.addAll(convertSet(pageResult.getList(), ImFriendDO::getUserId));
  204 + userIds.addAll(convertSet(pageResult.getList(), ImFriendDO::getFriendUserId));
  205 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
  206 +
  207 + // 3. 转换为 VO,填充昵称
  208 + return BeanUtils.toBean(pageResult, ImFriendManagerRespVO.class, vo -> {
  209 + MapUtils.findAndThen(userMap, vo.getUserId(), user -> vo.setUserNickname(user.getNickname()));
  210 + MapUtils.findAndThen(userMap, vo.getFriendUserId(), user -> vo.setFriendNickname(user.getNickname()));
  211 + });
  212 + }
  213 +
184 } 214 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java
1 package cn.iocoder.yudao.module.im.service.group; 1 package cn.iocoder.yudao.module.im.service.group;
2 2
3 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; 3 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO;
  4 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.member.ImGroupMemberManagerRespVO;
4 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 5 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
5 import jakarta.validation.Valid; 6 import jakarta.validation.Valid;
6 7
7 import java.time.LocalDateTime; 8 import java.time.LocalDateTime;
8 import java.util.Collection; 9 import java.util.Collection;
9 import java.util.List; 10 import java.util.List;
  11 +import java.util.Map;
10 12
11 /** 13 /**
12 * 群成员 Service 接口 14 * 群成员 Service 接口
@@ -24,6 +26,15 @@ public interface ImGroupMemberService { @@ -24,6 +26,15 @@ public interface ImGroupMemberService {
24 ImGroupMemberDO getGroupMember(Long id); 26 ImGroupMemberDO getGroupMember(Long id);
25 27
26 /** 28 /**
  29 + * 获得群成员
  30 + *
  31 + * @param groupId 群编号
  32 + * @param userId 用户编号
  33 + * @return 群成员
  34 + */
  35 + ImGroupMemberDO getGroupMember(Long groupId, Long userId);
  36 +
  37 + /**
27 * 根据群组 id 查询群成员(包含所有状态) 38 * 根据群组 id 查询群成员(包含所有状态)
28 * 39 *
29 * @param groupId 群组id 40 * @param groupId 群组id
@@ -137,4 +148,22 @@ public interface ImGroupMemberService { @@ -137,4 +148,22 @@ public interface ImGroupMemberService {
137 */ 148 */
138 void removeGroupMembersByGroupId(Long groupId); 149 void removeGroupMembersByGroupId(Long groupId);
139 150
  151 + /**
  152 + * 批量按 group 统计活跃成员数:(group_id → count)
  153 + *
  154 + * @param groupIds 群编号集合
  155 + * @return 群成员数 Map
  156 + */
  157 + Map<Long, Long> getActiveMemberCountMap(Collection<Long> groupIds);
  158 +
  159 + // ==================== 管理后台 ====================
  160 +
  161 + /**
  162 + * 【管理后台】获取群成员列表
  163 + *
  164 + * @param groupId 群编号
  165 + * @return 群成员列表
  166 + */
  167 + List<ImGroupMemberManagerRespVO> getGroupMemberList(Long groupId);
  168 +
140 } 169 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java
@@ -2,12 +2,16 @@ package cn.iocoder.yudao.module.im.service.group; @@ -2,12 +2,16 @@ package cn.iocoder.yudao.module.im.service.group;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  5 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 6 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; 7 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO;
  8 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.member.ImGroupMemberManagerRespVO;
7 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 9 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
8 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; 10 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper;
9 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 11 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
10 import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; 12 import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
  13 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  14 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
11 import jakarta.annotation.Resource; 15 import jakarta.annotation.Resource;
12 import lombok.extern.slf4j.Slf4j; 16 import lombok.extern.slf4j.Slf4j;
13 import org.springframework.cache.annotation.CacheEvict; 17 import org.springframework.cache.annotation.CacheEvict;
@@ -17,14 +21,10 @@ import org.springframework.stereotype.Service; @@ -17,14 +21,10 @@ import org.springframework.stereotype.Service;
17 import org.springframework.validation.annotation.Validated; 21 import org.springframework.validation.annotation.Validated;
18 22
19 import java.time.LocalDateTime; 23 import java.time.LocalDateTime;
20 -import java.util.ArrayList;  
21 -import java.util.Collection;  
22 -import java.util.List;  
23 -import java.util.Map; 24 +import java.util.*;
24 25
25 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 26 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
26 -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;  
27 -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; 27 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
28 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP_MEMBER_IDS; 28 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP_MEMBER_IDS;
29 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP; 29 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP;
30 30
@@ -44,12 +44,20 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { @@ -44,12 +44,20 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService {
44 @Resource 44 @Resource
45 private ImWebSocketService webSocketService; 45 private ImWebSocketService webSocketService;
46 46
  47 + @Resource
  48 + private AdminUserApi adminUserApi;
  49 +
47 @Override 50 @Override
48 public ImGroupMemberDO getGroupMember(Long id) { 51 public ImGroupMemberDO getGroupMember(Long id) {
49 return groupMemberMapper.selectById(id); 52 return groupMemberMapper.selectById(id);
50 } 53 }
51 54
52 @Override 55 @Override
  56 + public ImGroupMemberDO getGroupMember(Long groupId, Long userId) {
  57 + return groupMemberMapper.selectByGroupIdAndUserId(groupId, userId);
  58 + }
  59 +
  60 + @Override
53 public List<ImGroupMemberDO> getGroupMemberListByGroupId(Long groupId) { 61 public List<ImGroupMemberDO> getGroupMemberListByGroupId(Long groupId) {
54 return groupMemberMapper.selectListByGroupId(groupId); 62 return groupMemberMapper.selectListByGroupId(groupId);
55 } 63 }
@@ -209,4 +217,30 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { @@ -209,4 +217,30 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService {
209 .setQuitTime(LocalDateTime.now())); 217 .setQuitTime(LocalDateTime.now()));
210 } 218 }
211 219
  220 + @Override
  221 + public Map<Long, Long> getActiveMemberCountMap(Collection<Long> groupIds) {
  222 + return groupMemberMapper.selectCountMapByGroupIdsAndStatus(groupIds, CommonStatusEnum.ENABLE.getStatus());
  223 + }
  224 +
  225 + // ==================== 管理后台 ====================
  226 +
  227 + // TODO @AI:这个拼接的逻辑,拿到 controller,这里只返回 list do;
  228 + @Override
  229 + public List<ImGroupMemberManagerRespVO> getGroupMemberList(Long groupId) {
  230 + // 1. 查询有效群成员
  231 + List<ImGroupMemberDO> members = getActiveGroupMemberListByGroupId(groupId);
  232 + if (CollUtil.isEmpty(members)) {
  233 + return Collections.emptyList();
  234 + }
  235 +
  236 + // 2. 批量查询用户信息
  237 + Set<Long> userIds = convertSet(members, ImGroupMemberDO::getUserId);
  238 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
  239 +
  240 + // 3. 转换为 VO,填充昵称、头像
  241 + return BeanUtils.toBean(members, ImGroupMemberManagerRespVO.class, vo ->
  242 + MapUtils.findAndThen(userMap, vo.getUserId(), user ->
  243 + vo.setNickname(user.getNickname()).setAvatar(user.getAvatar())));
  244 + }
  245 +
212 } 246 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java
1 package cn.iocoder.yudao.module.im.service.group; 1 package cn.iocoder.yudao.module.im.service.group;
2 2
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
3 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; 4 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO;
4 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; 5 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO;
5 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; 6 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO;
6 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; 7 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO;
  8 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO;
  9 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO;
  10 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerRespVO;
7 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; 11 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
8 import jakarta.validation.Valid; 12 import jakarta.validation.Valid;
9 13
  14 +import java.util.Collection;
10 import java.util.List; 15 import java.util.List;
  16 +import java.util.Map;
11 17
12 /** 18 /**
13 * 群 Service 接口 19 * 群 Service 接口
@@ -59,6 +65,14 @@ public interface ImGroupService { @@ -59,6 +65,14 @@ public interface ImGroupService {
59 ImGroupDO getGroup(Long id); 65 ImGroupDO getGroup(Long id);
60 66
61 /** 67 /**
  68 + * 批量获得群 Map
  69 + *
  70 + * @param ids 群编号集合
  71 + * @return 群 Map(key = 群编号)
  72 + */
  73 + Map<Long, ImGroupDO> getGroupMap(Collection<Long> ids);
  74 +
  75 + /**
62 * 校验群存在且未封禁、未解散 76 * 校验群存在且未封禁、未解散
63 * 77 *
64 * @param groupId 群编号 78 * @param groupId 群编号
@@ -122,4 +136,30 @@ public interface ImGroupService { @@ -122,4 +136,30 @@ public interface ImGroupService {
122 */ 136 */
123 void removeGroupMember(Long userId, @Valid ImGroupMemberRemoveReqVO removeReqVO); 137 void removeGroupMember(Long userId, @Valid ImGroupMemberRemoveReqVO removeReqVO);
124 138
  139 + // ==================== 管理后台 ====================
  140 +
  141 + // TODO DONE @AI:是不是 controller 拆分哈;(getGroupPage 留在 ImGroupService;getGroupMemberList 已拆到 ImGroupMemberService + ImGroupMemberManagerController)
  142 + // TODO @AI:不对;应该 VO 放在 controller 拼接
  143 + /**
  144 + * 【管理后台】分页查询群列表
  145 + *
  146 + * @param pageReqVO 分页查询条件
  147 + * @return 群分页列表
  148 + */
  149 + PageResult<ImGroupManagerRespVO> getGroupPage(ImGroupManagerPageReqVO pageReqVO);
  150 +
  151 + /**
  152 + * 【管理后台】封禁群
  153 + *
  154 + * @param banReqVO 封禁信息(含群编号、封禁原因)
  155 + */
  156 + void banGroup(@Valid ImGroupManagerBanReqVO banReqVO);
  157 +
  158 + /**
  159 + * 【管理后台】解封群
  160 + *
  161 + * @param id 群编号
  162 + */
  163 + void unbanGroup(Long id);
  164 +
125 } 165 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
@@ -7,11 +7,16 @@ import cn.hutool.core.util.ObjUtil; @@ -7,11 +7,16 @@ import cn.hutool.core.util.ObjUtil;
7 import cn.hutool.core.util.StrUtil; 7 import cn.hutool.core.util.StrUtil;
8 import cn.hutool.extra.spring.SpringUtil; 8 import cn.hutool.extra.spring.SpringUtil;
9 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 9 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  10 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  11 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
10 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 12 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
11 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; 13 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO;
12 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; 14 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO;
13 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; 15 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO;
14 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; 16 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO;
  17 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO;
  18 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO;
  19 +import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerRespVO;
15 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; 20 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
16 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; 21 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
17 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 22 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
@@ -35,6 +40,7 @@ import java.util.*; @@ -35,6 +40,7 @@ import java.util.*;
35 import java.util.stream.Collectors; 40 import java.util.stream.Collectors;
36 41
37 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 42 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  43 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
38 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; 44 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
39 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP; 45 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP;
40 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; 46 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
@@ -256,6 +262,14 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -256,6 +262,14 @@ public class ImGroupServiceImpl implements ImGroupService {
256 } 262 }
257 263
258 @Override 264 @Override
  265 + public Map<Long, ImGroupDO> getGroupMap(Collection<Long> ids) {
  266 + if (CollUtil.isEmpty(ids)) {
  267 + return Collections.emptyMap();
  268 + }
  269 + return convertMap(groupMapper.selectByIds(ids), ImGroupDO::getId);
  270 + }
  271 +
  272 + @Override
259 public List<ImGroupDO> getMyGroupList(Long userId) { 273 public List<ImGroupDO> getMyGroupList(Long userId) {
260 // 1.1 查用户所在的、仍有效的群成员记录(仅 ENABLE 状态) 274 // 1.1 查用户所在的、仍有效的群成员记录(仅 ENABLE 状态)
261 List<ImGroupMemberDO> members = groupMemberService.getActiveGroupMemberListByUserId(userId); 275 List<ImGroupMemberDO> members = groupMemberService.getActiveGroupMemberListByUserId(userId);
@@ -271,6 +285,53 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -271,6 +285,53 @@ public class ImGroupServiceImpl implements ImGroupService {
271 return groupMapper.selectByIds(groupIds); 285 return groupMapper.selectByIds(groupIds);
272 } 286 }
273 287
  288 + // ==================== 管理后台 ====================
  289 +
  290 + @Override
  291 + public PageResult<ImGroupManagerRespVO> getGroupPage(ImGroupManagerPageReqVO pageReqVO) {
  292 + // 1. 分页查询群
  293 + PageResult<ImGroupDO> pageResult = groupMapper.selectPage(pageReqVO);
  294 + if (CollUtil.isEmpty(pageResult.getList())) {
  295 + return PageResult.empty(pageResult.getTotal());
  296 + }
  297 +
  298 + // 2.1 批量查询群主昵称
  299 + Set<Long> ownerUserIds = convertSet(pageResult.getList(), ImGroupDO::getOwnerUserId);
  300 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(ownerUserIds);
  301 + // 2.2 一次 GROUP BY 查出本页所有群的活跃成员数,避免按行 N+1
  302 + Set<Long> groupIds = convertSet(pageResult.getList(), ImGroupDO::getId);
  303 + Map<Long, Long> memberCountMap = groupMemberService.getActiveMemberCountMap(groupIds);
  304 + // 2.3 转换为 VO,填充群主昵称、群成员数量
  305 + return BeanUtils.toBean(pageResult, ImGroupManagerRespVO.class, vo -> {
  306 + MapUtils.findAndThen(userMap, vo.getOwnerUserId(), user -> vo.setOwnerNickname(user.getNickname()));
  307 + vo.setMemberCount(memberCountMap.getOrDefault(vo.getId(), 0L).intValue());
  308 + });
  309 + }
  310 +
  311 + @Override
  312 + @CacheEvict(cacheNames = GROUP, key = "#banReqVO.id")
  313 + public void banGroup(ImGroupManagerBanReqVO banReqVO) {
  314 + // 1. 校验群存在
  315 + if (getSelf().getGroup(banReqVO.getId()) == null) {
  316 + throw exception(GROUP_NOT_EXISTS);
  317 + }
  318 + // 2. 更新封禁状态
  319 + groupMapper.updateById(new ImGroupDO().setId(banReqVO.getId())
  320 + .setBanned(true).setBannedReason(banReqVO.getReason()).setBannedTime(LocalDateTime.now()));
  321 + }
  322 +
  323 + @Override
  324 + @CacheEvict(cacheNames = GROUP, key = "#id")
  325 + public void unbanGroup(Long id) {
  326 + // 1. 校验群存在
  327 + if (getSelf().getGroup(id) == null) {
  328 + throw exception(GROUP_NOT_EXISTS);
  329 + }
  330 + // 2. 解封(保留 bannedReason / bannedTime 作为历史记录)
  331 + // TODO DONE @AI:不要使用 mybatis plus 的方法,直接操作。另外,感觉封禁时间、封禁原因,没必要置空,留着问题不大。
  332 + groupMapper.updateById(new ImGroupDO().setId(id).setBanned(false));
  333 + }
  334 +
274 private ImGroupServiceImpl getSelf() { 335 private ImGroupServiceImpl getSelf() {
275 return SpringUtil.getBean(getClass()); 336 return SpringUtil.getBean(getClass());
276 } 337 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java
1 package cn.iocoder.yudao.module.im.service.message; 1 package cn.iocoder.yudao.module.im.service.message;
2 2
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO;
  5 +import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerRespVO;
3 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO; 6 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO;
4 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; 7 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO;
5 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; 8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
@@ -112,5 +115,17 @@ public interface ImGroupMessageService { @@ -112,5 +115,17 @@ public interface ImGroupMessageService {
112 */ 115 */
113 void sendTipGroupMessage(Long senderId, Long groupId, Set<Long> memberUserIds, String content); 116 void sendTipGroupMessage(Long senderId, Long groupId, Set<Long> memberUserIds, String content);
114 117
  118 + // ==================== 管理后台 ====================
  119 +
  120 + /**
  121 + * 【管理后台】分页查询群聊消息(已填充群名称、发送人昵称)
  122 + */
  123 + PageResult<ImGroupMessageManagerRespVO> getGroupMessagePage(ImGroupMessageManagerPageReqVO reqVO);
  124 +
  125 + /**
  126 + * 【管理后台】获取群聊消息详情
  127 + */
  128 + ImGroupMessageDO getGroupMessage(Long id);
  129 +
115 } 130 }
116 131
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
@@ -6,13 +6,17 @@ import cn.hutool.core.util.IdUtil; @@ -6,13 +6,17 @@ import cn.hutool.core.util.IdUtil;
6 import cn.hutool.core.util.ObjUtil; 6 import cn.hutool.core.util.ObjUtil;
7 import cn.hutool.extra.spring.SpringUtil; 7 import cn.hutool.extra.spring.SpringUtil;
8 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 8 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  9 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  10 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
9 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 11 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
10 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.manager.message.vo.group.ImGroupMessageManagerPageReqVO;
  14 +import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerRespVO;
11 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO; 15 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageListReqVO;
12 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; 16 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO;
  17 +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
13 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 18 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
14 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; 19 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
15 -import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage;  
16 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; 20 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper;
17 import cn.iocoder.yudao.module.im.dal.redis.message.GroupMessageReadRedisDAO; 21 import cn.iocoder.yudao.module.im.dal.redis.message.GroupMessageReadRedisDAO;
18 import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; 22 import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum;
@@ -23,6 +27,9 @@ import cn.iocoder.yudao.module.im.service.group.ImGroupService; @@ -23,6 +27,9 @@ import cn.iocoder.yudao.module.im.service.group.ImGroupService;
23 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; 27 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
24 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 28 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
25 import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; 29 import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
  30 +import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage;
  31 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  32 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
26 import jakarta.annotation.Resource; 33 import jakarta.annotation.Resource;
27 import lombok.extern.slf4j.Slf4j; 34 import lombok.extern.slf4j.Slf4j;
28 import org.springframework.scheduling.annotation.Async; 35 import org.springframework.scheduling.annotation.Async;
@@ -70,6 +77,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -70,6 +77,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
70 @Resource 77 @Resource
71 private ImWebSocketService imWebSocketService; 78 private ImWebSocketService imWebSocketService;
72 79
  80 + @Resource
  81 + private AdminUserApi adminUserApi;
  82 +
73 @Override 83 @Override
74 public ImGroupMessageDO sendGroupMessage(Long senderId, ImGroupMessageSendReqVO reqVO) { 84 public ImGroupMessageDO sendGroupMessage(Long senderId, ImGroupMessageSendReqVO reqVO) {
75 // 1.1 幂等校验:根据 senderId + clientMessageId 查重 85 // 1.1 幂等校验:根据 senderId + clientMessageId 查重
@@ -542,6 +552,35 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -542,6 +552,35 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
542 ImGroupMessageDTO.ofSend(tipMessage)); 552 ImGroupMessageDTO.ofSend(tipMessage));
543 } 553 }
544 554
  555 + // ==================== 管理后台 ====================
  556 +
  557 + // TODO @AI:这些挪到 controller 拼接 VO
  558 + @Override
  559 + public PageResult<ImGroupMessageManagerRespVO> getGroupMessagePage(ImGroupMessageManagerPageReqVO reqVO) {
  560 + // 1. 分页查询
  561 + PageResult<ImGroupMessageDO> pageResult = groupMessageMapper.selectPage(reqVO);
  562 + if (CollUtil.isEmpty(pageResult.getList())) {
  563 + return PageResult.empty(pageResult.getTotal());
  564 + }
  565 +
  566 + // 2. 批量查询群名称、发送人昵称
  567 + Set<Long> groupIds = convertSet(pageResult.getList(), ImGroupMessageDO::getGroupId);
  568 + Set<Long> senderIds = convertSet(pageResult.getList(), ImGroupMessageDO::getSenderId);
  569 + Map<Long, ImGroupDO> groupMap = groupService.getGroupMap(groupIds);
  570 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(senderIds);
  571 +
  572 + // 3. 转换为 VO,填充群名 / 发送人昵称
  573 + return BeanUtils.toBean(pageResult, ImGroupMessageManagerRespVO.class, vo -> {
  574 + MapUtils.findAndThen(groupMap, vo.getGroupId(), group -> vo.setGroupName(group.getName()));
  575 + MapUtils.findAndThen(userMap, vo.getSenderId(), user -> vo.setSenderNickname(user.getNickname()));
  576 + });
  577 + }
  578 +
  579 + @Override
  580 + public ImGroupMessageDO getGroupMessage(Long id) {
  581 + return groupMessageMapper.selectById(id);
  582 + }
  583 +
545 private ImGroupMessageServiceImpl getSelf() { 584 private ImGroupMessageServiceImpl getSelf() {
546 return SpringUtil.getBean(getClass()); 585 return SpringUtil.getBean(getClass());
547 } 586 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java
1 package cn.iocoder.yudao.module.im.service.message; 1 package cn.iocoder.yudao.module.im.service.message;
2 2
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerPageReqVO;
  5 +import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerRespVO;
3 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO; 6 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO;
4 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; 7 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
5 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
@@ -85,4 +88,16 @@ public interface ImPrivateMessageService { @@ -85,4 +88,16 @@ public interface ImPrivateMessageService {
85 */ 88 */
86 void sendTipPrivateMessage(Long senderId, Long receiverId, String content); 89 void sendTipPrivateMessage(Long senderId, Long receiverId, String content);
87 90
  91 + // ==================== 管理后台 ====================
  92 +
  93 + /**
  94 + * 【管理后台】分页查询私聊消息(已填充发送人 / 接收人昵称)
  95 + */
  96 + PageResult<ImPrivateMessageManagerRespVO> getPrivateMessagePage(ImPrivateMessageManagerPageReqVO reqVO);
  97 +
  98 + /**
  99 + * 【管理后台】获取私聊消息详情
  100 + */
  101 + ImPrivateMessageDO getPrivateMessage(Long id);
  102 +
88 } 103 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
1 package cn.iocoder.yudao.module.im.service.message; 1 package cn.iocoder.yudao.module.im.service.message;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
3 import cn.hutool.core.lang.Assert; 4 import cn.hutool.core.lang.Assert;
4 import cn.hutool.core.util.IdUtil; 5 import cn.hutool.core.util.IdUtil;
5 import cn.hutool.core.util.ObjUtil; 6 import cn.hutool.core.util.ObjUtil;
  7 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  8 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
6 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 9 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 10 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  11 +import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerPageReqVO;
  12 +import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerRespVO;
8 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO; 13 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO;
9 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; 14 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
10 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 15 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
@@ -16,6 +21,8 @@ import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; @@ -16,6 +21,8 @@ import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
16 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 21 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
17 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; 22 import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
18 import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage; 23 import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage;
  24 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  25 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
19 import jakarta.annotation.Resource; 26 import jakarta.annotation.Resource;
20 import lombok.extern.slf4j.Slf4j; 27 import lombok.extern.slf4j.Slf4j;
21 import org.springframework.stereotype.Service; 28 import org.springframework.stereotype.Service;
@@ -23,9 +30,13 @@ import org.springframework.transaction.annotation.Transactional; @@ -23,9 +30,13 @@ import org.springframework.transaction.annotation.Transactional;
23 import org.springframework.validation.annotation.Validated; 30 import org.springframework.validation.annotation.Validated;
24 31
25 import java.time.LocalDateTime; 32 import java.time.LocalDateTime;
  33 +import java.util.HashSet;
26 import java.util.List; 34 import java.util.List;
  35 +import java.util.Map;
  36 +import java.util.Set;
27 37
28 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 38 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  39 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
29 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; 40 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
30 import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.*; 41 import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.*;
31 42
@@ -50,6 +61,9 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -50,6 +61,9 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
50 @Resource 61 @Resource
51 private ImWebSocketService imWebSocketService; 62 private ImWebSocketService imWebSocketService;
52 63
  64 + @Resource
  65 + private AdminUserApi adminUserApi;
  66 +
53 @Override 67 @Override
54 public ImPrivateMessageDO sendPrivateMessage(Long senderId, ImPrivateMessageSendReqVO reqVO) { 68 public ImPrivateMessageDO sendPrivateMessage(Long senderId, ImPrivateMessageSendReqVO reqVO) {
55 // 1.1 幂等校验:根据 senderId + clientMessageId 查重 69 // 1.1 幂等校验:根据 senderId + clientMessageId 查重
@@ -167,6 +181,35 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -167,6 +181,35 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
167 return privateMessageMapper.selectHistoryList(userId, reqVO.getReceiverId(), reqVO.getMaxId(), reqVO.getLimit()); 181 return privateMessageMapper.selectHistoryList(userId, reqVO.getReceiverId(), reqVO.getMaxId(), reqVO.getLimit());
168 } 182 }
169 183
  184 + // ==================== 管理后台 ====================
  185 +
  186 + // TODO @AI:这些挪到 controller 拼接 VO
  187 + @Override
  188 + public PageResult<ImPrivateMessageManagerRespVO> getPrivateMessagePage(ImPrivateMessageManagerPageReqVO reqVO) {
  189 + // 1. 分页查询
  190 + PageResult<ImPrivateMessageDO> pageResult = privateMessageMapper.selectPage(reqVO);
  191 + if (CollUtil.isEmpty(pageResult.getList())) {
  192 + return PageResult.empty(pageResult.getTotal());
  193 + }
  194 +
  195 + // 2. 一次性批量查询发送人 + 接收人昵称
  196 + Set<Long> userIds = new HashSet<>();
  197 + userIds.addAll(convertSet(pageResult.getList(), ImPrivateMessageDO::getSenderId));
  198 + userIds.addAll(convertSet(pageResult.getList(), ImPrivateMessageDO::getReceiverId));
  199 + Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
  200 +
  201 + // 3. 转换为 VO,填充昵称
  202 + return BeanUtils.toBean(pageResult, ImPrivateMessageManagerRespVO.class, vo -> {
  203 + MapUtils.findAndThen(userMap, vo.getSenderId(), user -> vo.setSenderNickname(user.getNickname()));
  204 + MapUtils.findAndThen(userMap, vo.getReceiverId(), user -> vo.setReceiverNickname(user.getNickname()));
  205 + });
  206 + }
  207 +
  208 + @Override
  209 + public ImPrivateMessageDO getPrivateMessage(Long id) {
  210 + return privateMessageMapper.selectById(id);
  211 + }
  212 +
170 @Override 213 @Override
171 public void sendTipPrivateMessage(Long senderId, Long receiverId, String content) { 214 public void sendTipPrivateMessage(Long senderId, Long receiverId, String content) {
172 // 1. 插入 TIP_TEXT 消息 215 // 1. 插入 TIP_TEXT 消息
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordService.java
1 package cn.iocoder.yudao.module.im.service.sensitiveword; 1 package cn.iocoder.yudao.module.im.service.sensitiveword;
2 2
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordPageReqVO;
  5 +import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordSaveReqVO;
  6 +import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
  7 +import jakarta.validation.Valid;
  8 +
  9 +import java.util.List;
  10 +
3 /** 11 /**
4 * IM 敏感词 Service 接口 12 * IM 敏感词 Service 接口
5 * 13 *
@@ -14,4 +22,36 @@ public interface ImSensitiveWordService { @@ -14,4 +22,36 @@ public interface ImSensitiveWordService {
14 */ 22 */
15 void validateText(String text); 23 void validateText(String text);
16 24
  25 + // ==================== 管理后台 ====================
  26 +
  27 + /**
  28 + * 【管理后台】分页查询敏感词
  29 + */
  30 + PageResult<ImSensitiveWordDO> getSensitiveWordPage(ImSensitiveWordPageReqVO reqVO);
  31 +
  32 + /**
  33 + * 【管理后台】获取敏感词详情
  34 + */
  35 + ImSensitiveWordDO getSensitiveWord(Long id);
  36 +
  37 + /**
  38 + * 【管理后台】新增敏感词,返回新增 id
  39 + */
  40 + Long createSensitiveWord(@Valid ImSensitiveWordSaveReqVO reqVO);
  41 +
  42 + /**
  43 + * 【管理后台】修改敏感词
  44 + */
  45 + void updateSensitiveWord(@Valid ImSensitiveWordSaveReqVO reqVO);
  46 +
  47 + /**
  48 + * 【管理后台】删除敏感词
  49 + */
  50 + void deleteSensitiveWord(Long id);
  51 +
  52 + /**
  53 + * 【管理后台】批量删除敏感词
  54 + */
  55 + void deleteSensitiveWordList(List<Long> ids);
  56 +
17 } 57 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImpl.java
1 package cn.iocoder.yudao.module.im.service.sensitiveword; 1 package cn.iocoder.yudao.module.im.service.sensitiveword;
2 2
3 -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  8 +import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordPageReqVO;
  9 +import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordSaveReqVO;
4 import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO; 10 import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
5 import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper; 11 import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper;
6 -import jakarta.annotation.PostConstruct; 12 +import com.github.houbb.sensitive.word.bs.SensitiveWordBs;
7 import jakarta.annotation.Resource; 13 import jakarta.annotation.Resource;
8 import lombok.extern.slf4j.Slf4j; 14 import lombok.extern.slf4j.Slf4j;
9 -import org.springframework.scheduling.annotation.Scheduled;  
10 import org.springframework.stereotype.Service; 15 import org.springframework.stereotype.Service;
11 import org.springframework.validation.annotation.Validated; 16 import org.springframework.validation.annotation.Validated;
12 17
13 -import java.util.Collections;  
14 import java.util.List; 18 import java.util.List;
15 -import java.util.stream.Collectors;  
16 19
17 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 20 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
18 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSITIVE_WORD_BLOCKED; 21 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSITIVE_WORD_BLOCKED;
  22 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_DUPLICATED;
  23 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS;
19 24
20 -// DONE @AI:已实现内存化,启动时加载 + 定时刷新  
21 /** 25 /**
22 * IM 敏感词 Service 实现类 26 * IM 敏感词 Service 实现类
  27 + * <p>
  28 + * 词库匹配交给 houbb sensitive-word 库(trie 树 + 全/半角 / 大小写 / 繁简体 / 数字风格规范化)
23 * 29 *
24 * @author 芋道源码 30 * @author 芋道源码
25 */ 31 */
@@ -29,48 +35,122 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSIT @@ -29,48 +35,122 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSIT
29 public class ImSensitiveWordServiceImpl implements ImSensitiveWordService { 35 public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
30 36
31 @Resource 37 @Resource
32 - private ImSensitiveWordMapper imSensitiveWordMapper; 38 + private ImSensitiveWordMapper sensitiveWordMapper;
33 39
  40 + // TODO @AI:当前节点,立即刷新。其他节点,通过 job 扫描最后更新时间(只要有不同于目前最后一条记录的最大更新时间,就重新构建对应的树)。
34 /** 41 /**
35 - * 内存缓存的敏感词列表 42 + * houbb 敏感词检测器,词库由 ImSensitiveWordDeny 在 init 时回调加载
36 */ 43 */
37 - private volatile List<String> sensitiveWords = Collections.emptyList(); 44 + @Resource
  45 + private SensitiveWordBs sensitiveWordBs;
38 46
  47 + // TODO @AI:你看看现有仓库里,是否有类似这种的。按照最后更新判断,是否需要刷新本地缓存。
39 /** 48 /**
40 - * 初始化:启动时加载敏感词到内存 49 + * 触发 houbb 词库重新加载(重建 trie)
  50 + * <p>
  51 + * 加 synchronized 是因为 SensitiveWordBs.init() 内部对 wordData 做就地写入,
  52 + * 若与并发的 contains() 重叠会读到半构建的 trie。互斥后短暂阻塞 contains 即可保证可见性。
  53 + * <p>
  54 + * init() 本身幂等,单实例可以直接调;多实例部署时需配合 Redis pub/sub 广播让各实例 init。
41 */ 55 */
42 - @PostConstruct  
43 - public void init() {  
44 - loadSensitiveWords(); 56 + private synchronized void reloadSensitiveWords() {
  57 + sensitiveWordBs.init();
  58 + log.info("[reloadSensitiveWords][敏感词库已 reload]");
45 } 59 }
46 60
47 - /**  
48 - * 定时刷新:每 5 分钟重新加载一次  
49 - */  
50 - @Scheduled(fixedDelay = 5 * 60 * 1000, initialDelay = 5 * 60 * 1000)  
51 - public void refresh() {  
52 - loadSensitiveWords(); 61 + // TODO @AI:需要按照租户来。
  62 + @Override
  63 + public void validateText(String text) {
  64 + if (StrUtil.isBlank(text)) {
  65 + return;
  66 + }
  67 + if (sensitiveWordBs.contains(text)) {
  68 + throw exception(MESSAGE_SENSITIVE_WORD_BLOCKED);
  69 + }
53 } 70 }
54 71
55 - private void loadSensitiveWords() {  
56 - List<ImSensitiveWordDO> enabledWords = imSensitiveWordMapper.selectListByStatus(  
57 - CommonStatusEnum.ENABLE.getStatus());  
58 - sensitiveWords = enabledWords.stream()  
59 - .map(ImSensitiveWordDO::getWord)  
60 - .collect(Collectors.toList());  
61 - log.info("[loadSensitiveWords][加载 IM 敏感词 {} 个]", sensitiveWords.size()); 72 + // ==================== 管理后台 ====================
  73 +
  74 + @Override
  75 + public PageResult<ImSensitiveWordDO> getSensitiveWordPage(ImSensitiveWordPageReqVO reqVO) {
  76 + return sensitiveWordMapper.selectPage(reqVO);
62 } 77 }
63 78
64 @Override 79 @Override
65 - public void validateText(String text) {  
66 - if (text == null || text.isEmpty()) { 80 + public ImSensitiveWordDO getSensitiveWord(Long id) {
  81 + return sensitiveWordMapper.selectById(id);
  82 + }
  83 +
  84 + @Override
  85 + public Long createSensitiveWord(ImSensitiveWordSaveReqVO reqVO) {
  86 + // 1. 校验唯一
  87 + validateWordUnique(null, reqVO.getWord());
  88 +
  89 + // 2.1 入库
  90 + ImSensitiveWordDO word = BeanUtils.toBean(reqVO, ImSensitiveWordDO.class);
  91 + sensitiveWordMapper.insert(word);
  92 + // TODO @AI:houbb 这个注释有点奇怪!
  93 + // 2.2 触发 houbb 词库 reload
  94 + reloadSensitiveWords();
  95 + return word.getId();
  96 + }
  97 +
  98 + @Override
  99 + public void updateSensitiveWord(ImSensitiveWordSaveReqVO reqVO) {
  100 + // 1.1 校验存在
  101 + validateSensitiveWordExists(reqVO.getId());
  102 + // 1.2 校验唯一(排除自身)
  103 + validateWordUnique(reqVO.getId(), reqVO.getWord());
  104 +
  105 + // 2.1 更新
  106 + ImSensitiveWordDO updateObj = BeanUtils.toBean(reqVO, ImSensitiveWordDO.class);
  107 + sensitiveWordMapper.updateById(updateObj);
  108 + // 2.2 TODO @AI:houbb 这个注释有点奇怪!
  109 + // 2.2 触发 houbb 词库 reload
  110 + reloadSensitiveWords();
  111 + }
  112 +
  113 + @Override
  114 + public void deleteSensitiveWord(Long id) {
  115 + // 1. 校验存在
  116 + validateSensitiveWordExists(id);
  117 +
  118 + // 2.1 删除
  119 + sensitiveWordMapper.deleteById(id);
  120 + // 2.2 触发 houbb 词库 reload
  121 + reloadSensitiveWords();
  122 + }
  123 +
  124 +
  125 + @Override
  126 + public void deleteSensitiveWordList(List<Long> ids) {
  127 + if (CollUtil.isEmpty(ids)) {
  128 + return;
  129 + }
  130 +
  131 + // 1. 删除
  132 + sensitiveWordMapper.deleteByIds(ids);
  133 + // 触发 houbb 词库 reload
  134 + reloadSensitiveWords();
  135 + }
  136 +
  137 + private void validateSensitiveWordExists(Long id) {
  138 + if (sensitiveWordMapper.selectById(id) == null) {
  139 + throw exception(SENSITIVE_WORD_NOT_EXISTS);
  140 + }
  141 + }
  142 +
  143 + /**
  144 + * 校验敏感词唯一(修改时排除自身)
  145 + */
  146 + private void validateWordUnique(Long id, String word) {
  147 + ImSensitiveWordDO exist = sensitiveWordMapper.selectByWord(word);
  148 + if (exist == null) {
67 return; 149 return;
68 } 150 }
69 - // 内存匹配  
70 - for (String word : sensitiveWords) {  
71 - if (text.contains(word)) {  
72 - throw exception(MESSAGE_SENSITIVE_WORD_BLOCKED);  
73 - } 151 + // TODO @AI:是不是 id 不用判断
  152 + if (id == null || ObjUtil.notEqual(exist.getId(), id)) {
  153 + throw exception(SENSITIVE_WORD_DUPLICATED, word);
74 } 154 }
75 } 155 }
76 156