🐛 fix(im): 群聊离线拉取看不到撤回提示,pull 路径接入 recallMessage
pullByType 之前对 RECALL 信号一律 skip、只靠原消息 status=RECALL 走 OR 兜底渲染。 当 pull 的 minId 卡在原消息处、回拉只返回信号时,本地缓存里的老消息没人翻成 RECALL,会一直停在原态——配合后端群聊 mapper 过滤掉 status=RECALL 的原消息,群聊 离线撤回完全不可见。 改成 pull / WS 走同一套 dispatch: - pullByType 信号转 conversationStore.recallMessage(),跟 WS 路径一致 - recallMessage 把 parseRecallMessageId 收敛进内部,第 3 个参数从 messageId: number 改成 recallSignalContent: string,4 个调用点都缩成一行 - MessageItem.isRecall 只判 type=RECALL,去掉 status=RECALL OR 分支 (conversationStore 里跳未读 / 跳已读那两处对 status 的判断是业务逻辑保留)im
parent
66514fc597
commit
a35698fc07
|
|
@ -0,0 +1,209 @@
|
|||
import { watch } from 'vue'
|
||||
import { useConversationStore } from '../store/conversationStore'
|
||||
import { useImWebSocketStore } from '../store/websocketStore'
|
||||
import {
|
||||
pullPrivateMessages as apiPullPrivateMessages,
|
||||
type ImPrivateMessageRespVO
|
||||
} from '@/api/im/message/private'
|
||||
import {
|
||||
pullGroupMessages as apiPullGroupMessages,
|
||||
type ImGroupMessageRespVO
|
||||
} from '@/api/im/message/group'
|
||||
import {
|
||||
ImConversationType,
|
||||
ImMessageType,
|
||||
PRIVATE_MESSAGE_PULL_SIZE,
|
||||
GROUP_MESSAGE_PULL_SIZE
|
||||
} from '../../utils/constants'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import type { Message } from '../types'
|
||||
|
||||
/**
|
||||
* 消息增量拉取:登录后分页拉取离线期间的新消息
|
||||
*
|
||||
* 设计要点:
|
||||
* 1. 同时拉取私聊 + 群聊,使用各自的 `minId` 游标(privateMessageMaxId / groupMessageMaxId)
|
||||
* 2. 后端一次最多返回 size 条;前端按 minId 持续翻页,直到接口返回空列表为止
|
||||
* 3. 拉取期间 conversationStore.loading=true:
|
||||
* - conversationStore 跳过 localStorage 持久化,避免频繁写入卡顿
|
||||
* - websocketStore 把新来的 WS 普通消息丢进缓冲区,等循环结束后统一回放
|
||||
* 4. WebSocket 重连后会再触发一次拉取,补齐断网期间错过的消息
|
||||
*/
|
||||
export const useMessagePuller = () => {
|
||||
const conversationStore = useConversationStore()
|
||||
const wsStore = useImWebSocketStore()
|
||||
const userStore = useUserStore()
|
||||
const currentUserId = Number(userStore.getUser?.id) || 0
|
||||
|
||||
/** 服务端私聊消息 -> 本地 Message */
|
||||
const convertPrivateMessage = (message: ImPrivateMessageRespVO): Message => {
|
||||
return {
|
||||
id: message.id,
|
||||
clientMessageId: message.clientMessageId || '',
|
||||
type: message.type,
|
||||
content: message.content,
|
||||
status: message.status,
|
||||
sendTime: new Date(message.sendTime).getTime(),
|
||||
senderId: message.senderId,
|
||||
senderNickName: '',
|
||||
targetId: message.receiverId,
|
||||
selfSend: message.senderId === currentUserId
|
||||
}
|
||||
}
|
||||
|
||||
/** 服务端群聊消息 -> 本地 Message */
|
||||
const convertGroupMessage = (message: ImGroupMessageRespVO): Message => {
|
||||
return {
|
||||
id: message.id,
|
||||
clientMessageId: message.clientMessageId || '',
|
||||
type: message.type,
|
||||
content: message.content,
|
||||
status: message.status,
|
||||
sendTime: new Date(message.sendTime).getTime(),
|
||||
senderId: message.senderId,
|
||||
senderNickName: '',
|
||||
targetId: message.groupId,
|
||||
selfSend: message.senderId === currentUserId,
|
||||
atUserIds: message.atUserIds || [],
|
||||
receiverUserIds: message.receiverUserIds || [],
|
||||
receiptStatus: message.receiptStatus,
|
||||
readCount: message.readCount
|
||||
}
|
||||
}
|
||||
|
||||
/** 私聊:会话归属到对端 userId */
|
||||
const convertPrivateConversation = (message: ImPrivateMessageRespVO) => {
|
||||
const targetId = message.senderId === currentUserId ? message.receiverId : message.senderId
|
||||
return {
|
||||
type: ImConversationType.PRIVATE,
|
||||
targetId,
|
||||
name: String(targetId),
|
||||
avatar: ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 群聊:会话归属到 groupId */
|
||||
const convertGroupConversation = (message: ImGroupMessageRespVO) => {
|
||||
return {
|
||||
type: ImConversationType.GROUP,
|
||||
targetId: message.groupId,
|
||||
name: String(message.groupId),
|
||||
avatar: ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 循环拉取指定会话类型的消息:以列表最后一条 id 作为下次 minId,直到接口返回空列表 */
|
||||
const pullByType = async (conversationType: number, startMinId: number) => {
|
||||
// 私聊 / 群聊各自一套接口和分页大小,按 isPrivate 在循环内分支调度
|
||||
let minId = startMinId || 0
|
||||
const isPrivate = conversationType === ImConversationType.PRIVATE
|
||||
const size = isPrivate ? PRIVATE_MESSAGE_PULL_SIZE : GROUP_MESSAGE_PULL_SIZE
|
||||
while (true) {
|
||||
const list = isPrivate
|
||||
? await apiPullPrivateMessages({ minId, size })
|
||||
: await apiPullGroupMessages({ minId, size })
|
||||
if (!list || list.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
// 逐条 dispatch:原消息走 insertMessage;RECALL 信号走 recallMessage 把同批内已 insert 的原消息翻成撤回提示。
|
||||
// 后端按 id 升序返回,且信号 id 一定 > 原消息 id(先翻 status 再插信号),所以原消息一定先到、recallMessage 找得到
|
||||
for (const raw of list) {
|
||||
if (isPrivate) {
|
||||
const message = raw as ImPrivateMessageRespVO
|
||||
if (message.type === ImMessageType.RECALL) {
|
||||
conversationStore.recallMessage(
|
||||
ImConversationType.PRIVATE,
|
||||
message.senderId === currentUserId ? message.receiverId : message.senderId,
|
||||
message.content,
|
||||
'',
|
||||
message.senderId === currentUserId
|
||||
)
|
||||
continue
|
||||
}
|
||||
conversationStore.insertMessage(
|
||||
convertPrivateConversation(message),
|
||||
convertPrivateMessage(message)
|
||||
)
|
||||
} else {
|
||||
const message = raw as ImGroupMessageRespVO
|
||||
if (message.type === ImMessageType.RECALL) {
|
||||
conversationStore.recallMessage(
|
||||
ImConversationType.GROUP,
|
||||
message.groupId,
|
||||
message.content,
|
||||
'',
|
||||
message.senderId === currentUserId
|
||||
)
|
||||
continue
|
||||
}
|
||||
conversationStore.insertMessage(
|
||||
convertGroupConversation(message),
|
||||
convertGroupMessage(message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 游标推进到本批最后一条 id,下一轮从此处续翻
|
||||
const lastId = list[list.length - 1].id
|
||||
if (lastId != null) {
|
||||
minId = lastId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 同一时刻只允许一次 pull:Index.vue 的手动调用与重连 watch 触发可能并发,共用同一个 promise 即可去重 */
|
||||
let pullPromise: Promise<void> | null = null
|
||||
|
||||
/** 执行一次全量增量拉取(重入安全:进行中再次调用复用同一个 promise) */
|
||||
const pullOnce = (): Promise<void> => {
|
||||
if (!currentUserId) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (pullPromise) {
|
||||
return pullPromise
|
||||
}
|
||||
pullPromise = (async () => {
|
||||
conversationStore.loading = true
|
||||
try {
|
||||
// 并发拉取私聊 + 群聊,降低初始加载耗时
|
||||
await Promise.all([
|
||||
pullByType(ImConversationType.PRIVATE, conversationStore.privateMessageMaxId),
|
||||
pullByType(ImConversationType.GROUP, conversationStore.groupMessageMaxId)
|
||||
])
|
||||
|
||||
// 回放 WebSocket 在 loading 期间收到的缓冲消息
|
||||
const buffered = wsStore.flushBuffer()
|
||||
for (const item of buffered) {
|
||||
if (item.conversationType === ImConversationType.PRIVATE) {
|
||||
wsStore.handlePrivateMessage(item.payload)
|
||||
} else {
|
||||
wsStore.handleGroupMessage(item.payload)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[IM] 拉取离线消息失败:', e)
|
||||
} finally {
|
||||
conversationStore.loading = false
|
||||
conversationStore.sortConversations()
|
||||
pullPromise = null
|
||||
}
|
||||
})()
|
||||
return pullPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* 断网期间 WS 收不到推送,期间产生的消息只能靠拉取接口按 minId 游标补齐;
|
||||
* 首次连接由 Index.vue 显式调 pullOnce,这里订阅 isConnected 的 false→true 转换,覆盖后续每次重连
|
||||
*/
|
||||
watch(
|
||||
() => wsStore.isConnected,
|
||||
(isConnected) => {
|
||||
if (isConnected) {
|
||||
void pullOnce()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return { pullOnce }
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
buildRecallTip,
|
||||
generateClientMessageId,
|
||||
parseMessage,
|
||||
parseRecallMessageId,
|
||||
type TextMessage
|
||||
} from '../../utils/message'
|
||||
import type { Conversation, ConversationStoreMeta, Message } from '../types'
|
||||
|
|
@ -475,17 +476,18 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
this.saveConversations(conversation)
|
||||
},
|
||||
|
||||
/**
|
||||
* 撤回消息:将原消息 type 改为 RECALL,并刷新会话摘要
|
||||
* 对应后端 RECALL 事件:按原 messageId 更新
|
||||
*/
|
||||
/** 撤回消息:解析撤回信号 content(`{"messageId": xxx}`),找到原消息翻成 RECALL 态 + 刷新会话摘要 */
|
||||
recallMessage(
|
||||
conversationType: number,
|
||||
targetId: number,
|
||||
messageId: number,
|
||||
recallSignalContent: string,
|
||||
senderNickName: string,
|
||||
selfSend: boolean
|
||||
) {
|
||||
const messageId = parseRecallMessageId(recallSignalContent)
|
||||
if (messageId <= 0) {
|
||||
return
|
||||
}
|
||||
const conversation = this.getConversation(conversationType, targetId)
|
||||
if (!conversation) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
return
|
||||
}
|
||||
const list = await apiGetMyFriendList()
|
||||
this.friends = (list || []).map(toFriend)
|
||||
this.friends = (list || []).map(convertFriend)
|
||||
this.loaded = true
|
||||
// 同步 conversationStore 私聊会话的展示名 / 头像 / 免打扰
|
||||
const conversationStore = useConversationStore()
|
||||
|
|
@ -72,7 +72,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
if (!data) {
|
||||
return
|
||||
}
|
||||
this.upsertFriend(toFriend(data))
|
||||
this.upsertFriend(convertFriend(data))
|
||||
} catch (e) {
|
||||
console.warn('[IM friendStore] loadFriendInfo 失败', e)
|
||||
}
|
||||
|
|
@ -154,7 +154,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
}
|
||||
})
|
||||
|
||||
function toFriend(vo: ImFriendRespVO): Friend {
|
||||
function convertFriend(vo: ImFriendRespVO): Friend {
|
||||
return {
|
||||
id: vo.id,
|
||||
friendUserId: vo.friendUserId,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
}
|
||||
// 拉取当前登录用户加入的所有群(不带成员;成员按需再走 loadGroupMembers)
|
||||
const list = await apiGetMyGroupList()
|
||||
this.groups = (list || []).map(toGroup)
|
||||
this.groups = (list || []).map(convertGroup)
|
||||
this.loaded = true
|
||||
const conversationStore = useConversationStore()
|
||||
for (const g of this.groups) {
|
||||
|
|
@ -63,7 +63,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
if (!data) {
|
||||
return
|
||||
}
|
||||
this.upsertGroup(toGroup(data))
|
||||
this.upsertGroup(convertGroup(data))
|
||||
} catch (e) {
|
||||
console.warn('[IM groupStore] loadGroupInfo 失败', e)
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
|
||||
// 拉取该群所有成员(聚合自 AdminUser,含 nickname / avatar / displayUserName)
|
||||
const list = await apiGetGroupMemberList(groupId)
|
||||
const members = (list || []).map((member) => toGroupMember(member, groupId))
|
||||
const members = (list || []).map((member) => convertGroupMember(member, groupId))
|
||||
// 成员列表可能在群列表之前触发,此时需要占位一个 group
|
||||
if (!group) {
|
||||
this.upsertGroup({
|
||||
|
|
@ -138,7 +138,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
}
|
||||
})
|
||||
|
||||
function toGroup(vo: ImGroupRespVO): Group {
|
||||
function convertGroup(vo: ImGroupRespVO): Group {
|
||||
return {
|
||||
id: vo.id,
|
||||
name: vo.name,
|
||||
|
|
@ -148,7 +148,7 @@ function toGroup(vo: ImGroupRespVO): Group {
|
|||
}
|
||||
}
|
||||
|
||||
function toGroupMember(member: ImGroupMemberRespVO, groupId: number): GroupMember {
|
||||
function convertGroupMember(member: ImGroupMemberRespVO, groupId: number): GroupMember {
|
||||
return {
|
||||
id: member.id,
|
||||
userId: member.userId,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { getRefreshToken } from '@/utils/auth'
|
|||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
import { ImWebSocketMessageType, ImMessageType, ImConversationType } from '../../utils/constants'
|
||||
import { parseRecallMessageId, playAudioTip } from '../../utils/message'
|
||||
import { playAudioTip } from '../../utils/message'
|
||||
import { useConversationStore } from './conversationStore'
|
||||
import { useFriendStore } from './friendStore'
|
||||
import { useGroupStore } from './groupStore'
|
||||
|
|
@ -17,6 +17,44 @@ import type {
|
|||
Message
|
||||
} from '../types'
|
||||
|
||||
/** WebSocket 私聊 DTO -> 前端 Message:sendTime 转毫秒;senderNickName 由调用方按好友信息补 */
|
||||
const convertPrivateMessage = (
|
||||
websocketMessage: ImPrivateMessageDTO,
|
||||
currentUserId: number,
|
||||
senderNickName: string
|
||||
): Message => ({
|
||||
id: websocketMessage.id,
|
||||
clientMessageId: websocketMessage.clientMessageId,
|
||||
type: websocketMessage.type,
|
||||
content: websocketMessage.content,
|
||||
status: websocketMessage.status,
|
||||
sendTime: new Date(websocketMessage.sendTime).getTime(),
|
||||
senderId: websocketMessage.senderId,
|
||||
senderNickName,
|
||||
targetId: websocketMessage.receiverId,
|
||||
selfSend: websocketMessage.senderId === currentUserId
|
||||
})
|
||||
|
||||
/** WebSocket 群聊 DTO -> 前端 Message:群消息额外带 atUserIds / receiverUserIds,给 @ 标记和回执用 */
|
||||
const convertGroupMessage = (
|
||||
websocketMessage: ImGroupMessageDTO,
|
||||
currentUserId: number,
|
||||
senderNickName: string
|
||||
): Message => ({
|
||||
id: websocketMessage.id,
|
||||
clientMessageId: websocketMessage.clientMessageId,
|
||||
type: websocketMessage.type,
|
||||
content: websocketMessage.content,
|
||||
status: websocketMessage.status,
|
||||
sendTime: new Date(websocketMessage.sendTime).getTime(),
|
||||
senderId: websocketMessage.senderId,
|
||||
senderNickName,
|
||||
targetId: websocketMessage.groupId,
|
||||
selfSend: websocketMessage.senderId === currentUserId,
|
||||
atUserIds: websocketMessage.atUserIds || [],
|
||||
receiverUserIds: websocketMessage.receiverUserIds || []
|
||||
})
|
||||
|
||||
/**
|
||||
* IM WebSocket Store
|
||||
*
|
||||
|
|
@ -38,8 +76,8 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
reconnectTimer: null as ReturnType<typeof setTimeout> | null,
|
||||
heartbeatTimer: null as ReturnType<typeof setInterval> | null,
|
||||
messageBuffer: [] as Array<
|
||||
| { kind: 'private'; payload: ImPrivateMessageDTO }
|
||||
| { kind: 'group'; payload: ImGroupMessageDTO }
|
||||
| { conversationType: typeof ImConversationType.PRIVATE; payload: ImPrivateMessageDTO }
|
||||
| { conversationType: typeof ImConversationType.GROUP; payload: ImGroupMessageDTO }
|
||||
> // 初始化加载期内,先把普通消息丢进缓冲区,pull 完成后再一次性回放
|
||||
}),
|
||||
|
||||
|
|
@ -228,7 +266,10 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
const conversationStore = useConversationStore()
|
||||
// 1. 离线加载期间先缓冲,等 pull 完成后再统一回放,避免重复或顺序错乱
|
||||
if (conversationStore.loading) {
|
||||
this.messageBuffer.push({ kind: 'private', payload: websocketMessage })
|
||||
this.messageBuffer.push({
|
||||
conversationType: ImConversationType.PRIVATE,
|
||||
payload: websocketMessage
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -247,32 +288,18 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
// 3. 后端撤回:下发一条 RECALL 消息,content 为 `{"messageId": xxx}`(对齐 ImMessageTypeEnum.RECALL → RecallMessage)
|
||||
// 这里拦截下来改走 recallMessage(把原消息翻转为 RECALL 态),不让它作为新消息进列表
|
||||
if (websocketMessage.type === ImMessageType.RECALL) {
|
||||
const recallMessageId = parseRecallMessageId(websocketMessage.content)
|
||||
if (recallMessageId) {
|
||||
conversationStore.recallMessage(
|
||||
ImConversationType.PRIVATE,
|
||||
peerId,
|
||||
recallMessageId,
|
||||
friend?.nickname || '',
|
||||
selfSend
|
||||
)
|
||||
return
|
||||
}
|
||||
conversationStore.recallMessage(
|
||||
ImConversationType.PRIVATE,
|
||||
peerId,
|
||||
websocketMessage.content,
|
||||
friend?.nickname || '',
|
||||
selfSend
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 后端 DTO → 前端 Message:sendTime 转毫秒;selfSend / senderNickName 是前端补的
|
||||
const message: Message = {
|
||||
id: websocketMessage.id,
|
||||
clientMessageId: websocketMessage.clientMessageId,
|
||||
type: websocketMessage.type,
|
||||
content: websocketMessage.content,
|
||||
status: websocketMessage.status,
|
||||
sendTime: new Date(websocketMessage.sendTime).getTime(),
|
||||
senderId: websocketMessage.senderId,
|
||||
senderNickName: friend?.nickname || '',
|
||||
targetId: websocketMessage.receiverId,
|
||||
selfSend
|
||||
}
|
||||
// 4. 后端 DTO → 前端 Message
|
||||
const message = convertPrivateMessage(websocketMessage, currentUserId, friend?.nickname || '')
|
||||
conversationStore.insertMessage(
|
||||
{
|
||||
type: ImConversationType.PRIVATE,
|
||||
|
|
@ -339,7 +366,10 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
const conversationStore = useConversationStore()
|
||||
// 1. 离线加载期缓冲(与私聊对称)
|
||||
if (conversationStore.loading) {
|
||||
this.messageBuffer.push({ kind: 'group', payload: websocketMessage })
|
||||
this.messageBuffer.push({
|
||||
conversationType: ImConversationType.GROUP,
|
||||
payload: websocketMessage
|
||||
})
|
||||
return
|
||||
}
|
||||
const userStore = useUserStore()
|
||||
|
|
@ -359,34 +389,18 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
// 3. 后端撤回:下发一条 RECALL 消息,content 为 `{"messageId": xxx}`
|
||||
// 这里拦截下来改走 recallMessage(把原消息翻转为 RECALL 态)
|
||||
if (websocketMessage.type === ImMessageType.RECALL) {
|
||||
const recallMessageId = parseRecallMessageId(websocketMessage.content)
|
||||
if (recallMessageId) {
|
||||
conversationStore.recallMessage(
|
||||
ImConversationType.GROUP,
|
||||
websocketMessage.groupId,
|
||||
recallMessageId,
|
||||
senderNickName,
|
||||
selfSend
|
||||
)
|
||||
return
|
||||
}
|
||||
conversationStore.recallMessage(
|
||||
ImConversationType.GROUP,
|
||||
websocketMessage.groupId,
|
||||
websocketMessage.content,
|
||||
senderNickName,
|
||||
selfSend
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 后端 DTO → 前端 Message:群消息额外带 atUserIds / receiverUserIds,给 @ 标记和回执用
|
||||
const message: Message = {
|
||||
id: websocketMessage.id,
|
||||
clientMessageId: websocketMessage.clientMessageId,
|
||||
type: websocketMessage.type,
|
||||
content: websocketMessage.content,
|
||||
status: websocketMessage.status,
|
||||
sendTime: new Date(websocketMessage.sendTime).getTime(),
|
||||
senderId: websocketMessage.senderId,
|
||||
senderNickName,
|
||||
targetId: websocketMessage.groupId,
|
||||
selfSend,
|
||||
atUserIds: websocketMessage.atUserIds || [],
|
||||
receiverUserIds: websocketMessage.receiverUserIds || []
|
||||
}
|
||||
// 4. 后端 DTO → 前端 Message
|
||||
const message = convertGroupMessage(websocketMessage, currentUserId, senderNickName)
|
||||
conversationStore.insertMessage(
|
||||
{
|
||||
type: ImConversationType.GROUP,
|
||||
|
|
|
|||
|
|
@ -141,8 +141,8 @@ export interface Friend {
|
|||
avatar?: string // 好友头像
|
||||
muted?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
|
||||
status?: number // 好友状态,对齐 CommonStatusEnum(DISABLE = 已删除/墓碑)
|
||||
addTime?: number // 添加好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 toFriend 转换)
|
||||
deleteTime?: number // 删除好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 toFriend 转换)
|
||||
addTime?: number // 添加好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 convertFriend 转换)
|
||||
deleteTime?: number // 删除好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 convertFriend 转换)
|
||||
}
|
||||
|
||||
// ==================== 用户名片 ====================
|
||||
|
|
|
|||
Loading…
Reference in New Issue