fix(im):群昵称修改改为静默同步

- 后端将 GROUP_MEMBER_NICKNAME_UPDATE 改为非持久化事件,避免写入群聊历史消息
- 保留群昵称变更的 WebSocket 在线同步,继续更新成员 displayUserName
- Vue3 聊天侧栏从当前群成员的 displayUserName 回填「我在本群的昵称」
- Vue3 WebSocket 收到 GROUP_MEMBER_NICKNAME_UPDATE 时只同步 groupStore,不再插入消息列表
- 补充后端单测,覆盖群昵称变更事件不入库但仍推送
im
YunaiV 2026-06-18 06:57:05 -07:00
parent fc7cd7bc07
commit ab2fa4e6b8
3 changed files with 36 additions and 2 deletions

View File

@ -259,6 +259,7 @@ import { createCall } from '@/api/im/rtc'
import { ImRtcCallMediaType, ImRtcCallStatus, ImConversationType } from '@/views/im/utils/constants' import { ImRtcCallMediaType, ImRtcCallStatus, ImConversationType } from '@/views/im/utils/constants'
import { resolveCallEndReasonText } from '@/views/im/utils/message' import { resolveCallEndReasonText } from '@/views/im/utils/message'
import { getClientConversationId } from '@/views/im/utils/db' import { getClientConversationId } from '@/views/im/utils/db'
import { getCurrentUserId } from '@/utils/auth'
import { useRtcStore } from '../../../../store/rtcStore' import { useRtcStore } from '../../../../store/rtcStore'
import { useMessageStore } from '../../../../store/messageStore' import { useMessageStore } from '../../../../store/messageStore'
@ -403,12 +404,14 @@ const groupInfo = computed<
return undefined return undefined
} }
const group = groupStore.getGroup(conversation.targetId) const group = groupStore.getGroup(conversation.targetId)
const selfMember = group?.members?.find((member) => member.userId === getCurrentUserId())
return { return {
id: conversation.targetId, id: conversation.targetId,
name: group?.name || conversation.name, name: group?.name || conversation.name,
showGroupName: group?.name || conversation.name, showGroupName: group?.name || conversation.name,
showImage: group?.avatar || conversation.avatar, showImage: group?.avatar || conversation.avatar,
notice: group?.notice, notice: group?.notice,
remarkNickName: selfMember?.displayUserName,
groupRemark: group?.groupRemark, groupRemark: group?.groupRemark,
ownerId: group?.ownerUserId, ownerId: group?.ownerUserId,
memberCount: group?.memberCount, memberCount: group?.memberCount,

View File

@ -171,6 +171,7 @@ const convertGroupMessage = (
* - 已读 / 回执(READ / RECEIPT):多端已读同步、对方读后回执 * - 已读 / 回执(READ / RECEIPT):多端已读同步、对方读后回执
* - 好友变更(FRIEND_*):同步 friendStore + 级联刷新私聊会话;FRIEND_ADD / FRIEND_DELETE 额外插入会话气泡 * - 好友变更(FRIEND_*):同步 friendStore + 级联刷新私聊会话;FRIEND_ADD / FRIEND_DELETE 额外插入会话气泡
* - 群个人信号(GROUP_MEMBER_SETTING_UPDATE):同步 groupStore + 级联刷新群聊会话 * - 群个人信号(GROUP_MEMBER_SETTING_UPDATE):同步 groupStore + 级联刷新群聊会话
* - 群成员昵称变更(GROUP_MEMBER_NICKNAME_UPDATE):同步 groupStore,不插入消息列表
* - 群广播事件(GROUP_*):走 handleGroupMessage + applyGroupNotification 旁路(含 DISSOLVE / QUIT / KICK 自判清群) * - 群广播事件(GROUP_*):走 handleGroupMessage + applyGroupNotification 旁路(含 DISSOLVE / QUIT / KICK 自判清群)
*/ */
export const useImWebSocketStore = defineStore('imWebSocketStore', { export const useImWebSocketStore = defineStore('imWebSocketStore', {
@ -496,7 +497,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
/** /**
* 群聊统一帧分发:按 payload.type(ImContentType)分到已读 / 回执 / 群个人信号 / 普通消息 * 群聊统一帧分发:按 payload.type(ImContentType)分到已读 / 回执 / 群个人信号 / 普通消息
* *
* 1530 GROUP_MEMBER_SETTING_UPDATE 是个人信号;其它(普通消息 + 1501-1520 段位群广播事件)走 handleGroupMessage 入库 + 触发 applyGroupNotification 旁路 * GROUP_MEMBER_SETTING_UPDATE / GROUP_MEMBER_NICKNAME_UPDATE 是成员资料同步信号;其它群广播事件走 handleGroupMessage 入库 + 触发 applyGroupNotification 旁路
*/ */
dispatchGroupFrame(websocketMessage: ImGroupMessageNotification) { dispatchGroupFrame(websocketMessage: ImGroupMessageNotification) {
try { try {
@ -510,6 +511,9 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
case ImContentType.GROUP_MEMBER_SETTING_UPDATE: case ImContentType.GROUP_MEMBER_SETTING_UPDATE:
this.handleGroupMemberSettingUpdate(websocketMessage) this.handleGroupMemberSettingUpdate(websocketMessage)
break break
case ImContentType.GROUP_MEMBER_NICKNAME_UPDATE:
this.handleGroupMemberNicknameUpdate(websocketMessage)
break
case ImContentType.RTC_CALL_START: case ImContentType.RTC_CALL_START:
// 入库 + 渲染聊天 tip;胶囊条状态走 1602/1603,本帧不动 rtcStore,避免与首次填充竞争 // 入库 + 渲染聊天 tip;胶囊条状态走 1602/1603,本帧不动 rtcStore,避免与首次填充竞争
ignoreRealtimePersistError(this.handleGroupMessage(websocketMessage)) ignoreRealtimePersistError(this.handleGroupMessage(websocketMessage))
@ -921,6 +925,15 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
} }
}, },
/** GROUP_MEMBER_NICKNAME_UPDATE:同步成员在群里的昵称 */
handleGroupMemberNicknameUpdate(websocketMessage: ImGroupMessageNotification) {
useGroupStore().applyGroupNotification(
websocketMessage.groupId,
websocketMessage.type,
websocketMessage.content
)
},
// ==================== 心跳 / 重连 ==================== // ==================== 心跳 / 重连 ====================
/** 心跳包:纯文本 'ping',对应服务端 'pong'(后端这层用纯字符串约定,避免 JSON 解析开销) */ /** 心跳包:纯文本 'ping',对应服务端 'pong'(后端这层用纯字符串约定,避免 JSON 解析开销) */

View File

@ -202,7 +202,25 @@ function guardSession(session: number) {
/** 克隆可入库对象 */ /** 克隆可入库对象 */
function toDbValue<T>(value: T): T { function toDbValue<T>(value: T): T {
return toRaw(value) as T return cloneDbValue(value) as T
}
/** 转换为 IndexedDB 可克隆对象 */
function cloneDbValue(value: unknown): unknown {
const raw = toRaw(value)
if (Array.isArray(raw)) {
return raw.map((item) => cloneDbValue(item))
}
if (!raw || typeof raw !== 'object') {
return raw
}
const prototype = Object.getPrototypeOf(raw)
if (prototype !== Object.prototype && prototype !== null) {
return raw
}
return Object.fromEntries(
Object.entries(raw as Record<string, unknown>).map(([key, item]) => [key, cloneDbValue(item)])
)
} }
class DbClient { class DbClient {