diff --git a/pom.xml b/pom.xml
index 3a66524bc1..bb462a717e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,7 +15,9 @@
yudao-module-system
yudao-module-infra
-
+ yudao-module-im
+ yudao-module-im/yudao-module-im-api
+
@@ -23,6 +25,7 @@
+ yudao-module-im
diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java
index 120f529c23..88ceee627a 100644
--- a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java
+++ b/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);
}
}
diff --git a/yudao-module-im/pom.xml b/yudao-module-im/pom.xml
new file mode 100644
index 0000000000..d5f3d10151
--- /dev/null
+++ b/yudao-module-im/pom.xml
@@ -0,0 +1,29 @@
+
+
+
+ cn.iocoder.boot
+ yudao
+ ${revision}
+
+
+ yudao-module-im-api
+ yudao-module-im-biz
+
+ 4.0.0
+ yudao-module-im
+ pom
+
+ ${project.artifactId}
+
+ im 模块,主要提供能力:
+ 1. 通讯能力,例如:消息发送、消息接收、消息撤回、消息已读等。
+ 2. 通讯会话,例如:单聊、群聊、聊天室等。
+ 3. 通讯消息,例如:文本、图片、语音、视频、文件等。
+ 4. 通讯消息的存储,例如:消息存储、消息索引、消息搜索等。
+ 5. 通讯消息的推送,例如:消息推送、消息通知等。
+ 6. 通讯消息的安全,例如:消息加密、消息签名等。
+
+
+
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-api/pom.xml b/yudao-module-im/yudao-module-im-api/pom.xml
new file mode 100644
index 0000000000..871a6ef1d6
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-api/pom.xml
@@ -0,0 +1,26 @@
+
+
+
+ cn.iocoder.boot
+ yudao-module-im
+ ${revision}
+
+ 4.0.0
+ yudao-module-im-api
+ jar
+
+ ${project.artifactId}
+
+ im 模块 API,暴露给其它模块调用
+
+
+
+
+ cn.iocoder.boot
+ yudao-common
+
+
+
+
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/api/package-info.java b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/api/package-info.java
new file mode 100644
index 0000000000..5722d74f0c
--- /dev/null
+++ b/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;
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java
new file mode 100644
index 0000000000..ca674552f9
--- /dev/null
+++ b/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 错误码枚举类
+ *
+ * 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, "消息不存在");
+}
diff --git a/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/conversation/ImConversationTypeEnum.java b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/conversation/ImConversationTypeEnum.java
new file mode 100644
index 0000000000..92db90dd1e
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java
new file mode 100644
index 0000000000..92df801fff
--- /dev/null
+++ b/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 消息的类型枚举
+ *
+ * 参考 “消息类型” 文档
+ *
+ * @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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/package-info.java b/yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/package-info.java
new file mode 100644
index 0000000000..f7571fbf0a
--- /dev/null
+++ b/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;
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/pom.xml b/yudao-module-im/yudao-module-im-biz/pom.xml
new file mode 100644
index 0000000000..76c1a8b95e
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-biz/pom.xml
@@ -0,0 +1,65 @@
+
+
+
+ yudao-module-im
+ cn.iocoder.boot
+ ${revision}
+
+ 4.0.0
+ jar
+
+ yudao-module-im-biz
+
+ ${project.artifactId}
+
+ im 模块,主要实现 im 模块的业务逻辑。
+
+
+
+
+ cn.iocoder.boot
+ yudao-module-im-api
+ ${revision}
+
+
+
+
+ cn.iocoder.boot
+ yudao-spring-boot-starter-biz-operatelog
+
+
+
+
+ cn.iocoder.boot
+ yudao-spring-boot-starter-web
+
+
+
+ cn.iocoder.boot
+ yudao-spring-boot-starter-security
+
+
+
+
+ cn.iocoder.boot
+ yudao-spring-boot-starter-mybatis
+
+
+
+
+ cn.iocoder.boot
+ yudao-spring-boot-starter-test
+
+
+ cn.iocoder.boot
+ yudao-spring-boot-starter-excel
+
+
+ cn.iocoder.boot
+ yudao-spring-boot-starter-websocket
+
+
+
+
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ConversationController.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ConversationController.java
new file mode 100755
index 0000000000..266c026eed
--- /dev/null
+++ b/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 createConversation(@Valid @RequestBody ConversationSaveReqVO createReqVO) {
+ return success(conversationService.createConversation(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新会话")
+ @PreAuthorize("@ss.hasPermission('im:conversation:update')")
+ public CommonResult 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 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 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> getConversationPage(@Valid ConversationPageReqVO pageReqVO) {
+ PageResult 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 list = conversationService.getConversationPage(pageReqVO).getList();
+ // 导出 Excel
+ ExcelUtils.write(response, "会话.xls", "数据", ConversationRespVO.class,
+ BeanUtils.toBean(list, ConversationRespVO.class));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ConversationPageReqVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ConversationPageReqVO.java
new file mode 100755
index 0000000000..3325a64055
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ConversationRespVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ConversationRespVO.java
new file mode 100755
index 0000000000..6ffbb7c00a
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ConversationSaveReqVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ConversationSaveReqVO.java
new file mode 100755
index 0000000000..932fd5b489
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/InboxController.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/InboxController.java
new file mode 100755
index 0000000000..03e6083040
--- /dev/null
+++ b/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 createInbox(@Valid @RequestBody InboxSaveReqVO createReqVO) {
+ return success(inboxService.createInbox(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新收件箱")
+ @PreAuthorize("@ss.hasPermission('im:inbox:update')")
+ public CommonResult 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 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 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> getInboxPage(@Valid InboxPageReqVO pageReqVO) {
+ PageResult 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 list = inboxService.getInboxPage(pageReqVO).getList();
+ // 导出 Excel
+ ExcelUtils.write(response, "收件箱.xls", "数据", InboxRespVO.class,
+ BeanUtils.toBean(list, InboxRespVO.class));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/vo/InboxPageReqVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/vo/InboxPageReqVO.java
new file mode 100755
index 0000000000..7eba083fa0
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/vo/InboxRespVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/vo/InboxRespVO.java
new file mode 100755
index 0000000000..b098ce5a00
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/vo/InboxSaveReqVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/inbox/vo/InboxSaveReqVO.java
new file mode 100755
index 0000000000..cb6e38a874
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/MessageController.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/MessageController.java
new file mode 100755
index 0000000000..788488a03b
--- /dev/null
+++ b/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 createMessage(@Valid @RequestBody MessageSaveReqVO createReqVO) {
+ return success(messageService.createMessage(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新消息")
+ @PreAuthorize("@ss.hasPermission('im:message:update')")
+ public CommonResult 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 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 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> getMessagePage(@Valid MessagePageReqVO pageReqVO) {
+ PageResult 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 list = messageService.getMessagePage(pageReqVO).getList();
+ // 导出 Excel
+ ExcelUtils.write(response, "消息.xls", "数据", MessageRespVO.class,
+ BeanUtils.toBean(list, MessageRespVO.class));
+ }
+
+ @PostMapping("/send")
+ @Operation(summary = "发送私聊消息")
+ public CommonResult sendMessage(@Valid @RequestBody MessageSaveReqVO messageSaveReqVO) {
+ return success(messageService.sendPrivateMessage(messageSaveReqVO));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/MessagePageReqVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/MessagePageReqVO.java
new file mode 100755
index 0000000000..96ffd87c59
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/MessageRespVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/MessageRespVO.java
new file mode 100755
index 0000000000..2949ade767
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/MessageSaveReqVO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/MessageSaveReqVO.java
new file mode 100755
index 0000000000..55ccd14327
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/conversation/ConversationDO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/conversation/ConversationDO.java
new file mode 100755
index 0000000000..12a083245c
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/inbox/InboxDO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/inbox/InboxDO.java
new file mode 100755
index 0000000000..fd60ea1735
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/MessageDO.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/MessageDO.java
new file mode 100755
index 0000000000..98a289c13e
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImAudioMessageBody.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImAudioMessageBody.java
new file mode 100644
index 0000000000..acc8d84103
--- /dev/null
+++ b/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)
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImFileMessageBody.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImFileMessageBody.java
new file mode 100644
index 0000000000..2ed2384e50
--- /dev/null
+++ b/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)
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImImageMessageBody.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImImageMessageBody.java
new file mode 100644
index 0000000000..1a81c284de
--- /dev/null
+++ b/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)
+
+}
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImLocationMessageBody.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImLocationMessageBody.java
new file mode 100644
index 0000000000..3e9db266bd
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImMessageBody.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImMessageBody.java
new file mode 100644
index 0000000000..e5eddc0813
--- /dev/null
+++ b/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 {
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImTextMessageBody.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImTextMessageBody.java
new file mode 100644
index 0000000000..9e6f973761
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImVideoMessageBody.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/body/ImVideoMessageBody.java
new file mode 100644
index 0000000000..5d643474db
--- /dev/null
+++ b/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", //视频格式
+
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ConversationMapper.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ConversationMapper.java
new file mode 100755
index 0000000000..e371d99d7d
--- /dev/null
+++ b/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 {
+
+ default PageResult selectPage(ConversationPageReqVO reqVO) {
+ return selectPage(reqVO, new LambdaQueryWrapperX()
+ .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));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/inbox/InboxMapper.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/inbox/InboxMapper.java
new file mode 100755
index 0000000000..1b0822b775
--- /dev/null
+++ b/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 {
+
+ default PageResult selectPage(InboxPageReqVO reqVO) {
+ return selectPage(reqVO, new LambdaQueryWrapperX()
+ .eqIfPresent(InboxDO::getUserId, reqVO.getUserId())
+ .eqIfPresent(InboxDO::getMessageId, reqVO.getMessageId())
+ .eqIfPresent(InboxDO::getSequence, reqVO.getSequence())
+ .betweenIfPresent(InboxDO::getCreateTime, reqVO.getCreateTime())
+ .orderByDesc(InboxDO::getId));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/MessageMapper.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/MessageMapper.java
new file mode 100755
index 0000000000..ac54b46ca3
--- /dev/null
+++ b/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 {
+
+ default PageResult selectPage(MessagePageReqVO reqVO) {
+ return selectPage(reqVO, new LambdaQueryWrapperX()
+ .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));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/framework/package-info.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/framework/package-info.java
new file mode 100644
index 0000000000..61b6dae9e8
--- /dev/null
+++ b/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;
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/framework/web/config/ImWebConfiguration.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/framework/web/config/ImWebConfiguration.java
new file mode 100644
index 0000000000..7e2b2ed5cc
--- /dev/null
+++ b/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");
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/framework/web/package-info.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/framework/web/package-info.java
new file mode 100644
index 0000000000..e000c8e86c
--- /dev/null
+++ b/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;
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/jackson/ImMessageBodyDeserializer.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/jackson/ImMessageBodyDeserializer.java
new file mode 100644
index 0000000000..7df70ce4c1
--- /dev/null
+++ b/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 {
+
+ @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");
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ConversationService.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ConversationService.java
new file mode 100755
index 0000000000..61175b1788
--- /dev/null
+++ b/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 getConversationPage(ConversationPageReqVO pageReqVO);
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ConversationServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ConversationServiceImpl.java
new file mode 100755
index 0000000000..9ede7d5127
--- /dev/null
+++ b/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 getConversationPage(ConversationPageReqVO pageReqVO) {
+ return conversationMapper.selectPage(pageReqVO);
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/InboxService.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/InboxService.java
new file mode 100755
index 0000000000..c61af653e3
--- /dev/null
+++ b/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 getInboxPage(InboxPageReqVO pageReqVO);
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/InboxServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/InboxServiceImpl.java
new file mode 100755
index 0000000000..35d1191b4d
--- /dev/null
+++ b/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 getInboxPage(InboxPageReqVO pageReqVO) {
+ return inboxMapper.selectPage(pageReqVO);
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/MessageService.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/MessageService.java
new file mode 100755
index 0000000000..5981dbdcca
--- /dev/null
+++ b/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 getMessagePage(MessagePageReqVO pageReqVO);
+
+ /**
+ * 发送私聊消息
+ * @param messageSaveReqVO 消息信息
+ * @return 消息编号
+ */
+ Long sendPrivateMessage(MessageSaveReqVO messageSaveReqVO);
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/MessageServiceImpl.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/MessageServiceImpl.java
new file mode 100755
index 0000000000..350d6c4472
--- /dev/null
+++ b/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 getMessagePage(MessagePageReqVO pageReqVO) {
+ return messageMapper.selectPage(pageReqVO);
+ }
+
+ @Override
+ public Long sendPrivateMessage(MessageSaveReqVO messageSaveReqVO) {
+ return 0L;
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImWebSocketMessageListener.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImWebSocketMessageListener.java
new file mode 100644
index 0000000000..0dd9bb1cf0
--- /dev/null
+++ b/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 {
+
+ @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";
+ }
+
+}
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/message/ImReceiveMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/message/ImReceiveMessage.java
new file mode 100644
index 0000000000..663b65d69e
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/message/ImSendMessage.java b/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/message/ImSendMessage.java
new file mode 100644
index 0000000000..71d5f5b5d9
--- /dev/null
+++ b/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;
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/conversation/ConversationMapper.xml b/yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/conversation/ConversationMapper.xml
new file mode 100755
index 0000000000..a876b91615
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/conversation/ConversationMapper.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/inbox/InboxMapper.xml b/yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/inbox/InboxMapper.xml
new file mode 100755
index 0000000000..5d6bd36ec3
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/inbox/InboxMapper.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/message/MessageMapper.xml b/yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/message/MessageMapper.xml
new file mode 100755
index 0000000000..46521ed613
--- /dev/null
+++ b/yudao-module-im/yudao-module-im-biz/src/main/resources/mapper/message/MessageMapper.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/conversation/ConversationServiceImplTest.java b/yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/conversation/ConversationServiceImplTest.java
new file mode 100755
index 0000000000..a1082248d7
--- /dev/null
+++ b/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 pageResult = conversationService.getConversationPage(reqVO);
+ // 断言
+ assertEquals(1, pageResult.getTotal());
+ assertEquals(1, pageResult.getList().size());
+ assertPojoEquals(dbConversation, pageResult.getList().get(0));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/inbox/InboxServiceImplTest.java b/yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/inbox/InboxServiceImplTest.java
new file mode 100755
index 0000000000..5df121c31f
--- /dev/null
+++ b/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 pageResult = inboxService.getInboxPage(reqVO);
+ // 断言
+ assertEquals(1, pageResult.getTotal());
+ assertEquals(1, pageResult.getList().size());
+ assertPojoEquals(dbInbox, pageResult.getList().get(0));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/MessageServiceImplTest.java b/yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/MessageServiceImplTest.java
new file mode 100755
index 0000000000..22713bd0e0
--- /dev/null
+++ b/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 pageResult = messageService.getMessagePage(reqVO);
+ // 断言
+ assertEquals(1, pageResult.getTotal());
+ assertEquals(1, pageResult.getList().size());
+ assertPojoEquals(dbMessage, pageResult.getList().get(0));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml
index bc850b5902..5d8f8c8c14 100644
--- a/yudao-server/pom.xml
+++ b/yudao-server/pom.xml
@@ -101,6 +101,13 @@
+
+
+ cn.iocoder.boot
+ yudao-module-im-biz
+ ${revision}
+
+
org.springframework.boot
diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml
index cddf42014b..e2934603f9 100644
--- a/yudao-server/src/main/resources/application-local.yaml
+++ b/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
diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml
index 4eaba09c83..fd61e0c457 100644
--- a/yudao-server/src/main/resources/application.yaml
+++ b/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: