Commit f495f9c8054e78df95498b42f1eb7cba47b5434b

Authored by YunaiV
1 parent 2ce3a993

✨ feat(im): 添加私聊私聊历史消息查询功能

yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImPrivateMessageController.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.im.controller.admin.message;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO;
5 6 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageRespVO;
6 7 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
7 8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
... ... @@ -60,9 +61,16 @@ public class ImPrivateMessageController {
60 61 @DeleteMapping("/recall")
61 62 @Operation(summary = "撤回私聊消息")
62 63 @Parameter(name = "id", description = "消息编号", required = true, example = "1")
63   - public CommonResult<Boolean> recallPrivateMessage(@RequestParam("id") Long id) {
64   - privateMessageService.recallPrivateMessage(getLoginUserId(), id);
65   - return success(true);
  64 + public CommonResult<ImPrivateMessageRespVO> recallPrivateMessage(@RequestParam("id") Long id) {
  65 + ImPrivateMessageDO message = privateMessageService.recallPrivateMessage(getLoginUserId(), id);
  66 + return success(BeanUtils.toBean(message, ImPrivateMessageRespVO.class));
  67 + }
  68 +
  69 + @GetMapping("/list")
  70 + @Operation(summary = "查询私聊历史消息")
  71 + public CommonResult<List<ImPrivateMessageRespVO>> getPrivateMessageList(@Valid ImPrivateMessageListReqVO reqVO) {
  72 + List<ImPrivateMessageDO> messages = privateMessageService.getPrivateMessageList(getLoginUserId(), reqVO);
  73 + return success(BeanUtils.toBean(messages, ImPrivateMessageRespVO.class));
66 74 }
67 75  
68 76 }
... ...
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapper.java
... ... @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
6 6 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
  7 +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
7 8 import org.apache.ibatis.annotations.Mapper;
8 9  
9 10 import java.util.List;
... ... @@ -27,29 +28,52 @@ public interface ImPrivateMessageMapper extends BaseMapperX&lt;ImPrivateMessageDO&gt;
27 28 default List<ImPrivateMessageDO> selectListByMinId(Long userId, Long minId, Integer size) {
28 29 QueryWrapperX<ImPrivateMessageDO> wrapper = new QueryWrapperX<>();
29 30 wrapper.and(w -> w.eq("sender_id", userId)
30   - .or().eq("receiver_id", userId))
  31 + .or()
  32 + .eq("receiver_id", userId))
31 33 .gt("id", minId)
32 34 .orderByAsc("id");
33 35 wrapper.limitN(size);
34 36 return selectList(wrapper);
35 37 }
36 38  
  39 + /**
  40 + * 查询私聊历史消息(游标拉取)
  41 + *
  42 + * @param userId 当前用户编号
  43 + * @param receiverId 对方用户编号
  44 + * @param maxId 起始消息 id(不含),为空则从最新开始
  45 + * @param limit 拉取数量
  46 + * @return 消息列表(按 id 倒序)
  47 + */
  48 + default List<ImPrivateMessageDO> selectHistoryList(Long userId, Long receiverId, Long maxId, Integer limit) {
  49 + QueryWrapperX<ImPrivateMessageDO> wrapper = new QueryWrapperX<>();
  50 + wrapper.and(w -> w.eq("sender_id", userId).eq("receiver_id", receiverId)
  51 + .or()
  52 + .eq("sender_id", receiverId).eq("receiver_id", userId))
  53 + .ne("status", ImMessageStatusEnum.RECALL.getStatus())
  54 + .lt(maxId != null, "id", maxId)
  55 + .orderByDesc("id");
  56 + wrapper.limitN(limit);
  57 + return selectList(wrapper);
  58 + }
  59 +
37 60 default ImPrivateMessageDO selectBySenderIdAndClientMessageId(Long senderId, String clientMessageId) {
38 61 return selectOne(new LambdaQueryWrapperX<ImPrivateMessageDO>()
39 62 .eq(ImPrivateMessageDO::getSenderId, senderId)
40 63 .eq(ImPrivateMessageDO::getClientMessageId, clientMessageId));
41 64 }
42 65  
43   - /**
44   - * 批量更新私聊消息已读状态
45   - * 将发送方 = friendId、接收方 = userId 且状态为 UNREAD 的消息更新为 READ
46   - */
47   - default int updateStatusToRead(Long userId, Long friendId) {
48   - return update(new ImPrivateMessageDO().setStatus(1), // READ
49   - new LambdaQueryWrapperX<ImPrivateMessageDO>()
50   - .eq(ImPrivateMessageDO::getSenderId, friendId)
51   - .eq(ImPrivateMessageDO::getReceiverId, userId)
52   - .eq(ImPrivateMessageDO::getStatus, 0)); // UNREAD
  66 + default List<ImPrivateMessageDO> selectListBySenderIdAndReceiverIdAndStatus(Long senderId, Long receiverId, Integer status) {
  67 + return selectList(new LambdaQueryWrapperX<ImPrivateMessageDO>()
  68 + .eq(ImPrivateMessageDO::getSenderId, senderId)
  69 + .eq(ImPrivateMessageDO::getReceiverId, receiverId)
  70 + .eq(ImPrivateMessageDO::getStatus, status));
  71 + }
  72 +
  73 + default void updateByIdsAndStatus(List<Long> ids, Integer whereStatus, ImPrivateMessageDO updateObj) {
  74 + update(updateObj, new LambdaQueryWrapperX<ImPrivateMessageDO>()
  75 + .in(ImPrivateMessageDO::getId, ids)
  76 + .eq(ImPrivateMessageDO::getStatus, whereStatus));
53 77 }
54 78  
55 79 }
... ...
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java
1 1 package cn.iocoder.yudao.module.im.service.message;
2 2  
  3 +import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO;
3 4 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
4 5 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
5 6  
... ... @@ -48,4 +49,13 @@ public interface ImPrivateMessageService {
48 49 */
49 50 ImPrivateMessageDO recallPrivateMessage(Long userId, Long messageId);
50 51  
  52 + /**
  53 + * 查询私聊历史消息(游标拉取)
  54 + *
  55 + * @param userId 当前用户编号
  56 + * @param reqVO 拉取请求
  57 + * @return 消息列表(按 id 倒序)
  58 + */
  59 + List<ImPrivateMessageDO> getPrivateMessageList(Long userId, ImPrivateMessageListReqVO reqVO);
  60 +
51 61 }
... ...
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
... ... @@ -8,6 +8,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
8 8 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
9 9 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
10 10 import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
  11 +import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageListReqVO;
11 12 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
12 13 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
13 14 import cn.iocoder.yudao.module.im.dal.dataobject.message.content.RecallMessage;
... ... @@ -189,4 +190,9 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
189 190 return SpringUtil.getBean(getClass());
190 191 }
191 192  
  193 + @Override
  194 + public List<ImPrivateMessageDO> getPrivateMessageList(Long userId, ImPrivateMessageListReqVO reqVO) {
  195 + return privateMessageMapper.selectHistoryList(userId, reqVO.getReceiverId(), reqVO.getMaxId(), reqVO.getLimit());
  196 + }
  197 +
