新建:im 模块

This commit is contained in:
安浩浩 2024-03-12 17:01:37 +08:00
parent dda0909843
commit bb0b7056cf
57 changed files with 2423 additions and 2 deletions

View File

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

View File

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

29
yudao-module-im/pom.xml Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 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;
}

View File

@ -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") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 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;
}

View File

@ -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") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 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;
}

View File

@ -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;
/**
* 语音格式例如说 armmp3speex
*/
private String format;
// TODO 芋艿要不要以下字段待定云信有企业微信没有
//"dur":4551, //语音持续时长ms
// "md5":"87b94a090dec5c58f242b7132a530a01", //语音文件的md5值按照字节流加密
// "size":16420 //语音文件大小单位为字节Byte
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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;
}

View File

@ -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 {
}

View File

@ -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;
}

View File

@ -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", //视频格式
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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: