gaoluyang
2026-06-29 27cd042df9aca0383a49f3514bc21958dd890912
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
// ==================== WebSocket 帧 / 事件 ====================
 
// 后端 WebSocket 统一帧结构:{ type, content }
export interface WebSocketFrame {
  type: string // 帧类型,对齐 ImWebSocketMessageType
  content: string // 帧内容(JSON 字符串)
}
 
// IM WebSocket 通知 DTO(对齐后端 ImNotificationWebSocketDTO)
export interface ImNotificationWebSocketDTO {
  conversationType: number // 会话类型
  contentType: number // 内容类型
  payload: Record<string, any> // 负载数据
}
 
// 无会话在线通知(对齐后端 conversationType = NONE 的独立 payload)
export interface ImNoConversationNotification {
  type: number // 内容类型
  [key: string]: any
}
 
// 私聊消息 DTO(对齐后端 ImPrivateMessageNotification)
export interface ImPrivateMessageNotification {
  id: number // 消息编号
  clientMessageId: string // 客户端消息编号
  senderId: number // 发送人编号
  receiverId: number // 接收人编号
  type: number // 内容类型
  content: string // 消息内容
  status: number // 消息状态
  receiptStatus?: number // 回执状态(不需要 / 待完成 / 已完成)
  sendTime: string // 发送时间
}
 
// 群聊消息 DTO(对齐后端 ImGroupMessageNotification)
export interface ImGroupMessageNotification {
  id: number // 消息编号
  clientMessageId: string // 客户端消息编号
  senderId: number // 发送人编号
  groupId: number // 群编号
  type: number // 内容类型
  content: string // 消息内容
  status: number // 消息状态
  sendTime: string // 发送时间
  atUserIds?: number[] // 群 @ 目标用户列表
  receiverUserIds?: number[] // 群定向接收用户列表
  readCount?: number // 群回执已读人数(type = RECEIPT 时使用)
  receiptStatus?: number // 群回执状态(type = RECEIPT 时使用)
  readId?: number // 已读位置
}
 
// 消息已读同步通知(对齐后端 ImMessageReadNotification)
export interface ImMessageReadNotification {
  id: number // 已读位置
  type: number // 内容类型
  senderId?: number // 发送人编号
  receiverId?: number // 私聊接收人编号
  groupId?: number // 群编号
  channelId?: number // 频道编号
  readId?: number // 已读位置
}
 
// 消息回执通知(对齐后端 ImMessageReceiptNotification)
export interface ImMessageReceiptNotification {
  id: number // 消息编号
  type: number // 内容类型
  senderId?: number // 已读方用户编号
  receiverId?: number // 私聊接收人编号
  groupId?: number // 群编号
  readCount?: number // 群回执已读人数
  receiptStatus?: number // 群回执状态
}
 
// ==================== 本地会话 / 消息结构 ====================
 
/** 引用消息 */
export interface QuoteMessage {
  messageId: number // 引用消息编号
  senderId: number // 引用消息发送人编号
  type: number // 引用内容类型
  content: string // 引用消息内容
}
 
// 会话数据结构(前端自有结构,后端无对应实体)
export interface Conversation {
  // ========== 核心标识 ==========
  targetId: number // 会话目标编号:私聊=对方 userId;群聊=groupId
  type: number // 会话类型,对齐 ImConversationType
 
  // ========== 展示字段 ==========
  name: string // 展示名称(私聊=好友昵称;群聊=群名)
  avatar: string // 头像
  unreadCount: number // 未读数
 
  // ========== 最后一条消息事实索引 ==========
  lastContent: string // 会话列表展示的最后一条消息摘要
  lastSendTime: number // 最后一条消息时间,用于排序
  lastSenderId?: number // 发送人编号
  lastMessageType?: number // 内容类型,对齐 ImContentType
  lastMessageId?: number // 最后一条服务端消息编号
  lastClientMessageId?: string // 最后一条客户端消息编号
  lastMessageStatus?: number // 最后一条消息状态
  lastReceiptStatus?: number // 最后一条群回执状态
  lastSelfSend?: boolean // 是否自己发的
  lastSenderDisplayName?: string // 发送人显示名快照——仅作 utils/user.getSenderDisplayName 实时算不出真名时的 fallback
 
  // ========== UI 状态 ==========
  deleted?: boolean // 是否已删除(软删标记,持久化时过滤)
  top?: boolean // 是否置顶(排序时优先)
  silent?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
  atMe?: boolean // 群聊:是否有人 @我
  atAll?: boolean // 群聊:是否有人 @全体成员
  reportedReadMessageId?: number // 已上报到服务端的最大已读消息编号
  draft?: {
    html: string // 输入框 HTML
    plain: string // 输入框纯文本
    reply?: QuoteMessage // 引用消息
  } // 输入框草稿
}
 
// 消息数据结构
export interface Message {
  // ========== 后端字段(对齐 ImPrivateMessageNotification / ImGroupMessageNotification) ==========
  id?: number // 服务端消息编号,发送中为空
  clientMessageId: string // 客户端消息编号,本地生成用于合并去重
  type: number // 内容类型,对齐 ImContentType
  content: string // 消息内容,JSON 字符串
  status: number // 消息状态,对齐 ImMessageStatus
  sendTime: number // 发送时间(前端转毫秒时间戳;后端为 LocalDateTime 字符串)
  senderId: number // 发送人编号
  atUserIds?: number[] // 群 @ 目标用户列表
  receiverUserIds?: number[] // 群定向接收用户列表
  receiptStatus?: number // 回执状态,对齐 ImMessageReceiptStatus(私聊 / 群 / 频道通用)
  readCount?: number // 群回执已读人数(仅群消息)
  materialId?: number // 关联频道素材编号(仅频道消息 type=MATERIAL)
 
  // ========== 前端扩展字段 ==========
  // 发送人显示名一律渲染时实时算:utils/user.getSenderDisplayName / getSenderRealNickname
  // 不在 Message 上存任何名字快照,避免备注 / 群昵称变更后历史消息显示陈旧
  targetId: number // 会话目标编号(私聊=对端 userId / 群聊=groupId),与 Conversation.targetId 一致
  selfSend: boolean // 是否自己发送(前端按 senderId 计算)
  uploadProgress?: number // 媒体消息上传进度(0-100);status=SENDING 期间持续更新;ack 后置 undefined
  // 媒体消息内存中保留的原始 File;下划线前缀表示不进 JSON / 不持久化(IDB 恢复后必为 undefined)
  // 失败重试时按它重走上传;页面刷新后该字段丢失,恢复阶段直接 drop 整条消息
  _localFile?: File
  _ackMerging?: boolean // ack 合并中标记,不持久化
}
 
// ==================== IndexedDB 本地存储结构 ====================
 
/** 会话 IndexedDB 存储结构 */
export interface ConversationDO extends Conversation {
  clientConversationId: string // `${type}:${targetId}`
}
 
export interface ConversationRead {
  conversationType: number // 会话类型,对齐 ImConversationType
  targetId: number // 会话目标编号
  messageId: number // 当前用户已读到的最大消息编号
  updateTime?: number // 更新时间
}
 
/** 会话读位置 IndexedDB 存储结构 */
export interface ConversationReadDO extends ConversationRead {
  clientConversationId: string // `${conversationType}:${targetId}`
}
 
/** 消息 IndexedDB 存储结构 */
export interface MessageDO extends Omit<Message, '_ackMerging' | '_localFile' | 'uploadProgress'> {
  messageKey: string // `${conversationType}:${id}` 或 `client:${clientMessageId}`
  conversationType: number // 会话类型,对齐 ImConversationType
  clientConversationId: string // ConversationDO.clientConversationId
}
 
/** 设置 IndexedDB 存储结构 */
export interface SettingDO<T = unknown> {
  key: string
  value: T
  updateTime?: number
}
 
// ==================== 群 / 群成员 ====================
 
// 群实体(前端内部结构)
export interface Group {
  // ========== 后端字段(对齐 ImGroupApi.GroupRespVO) ==========
  id: number // 群编号
  name: string // 群名称
  avatar?: string // 群头像
  notice?: string // 群公告
  ownerUserId?: number // 群主用户编号
  pinnedMessages?: Message[] // 群置顶消息列表
  mutedAll?: boolean // 是否全群禁言
  banned?: boolean // 是否被管理员封禁
  joinApproval?: boolean // 进群是否需群主 / 管理员审批
  joinStatus?: number // 当前登录用户在该群的成员状态(参见 CommonStatusEnum:0 在群 / 1 已退群);历史退群群仍返回,供展示历史消息的群名 / 头像
 
  // ========== 前端扩展字段(user-per-group 维度) ==========
  silent?: boolean // 是否免打扰。从当前用户的 GroupMember 回填
  groupRemark?: string // 群备注。从当前用户的 GroupMember 回填(当前用户对该群的自定义名)
  members?: GroupMember[] // 群成员缓存(按需懒加载)
  infoLoaded?: boolean // 群详情是否已加载,本轮会话内存标记,不持久化
  activeCallLoaded?: boolean // 群活跃通话是否已探测,本轮会话内存标记,不持久化
  activeCallExpired?: boolean // 群活跃通话探测是否已过期
  membersLoaded?: boolean // members 是否"完整加载"——只有整群 loadGroupMemberList / fetchGroupMemberList 命中时为 true;fetchGroupMember 单成员补齐不置位,避免 fetchGroupMemberList(force=false) 命中缓存时误判整群已加载
  membersExpired?: boolean // 群成员缓存是否已过期;重连 / 重新进入 IM 后只标记不删除,下次进入群会话再刷新
  memberCount?: number // 成员总数
}
 
/** 群 IndexedDB 存储结构 */
export type GroupDO = Omit<
  Group,
  'activeCallExpired' | 'activeCallLoaded' | 'infoLoaded' | 'members' | 'membersExpired' | 'membersLoaded'
>
 
// 群成员实体(前端内部结构)
export interface GroupMember {
  // ========== 后端字段(对齐 ImGroupMemberApi.GroupMemberRespVO) ==========
  id?: number // 群成员关系记录编号
  groupId: number // 群编号
  userId: number // 用户编号
  avatar?: string // 头像
  nickname: string // 用户昵称
  displayUserName?: string // 该成员在群内自定义昵称(每个 member 一份;不与 nickname 合并,由消费方按需取舍)
  status?: number // 在群 / 退群状态,对齐 CommonStatusEnum
  role?: number // 成员角色,参见 ImGroupMemberRole 枚举:1=群主 2=管理员 3=普通成员
  muteEndTime?: string // 禁言到期时间(ISO 字符串)
 
  // ========== 前端扩展字段 ==========
  isOwner?: boolean // 是否群主(前端从 Group.ownerUserId 计算)
}
 
// ==================== 好友 ====================
 
// 好友实体(前端内部结构)
export interface Friend {
  // ========== 后端字段(对齐 ImFriendApi.FriendRespVO) ==========
  id?: number // 好友关系记录编号(本地乐观新增时可能暂缺)
  friendUserId: number // 好友用户编号(与 Conversation.targetId 对齐)
  nickname: string // 好友昵称(对方真实昵称,永远不被备注覆盖;UI 显示走 displayName || nickname)
  nicknamePinyin?: string // 昵称的拼音(后端用 Pinyin4j 算好回填,小写无空格)
  avatar?: string // 好友头像
  silent?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
  displayName?: string // 好友展示备注:仅自己可见的别名(单字段不歧义,不带 Friend 前缀)
  displayNamePinyin?: string // 备注的拼音(后端用 Pinyin4j 算好回填,小写无空格)
  status?: number // 好友状态,对齐 CommonStatusEnum(DISABLE = 已删除,软删保留记录)
  addSource?: number // 添加来源;参见 ImFriendAddSourceEnum
  pinned?: boolean // 是否置顶联系人
  blocked?: boolean // 是否拉黑(仅自己可见,单边屏蔽对方私聊消息)
  addTime?: number // 添加好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 convertFriend 转换)
  deleteTime?: number // 删除好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 convertFriend 转换)
}
 
/**
 * 好友申请记录(前端内部结构,对齐后端 ImFriendRequestApi.FriendRequestRespVO)
 */
export interface FriendRequest {
  // ========== 后端字段(对齐 ImFriendRequestApi.FriendRequestRespVO) ==========
  id: number // 申请编号
  fromUserId: number // 发起方用户编号
  toUserId: number // 接收方用户编号
  handleResult: number // 处理结果:0=未处理;1=同意;2=拒绝
  applyContent?: string // 申请理由(发起方填写)
  handleContent?: string // 处理理由(接收方拒绝时可选填)
  addSource?: number // 添加来源;参见 ImFriendAddSourceEnum
  handleTime?: number // 处理时间(毫秒时间戳)
  createTime: number // 申请创建时间(毫秒时间戳)
 
  // ========== 聚合字段(自 AdminUser,仅展示用) ==========
  fromNickname?: string // 发起方昵称
  fromAvatar?: string // 发起方头像
  toNickname?: string // 接收方昵称
  toAvatar?: string // 接收方头像
}
 
// ==================== 用户名片 ====================
 
// 用户精简信息(对齐后端 UserSimpleRespVO,名片 / 头像 hover 等场景共用)
export interface User {
  id: number
  nickname?: string
  avatar?: string
  sex?: number
  deptId?: number
  deptName?: string
}
 
// ==================== 列表行展示用 Lite 类型 ====================
 
/**
 * 好友列表行:从 Friend 派生的展示快照
 * - id 用 friendUserId(与列表 click / 选中比对一致),不是 Friend.id(关系记录主键)
 * - 软删(status === DISABLE)由上游 friendStore.getActiveFriendList / getActiveFriendLiteList 统一过滤掉
 */
export interface FriendLite {
  id: number
  nickname: string
  nicknamePinyin?: string // 昵称拼音(用于字母分桶 / 拼音搜索)
  avatar?: string
  displayName?: string
  displayNamePinyin?: string // 备注拼音(优先于 nicknamePinyin 参与分桶)
}
 
/**
 * 群列表行:从 Group 派生的展示快照
 * - showGroupName / showImage:调用方决定带不带备注(如个人备注群名);展示按 show* > 原值兜底
 * - showImageThumb:高频列表用缩略图,避免拉原图阻塞滚动
 */
export interface GroupLite {
  id: number
  name?: string
  showGroupName?: string
  showImage?: string
  showImageThumb?: string
  memberCount?: number
  ownerId?: number
  joinApproval?: boolean // 进群是否需群主 / 管理员审批
}