安浩浩 1 rok pred
rodič
commit
bb0b7056cf
57 zmenil súbory, kde vykonal 2423 pridanie a 2 odobranie
  1. 4 1
      pom.xml
  2. 1 1
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java
  3. 29 0
      yudao-module-im/pom.xml
  4. 26 0
      yudao-module-im/yudao-module-im-api/pom.xml
  5. 5 0
      yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/api/package-info.java
  6. 20 0
      yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java
  7. 27 0
      yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/conversation/ImConversationTypeEnum.java
  8. 37 0
      yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java
  9. 5 0
      yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/package-info.java
  10. 65 0
      yudao-module-im/yudao-module-im-biz/pom.xml
  11. 93 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ConversationController.java
  12. 43 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ConversationPageReqVO.java
  13. 47 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ConversationRespVO.java
  14. 40 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ConversationSaveReqVO.java
  15. 93 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/InboxController.java
  16. 33 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/vo/InboxPageReqVO.java
  17. 35 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/vo/InboxRespVO.java
  18. 26 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/vo/InboxSaveReqVO.java
  19. 99 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/MessageController.java
  20. 58 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/MessagePageReqVO.java
  21. 67 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/MessageRespVO.java
  22. 61 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/MessageSaveReqVO.java
  23. 56 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/conversation/ConversationDO.java
  24. 42 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/inbox/InboxDO.java
  25. 76 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/MessageDO.java
  26. 31 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImAudioMessageBody.java
  27. 31 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImFileMessageBody.java
  28. 30 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImImageMessageBody.java
  29. 32 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImLocationMessageBody.java
  30. 12 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImMessageBody.java
  31. 22 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImTextMessageBody.java
  32. 27 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImVideoMessageBody.java
  33. 30 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ConversationMapper.java
  34. 27 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/inbox/InboxMapper.java
  35. 35 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/MessageMapper.java
  36. 6 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/framework/package-info.java
  37. 22 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/framework/web/config/ImWebConfiguration.java
  38. 4 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/framework/web/package-info.java
  39. 36 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/jackson/ImMessageBodyDeserializer.java
  40. 54 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ConversationService.java
  41. 70 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ConversationServiceImpl.java
  42. 54 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/InboxService.java
  43. 70 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/InboxServiceImpl.java
  44. 60 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/MessageService.java
  45. 75 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/MessageServiceImpl.java
  46. 46 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImWebSocketMessageListener.java
  47. 23 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/message/ImReceiveMessage.java
  48. 23 0
      yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/message/ImSendMessage.java
  49. 12 0
      yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/conversation/ConversationMapper.xml
  50. 12 0
      yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/inbox/InboxMapper.xml
  51. 12 0
      yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/message/MessageMapper.xml
  52. 154 0
      yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/conversation/ConversationServiceImplTest.java
  53. 142 0
      yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/inbox/InboxServiceImplTest.java
  54. 174 0
      yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/MessageServiceImplTest.java
  55. 7 0
      yudao-server/pom.xml
  56. 1 0
      yudao-server/src/main/resources/application-local.yaml
  57. 1 0
      yudao-server/src/main/resources/application.yaml

+ 4 - 1
pom.xml

@@ -15,7 +15,9 @@
         <!-- 各种 module 拓展 -->
         <module>yudao-module-system</module>
         <module>yudao-module-infra</module>
-<!--        <module>yudao-module-member</module>-->
+        <module>yudao-module-im</module>
+        <module>yudao-module-im/yudao-module-im-api</module>
+        <!--        <module>yudao-module-member</module>-->
 <!--        <module>yudao-module-bpm</module>-->
 <!--        <module>yudao-module-report</module>-->
 <!--        <module>yudao-module-mp</module>-->
@@ -23,6 +25,7 @@
 <!--        <module>yudao-module-mall</module>-->
 <!--        <module>yudao-module-crm</module>-->
 <!--        <module>yudao-module-erp</module>-->
+        <module>yudao-module-im</module>
         <!-- 示例项目 -->
 <!--        <module>yudao-example</module>-->
     </modules>

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java

@@ -76,7 +76,7 @@ public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
             Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
             TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
         } catch (Throwable ex) {
-            log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
+            log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload(), ex);
         }
     }
 

+ 29 - 0
yudao-module-im/pom.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modules>
+        <module>yudao-module-im-api</module>
+        <module>yudao-module-im-biz</module>
+    </modules>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-module-im</artifactId>
+    <packaging>pom</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        im 模块,主要提供能力:
+        1. 通讯能力,例如:消息发送、消息接收、消息撤回、消息已读等。
+        2. 通讯会话,例如:单聊、群聊、聊天室等。
+        3. 通讯消息,例如:文本、图片、语音、视频、文件等。
+        4. 通讯消息的存储,例如:消息存储、消息索引、消息搜索等。
+        5. 通讯消息的推送,例如:消息推送、消息通知等。
+        6. 通讯消息的安全,例如:消息加密、消息签名等。
+    </description>
+
+</project>

+ 26 - 0
yudao-module-im/yudao-module-im-api/pom.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao-module-im</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-module-im-api</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        im 模块 API,暴露给其它模块调用
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-common</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 5 - 0
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/api/package-info.java

@@ -0,0 +1,5 @@
+/**
+ * @author anhaohao
+ * @date 2024/3/9 下午8:59
+ */
+package cn.iocoder.yudao.module.im.api;

+ 20 - 0
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.im.enums;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+
+/**
+ * IM 错误码枚举类
+ * <p>
+ * im 系统,使用 1-040-000-000 段
+ */
+public interface ErrorCodeConstants {
+
+    // ========== 会话 (1-040-100-000)  ==========
+    ErrorCode CONVERSATION_NOT_EXISTS = new ErrorCode(1_040_100_000, "会话不存在");
+
+    // ========== 收件箱 (1-040-200-000) ==========
+    ErrorCode INBOX_NOT_EXISTS = new ErrorCode(1_040_200_000, "收件箱不存在");
+
+    // ========== 消息 (1-040-300-000) ==========
+    ErrorCode MESSAGE_NOT_EXISTS = new ErrorCode(1_040_300_000, "消息不存在");
+}

+ 27 - 0
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/conversation/ImConversationTypeEnum.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.im.enums.conversation;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * IM 会话的类型枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum ImConversationTypeEnum {
+
+    PRIVATE(1, "单聊"),
+    GROUP(2, "群聊");
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+    /**
+     * 名字
+     */
+    private final String name;
+
+}

+ 37 - 0
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.im.enums.message;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * IM 消息的类型枚举
+ *
+ * 参考 <a href="https://doc.yunxin.163.com/messaging/docs/zg3NzA3NTA?platform=web#消息类型">“消息类型”</a> 文档
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum ImMessageTypeEnum {
+
+    TEXT(1, "文本"), // 消息内容为普通文本
+    IMAGE(2, "图片"), // 消息内容为图片 URL 地址、尺寸、图片大小等信息
+    AUDIO(3, "语音"), // 消息内容为语音文件的 URL 地址、时长、大小、格式等信息
+    VIDEO(4, "视频"), // 消息内容为视频文件的 URL 地址、时长、大小、格式等信息
+    FILE(5, "文件"), // 消息内容为文件的 URL 地址、大小、格式等信息
+    LOCATION(6, "地理位置"), // 消息内容为地理位置标题、经度、纬度信息
+    // TODO @芋艿:下面两种,貌似企业微信设计的更好:https://developer.work.weixin.qq.com/document/path/90240
+    TIP(7, "提示"), // 又叫做 Tip 消息,没有推送和通知栏提醒,主要用于会话内的通知提醒,例如进入会话时出现的欢迎消息,或是会话过程中命中敏感词后的提示消息等场景
+    NOTIFICATION(8, "通知"), // 主要用于群组、聊天室和超大群的事件通知,由服务端下发,客户端无法发送事件通知消息。通知类消息有在线、离线、漫游机制;没有通知栏提醒
+    ;
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+    /**
+     * 名字
+     */
+    private final String name;
+
+}