192 198 }
... ...
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapperTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.dal.mysql.message;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  4 +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
  5 +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
  6 +import jakarta.annotation.Resource;
  7 +import org.junit.jupiter.api.Test;
  8 +import org.springframework.context.annotation.Import;
  9 +
  10 +import java.time.LocalDateTime;
  11 +import java.util.List;
  12 +
  13 +import static org.junit.jupiter.api.Assertions.*;
  14 +
  15 +/**
  16 + * IM 私聊消息 Mapper 单元测试
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +@Import(ImPrivateMessageMapper.class)
  21 +public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
  22 +
  23 + @Resource
  24 + private ImPrivateMessageMapper mapper;
  25 +
  26 + // ========== selectListByMinId ==========
  27 +
  28 + @Test
  29 + public void testSelectListByMinId() {
  30 + // 准备:用户 1 发给用户 2
  31 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  32 + mapper.insert(msg1);
  33 + // 用户 2 发给用户 1
  34 + ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD);
  35 + mapper.insert(msg2);
  36 + // 用户 3 发给用户 4(不相关)
  37 + ImPrivateMessageDO msg3 = buildMessage(3L, 4L, ImMessageStatusEnum.UNREAD);
  38 + mapper.insert(msg3);
  39 +
  40 + // 调用:用户 1 从 id=0 拉取
  41 + List<ImPrivateMessageDO> result = mapper.selectListByMinId(1L, 0L, 100);
  42 +
  43 + // 断言:只包含与用户 1 相关的 2 条
  44 + assertEquals(2, result.size());
  45 + assertTrue(result.stream().allMatch(m ->
  46 + m.getSenderId().equals(1L) || m.getReceiverId().equals(1L)));
  47 + }
  48 +
  49 + @Test
  50 + public void testSelectListByMinId_withMinId() {
  51 + // 准备
  52 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  53 + mapper.insert(msg1);
  54 + ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD);
  55 + mapper.insert(msg2);
  56 +
  57 + // 调用:从 msg1.id 之后拉取
  58 + List<ImPrivateMessageDO> result = mapper.selectListByMinId(1L, msg1.getId(), 100);
  59 +
  60 + // 断言:只有 msg2
  61 + assertEquals(1, result.size());
  62 + assertEquals(msg2.getId(), result.get(0).getId());
  63 + }
  64 +
  65 + @Test
  66 + public void testSelectListByMinId_limitSize() {
  67 + // 准备:插入 3 条
  68 + mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD));
  69 + mapper.insert(buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD));
  70 + mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD));
  71 +
  72 + // 调用:limit 2
  73 + List<ImPrivateMessageDO> result = mapper.selectListByMinId(1L, 0L, 2);
  74 +
  75 + // 断言
  76 + assertEquals(2, result.size());
  77 + }
  78 +
  79 + // ========== selectHistoryList ==========
  80 +
  81 + @Test
  82 + public void testSelectHistoryList_basic() {
  83 + // 准备:用户 1 <-> 用户 2 的消息
  84 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  85 + mapper.insert(msg1);
  86 + ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD);
  87 + mapper.insert(msg2);
  88 + // 用户 1 <-> 用户 3 的消息(不相关)
  89 + ImPrivateMessageDO msg3 = buildMessage(1L, 3L, ImMessageStatusEnum.UNREAD);
  90 + mapper.insert(msg3);
  91 +
  92 + // 调用
  93 + List<ImPrivateMessageDO> result = mapper.selectHistoryList(1L, 2L, null, 100);
  94 +
  95 + // 断言:只有与用户 2 对话的 2 条,按 id 倒序
  96 + assertEquals(2, result.size());
  97 + assertTrue(result.get(0).getId() > result.get(1).getId());
  98 + }
  99 +
  100 + @Test
  101 + public void testSelectHistoryList_excludeRecall() {
  102 + // 准备
  103 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  104 + mapper.insert(msg1);
  105 + ImPrivateMessageDO msg2 = buildMessage(1L, 2L, ImMessageStatusEnum.RECALL);
  106 + mapper.insert(msg2);
  107 +
  108 + // 调用
  109 + List<ImPrivateMessageDO> result = mapper.selectHistoryList(1L, 2L, null, 100);
  110 +
  111 + // 断言:撤回的不返回
  112 + assertEquals(1, result.size());
  113 + assertEquals(msg1.getId(), result.get(0).getId());
  114 + }
  115 +
  116 + @Test
  117 + public void testSelectHistoryList_withMaxId() {
  118 + // 准备
  119 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  120 + mapper.insert(msg1);
  121 + ImPrivateMessageDO msg2 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  122 + mapper.insert(msg2);
  123 + ImPrivateMessageDO msg3 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  124 + mapper.insert(msg3);
  125 +
  126 + // 调用:从 msg3 往前拉
  127 + List<ImPrivateMessageDO> result = mapper.selectHistoryList(1L, 2L, msg3.getId(), 100);
  128 +
  129 + // 断言:只有 msg1 和 msg2
  130 + assertEquals(2, result.size());
  131 + assertTrue(result.stream().allMatch(m -> m.getId() < msg3.getId()));
  132 + }
  133 +
  134 + @Test
  135 + public void testSelectHistoryList_limit() {
  136 + // 准备
  137 + mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD));
  138 + mapper.insert(buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD));
  139 + mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD));
  140 +
  141 + // 调用:limit 2
  142 + List<ImPrivateMessageDO> result = mapper.selectHistoryList(1L, 2L, null, 2);
  143 +
  144 + // 断言
  145 + assertEquals(2, result.size());
  146 + }
  147 +
  148 + @Test
  149 + public void testSelectHistoryList_bidirectional() {
  150 + // 准备:验证双向查询
  151 + ImPrivateMessageDO msg1 = buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD); // 对方发的
  152 + mapper.insert(msg1);
  153 +
  154 + // 调用:以用户 1 的视角查
  155 + List<ImPrivateMessageDO> result = mapper.selectHistoryList(1L, 2L, null, 100);
  156 +
  157 + // 断言:能查到对方发来的消息
  158 + assertEquals(1, result.size());
  159 + assertEquals(2L, result.get(0).getSenderId());
  160 + }
  161 +
  162 + // ========== selectBySenderIdAndClientMessageId ==========
  163 +
  164 + @Test
  165 + public void testSelectBySenderIdAndClientMessageId() {
  166 + // 准备
  167 + ImPrivateMessageDO msg = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  168 + msg.setClientMessageId("uuid-001");
  169 + mapper.insert(msg);
  170 +
  171 + // 调用
  172 + ImPrivateMessageDO result = mapper.selectBySenderIdAndClientMessageId(1L, "uuid-001");
  173 +
  174 + // 断言
  175 + assertNotNull(result);
  176 + assertEquals(msg.getId(), result.getId());
  177 + }
  178 +
  179 + @Test
  180 + public void testSelectBySenderIdAndClientMessageId_notFound() {
  181 + ImPrivateMessageDO result = mapper.selectBySenderIdAndClientMessageId(1L, "not-exist");
  182 + assertNull(result);
  183 + }
  184 +
  185 + // ========== 工具方法 ==========
  186 +
  187 + private ImPrivateMessageDO buildMessage(Long senderId, Long receiverId, ImMessageStatusEnum status) {
  188 + return ImPrivateMessageDO.builder()
  189 + .clientMessageId("uuid-" + System.nanoTime())
  190 + .senderId(senderId)
  191 + .receiverId(receiverId)
  192 + .type(0)
  193 + .content("{\"content\":\"test\"}")
  194 + .status(status.getStatus())
  195 + .sendTime(LocalDateTime.now())
  196 + .build();
  197 + }
  198 +
  199 +}
... ...
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java
... ... @@ -177,20 +177,30 @@ public class ImPrivateMessageServiceImplTest {
177 177  
178 178 @Test
179 179 public void testRecallMessage_success() {
180   - // 准备
181   - ImPrivateMessageDO message = ImPrivateMessageDO.builder()
182   - .id(10L).senderId(1L).receiverId(2L)
183   - .status(ImMessageStatusEnum.UNREAD.getStatus()).build();
184   - when(imPrivateMessageMapper.selectById(10L)).thenReturn(message);
185   - when(imPrivateMessageMapper.updateById(any(ImPrivateMessageDO.class))).thenReturn(1);
  180 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  181 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImPrivateMessageServiceImpl.class)))
  182 + .thenReturn(privateMessageService);
186 183  
187   - // 调用
188   - privateMessageService.recallPrivateMessage(1L, 10L);
  184 + // 准备
  185 + ImPrivateMessageDO message = ImPrivateMessageDO.builder()
  186 + .id(10L).senderId(1L).receiverId(2L)
  187 + .status(ImMessageStatusEnum.UNREAD.getStatus())
  188 + .sendTime(LocalDateTime.now()).build(); // 刚发送,5 分钟内
  189 + when(imPrivateMessageMapper.selectById(10L)).thenReturn(message);
  190 + when(imPrivateMessageMapper.updateById(any(ImPrivateMessageDO.class))).thenReturn(1);
  191 + when(imPrivateMessageMapper.insert(any(ImPrivateMessageDO.class))).thenReturn(1);
189 192  
190   - // 断言
191   - verify(imPrivateMessageMapper).updateById(any(ImPrivateMessageDO.class));
192   - // 验证推送了 RECALL 事件(给接收方和发送方)
193   - verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), anyString(), any());
  193 + // 调用
  194 + ImPrivateMessageDO result = privateMessageService.recallPrivateMessage(1L, 10L);
  195 +
  196 + // 断言:返回 TIP_TEXT 消息
  197 + assertNotNull(result);
  198 + // 验证:更新原消息状态 + 插入 tipMessage
  199 + verify(imPrivateMessageMapper).updateById(any(ImPrivateMessageDO.class));
  200 + verify(imPrivateMessageMapper).insert(any(ImPrivateMessageDO.class));
  201 + // 验证推送了消息(给接收方和发送方)
  202 + verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), anyString(), any());
  203 + }
194 204 }
195 205  
196 206 @Test
... ...
yudao-module-im/yudao-module-im-biz/src/test/resources/application-unit-test.yaml 0 → 100644
  1 +spring:
  2 + main:
  3 + lazy-initialization: true
  4 + banner-mode: off
  5 +
  6 +--- #################### 数据库相关配置 ####################
  7 +
  8 +spring:
  9 + datasource:
  10 + name: ruoyi-vue-pro
  11 + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value;
  12 + driver-class-name: org.h2.Driver
  13 + username: sa
  14 + password:
  15 + druid:
  16 + async-init: true
  17 + initial-size: 1
  18 + sql:
  19 + init:
  20 + schema-locations: classpath:/sql/create_tables.sql
  21 + encoding: UTF-8
  22 +
  23 + data:
  24 + redis:
  25 + host: 127.0.0.1
  26 + port: 16379
  27 + database: 0
  28 +
  29 +mybatis-plus:
  30 + lazy-initialization: true
  31 + type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject
  32 +
  33 +--- #################### 芋道相关配置 ####################
  34 +
  35 +yudao:
  36 + info:
  37 + base-package: cn.iocoder.yudao
... ...
yudao-module-im/yudao-module-im-biz/src/test/resources/sql/clean.sql 0 → 100644
  1 +DELETE FROM "im_private_message";
... ...
yudao-module-im/yudao-module-im-biz/src/test/resources/sql/create_tables.sql 0 → 100644
  1 +CREATE TABLE IF NOT EXISTS "im_private_message" (
  2 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号',
  3 + "client_message_id" varchar(64) DEFAULT NULL COMMENT '客户端消息编号',
  4 + "sender_id" bigint NOT NULL COMMENT '发送人编号',
  5 + "receiver_id" bigint NOT NULL COMMENT '接收人编号',
  6 + "type" tinyint NOT NULL COMMENT '消息类型',
  7 + "content" varchar(4096) DEFAULT NULL COMMENT '消息内容',
  8 + "status" tinyint NOT NULL COMMENT '消息状态',
  9 + "send_time" timestamp NOT NULL COMMENT '发送时间',
  10 + "creator" varchar(64) DEFAULT '',
  11 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  12 + "updater" varchar(64) DEFAULT '',
  13 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  14 + "deleted" bit NOT NULL DEFAULT FALSE,
  15 + "tenant_id" bigint NOT NULL DEFAULT 0,
  16 + PRIMARY KEY ("id")
  17 +) COMMENT 'IM 私聊消息表';
... ...