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: