✨ feat(im): 增加好友申请的逻辑(v1)
parent
bf79e07d5c
commit
f86cd30af4
|
|
@ -7,6 +7,9 @@ export interface ImFriendRespVO {
|
|||
muted?: boolean // 是否免打扰
|
||||
displayName?: string // 好友展示备注(仅自己可见)
|
||||
displayNamePinyin?: string // 备注的拼音(小写无空格,前端按首字母分桶 / 拼音搜索)
|
||||
addSource?: number // 添加来源;参见 ImFriendAddSourceEnum
|
||||
pinned?: boolean // 是否置顶联系人
|
||||
blocked?: boolean // 是否拉黑
|
||||
status?: number // 好友状态(0=正常,1=已删除)
|
||||
addTime?: string // 添加好友时间
|
||||
deleteTime?: string // 删除好友时间
|
||||
|
|
@ -21,6 +24,7 @@ export interface ImFriendUpdateReqVO {
|
|||
friendUserId: number // 好友的用户编号
|
||||
muted?: boolean // 是否免打扰
|
||||
displayName?: string // 好友展示备注
|
||||
pinned?: boolean // 是否置顶联系人
|
||||
}
|
||||
|
||||
// 获得当前登录用户的好友列表
|
||||
|
|
@ -33,17 +37,13 @@ export const getFriend = (friendUserId: number | string) => {
|
|||
return request.get<ImFriendRespVO>({ url: '/im/friend/get', params: { friendUserId } })
|
||||
}
|
||||
|
||||
// 添加好友(双向建立关系)
|
||||
export const addFriend = (friendUserId: number | string) => {
|
||||
return request.post<boolean>({ url: '/im/friend/add', params: { friendUserId } })
|
||||
}
|
||||
|
||||
// 删除好友(双向软删除)
|
||||
// 删除好友(单向软删除)
|
||||
export const deleteFriend = (friendUserId: number | string) => {
|
||||
return request.delete<boolean>({ url: '/im/friend/delete', params: { friendUserId } })
|
||||
}
|
||||
|
||||
// 更新好友信息
|
||||
// 更新好友信息(备注 / 免打扰 / 联系人置顶)
|
||||
export const updateFriend = (data: ImFriendUpdateReqVO) => {
|
||||
return request.put<boolean>({ url: '/im/friend/update', data })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
// TODO DONE @AI:路径迁移到 api/im/friend/request/index.ts,与 api/im/group/member 这种嵌套结构对齐
|
||||
// IM 好友申请 Response VO
|
||||
export interface ImFriendRequestRespVO {
|
||||
id: number // 申请编号
|
||||
fromUserId: number // 发起方用户编号
|
||||
toUserId: number // 接收方用户编号
|
||||
handleResult: number // 处理结果;0=未处理;1=同意;2=拒绝
|
||||
applyContent?: string // 申请理由
|
||||
handleContent?: string // 处理理由(接收方拒绝时可选填)
|
||||
addSource?: number // 添加来源;参见 ImFriendAddSourceEnum
|
||||
handleTime?: string // 处理时间
|
||||
createTime: string // 申请创建时间
|
||||
// 聚合字段(自 AdminUser)
|
||||
fromNickname?: string // 发起方昵称
|
||||
fromAvatar?: string // 发起方头像
|
||||
toNickname?: string // 接收方昵称
|
||||
toAvatar?: string // 接收方头像
|
||||
}
|
||||
|
||||
// IM 好友申请发起 Request VO
|
||||
export interface ImFriendRequestApplyReqVO {
|
||||
toUserId: number // 接收方用户编号
|
||||
applyContent?: string // 申请理由
|
||||
displayName?: string // 对接收方的备注(仅自己可见)
|
||||
addSource?: number // 添加来源
|
||||
}
|
||||
|
||||
// 发起好友申请
|
||||
export const applyFriendRequest = (data: ImFriendRequestApplyReqVO) => {
|
||||
return request.post<number | null>({ url: '/im/friend-request/apply', data })
|
||||
}
|
||||
|
||||
// 同意好友申请
|
||||
export const agreeFriendRequest = (id: number | string) => {
|
||||
return request.put<boolean>({ url: '/im/friend-request/agree', params: { id } })
|
||||
}
|
||||
|
||||
// 拒绝好友申请
|
||||
export const refuseFriendRequest = (id: number | string, handleContent?: string) => {
|
||||
return request.put<boolean>({
|
||||
url: '/im/friend-request/refuse',
|
||||
params: { id, handleContent }
|
||||
})
|
||||
}
|
||||
|
||||
// 查询「我相关」的好友申请列表(含我发起的、别人加我的)
|
||||
export const getMyFriendRequestList = () => {
|
||||
return request.get<ImFriendRequestRespVO[]>({ url: '/im/friend-request/list' })
|
||||
}
|
||||
|
|
@ -5,30 +5,61 @@ import { CommonStatusEnum } from '@/utils/constants'
|
|||
import {
|
||||
getMyFriendList as apiGetMyFriendList,
|
||||
getFriend as apiGetFriend,
|
||||
addFriend as apiAddFriend,
|
||||
deleteFriend as apiDeleteFriend,
|
||||
updateFriend as apiUpdateFriend,
|
||||
type ImFriendRespVO
|
||||
} from '@/api/im/friend'
|
||||
import {
|
||||
applyFriendRequest as apiApplyFriendRequest,
|
||||
agreeFriendRequest as apiAgreeFriendRequest,
|
||||
refuseFriendRequest as apiRefuseFriendRequest,
|
||||
getMyFriendRequestList as apiGetMyFriendRequestList,
|
||||
type ImFriendRequestApplyReqVO,
|
||||
type ImFriendRequestRespVO
|
||||
} from '@/api/im/friend/request'
|
||||
import { useConversationStore } from './conversationStore'
|
||||
import { ImConversationType } from '../../utils/constants'
|
||||
import { getCurrentUserId, imStorage, setQuietly, StorageKeys } from '../../utils/storage'
|
||||
import { getFriendDisplayName } from '../../utils/user'
|
||||
import type { Friend } from '../types'
|
||||
import type { Friend, FriendRequest } from '../types'
|
||||
|
||||
/** 好友申请处理结果(对齐后端 ImFriendRequestHandleResultEnum) */
|
||||
const FriendRequestHandleResult = {
|
||||
UNHANDLED: 0,
|
||||
AGREED: 1,
|
||||
REFUSED: 2
|
||||
} as const
|
||||
|
||||
/** 好友通知 payload(对齐后端 BaseFriendNotification + 子类裁减后的字段) */
|
||||
export interface FriendNotificationPayload {
|
||||
operatorUserId: number
|
||||
friendUserId: number
|
||||
// FRIEND_APPLICATION 系列:申请记录的核心字段(避免 payload 携带完整 DO)
|
||||
requestId?: number
|
||||
applyContent?: string
|
||||
handleContent?: string
|
||||
addSource?: number
|
||||
// FRIEND_UPDATE:单边属性变更
|
||||
displayName?: string
|
||||
muted?: boolean
|
||||
pinned?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* IM 好友 Store
|
||||
*
|
||||
* 负责:
|
||||
* - 拉取 / 缓存当前登录用户的好友列表
|
||||
* - 加好友 / 删好友(走后端 API + 本地乐观同步)
|
||||
* - 被 ConversationItem / FriendPage / MessageInput 等多处消费
|
||||
* - 拉取 / 缓存当前登录用户的好友列表 + 申请列表
|
||||
* - 申请-审批流程(apply / agree / refuse)+ 备注 / 免打扰 / 联系人置顶 / 拉黑
|
||||
* - 接收 WebSocket 1201-1210 段位通知,按事件分发到 friendStore 内部各 dispatcher
|
||||
*/
|
||||
export const useFriendStore = defineStore('imFriendStore', {
|
||||
state: () => ({
|
||||
friends: [] as Friend[],
|
||||
// 仅 fetchFriends 成功后置位;loadFriends(IDB)不置位,否则后台 SWR 刷新会被缓存命中跳过
|
||||
loaded: false
|
||||
loaded: false,
|
||||
/** 我相关的好友申请列表(含我发起的 + 别人加我的;后端按 id 倒序,前端不再分页) */
|
||||
friendRequests: [] as FriendRequest[]
|
||||
}),
|
||||
|
||||
getters: {
|
||||
|
|
@ -48,17 +79,26 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
const entry = this.getFriend(friendUserId)
|
||||
return !!entry && entry.status !== CommonStatusEnum.DISABLE
|
||||
}
|
||||
},
|
||||
/** 我的黑名单(blocked=true 且 ENABLE) */
|
||||
getBlockedFriends: (state): Friend[] => {
|
||||
return state.friends.filter(
|
||||
(f) => f.status !== CommonStatusEnum.DISABLE && f.blocked === true
|
||||
)
|
||||
},
|
||||
/** 未处理申请数(接收方=我)—— 实时派生,「新的朋友」红点用 */
|
||||
getUnhandledRequestCount: (state): number => {
|
||||
const me = Number(getCurrentUserId() || 0)
|
||||
return state.friendRequests.filter(
|
||||
(r) => r.handleResult === FriendRequestHandleResult.UNHANDLED && r.toUserId === me
|
||||
).length
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
// ==================== 本地缓存 ====================
|
||||
|
||||
/**
|
||||
* 从 IDB 恢复好友列表
|
||||
*
|
||||
* @return 返回是否命中缓存
|
||||
*/
|
||||
/** 从 IDB 恢复好友列表 */
|
||||
async loadFriends(): Promise<boolean> {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) {
|
||||
|
|
@ -121,30 +161,96 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
}
|
||||
},
|
||||
|
||||
/** 添加好友:后端双向建立关系后,本地占位插入(服务端返回后可 fetchFriends 刷新) */
|
||||
async addFriend(friendUserId: number, preview?: Partial<Friend>) {
|
||||
await apiAddFriend(friendUserId)
|
||||
if (preview) {
|
||||
this.upsertFriend({
|
||||
friendUserId,
|
||||
nickname: preview.nickname || String(friendUserId),
|
||||
avatar: preview.avatar,
|
||||
status: CommonStatusEnum.ENABLE
|
||||
})
|
||||
// ==================== 申请-审批 ====================
|
||||
|
||||
/** 发起好友申请:成功后等待对方同意(不直接落地为好友) */
|
||||
async applyFriend(reqVO: ImFriendRequestApplyReqVO): Promise<number | null> {
|
||||
return await apiApplyFriendRequest(reqVO)
|
||||
},
|
||||
|
||||
/** 同意一条好友申请;后端会双向落库 + 推 FRIEND_ADD,本端等通知到达再 upsertFriend */
|
||||
async agreeFriendRequest(requestId: number) {
|
||||
await apiAgreeFriendRequest(requestId)
|
||||
const request = this.findFriendRequest(requestId)
|
||||
if (request) {
|
||||
request.handleResult = FriendRequestHandleResult.AGREED
|
||||
request.handleTime = Date.now()
|
||||
} else {
|
||||
await this.loadFriendInfo(friendUserId)
|
||||
// 列表过期场景兜底重拉
|
||||
await this.fetchFriendRequests()
|
||||
}
|
||||
},
|
||||
|
||||
/** 删除好友(软删,保留记录但置 DISABLE;同时级联清理本地私聊会话) */
|
||||
/** 拒绝一条好友申请 */
|
||||
async refuseFriendRequest(requestId: number, handleContent?: string) {
|
||||
await apiRefuseFriendRequest(requestId, handleContent)
|
||||
const request = this.findFriendRequest(requestId)
|
||||
if (request) {
|
||||
request.handleResult = FriendRequestHandleResult.REFUSED
|
||||
request.handleContent = handleContent
|
||||
request.handleTime = Date.now()
|
||||
} else {
|
||||
await this.fetchFriendRequests()
|
||||
}
|
||||
},
|
||||
|
||||
/** 拉取「我相关」的好友申请列表(页面打开时 / 收到 FRIEND_APPLICATION 时刷新) */
|
||||
async fetchFriendRequests() {
|
||||
const list = await apiGetMyFriendRequestList()
|
||||
this.friendRequests = (list || []).map(convertFriendRequest)
|
||||
},
|
||||
|
||||
/** 按 id 查申请记录 */
|
||||
findFriendRequest(requestId: number): FriendRequest | undefined {
|
||||
return this.friendRequests.find((r) => r.id === requestId)
|
||||
},
|
||||
|
||||
// ==================== 好友关系操作 ====================
|
||||
|
||||
/** 删除好友(单向软删,本端置 DISABLE;级联清理本地私聊会话) */
|
||||
async deleteFriend(friendUserId: number) {
|
||||
await apiDeleteFriend(friendUserId)
|
||||
this.removeFriend(friendUserId)
|
||||
},
|
||||
|
||||
/** 切换免打扰 */
|
||||
async setMuted(friendUserId: number, muted: boolean) {
|
||||
await apiUpdateFriend({ friendUserId, muted })
|
||||
const friend = this.getFriend(friendUserId)
|
||||
if (friend) {
|
||||
friend.muted = muted
|
||||
this.saveFriends()
|
||||
}
|
||||
},
|
||||
|
||||
/** 切换联系人置顶 */
|
||||
async setPinned(friendUserId: number, pinned: boolean) {
|
||||
await apiUpdateFriend({ friendUserId, pinned })
|
||||
const friend = this.getFriend(friendUserId)
|
||||
if (friend) {
|
||||
friend.pinned = pinned
|
||||
this.saveFriends()
|
||||
}
|
||||
},
|
||||
|
||||
/** 修改好友展示备注(仅自己可见) */
|
||||
async setDisplayName(friendUserId: number, displayName: string) {
|
||||
const value = displayName.trim()
|
||||
// 后端 displayName 语义:null/undefined = 不改,"" = 清空,所以这里直接传 value(可能是空串)
|
||||
await apiUpdateFriend({ friendUserId, displayName: value })
|
||||
const friend = this.getFriend(friendUserId)
|
||||
if (friend) {
|
||||
friend.displayName = value
|
||||
const conversationStore = useConversationStore()
|
||||
conversationStore.updateConversation(ImConversationType.PRIVATE, friendUserId, {
|
||||
name: getFriendDisplayName(friend)
|
||||
})
|
||||
this.saveFriends()
|
||||
}
|
||||
},
|
||||
|
||||
/** 本地合并 / 新增某个好友(WebSocket 事件 & 手动刷新都用) */
|
||||
upsertFriend(friend: Friend) {
|
||||
// 按 friendUserId 查已有记录下标:>=0 命中则覆盖合并,<0 则追加
|
||||
const index = this.friends.findIndex((f) => f.friendUserId === friend.friendUserId)
|
||||
if (index >= 0) {
|
||||
this.friends[index] = {
|
||||
|
|
@ -158,7 +264,6 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
status: friend.status ?? CommonStatusEnum.ENABLE
|
||||
})
|
||||
}
|
||||
// 同步对应私聊会话的展示
|
||||
const conversationStore = useConversationStore()
|
||||
const merged = this.getFriend(friend.friendUserId)
|
||||
conversationStore.updateConversation(ImConversationType.PRIVATE, friend.friendUserId, {
|
||||
|
|
@ -169,13 +274,13 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
this.saveFriends()
|
||||
},
|
||||
|
||||
/** 本地标记删除(WebSocket FRIEND_DEL 事件触发;同时级联清私聊会话) */
|
||||
/** 本地标记删除(WebSocket FRIEND_DELETE 事件触发;同时级联清私聊会话) */
|
||||
removeFriend(friendUserId: number) {
|
||||
// 软删:保留记录但置为 DISABLE,避免后续误判"陌生人"
|
||||
const friend = this.getFriend(friendUserId)
|
||||
if (friend) {
|
||||
friend.status = CommonStatusEnum.DISABLE
|
||||
friend.deleteTime = Date.now()
|
||||
friend.blocked = false
|
||||
}
|
||||
// 级联清理:把对应的私聊会话也软删,避免会话列表里留着已删好友
|
||||
const conversationStore = useConversationStore()
|
||||
|
|
@ -183,39 +288,105 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
this.saveFriends()
|
||||
},
|
||||
|
||||
/** 切换免打扰 */
|
||||
async setMuted(friendUserId: number, muted: boolean) {
|
||||
await apiUpdateFriend({ friendUserId, muted })
|
||||
const friend = this.getFriend(friendUserId)
|
||||
// ==================== WebSocket 事件 dispatcher(1201-1210 段) ====================
|
||||
|
||||
/** FRIEND_APPLICATION(1203):收到新申请;payload 已裁减为核心字段,本地拉一次列表补齐 fromUser 等聚合字段 */
|
||||
applyFriendRequestNotification(_payload: FriendNotificationPayload) {
|
||||
this.fetchFriendRequests().catch(() => undefined)
|
||||
},
|
||||
|
||||
/** FRIEND_REQUEST_APPROVED(1201):我的申请被同意;按 requestId 更新状态(FRIEND_ADD 会另外推) */
|
||||
applyFriendRequestApprovedNotification(payload: FriendNotificationPayload) {
|
||||
const request = payload.requestId ? this.findFriendRequest(payload.requestId) : undefined
|
||||
if (request) {
|
||||
request.handleResult = FriendRequestHandleResult.AGREED
|
||||
request.handleTime = Date.now()
|
||||
} else {
|
||||
this.fetchFriendRequests().catch(() => undefined)
|
||||
}
|
||||
},
|
||||
|
||||
/** FRIEND_REQUEST_REJECTED(1202):我的申请被拒绝;按 requestId 更新状态 */
|
||||
applyFriendRequestRejectedNotification(payload: FriendNotificationPayload) {
|
||||
const request = payload.requestId ? this.findFriendRequest(payload.requestId) : undefined
|
||||
if (request) {
|
||||
request.handleResult = FriendRequestHandleResult.REFUSED
|
||||
request.handleContent = payload.handleContent
|
||||
request.handleTime = Date.now()
|
||||
} else {
|
||||
this.fetchFriendRequests().catch(() => undefined)
|
||||
}
|
||||
},
|
||||
|
||||
/** FRIEND_ADD(1204):新增好友;本端拉取好友详情并入库 */
|
||||
applyFriendAddNotification(payload: FriendNotificationPayload) {
|
||||
if (payload.friendUserId) {
|
||||
this.loadFriendInfo(payload.friendUserId).catch(() => undefined)
|
||||
}
|
||||
},
|
||||
|
||||
/** FRIEND_DELETE(1205):好友被删除;本端清理 + 级联会话 */
|
||||
applyFriendDeleteNotification(payload: FriendNotificationPayload) {
|
||||
if (payload.friendUserId) {
|
||||
this.removeFriend(payload.friendUserId)
|
||||
}
|
||||
},
|
||||
|
||||
/** FRIEND_BLOCK(1207):拉黑;多端同步 */
|
||||
applyFriendBlockNotification(payload: FriendNotificationPayload) {
|
||||
const friend = this.getFriend(payload.friendUserId)
|
||||
if (friend) {
|
||||
friend.muted = muted
|
||||
friend.blocked = true
|
||||
this.saveFriends()
|
||||
}
|
||||
},
|
||||
|
||||
/** FRIEND_UNBLOCK(1208):移出黑名单;多端同步 */
|
||||
applyFriendUnblockNotification(payload: FriendNotificationPayload) {
|
||||
const friend = this.getFriend(payload.friendUserId)
|
||||
if (friend) {
|
||||
friend.blocked = false
|
||||
this.saveFriends()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 修改好友展示备注(仅自己可见)
|
||||
*
|
||||
* 走后端 /im/friend/update 接口;保存成功后再同步本地 friend + 会话列表 name,失败直接抛给上层让 UI 决定回滚 / 提示
|
||||
* FRIEND_INFO_UPDATED(1209):好友资料变更(昵称 / 头像);重拉详情
|
||||
* TODO @AI:后端暂未实现 1209 推送;待 system 模块改昵称 / 头像时回调触发,本 dispatcher 已就绪
|
||||
*/
|
||||
async setDisplayName(friendUserId: number, displayName: string) {
|
||||
const value = displayName.trim()
|
||||
// 后端的 displayName 语义:null/undefined = 不改,"" = 清空,所以这里直接传 value(可能是空串)
|
||||
await apiUpdateFriend({ friendUserId, displayName: value })
|
||||
const friend = this.getFriend(friendUserId)
|
||||
if (friend) {
|
||||
friend.displayName = value
|
||||
const conversationStore = useConversationStore()
|
||||
conversationStore.updateConversation(ImConversationType.PRIVATE, friendUserId, {
|
||||
name: getFriendDisplayName(friend)
|
||||
})
|
||||
this.saveFriends()
|
||||
applyFriendInfoUpdatedNotification(payload: FriendNotificationPayload) {
|
||||
if (payload.friendUserId) {
|
||||
this.loadFriendInfo(payload.friendUserId).catch(() => undefined)
|
||||
}
|
||||
},
|
||||
|
||||
/** FRIEND_UPDATE(1210):批量更新(备注 / 免打扰 / 联系人置顶);多端同步 */
|
||||
applyFriendUpdateNotification(payload: FriendNotificationPayload) {
|
||||
const friend = this.getFriend(payload.friendUserId)
|
||||
if (!friend) {
|
||||
return
|
||||
}
|
||||
if (payload.displayName != null) {
|
||||
friend.displayName = payload.displayName
|
||||
}
|
||||
if (payload.muted != null) {
|
||||
friend.muted = payload.muted
|
||||
}
|
||||
if (payload.pinned != null) {
|
||||
friend.pinned = payload.pinned
|
||||
}
|
||||
const conversationStore = useConversationStore()
|
||||
conversationStore.updateConversation(ImConversationType.PRIVATE, payload.friendUserId, {
|
||||
name: getFriendDisplayName(friend),
|
||||
muted: friend.muted
|
||||
})
|
||||
this.saveFriends()
|
||||
},
|
||||
|
||||
/** 切账号时仅清 in-memory,IDB 按 userId 分桶天然隔离,回切秒开 */
|
||||
clear() {
|
||||
this.friends = []
|
||||
this.friendRequests = []
|
||||
this.loaded = false
|
||||
}
|
||||
}
|
||||
|
|
@ -231,16 +402,36 @@ function convertFriend(vo: ImFriendRespVO): Friend {
|
|||
muted: !!vo.muted,
|
||||
displayName: vo.displayName || '',
|
||||
displayNamePinyin: vo.displayNamePinyin,
|
||||
addSource: vo.addSource,
|
||||
pinned: !!vo.pinned,
|
||||
blocked: !!vo.blocked,
|
||||
status: vo.status,
|
||||
addTime: vo.addTime ? new Date(vo.addTime).getTime() : undefined,
|
||||
deleteTime: vo.deleteTime ? new Date(vo.deleteTime).getTime() : undefined
|
||||
}
|
||||
}
|
||||
|
||||
function convertFriendRequest(vo: ImFriendRequestRespVO): FriendRequest {
|
||||
return {
|
||||
id: vo.id,
|
||||
fromUserId: vo.fromUserId,
|
||||
toUserId: vo.toUserId,
|
||||
handleResult: vo.handleResult,
|
||||
applyContent: vo.applyContent,
|
||||
handleContent: vo.handleContent,
|
||||
addSource: vo.addSource,
|
||||
handleTime: vo.handleTime ? new Date(vo.handleTime).getTime() : undefined,
|
||||
createTime: vo.createTime ? new Date(vo.createTime).getTime() : 0,
|
||||
fromNickname: vo.fromNickname,
|
||||
fromAvatar: vo.fromAvatar,
|
||||
toNickname: vo.toNickname,
|
||||
toAvatar: vo.toAvatar
|
||||
}
|
||||
}
|
||||
|
||||
export const useFriendStoreWithOut = () => useFriendStore(store)
|
||||
|
||||
// dev: 让 Pinia 的 actions / state 改动支持 HMR,避免每次改 store 都得硬刷
|
||||
// 否则 Vite 把新模块推下来后,老 store 实例的 action 闭包仍指向旧函数体
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useFriendStore, import.meta.hot))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,15 @@ import { store } from '@/store'
|
|||
import { getRefreshToken } from '@/utils/auth'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
import { ImWebSocketMessageType, ImMessageType, ImConversationType } from '../../utils/constants'
|
||||
import {
|
||||
ImWebSocketMessageType,
|
||||
ImMessageType,
|
||||
ImConversationType,
|
||||
isFriendNotification
|
||||
} from '../../utils/constants'
|
||||
import { playAudioTip } from '../../utils/message'
|
||||
import { useConversationStore } from './conversationStore'
|
||||
import { useFriendStore } from './friendStore'
|
||||
import { useFriendStore, type FriendNotificationPayload } from './friendStore'
|
||||
import { getFriendDisplayName } from '../../utils/user'
|
||||
import { useGroupStore } from './groupStore'
|
||||
import { readPrivateMessages as apiReadPrivateMessages } from '@/api/im/message/private'
|
||||
|
|
@ -202,30 +207,30 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
// ==================== 普通消息 ====================
|
||||
|
||||
/**
|
||||
* 私聊统一帧分发:按 payload.type(ImMessageType)分到已读 / 回执 / 好友变更 / 普通消息
|
||||
* 私聊统一帧分发:按 payload.type(ImMessageType)分到已读 / 回执 / 好友通知 / 普通消息
|
||||
*
|
||||
* 对应后端 ImPrivateMessageDTO 的 ofRead / ofReceipt / ofFriendAdd / ofFriendDelete / ofFriendUpdate / ofSend
|
||||
* 对应后端 ImPrivateMessageDTO 的 ofRead / ofReceipt / ofFriendNotification / ofSend
|
||||
*/
|
||||
dispatchPrivateFrame(websocketMessage: ImPrivateMessageDTO) {
|
||||
switch (websocketMessage.type) {
|
||||
case ImMessageType.READ:
|
||||
this.handlePrivateRead(websocketMessage)
|
||||
break
|
||||
case ImMessageType.RECEIPT:
|
||||
this.handlePrivateReceipt(websocketMessage)
|
||||
break
|
||||
case ImMessageType.FRIEND_ADD:
|
||||
this.handleFriendAdd(websocketMessage)
|
||||
break
|
||||
case ImMessageType.FRIEND_DELETE:
|
||||
this.handleFriendDelete(websocketMessage)
|
||||
break
|
||||
case ImMessageType.FRIEND_UPDATE:
|
||||
this.handleFriendUpdate(websocketMessage)
|
||||
break
|
||||
default:
|
||||
// TEXT / IMAGE / FILE / VOICE / VIDEO / TIP_TEXT 等普通消息
|
||||
this.handlePrivateMessage(websocketMessage)
|
||||
try {
|
||||
switch (websocketMessage.type) {
|
||||
case ImMessageType.READ:
|
||||
this.handlePrivateRead(websocketMessage)
|
||||
break
|
||||
case ImMessageType.RECEIPT:
|
||||
this.handlePrivateReceipt(websocketMessage)
|
||||
break
|
||||
default:
|
||||
if (isFriendNotification(websocketMessage.type)) {
|
||||
this.handleFriendNotification(websocketMessage)
|
||||
} else {
|
||||
// TEXT / IMAGE / FILE / VOICE / VIDEO / TIP_TEXT 等普通消息
|
||||
this.handlePrivateMessage(websocketMessage)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 单条帧的处理异常不应阻断后续帧;打印完整 websocketMessage 便于排查
|
||||
console.warn('[IM WS] dispatchPrivateFrame 处理失败', websocketMessage, e)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -235,19 +240,24 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
* 1530 GROUP_MEMBER_SETTING_UPDATE 是个人信号;其它(普通消息 + 1501-1520 OpenIM 段位群广播事件)走 handleGroupMessage 入库 + 触发 applyGroupNotification 旁路
|
||||
*/
|
||||
dispatchGroupFrame(websocketMessage: ImGroupMessageDTO) {
|
||||
switch (websocketMessage.type) {
|
||||
case ImMessageType.READ:
|
||||
this.handleGroupRead(websocketMessage)
|
||||
break
|
||||
case ImMessageType.RECEIPT:
|
||||
this.handleGroupReceipt(websocketMessage)
|
||||
break
|
||||
case ImMessageType.GROUP_MEMBER_SETTING_UPDATE:
|
||||
this.handleGroupMemberSettingUpdate(websocketMessage)
|
||||
break
|
||||
default:
|
||||
// TEXT / IMAGE / FILE / VOICE / VIDEO / TIP_TEXT + GROUP_* 群广播事件
|
||||
this.handleGroupMessage(websocketMessage)
|
||||
try {
|
||||
switch (websocketMessage.type) {
|
||||
case ImMessageType.READ:
|
||||
this.handleGroupRead(websocketMessage)
|
||||
break
|
||||
case ImMessageType.RECEIPT:
|
||||
this.handleGroupReceipt(websocketMessage)
|
||||
break
|
||||
case ImMessageType.GROUP_MEMBER_SETTING_UPDATE:
|
||||
this.handleGroupMemberSettingUpdate(websocketMessage)
|
||||
break
|
||||
default:
|
||||
// TEXT / IMAGE / FILE / VOICE / VIDEO / TIP_TEXT + GROUP_* 群广播事件
|
||||
this.handleGroupMessage(websocketMessage)
|
||||
}
|
||||
} catch (e) {
|
||||
// 单条帧的处理异常不应阻断后续帧;打印完整 websocketMessage 便于排查
|
||||
console.warn('[IM WS] dispatchGroupFrame 处理失败', websocketMessage, e)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -461,43 +471,49 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
})
|
||||
},
|
||||
|
||||
// ==================== 好友关系事件(承载于私聊通道,按 inner type 分流) ====================
|
||||
// ==================== 好友通知(1201-1210 段位,承载于私聊通道) ====================
|
||||
|
||||
/** FRIEND_ADD:后端推送给好友双方;本端拉取好友详情并入库,级联刷新私聊会话展示 */
|
||||
handleFriendAdd(websocketMessage: ImPrivateMessageDTO) {
|
||||
/**
|
||||
* 好友通知统一入口:解析 content 里的 payload,按 type 分发到 friendStore 内部 dispatcher
|
||||
*
|
||||
* 对应后端 ImPrivateMessageDTO.ofFriendNotification 系列;payload 实际类型见
|
||||
* BaseFriendNotification 子类(FriendRequestNotification / FriendAddNotification 等)
|
||||
*/
|
||||
handleFriendNotification(websocketMessage: ImPrivateMessageDTO) {
|
||||
// content 解析失败由外层 dispatchPrivateFrame 的 try-catch 兜底(含 websocketMessage 打印),不重复 catch
|
||||
const payload = JSON.parse(websocketMessage.content || '{}') as FriendNotificationPayload
|
||||
const friendStore = useFriendStore()
|
||||
// 后端 DTO 里只带 senderId/receiverId;收到这条时,对端 = 非自己的那一方
|
||||
const userStore = useUserStore()
|
||||
const selfId = Number(userStore.getUser?.id) || 0
|
||||
const friendUserId =
|
||||
websocketMessage.senderId === selfId
|
||||
? websocketMessage.receiverId
|
||||
: websocketMessage.senderId
|
||||
friendStore.loadFriendInfo(friendUserId).catch(() => undefined)
|
||||
},
|
||||
|
||||
/** FRIEND_DELETE:本端标记好友已删 + 级联清理私聊会话 */
|
||||
handleFriendDelete(websocketMessage: ImPrivateMessageDTO) {
|
||||
const friendStore = useFriendStore()
|
||||
const userStore = useUserStore()
|
||||
const selfId = Number(userStore.getUser?.id) || 0
|
||||
const friendUserId =
|
||||
websocketMessage.senderId === selfId
|
||||
? websocketMessage.receiverId
|
||||
: websocketMessage.senderId
|
||||
friendStore.removeFriend(friendUserId)
|
||||
},
|
||||
|
||||
/** FRIEND_UPDATE:多端同步好友属性变更(当前主要是免打扰);重新拉取好友详情即可 */
|
||||
handleFriendUpdate(websocketMessage: ImPrivateMessageDTO) {
|
||||
const friendStore = useFriendStore()
|
||||
const userStore = useUserStore()
|
||||
const selfId = Number(userStore.getUser?.id) || 0
|
||||
const friendUserId =
|
||||
websocketMessage.senderId === selfId
|
||||
? websocketMessage.receiverId
|
||||
: websocketMessage.senderId
|
||||
friendStore.loadFriendInfo(friendUserId).catch(() => undefined)
|
||||
switch (websocketMessage.type) {
|
||||
case ImMessageType.FRIEND_APPLICATION:
|
||||
friendStore.applyFriendRequestNotification(payload)
|
||||
break
|
||||
case ImMessageType.FRIEND_REQUEST_APPROVED:
|
||||
friendStore.applyFriendRequestApprovedNotification(payload)
|
||||
break
|
||||
case ImMessageType.FRIEND_REQUEST_REJECTED:
|
||||
friendStore.applyFriendRequestRejectedNotification(payload)
|
||||
break
|
||||
case ImMessageType.FRIEND_ADD:
|
||||
friendStore.applyFriendAddNotification(payload)
|
||||
break
|
||||
case ImMessageType.FRIEND_DELETE:
|
||||
friendStore.applyFriendDeleteNotification(payload)
|
||||
break
|
||||
case ImMessageType.FRIEND_BLOCK:
|
||||
friendStore.applyFriendBlockNotification(payload)
|
||||
break
|
||||
case ImMessageType.FRIEND_UNBLOCK:
|
||||
friendStore.applyFriendUnblockNotification(payload)
|
||||
break
|
||||
case ImMessageType.FRIEND_INFO_UPDATED:
|
||||
friendStore.applyFriendInfoUpdatedNotification(payload)
|
||||
break
|
||||
case ImMessageType.FRIEND_UPDATE:
|
||||
friendStore.applyFriendUpdateNotification(payload)
|
||||
break
|
||||
default:
|
||||
console.debug('[IM WS] 未识别好友通知', websocketMessage)
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 群关系事件(承载于群聊通道,按 inner type 分流) ====================
|
||||
|
|
@ -508,13 +524,10 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
* payload 携带变更字段,按非 null 字段直接局部更新;省一次 fetchGroupMembers 接口
|
||||
*/
|
||||
handleGroupMemberSettingUpdate(websocketMessage: ImGroupMessageDTO) {
|
||||
let payload: { muted?: boolean; groupRemark?: string } = {}
|
||||
try {
|
||||
payload = JSON.parse(websocketMessage.content || '{}')
|
||||
} catch (error) {
|
||||
console.warn('[IM WS] handleGroupMemberSettingUpdate 解析 content 失败', error)
|
||||
return
|
||||
}
|
||||
// content 解析失败由外层 dispatchGroupFrame 的 try-catch 兜底(含 websocketMessage 打印),不重复 catch
|
||||
const payload: { muted?: boolean; groupRemark?: string } = JSON.parse(
|
||||
websocketMessage.content || '{}'
|
||||
)
|
||||
const groupStore = useGroupStore()
|
||||
const group = groupStore.getGroup(websocketMessage.groupId)
|
||||
if (!group) {
|
||||
|
|
|
|||
|
|
@ -153,10 +153,35 @@ export interface Friend {
|
|||
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 转换)
|
||||
}
|
||||
|
||||
/**
|
||||
* 好友申请记录(前端内部结构,对齐后端 ImFriendRequestRespVO)
|
||||
*/
|
||||
export interface FriendRequest {
|
||||
// ========== 后端字段(对齐 ImFriendRequestRespVO) ==========
|
||||
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 等场景共用)
|
||||
|
|
|
|||
|
|
@ -10,9 +10,17 @@ export const ImMessageType = {
|
|||
RECEIPT: 12, // 回执
|
||||
TIP_TIME: 20, // 时间分隔线(前端本地生成,不发送到后端)
|
||||
TIP_TEXT: 21, // 提示文本(撤回提示等)
|
||||
FRIEND_ADD: 100, // 好友添加
|
||||
FRIEND_DELETE: 101, // 好友删除
|
||||
FRIEND_UPDATE: 102, // 好友更新(客户端收到后自行拉取)
|
||||
// 好友通知(1201-1210 复用 OpenIM 段位编号)
|
||||
FRIEND_REQUEST_APPROVED: 1201, // 好友申请被同意
|
||||
FRIEND_REQUEST_REJECTED: 1202, // 好友申请被拒绝
|
||||
FRIEND_APPLICATION: 1203, // 收到新的好友申请
|
||||
FRIEND_ADD: 1204, // 新增好友(双方建立关系)
|
||||
FRIEND_DELETE: 1205, // 好友被删除
|
||||
// 1206 对应 OpenIM FriendRemarkSetNotification;本系统并入 FRIEND_UPDATE(1210) 统一推送
|
||||
FRIEND_BLOCK: 1207, // 加入黑名单
|
||||
FRIEND_UNBLOCK: 1208, // 移出黑名单
|
||||
FRIEND_INFO_UPDATED: 1209, // 好友资料变更(昵称 / 头像)
|
||||
FRIEND_UPDATE: 1210, // 好友信息批量更新(muted / pinned)
|
||||
// 群事件(1501-1520 复用 OpenIM 段位编号;1530+ 自有扩展段)
|
||||
GROUP_CREATE: 1501, // 群创建
|
||||
GROUP_INFO_UPDATE: 1502, // 群信息变更(NAME / NOTICE 之外字段兜底)
|
||||
|
|
@ -48,6 +56,11 @@ export function isGroupNotification(type: number): boolean {
|
|||
)
|
||||
}
|
||||
|
||||
/** 判断是否「好友通知事件」:1201-1210 段位 */
|
||||
export function isFriendNotification(type: number): boolean {
|
||||
return type >= ImMessageType.FRIEND_REQUEST_APPROVED && type <= ImMessageType.FRIEND_UPDATE
|
||||
}
|
||||
|
||||
/** IM 普通消息类型集合(聊天气泡中显示,并作为会话最后一条摘要) */
|
||||
const ImMessageTypeNormals: number[] = [
|
||||
ImMessageType.TEXT,
|
||||
|
|
|
|||
Loading…
Reference in New Issue