+ 5 - 0
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/package-info.java

@@ -0,0 +1,5 @@
+/**
+ * @author anhaohao
+ * @date 2024/3/9 下午8:59
+ */
+package cn.iocoder.yudao.module.im.enums;

+ 65 - 0
yudao-module-im/yudao-module-im-biz/pom.xml

@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>yudao-module-im</artifactId>
+        <groupId>cn.iocoder.boot</groupId>
+        <version>${revision}</version> <!-- 1. 修改 version 为 ${revision} -->
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>jar</packaging> <!-- 2. 新增 packaging 为 jar -->
+
+    <artifactId>yudao-module-im-biz</artifactId>
+
+    <name>${project.artifactId}</name> <!-- 3. 新增 name 为 ${project.artifactId} -->
+    <description> <!-- 4. 新增 description 为该模块的描述 -->
+        im 模块,主要实现 im 模块的业务逻辑。
+    </description>
+
+    <dependencies>  <!-- 5. 新增依赖,这里引入的都是比较常用的业务组件、技术组件 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-im-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-test</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-excel</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-websocket</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 93 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ConversationController.java

@@ -0,0 +1,93 @@
+package cn.iocoder.yudao.module.im.controller.admin.conversation;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ConversationPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ConversationRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ConversationSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ConversationDO;
+import cn.iocoder.yudao.module.im.service.conversation.ConversationService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - 会话")
+@RestController
+@RequestMapping("/im/conversation")
+@Validated
+public class ConversationController {
+
+    @Resource
+    private ConversationService conversationService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建会话")
+    @PreAuthorize("@ss.hasPermission('im:conversation:create')")
+    public CommonResult<Long> createConversation(@Valid @RequestBody ConversationSaveReqVO createReqVO) {
+        return success(conversationService.createConversation(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新会话")
+    @PreAuthorize("@ss.hasPermission('im:conversation:update')")
+    public CommonResult<Boolean> updateConversation(@Valid @RequestBody ConversationSaveReqVO updateReqVO) {
+        conversationService.updateConversation(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除会话")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('im:conversation:delete')")
+    public CommonResult<Boolean> deleteConversation(@RequestParam("id") Long id) {
+        conversationService.deleteConversation(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得会话")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('im:conversation:query')")
+    public CommonResult<ConversationRespVO> getConversation(@RequestParam("id") Long id) {
+        ConversationDO conversation = conversationService.getConversation(id);
+        return success(BeanUtils.toBean(conversation, ConversationRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得会话分页")
+    @PreAuthorize("@ss.hasPermission('im:conversation:query')")
+    public CommonResult<PageResult<ConversationRespVO>> getConversationPage(@Valid ConversationPageReqVO pageReqVO) {
+        PageResult<ConversationDO> pageResult = conversationService.getConversationPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, ConversationRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出会话 Excel")
+    @PreAuthorize("@ss.hasPermission('im:conversation:export')")
+    @OperateLog(type = EXPORT)
+    public void exportConversationExcel(@Valid ConversationPageReqVO pageReqVO,
+                                        HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ConversationDO> list = conversationService.getConversationPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "会话.xls", "数据", ConversationRespVO.class,
+                BeanUtils.toBean(list, ConversationRespVO.class));
+    }
+
+}

+ 43 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ConversationPageReqVO.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.im.controller.admin.conversation.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 会话分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ConversationPageReqVO extends PageParam {
+
+    @Schema(description = "所属用户", example = "11545")
+    private Long userId;
+
+    @Schema(description = "类型:1 单聊;2 群聊;4 通知会话(预留)", example = "1")
+    private Boolean conversationType;
+
+    @Schema(description = "单聊时,用户编号;群聊时,群编号", example = "21454")
+    private String targetId;
+
+    @Schema(description = "会话标志 单聊:s_{userId}_{targetId},需要排序 userId 和 targetId 群聊:g_groupId")
+    private String no;
+
+    @Schema(description = "是否置顶 0否 1是")
+    private Boolean pinned;
+
+    @Schema(description = "最后已读时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] lastReadTime;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 47 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ConversationRespVO.java

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.im.controller.admin.conversation.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 会话 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ConversationRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13905")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "所属用户", requiredMode = Schema.RequiredMode.REQUIRED, example = "11545")
+    @ExcelProperty("所属用户")
+    private Long userId;
+
+    @Schema(description = "类型:1 单聊;2 群聊;4 通知会话(预留)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty("类型:1 单聊;2 群聊;4 通知会话(预留)")
+    private Boolean conversationType;
+
+    @Schema(description = "单聊时,用户编号;群聊时,群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21454")
+    @ExcelProperty("单聊时,用户编号;群聊时,群编号")
+    private String targetId;
+
+    @Schema(description = "会话标志 单聊:s_{userId}_{targetId},需要排序 userId 和 targetId 群聊:g_groupId", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("会话标志 单聊:s_{userId}_{targetId},需要排序 userId 和 targetId 群聊:g_groupId")
+    private String no;
+
+    @Schema(description = "是否置顶 0否 1是", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("是否置顶 0否 1是")
+    private Boolean pinned;
+
+    @Schema(description = "最后已读时间")
+    @ExcelProperty("最后已读时间")
+    private LocalDateTime lastReadTime;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 40 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ConversationSaveReqVO.java

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.im.controller.admin.conversation.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 会话新增/修改 Request VO")
+@Data
+public class ConversationSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13905")
+    private Long id;
+
+    @Schema(description = "所属用户", requiredMode = Schema.RequiredMode.REQUIRED, example = "11545")
+    @NotNull(message = "所属用户不能为空")
+    private Long userId;
+
+    @Schema(description = "类型:1 单聊;2 群聊;4 通知会话(预留)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "类型:1 单聊;2 群聊;4 通知会话(预留)不能为空")
+    private Boolean conversationType;
+
+    @Schema(description = "单聊时,用户编号;群聊时,群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21454")
+    @NotEmpty(message = "单聊时,用户编号;群聊时,群编号不能为空")
+    private String targetId;
+
+    @Schema(description = "会话标志 单聊:s_{userId}_{targetId},需要排序 userId 和 targetId 群聊:g_groupId", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "会话标志 单聊:s_{userId}_{targetId},需要排序 userId 和 targetId 群聊:g_groupId不能为空")
+    private String no;
+
+    @Schema(description = "是否置顶 0否 1是", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "是否置顶 0否 1是不能为空")
+    private Boolean pinned;
+
+    @Schema(description = "最后已读时间")
+    private LocalDateTime lastReadTime;
+
+}

+ 93 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/InboxController.java

@@ -0,0 +1,93 @@
+package cn.iocoder.yudao.module.im.controller.admin.inbox;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.im.controller.admin.inbox.vo.InboxPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.inbox.vo.InboxRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.inbox.vo.InboxSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.inbox.InboxDO;
+import cn.iocoder.yudao.module.im.service.inbox.InboxService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - 收件箱")
+@RestController
+@RequestMapping("/im/inbox")
+@Validated
+public class InboxController {
+
+    @Resource
+    private InboxService inboxService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建收件箱")
+    @PreAuthorize("@ss.hasPermission('im:inbox:create')")
+    public CommonResult<Long> createInbox(@Valid @RequestBody InboxSaveReqVO createReqVO) {
+        return success(inboxService.createInbox(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新收件箱")
+    @PreAuthorize("@ss.hasPermission('im:inbox:update')")
+    public CommonResult<Boolean> updateInbox(@Valid @RequestBody InboxSaveReqVO updateReqVO) {
+        inboxService.updateInbox(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除收件箱")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('im:inbox:delete')")
+    public CommonResult<Boolean> deleteInbox(@RequestParam("id") Long id) {
+        inboxService.deleteInbox(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得收件箱")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('im:inbox:query')")
+    public CommonResult<InboxRespVO> getInbox(@RequestParam("id") Long id) {
+        InboxDO inbox = inboxService.getInbox(id);
+        return success(BeanUtils.toBean(inbox, InboxRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得收件箱分页")
+    @PreAuthorize("@ss.hasPermission('im:inbox:query')")
+    public CommonResult<PageResult<InboxRespVO>> getInboxPage(@Valid InboxPageReqVO pageReqVO) {
+        PageResult<InboxDO> pageResult = inboxService.getInboxPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, InboxRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出收件箱 Excel")
+    @PreAuthorize("@ss.hasPermission('im:inbox:export')")
+    @OperateLog(type = EXPORT)
+    public void exportInboxExcel(@Valid InboxPageReqVO pageReqVO,
+                                 HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<InboxDO> list = inboxService.getInboxPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "收件箱.xls", "数据", InboxRespVO.class,
+                BeanUtils.toBean(list, InboxRespVO.class));
+    }
+
+}

+ 33 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/vo/InboxPageReqVO.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.im.controller.admin.inbox.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 收件箱分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class InboxPageReqVO extends PageParam {
+
+    @Schema(description = "用户编号", example = "3979")
+    private Long userId;
+
+    @Schema(description = "消息编号", example = "12454")
+    private Long messageId;
+
+    @Schema(description = "序号,按照 user 递增")
+    private Long sequence;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 35 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/vo/InboxRespVO.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.im.controller.admin.inbox.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 收件箱 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class InboxRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18389")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3979")
+    @ExcelProperty("用户编号")
+    private Long userId;
+
+    @Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12454")
+    @ExcelProperty("消息编号")
+    private Long messageId;
+
+    @Schema(description = "序号,按照 user 递增", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("序号,按照 user 递增")
+    private Long sequence;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 26 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/vo/InboxSaveReqVO.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.im.controller.admin.inbox.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 收件箱新增/修改 Request VO")
+@Data
+public class InboxSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18389")
+    private Long id;
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3979")
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    @Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12454")
+    @NotNull(message = "消息编号不能为空")
+    private Long messageId;
+
+    @Schema(description = "序号,按照 user 递增", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "序号,按照 user 递增不能为空")
+    private Long sequence;
+
+}

+ 99 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/MessageController.java

@@ -0,0 +1,99 @@
+package cn.iocoder.yudao.module.im.controller.admin.message;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.MessagePageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.MessageRespVO;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.MessageSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.message.MessageDO;
+import cn.iocoder.yudao.module.im.service.message.MessageService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - 消息")
+@RestController
+@RequestMapping("/im/message")
+@Validated
+public class MessageController {
+
+    @Resource
+    private MessageService messageService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建消息")
+    @PreAuthorize("@ss.hasPermission('im:message:create')")
+    public CommonResult<Long> createMessage(@Valid @RequestBody MessageSaveReqVO createReqVO) {
+        return success(messageService.createMessage(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新消息")
+    @PreAuthorize("@ss.hasPermission('im:message:update')")
+    public CommonResult<Boolean> updateMessage(@Valid @RequestBody MessageSaveReqVO updateReqVO) {
+        messageService.updateMessage(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除消息")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('im:message:delete')")
+    public CommonResult<Boolean> deleteMessage(@RequestParam("id") Long id) {
+        messageService.deleteMessage(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得消息")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('im:message:query')")
+    public CommonResult<MessageRespVO> getMessage(@RequestParam("id") Long id) {
+        MessageDO message = messageService.getMessage(id);
+        return success(BeanUtils.toBean(message, MessageRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得消息分页")
+    @PreAuthorize("@ss.hasPermission('im:message:query')")
+    public CommonResult<PageResult<MessageRespVO>> getMessagePage(@Valid MessagePageReqVO pageReqVO) {
+        PageResult<MessageDO> pageResult = messageService.getMessagePage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, MessageRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出消息 Excel")
+    @PreAuthorize("@ss.hasPermission('im:message:export')")
+    @OperateLog(type = EXPORT)
+    public void exportMessageExcel(@Valid MessagePageReqVO pageReqVO,
+                                   HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<MessageDO> list = messageService.getMessagePage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "消息.xls", "数据", MessageRespVO.class,
+                BeanUtils.toBean(list, MessageRespVO.class));
+    }
+
+    @PostMapping("/send")
+    @Operation(summary = "发送私聊消息")
+    public CommonResult<Long> sendMessage(@Valid @RequestBody MessageSaveReqVO messageSaveReqVO) {
+        return success(messageService.sendPrivateMessage(messageSaveReqVO));
+    }
+
+}

+ 58 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/MessagePageReqVO.java

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.im.controller.admin.message.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 消息分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MessagePageReqVO extends PageParam {
+
+    @Schema(description = "客户端消息编号 uuid,用于排重", example = "3331")
+    private String clientMessageId;
+
+    @Schema(description = "发送人编号", example = "23239")
+    private Long senderId;
+
+    @Schema(description = "接收人编号", example = "32494")
+    private Long receiverId;
+
+    @Schema(description = "发送人昵称", example = "李四")
+    private String senderNickname;
+
+    @Schema(description = "发送人头像")
+    private String senderAvatar;
+
+    @Schema(description = "会话类型", example = "2")
+    private Boolean conversationType;
+
+    @Schema(description = "会话标志")
+    private String conversationNo;
+
+    @Schema(description = "消息类型", example = "1")
+    private Boolean contentType;
+
+    @Schema(description = "消息内容")
+    private String content;
+
+    @Schema(description = "发送时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] sendTime;
+
+    @Schema(description = "消息来源 100-用户发送;200-系统发送(一般是通知);")
+    private Boolean sendFrom;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 67 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/MessageRespVO.java

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.im.controller.admin.message.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 消息 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class MessageRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "30713")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "客户端消息编号 uuid,用于排重", requiredMode = Schema.RequiredMode.REQUIRED, example = "3331")
+    @ExcelProperty("客户端消息编号 uuid,用于排重")
+    private String clientMessageId;
+
+    @Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23239")
+    @ExcelProperty("发送人编号")
+    private Long senderId;
+
+    @Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32494")
+    @ExcelProperty("接收人编号")
+    private Long receiverId;
+
+    @Schema(description = "发送人昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @ExcelProperty("发送人昵称")
+    private String senderNickname;
+
+    @Schema(description = "发送人头像", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("发送人头像")
+    private String senderAvatar;
+
+    @Schema(description = "会话类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("会话类型")
+    private Boolean conversationType;
+
+    @Schema(description = "会话标志", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("会话标志")
+    private String conversationNo;
+
+    @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty("消息类型")
+    private Boolean contentType;
+
+    @Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("消息内容")
+    private String content;
+
+    @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("发送时间")
+    private LocalDateTime sendTime;
+
+    @Schema(description = "消息来源 100-用户发送;200-系统发送(一般是通知);", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("消息来源 100-用户发送;200-系统发送(一般是通知);")
+    private Boolean sendFrom;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 61 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/MessageSaveReqVO.java

@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.im.controller.admin.message.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 消息新增/修改 Request VO")
+@Data
+public class MessageSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "30713")
+    private Long id;
+
+    @Schema(description = "客户端消息编号 uuid,用于排重", requiredMode = Schema.RequiredMode.REQUIRED, example = "3331")
+    @NotEmpty(message = "客户端消息编号 uuid,用于排重不能为空")
+    private String clientMessageId;
+
+    @Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23239")
+    @NotNull(message = "发送人编号不能为空")
+    private Long senderId;
+
+    @Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32494")
+    @NotNull(message = "接收人编号不能为空")
+    private Long receiverId;
+
+    @Schema(description = "发送人昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+    @NotEmpty(message = "发送人昵称不能为空")
+    private String senderNickname;
+
+    @Schema(description = "发送人头像", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "发送人头像不能为空")
+    private String senderAvatar;
+
+    @Schema(description = "会话类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @NotNull(message = "会话类型不能为空")
+    private Boolean conversationType;
+
+    @Schema(description = "会话标志", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "会话标志不能为空")
+    private String conversationNo;
+
+    @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "消息类型不能为空")
+    private Boolean contentType;
+
+    @Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "消息内容不能为空")
+    private String content;
+
+    @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "发送时间不能为空")
+    private LocalDateTime sendTime;
+
+    @Schema(description = "消息来源 100-用户发送;200-系统发送(一般是通知);", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "消息来源 100-用户发送;200-系统发送(一般是通知);不能为空")
+    private Boolean sendFrom;
+
+}

+ 56 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/conversation/ConversationDO.java

@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.im.dal.dataobject.conversation;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * 会话 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("im_conversation")
+@KeySequence("im_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ConversationDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 所属用户
+     */
+    private Long userId;
+    /**
+     * 类型:1 单聊;2 群聊;4 通知会话(预留)
+     */
+    private Boolean conversationType;
+    /**
+     * 单聊时,用户编号;群聊时,群编号
+     */
+    private String targetId;
+    /**
+     * 会话标志 单聊:s_{userId}_{targetId},需要排序 userId 和 targetId 群聊:g_groupId
+     */
+    private String no;
+    /**
+     * 是否置顶 0否 1是
+     */
+    private Boolean pinned;
+    /**
+     * 最后已读时间
+     */
+    private LocalDateTime lastReadTime;
+
+}

+ 42 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/inbox/InboxDO.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.im.dal.dataobject.inbox;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 收件箱 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("im_inbox")
+@KeySequence("im_inbox_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InboxDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+    /**
+     * 消息编号
+     */
+    private Long messageId;
+    /**
+     * 序号,按照 user 递增
+     */
+    private Long sequence;
+
+}

+ 76 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/MessageDO.java

@@ -0,0 +1,76 @@
+package cn.iocoder.yudao.module.im.dal.dataobject.message;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * 消息 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("im_message")
+@KeySequence("im_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MessageDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 客户端消息编号 uuid,用于排重
+     */
+    private String clientMessageId;
+    /**
+     * 发送人编号
+     */
+    private Long senderId;
+    /**
+     * 接收人编号
+     */
+    private Long receiverId;
+    /**
+     * 发送人昵称
+     */
+    private String senderNickname;
+    /**
+     * 发送人头像
+     */
+    private String senderAvatar;
+    /**
+     * 会话类型
+     */
+    private Boolean conversationType;
+    /**
+     * 会话标志
+     */
+    private String conversationNo;
+    /**
+     * 消息类型
+     */
+    private Boolean contentType;
+    /**
+     * 消息内容
+     */
+    private String content;
+    /**
+     * 发送时间
+     */
+    private LocalDateTime sendTime;
+    /**
+     * 消息来源 100-用户发送;200-系统发送(一般是通知);
+     */
+    private Boolean sendFrom;
+
+}

+ 31 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImAudioMessageBody.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.im.dal.dataobject.message.body;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 语音消息的 {@link ImMessageBody}
+ *
+ * @author 芋道源码
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ImAudioMessageBody implements ImMessageBody {
+
+    /**
+     * 语音 URL
+     */
+    private String url;
+    /**
+     * 语音格式,例如说 arm、mp3、speex 等
+     */
+    private String format;
+
+    // TODO 芋艿:要不要以下字段?待定;云信有、企业微信没有
+//"dur":4551,		//语音持续时长ms
+//        "md5":"87b94a090dec5c58f242b7132a530a01",	//语音文件的md5值,按照字节流加密
+//        "size":16420		//语音文件大小,单位为字节(Byte)
+
+}

+ 31 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImFileMessageBody.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.im.dal.dataobject.message.body;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 文件消息的 {@link ImMessageBody}
+ *
+ * @author 芋道源码
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ImFileMessageBody implements ImMessageBody {
+
+    /**
+     * 文件名
+     */
+    private String name;
+    /**
+     * 文件 URL
+     */
+    private String url;
+
+    // TODO 芋艿:要不要以下字段?待定;云信有、企业微信没有
+//  "md5":"79d62a35fa3d34c367b20c66afc2a500", //文件MD5,按照字节流加密
+//  "ext":"ttf",	//文件后缀类型
+//  "size":91680	//大小,单位为字节(Byte)
+
+}

+ 30 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImImageMessageBody.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.im.dal.dataobject.message.body;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 图片消息的 {@link ImMessageBody}
+ *
+ * @author 芋道源码
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ImImageMessageBody implements ImMessageBody {
+
+    /**
+     * 图片地址
+     */
+    private String url;
+
+    // TODO 芋艿:要不要以下字段?待定;云信有、企业微信没有
+//    "name":"图片发送于2015-05-07 13:59",   //图片name
+//            "md5":"9894907e4ad9de4678091277509361f7",	//图片文件md5,按照字节流加密
+//            "ext":"jpg",	//图片后缀
+//            "w":6814,	//宽,单位为像素
+//            "h":2332,	//高,单位为像素
+//            "size":388245	//图片文件大小,单位为字节(Byte)
+
+}

+ 32 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImLocationMessageBody.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.im.dal.dataobject.message.body;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 地址位置消息的 {@link ImMessageBody}
+ *
+ * @author 芋道源码
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ImLocationMessageBody implements ImMessageBody {
+
+    /**
+     * 地理位置
+     *
+     * 例如说:中国 浙江省 杭州市 网商路 599号
+     */
+    private String address;
+    /**
+     * 经度
+     */
+    private Double longitude;
+    /**
+     * 纬度
+     */
+    private Double latitude;
+
+}

+ 12 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImMessageBody.java

@@ -0,0 +1,12 @@
+package cn.iocoder.yudao.module.im.dal.dataobject.message.body;
+
+
+import cn.iocoder.yudao.module.im.jackson.ImMessageBodyDeserializer;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+/**
+ * IM 消息的 body 内容
+ */
+@JsonDeserialize(using = ImMessageBodyDeserializer.class)
+public interface ImMessageBody {
+}

+ 22 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImTextMessageBody.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.im.dal.dataobject.message.body;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 文本消息的 {@link ImMessageBody}
+ *
+ * @author 芋道源码
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ImTextMessageBody implements ImMessageBody {
+
+    /**
+     * 文本消息内容
+     */
+    private String content;
+
+}

+ 27 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImVideoMessageBody.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.im.dal.dataobject.message.body;
+
+import lombok.Data;
+
+/**
+ * 视频消息的 {@link ImMessageBody}
+ *
+ * @author 芋道源码
+ */
+@Data
+public class ImVideoMessageBody implements ImMessageBody {
+
+    /**
+     * 视频地址
+     */
+    private String url;
+
+    // TODO 芋艿:要不要以下字段?待定;云信有、企业微信没有
+//  "dur":8003,		//视频持续时长ms
+//          "md5":"da2cef3e5663ee9c3547ef5d127f7e3e",	//视频文件的md5值,按照字节流加密
+//          "w":360,	//宽,单位为像素
+//          "h":480,	//高,单位为像素
+//          "size":16420	//视频文件大小,单位为字节(Byte)】
+//      "ext":"mp4",	//视频格式
+
+
+}

+ 30 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ConversationMapper.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.im.dal.mysql.conversation;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ConversationPageReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ConversationDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 会话 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ConversationMapper extends BaseMapperX<ConversationDO> {
+
+    default PageResult<ConversationDO> selectPage(ConversationPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<ConversationDO>()
+                .eqIfPresent(ConversationDO::getUserId, reqVO.getUserId())
+                .eqIfPresent(ConversationDO::getConversationType, reqVO.getConversationType())
+                .eqIfPresent(ConversationDO::getTargetId, reqVO.getTargetId())
+                .eqIfPresent(ConversationDO::getNo, reqVO.getNo())
+                .eqIfPresent(ConversationDO::getPinned, reqVO.getPinned())
+                .betweenIfPresent(ConversationDO::getLastReadTime, reqVO.getLastReadTime())
+                .betweenIfPresent(ConversationDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(ConversationDO::getId));
+    }
+
+}

+ 27 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/inbox/InboxMapper.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.im.dal.mysql.inbox;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.im.controller.admin.inbox.vo.InboxPageReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.inbox.InboxDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 收件箱 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InboxMapper extends BaseMapperX<InboxDO> {
+
+    default PageResult<InboxDO> selectPage(InboxPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<InboxDO>()
+                .eqIfPresent(InboxDO::getUserId, reqVO.getUserId())
+                .eqIfPresent(InboxDO::getMessageId, reqVO.getMessageId())
+                .eqIfPresent(InboxDO::getSequence, reqVO.getSequence())
+                .betweenIfPresent(InboxDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(InboxDO::getId));
+    }
+
+}

+ 35 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/MessageMapper.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.im.dal.mysql.message;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.MessagePageReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.message.MessageDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 消息 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface MessageMapper extends BaseMapperX<MessageDO> {
+
+    default PageResult<MessageDO> selectPage(MessagePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<MessageDO>()
+                .eqIfPresent(MessageDO::getClientMessageId, reqVO.getClientMessageId())
+                .eqIfPresent(MessageDO::getSenderId, reqVO.getSenderId())
+                .eqIfPresent(MessageDO::getReceiverId, reqVO.getReceiverId())
+                .likeIfPresent(MessageDO::getSenderNickname, reqVO.getSenderNickname())
+                .eqIfPresent(MessageDO::getSenderAvatar, reqVO.getSenderAvatar())
+                .eqIfPresent(MessageDO::getConversationType, reqVO.getConversationType())
+                .eqIfPresent(MessageDO::getConversationNo, reqVO.getConversationNo())
+                .eqIfPresent(MessageDO::getContentType, reqVO.getContentType())
+                .eqIfPresent(MessageDO::getContent, reqVO.getContent())
+                .betweenIfPresent(MessageDO::getSendTime, reqVO.getSendTime())
+                .eqIfPresent(MessageDO::getSendFrom, reqVO.getSendFrom())
+                .betweenIfPresent(MessageDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(MessageDO::getId));
+    }
+
+}

+ 6 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/framework/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * 属于 erp 模块的 framework 封装
+ *
+ * @author 芋道源码
+ */
+package cn.iocoder.yudao.module.im.framework;

+ 22 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/framework/web/config/ImWebConfiguration.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.im.framework.web.config;
+
+import cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration;
+import org.springdoc.core.models.GroupedOpenApi;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * im 模块的 web 组件的 Configuration
+ */
+@Configuration(proxyBeanMethods = false)
+public class ImWebConfiguration {
+
+    /**
+     * im 模块的 API 分组
+     */
+    @Bean
+    public GroupedOpenApi imGroupedOpenApi() {
+        return YudaoSwaggerAutoConfiguration.buildGroupedOpenApi("im");
+    }
+
+}

+ 4 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/framework/web/package-info.java

@@ -0,0 +1,4 @@
+/**
+ * trade 模块的 web 配置
+ */
+package cn.iocoder.yudao.module.im.framework.web;

+ 36 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/jackson/ImMessageBodyDeserializer.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.im.jackson;
+
+import cn.iocoder.yudao.module.im.dal.dataobject.message.body.*;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.io.IOException;
+
+public class ImMessageBodyDeserializer extends JsonDeserializer<ImMessageBody> {
+
+    @Override
+    public ImMessageBody deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+        JsonNode node = p.getCodec().readTree(p);
+        // 根据 node 中的内容来判断应该实例化哪个子类
+        if (node.has("content")) {
+            return new ImTextMessageBody(node.get("content").asText());
+        }
+        if (node.has("url")) {
+            String url = node.get("url").asText();
+            if (node.has("format")) {
+                return new ImAudioMessageBody(url, node.get("format").asText());
+            }
+            return new ImImageMessageBody(url);
+        }
+        if (node.has("name")) {
+            return new ImFileMessageBody(node.get("name").asText(), node.get("url").asText());
+        }
+        if (node.has("address")) {
+            return new ImLocationMessageBody(node.get("address").asText(), node.get("longitude").asDouble(), node.get("latitude").asDouble());
+        }
+        // 如果没有匹配的属性,抛出异常
+        throw ctxt.mappingException("Cannot deserialize to an instance of ImMessageBody");
+    }
+}

+ 54 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ConversationService.java

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.im.service.conversation;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ConversationPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ConversationSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ConversationDO;
+import jakarta.validation.Valid;
+
+/**
+ * 会话 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ConversationService {
+
+    /**
+     * 创建会话
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createConversation(@Valid ConversationSaveReqVO createReqVO);
+
+    /**
+     * 更新会话
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateConversation(@Valid ConversationSaveReqVO updateReqVO);
+
+    /**
+     * 删除会话
+     *
+     * @param id 编号
+     */
+    void deleteConversation(Long id);
+
+    /**
+     * 获得会话
+     *
+     * @param id 编号
+     * @return 会话
+     */
+    ConversationDO getConversation(Long id);
+
+    /**
+     * 获得会话分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 会话分页
+     */
+    PageResult<ConversationDO> getConversationPage(ConversationPageReqVO pageReqVO);
+
+}

+ 70 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ConversationServiceImpl.java

@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.im.service.conversation;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ConversationPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ConversationSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ConversationDO;
+import cn.iocoder.yudao.module.im.dal.mysql.conversation.ConversationMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.CONVERSATION_NOT_EXISTS;
+
+/**
+ * 会话 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ConversationServiceImpl implements ConversationService {
+
+    @Resource
+    private ConversationMapper conversationMapper;
+
+    @Override
+    public Long createConversation(ConversationSaveReqVO createReqVO) {
+        // 插入
+        ConversationDO conversation = BeanUtils.toBean(createReqVO, ConversationDO.class);
+        conversationMapper.insert(conversation);
+        // 返回
+        return conversation.getId();
+    }
+
+    @Override
+    public void updateConversation(ConversationSaveReqVO updateReqVO) {
+        // 校验存在
+        validateConversationExists(updateReqVO.getId());
+        // 更新
+        ConversationDO updateObj = BeanUtils.toBean(updateReqVO, ConversationDO.class);
+        conversationMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteConversation(Long id) {
+        // 校验存在
+        validateConversationExists(id);
+        // 删除
+        conversationMapper.deleteById(id);
+    }
+
+    private void validateConversationExists(Long id) {
+        if (conversationMapper.selectById(id) == null) {
+            throw exception(CONVERSATION_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public ConversationDO getConversation(Long id) {
+        return conversationMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<ConversationDO> getConversationPage(ConversationPageReqVO pageReqVO) {
+        return conversationMapper.selectPage(pageReqVO);
+    }
+
+}

+ 54 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/InboxService.java

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.im.service.inbox;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.im.controller.admin.inbox.vo.InboxPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.inbox.vo.InboxSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.inbox.InboxDO;
+import jakarta.validation.Valid;
+
+/**
+ * 收件箱 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface InboxService {
+
+    /**
+     * 创建收件箱
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createInbox(@Valid InboxSaveReqVO createReqVO);
+
+    /**
+     * 更新收件箱
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateInbox(@Valid InboxSaveReqVO updateReqVO);
+
+    /**
+     * 删除收件箱
+     *
+     * @param id 编号
+     */
+    void deleteInbox(Long id);
+
+    /**
+     * 获得收件箱
+     *
+     * @param id 编号
+     * @return 收件箱
+     */
+    InboxDO getInbox(Long id);
+
+    /**
+     * 获得收件箱分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 收件箱分页
+     */
+    PageResult<InboxDO> getInboxPage(InboxPageReqVO pageReqVO);
+
+}

+ 70 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/InboxServiceImpl.java

@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.im.service.inbox;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.inbox.vo.InboxPageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.inbox.vo.InboxSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.inbox.InboxDO;
+import cn.iocoder.yudao.module.im.dal.mysql.inbox.InboxMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.INBOX_NOT_EXISTS;
+
+/**
+ * 收件箱 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class InboxServiceImpl implements InboxService {
+
+    @Resource
+    private InboxMapper inboxMapper;
+
+    @Override
+    public Long createInbox(InboxSaveReqVO createReqVO) {
+        // 插入
+        InboxDO inbox = BeanUtils.toBean(createReqVO, InboxDO.class);
+        inboxMapper.insert(inbox);
+        // 返回
+        return inbox.getId();
+    }
+
+    @Override
+    public void updateInbox(InboxSaveReqVO updateReqVO) {
+        // 校验存在
+        validateInboxExists(updateReqVO.getId());
+        // 更新
+        InboxDO updateObj = BeanUtils.toBean(updateReqVO, InboxDO.class);
+        inboxMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteInbox(Long id) {
+        // 校验存在
+        validateInboxExists(id);
+        // 删除
+        inboxMapper.deleteById(id);
+    }
+
+    private void validateInboxExists(Long id) {
+        if (inboxMapper.selectById(id) == null) {
+            throw exception(INBOX_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public InboxDO getInbox(Long id) {
+        return inboxMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<InboxDO> getInboxPage(InboxPageReqVO pageReqVO) {
+        return inboxMapper.selectPage(pageReqVO);
+    }
+
+}

+ 60 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/MessageService.java

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.im.service.message;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.MessagePageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.MessageSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.message.MessageDO;
+import jakarta.validation.Valid;
+
+/**
+ * 消息 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface MessageService {
+
+    /**
+     * 创建消息
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createMessage(@Valid MessageSaveReqVO createReqVO);
+
+    /**
+     * 更新消息
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateMessage(@Valid MessageSaveReqVO updateReqVO);
+
+    /**
+     * 删除消息
+     *
+     * @param id 编号
+     */
+    void deleteMessage(Long id);
+
+    /**
+     * 获得消息
+     *
+     * @param id 编号
+     * @return 消息
+     */
+    MessageDO getMessage(Long id);
+
+    /**
+     * 获得消息分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 消息分页
+     */
+    PageResult<MessageDO> getMessagePage(MessagePageReqVO pageReqVO);
+
+    /**
+     * 发送私聊消息
+     * @param messageSaveReqVO 消息信息
+     * @return 消息编号
+     */
+    Long sendPrivateMessage(MessageSaveReqVO messageSaveReqVO);
+}

+ 75 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/MessageServiceImpl.java

@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.module.im.service.message;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.MessagePageReqVO;
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.MessageSaveReqVO;
+import cn.iocoder.yudao.module.im.dal.dataobject.message.MessageDO;
+import cn.iocoder.yudao.module.im.dal.mysql.message.MessageMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_NOT_EXISTS;
+
+/**
+ * 消息 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class MessageServiceImpl implements MessageService {
+
+    @Resource
+    private MessageMapper messageMapper;
+
+    @Override
+    public Long createMessage(MessageSaveReqVO createReqVO) {
+        // 插入
+        MessageDO message = BeanUtils.toBean(createReqVO, MessageDO.class);
+        messageMapper.insert(message);
+        // 返回
+        return message.getId();
+    }
+
+    @Override
+    public void updateMessage(MessageSaveReqVO updateReqVO) {
+        // 校验存在
+        validateMessageExists(updateReqVO.getId());
+        // 更新
+        MessageDO updateObj = BeanUtils.toBean(updateReqVO, MessageDO.class);
+        messageMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteMessage(Long id) {
+        // 校验存在
+        validateMessageExists(id);
+        // 删除
+        messageMapper.deleteById(id);
+    }
+
+    private void validateMessageExists(Long id) {
+        if (messageMapper.selectById(id) == null) {
+            throw exception(MESSAGE_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public MessageDO getMessage(Long id) {
+        return messageMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<MessageDO> getMessagePage(MessagePageReqVO pageReqVO) {
+        return messageMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public Long sendPrivateMessage(MessageSaveReqVO messageSaveReqVO) {
+        return 0L;
+    }
+
+}

+ 46 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImWebSocketMessageListener.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.im.websocket;
+
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
+import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
+import cn.iocoder.yudao.module.im.enums.conversation.ImConversationTypeEnum;
+import cn.iocoder.yudao.module.im.websocket.message.ImReceiveMessage;
+import cn.iocoder.yudao.module.im.websocket.message.ImSendMessage;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.WebSocketSession;
+
+/**
+ * WebSocket 示例:单发消息
+ *
+ * @author 芋道源码
+ */
+@Component
+public class ImWebSocketMessageListener implements WebSocketMessageListener<ImSendMessage> {
+
+    @Resource
+    private WebSocketMessageSender webSocketMessageSender;
+
+    @Override
+    public void onMessage(WebSocketSession session, ImSendMessage message) {
+        Long fromUserId = WebSocketFrameworkUtils.getLoginUserId(session);
+        // 私聊
+        if (message.getConversationType().equals(ImConversationTypeEnum.PRIVATE.getType())) {
+            ImReceiveMessage toMessage = new ImReceiveMessage();
+            toMessage.setToId(fromUserId);
+            toMessage.setConversationType(ImConversationTypeEnum.PRIVATE.getType());
+            //消息类型
+            toMessage.setType(message.getType());
+            toMessage.setBody(message.getBody());
+            webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getToId(), // 给指定用户
+                    "im-message-receive", toMessage);
+        }
+    }
+
+    @Override
+    public String getType() {
+        return "im-message-send";
+    }
+
+}

+ 23 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/message/ImReceiveMessage.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.im.websocket.message;
+
+import cn.iocoder.yudao.module.im.dal.dataobject.message.body.ImMessageBody;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 消息发送 send")
+@Data
+public class ImReceiveMessage {
+
+    @Schema(description = "会话类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer conversationType; // 对应 ImConversationTypeEnum 枚举
+
+    @Schema(description = "聊天对象,用户编号或群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long toId;  // 根据 conversationType 区分
+
+    @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer type; // 参见 ImMessageTypeEnum 枚举
+
+    @Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED)
+    private ImMessageBody body;
+
+}

+ 23 - 0
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/message/ImSendMessage.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.im.websocket.message;
+
+import cn.iocoder.yudao.module.im.dal.dataobject.message.body.ImMessageBody;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 消息发送 send")
+@Data
+public class ImSendMessage {
+
+    @Schema(description = "会话类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer conversationType; // 对应 ImConversationTypeEnum 枚举
+
+    @Schema(description = "聊天对象,用户编号或群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long toId;  // 根据 conversationType 区分
+
+    @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer type; // 参见 ImMessageTypeEnum 枚举
+
+    @Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED)
+    private ImMessageBody body;
+
+}

+ 12 - 0
yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/conversation/ConversationMapper.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.im.dal.mysql.conversation.ConversationMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 12 - 0
yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/inbox/InboxMapper.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.im.dal.mysql.inbox.InboxMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 12 - 0
yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/message/MessageMapper.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.im.dal.mysql.message.MessageMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 154 - 0
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/conversation/ConversationServiceImplTest.java

@@ -0,0 +1,154 @@
+package cn.iocoder.yudao.module.im.service.conversation;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import jakarta.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.*;
+import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ConversationDO;
+import cn.iocoder.yudao.module.im.dal.mysql.conversation.ConversationMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link ConversationServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(ConversationServiceImpl.class)
+public class ConversationServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private ConversationServiceImpl conversationService;
+
+    @Resource
+    private ConversationMapper conversationMapper;
+
+    @Test
+    public void testCreateConversation_success() {
+        // 准备参数
+        ConversationSaveReqVO createReqVO = randomPojo(ConversationSaveReqVO.class).setId(null);
+
+        // 调用
+        Long conversationId = conversationService.createConversation(createReqVO);
+        // 断言
+        assertNotNull(conversationId);
+        // 校验记录的属性是否正确
+        ConversationDO conversation = conversationMapper.selectById(conversationId);
+        assertPojoEquals(createReqVO, conversation, "id");
+    }
+
+    @Test
+    public void testUpdateConversation_success() {
+        // mock 数据
+        ConversationDO dbConversation = randomPojo(ConversationDO.class);
+        conversationMapper.insert(dbConversation);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        ConversationSaveReqVO updateReqVO = randomPojo(ConversationSaveReqVO.class, o -> {
+            o.setId(dbConversation.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        conversationService.updateConversation(updateReqVO);
+        // 校验是否更新正确
+        ConversationDO conversation = conversationMapper.selectById(updateReqVO.getId()); // 获取最新的
+        assertPojoEquals(updateReqVO, conversation);
+    }
+
+    @Test
+    public void testUpdateConversation_notExists() {
+        // 准备参数
+        ConversationSaveReqVO updateReqVO = randomPojo(ConversationSaveReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> conversationService.updateConversation(updateReqVO), CONVERSATION_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteConversation_success() {
+        // mock 数据
+        ConversationDO dbConversation = randomPojo(ConversationDO.class);
+        conversationMapper.insert(dbConversation);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbConversation.getId();
+
+        // 调用
+        conversationService.deleteConversation(id);
+       // 校验数据不存在了
+       assertNull(conversationMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteConversation_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> conversationService.deleteConversation(id), CONVERSATION_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetConversationPage() {
+       // mock 数据
+       ConversationDO dbConversation = randomPojo(ConversationDO.class, o -> { // 等会查询到
+           o.setUserId(null);
+           o.setConversationType(null);
+           o.setTargetId(null);
+           o.setNo(null);
+           o.setPinned(null);
+           o.setLastReadTime(null);
+           o.setCreateTime(null);
+       });
+       conversationMapper.insert(dbConversation);
+       // 测试 userId 不匹配
+       conversationMapper.insert(cloneIgnoreId(dbConversation, o -> o.setUserId(null)));
+       // 测试 conversationType 不匹配
+       conversationMapper.insert(cloneIgnoreId(dbConversation, o -> o.setConversationType(null)));
+       // 测试 targetId 不匹配
+       conversationMapper.insert(cloneIgnoreId(dbConversation, o -> o.setTargetId(null)));
+       // 测试 no 不匹配
+       conversationMapper.insert(cloneIgnoreId(dbConversation, o -> o.setNo(null)));
+       // 测试 pinned 不匹配
+       conversationMapper.insert(cloneIgnoreId(dbConversation, o -> o.setPinned(null)));
+       // 测试 lastReadTime 不匹配
+       conversationMapper.insert(cloneIgnoreId(dbConversation, o -> o.setLastReadTime(null)));
+       // 测试 createTime 不匹配
+       conversationMapper.insert(cloneIgnoreId(dbConversation, o -> o.setCreateTime(null)));
+       // 准备参数
+       ConversationPageReqVO reqVO = new ConversationPageReqVO();
+       reqVO.setUserId(null);
+       reqVO.setConversationType(null);
+       reqVO.setTargetId(null);
+       reqVO.setNo(null);
+       reqVO.setPinned(null);
+       reqVO.setLastReadTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<ConversationDO> pageResult = conversationService.getConversationPage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbConversation, pageResult.getList().get(0));
+    }
+
+}

+ 142 - 0
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/inbox/InboxServiceImplTest.java

@@ -0,0 +1,142 @@
+package cn.iocoder.yudao.module.im.service.inbox;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import jakarta.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.im.controller.admin.inbox.vo.*;
+import cn.iocoder.yudao.module.im.dal.dataobject.inbox.InboxDO;
+import cn.iocoder.yudao.module.im.dal.mysql.inbox.InboxMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link InboxServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(InboxServiceImpl.class)
+public class InboxServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private InboxServiceImpl inboxService;
+
+    @Resource
+    private InboxMapper inboxMapper;
+
+    @Test
+    public void testCreateInbox_success() {
+        // 准备参数
+        InboxSaveReqVO createReqVO = randomPojo(InboxSaveReqVO.class).setId(null);
+
+        // 调用
+        Long inboxId = inboxService.createInbox(createReqVO);
+        // 断言
+        assertNotNull(inboxId);
+        // 校验记录的属性是否正确
+        InboxDO inbox = inboxMapper.selectById(inboxId);
+        assertPojoEquals(createReqVO, inbox, "id");
+    }
+
+    @Test
+    public void testUpdateInbox_success() {
+        // mock 数据
+        InboxDO dbInbox = randomPojo(InboxDO.class);
+        inboxMapper.insert(dbInbox);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        InboxSaveReqVO updateReqVO = randomPojo(InboxSaveReqVO.class, o -> {
+            o.setId(dbInbox.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        inboxService.updateInbox(updateReqVO);
+        // 校验是否更新正确
+        InboxDO inbox = inboxMapper.selectById(updateReqVO.getId()); // 获取最新的
+        assertPojoEquals(updateReqVO, inbox);
+    }
+
+    @Test
+    public void testUpdateInbox_notExists() {
+        // 准备参数
+        InboxSaveReqVO updateReqVO = randomPojo(InboxSaveReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> inboxService.updateInbox(updateReqVO), INBOX_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteInbox_success() {
+        // mock 数据
+        InboxDO dbInbox = randomPojo(InboxDO.class);
+        inboxMapper.insert(dbInbox);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbInbox.getId();
+
+        // 调用
+        inboxService.deleteInbox(id);
+       // 校验数据不存在了
+       assertNull(inboxMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteInbox_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> inboxService.deleteInbox(id), INBOX_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetInboxPage() {
+       // mock 数据
+       InboxDO dbInbox = randomPojo(InboxDO.class, o -> { // 等会查询到
+           o.setUserId(null);
+           o.setMessageId(null);
+           o.setSequence(null);
+           o.setCreateTime(null);
+       });
+       inboxMapper.insert(dbInbox);
+       // 测试 userId 不匹配
+       inboxMapper.insert(cloneIgnoreId(dbInbox, o -> o.setUserId(null)));
+       // 测试 messageId 不匹配
+       inboxMapper.insert(cloneIgnoreId(dbInbox, o -> o.setMessageId(null)));
+       // 测试 sequence 不匹配
+       inboxMapper.insert(cloneIgnoreId(dbInbox, o -> o.setSequence(null)));
+       // 测试 createTime 不匹配
+       inboxMapper.insert(cloneIgnoreId(dbInbox, o -> o.setCreateTime(null)));
+       // 准备参数
+       InboxPageReqVO reqVO = new InboxPageReqVO();
+       reqVO.setUserId(null);
+       reqVO.setMessageId(null);
+       reqVO.setSequence(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<InboxDO> pageResult = inboxService.getInboxPage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbInbox, pageResult.getList().get(0));
+    }
+
+}

+ 174 - 0
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/MessageServiceImplTest.java

@@ -0,0 +1,174 @@
+package cn.iocoder.yudao.module.im.service.message;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import jakarta.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.im.controller.admin.message.vo.*;
+import cn.iocoder.yudao.module.im.dal.dataobject.message.MessageDO;
+import cn.iocoder.yudao.module.im.dal.mysql.message.MessageMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link MessageServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(MessageServiceImpl.class)
+public class MessageServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private MessageServiceImpl messageService;
+
+    @Resource
+    private MessageMapper messageMapper;
+
+    @Test
+    public void testCreateMessage_success() {
+        // 准备参数
+        MessageSaveReqVO createReqVO = randomPojo(MessageSaveReqVO.class).setId(null);
+
+        // 调用
+        Long messageId = messageService.createMessage(createReqVO);
+        // 断言
+        assertNotNull(messageId);
+        // 校验记录的属性是否正确
+        MessageDO message = messageMapper.selectById(messageId);
+        assertPojoEquals(createReqVO, message, "id");
+    }
+
+    @Test
+    public void testUpdateMessage_success() {
+        // mock 数据
+        MessageDO dbMessage = randomPojo(MessageDO.class);
+        messageMapper.insert(dbMessage);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        MessageSaveReqVO updateReqVO = randomPojo(MessageSaveReqVO.class, o -> {
+            o.setId(dbMessage.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        messageService.updateMessage(updateReqVO);
+        // 校验是否更新正确
+        MessageDO message = messageMapper.selectById(updateReqVO.getId()); // 获取最新的
+        assertPojoEquals(updateReqVO, message);
+    }
+
+    @Test
+    public void testUpdateMessage_notExists() {
+        // 准备参数
+        MessageSaveReqVO updateReqVO = randomPojo(MessageSaveReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> messageService.updateMessage(updateReqVO), MESSAGE_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteMessage_success() {
+        // mock 数据
+        MessageDO dbMessage = randomPojo(MessageDO.class);
+        messageMapper.insert(dbMessage);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbMessage.getId();
+
+        // 调用
+        messageService.deleteMessage(id);
+       // 校验数据不存在了
+       assertNull(messageMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteMessage_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> messageService.deleteMessage(id), MESSAGE_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetMessagePage() {
+       // mock 数据
+       MessageDO dbMessage = randomPojo(MessageDO.class, o -> { // 等会查询到
+           o.setClientMessageId(null);
+           o.setSenderId(null);
+           o.setReceiverId(null);
+           o.setSenderNickname(null);
+           o.setSenderAvatar(null);
+           o.setConversationType(null);
+           o.setConversationNo(null);
+           o.setContentType(null);
+           o.setContent(null);
+           o.setSendTime(null);
+           o.setSendFrom(null);
+           o.setCreateTime(null);
+       });
+       messageMapper.insert(dbMessage);
+       // 测试 clientMessageId 不匹配
+       messageMapper.insert(cloneIgnoreId(dbMessage, o -> o.setClientMessageId(null)));
+       // 测试 senderId 不匹配
+       messageMapper.insert(cloneIgnoreId(dbMessage, o -> o.setSenderId(null)));
+       // 测试 receiverId 不匹配
+       messageMapper.insert(cloneIgnoreId(dbMessage, o -> o.setReceiverId(null)));
+       // 测试 senderNickname 不匹配
+       messageMapper.insert(cloneIgnoreId(dbMessage, o -> o.setSenderNickname(null)));
+       // 测试 senderAvatar 不匹配
+       messageMapper.insert(cloneIgnoreId(dbMessage, o -> o.setSenderAvatar(null)));
+       // 测试 conversationType 不匹配
+       messageMapper.insert(cloneIgnoreId(dbMessage, o -> o.setConversationType(null)));
+       // 测试 conversationNo 不匹配
+       messageMapper.insert(cloneIgnoreId(dbMessage, o -> o.setConversationNo(null)));
+       // 测试 contentType 不匹配
+       messageMapper.insert(cloneIgnoreId(dbMessage, o -> o.setContentType(null)));
+       // 测试 content 不匹配
+       messageMapper.insert(cloneIgnoreId(dbMessage, o -> o.setContent(null)));
+       // 测试 sendTime 不匹配
+       messageMapper.insert(cloneIgnoreId(dbMessage, o -> o.setSendTime(null)));
+       // 测试 sendFrom 不匹配
+       messageMapper.insert(cloneIgnoreId(dbMessage, o -> o.setSendFrom(null)));
+       // 测试 createTime 不匹配
+       messageMapper.insert(cloneIgnoreId(dbMessage, o -> o.setCreateTime(null)));
+       // 准备参数
+       MessagePageReqVO reqVO = new MessagePageReqVO();
+       reqVO.setClientMessageId(null);
+       reqVO.setSenderId(null);
+       reqVO.setReceiverId(null);
+       reqVO.setSenderNickname(null);
+       reqVO.setSenderAvatar(null);
+       reqVO.setConversationType(null);
+       reqVO.setConversationNo(null);
+       reqVO.setContentType(null);
+       reqVO.setContent(null);
+       reqVO.setSendTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+       reqVO.setSendFrom(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<MessageDO> pageResult = messageService.getMessagePage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbMessage, pageResult.getList().get(0));
+    }
+
+}

+ 7 - 0
yudao-server/pom.xml

@@ -101,6 +101,13 @@
 <!--            <version>${revision}</version>-->
 <!--        </dependency>-->
 
+        <!-- IM 相关模块。默认注释,保证编译速度 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-im-biz</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
         <!-- spring boot 配置所需依赖 -->
         <dependency>
             <groupId>org.springframework.boot</groupId>

+ 1 - 0
yudao-server/src/main/resources/application-local.yaml

@@ -189,6 +189,7 @@ logging:
     cn.iocoder.yudao.module.statistics.dal.mysql: debug
     cn.iocoder.yudao.module.crm.dal.mysql: debug
     cn.iocoder.yudao.module.erp.dal.mysql: debug
+    cn.iocoder.yudao.module.im.dal.mysql: debug
     org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示
 
 debug: false

+ 1 - 0
yudao-server/src/main/resources/application.yaml

@@ -183,6 +183,7 @@ yudao:
       - cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants
       - cn.iocoder.yudao.module.system.enums.ErrorCodeConstants
       - cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants
+      - cn.iocoder.yudao.module.im.enums.ErrorCodeConstants
   tenant: # 多租户相关配置项
     enable: true
     ignore-urls: