admin-vue3/src/views/im/home/store/friendStore.ts

468 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { defineStore, acceptHMRUpdate } from 'pinia'
import { store } from '@/store'
import { CommonStatusEnum } from '@/utils/constants'
import {
getMyFriendList as apiGetMyFriendList,
getFriend as apiGetFriend,
deleteFriend as apiDeleteFriend,
updateFriend as apiUpdateFriend,
blockFriend as apiBlockFriend,
unblockFriend as apiUnblockFriend,
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, ImFriendRequestHandleResult } from '../../utils/constants'
import { getCurrentUserId, imStorage, setQuietly, StorageKeys } from '../../utils/storage'
import { getFriendDisplayName } from '../../utils/user'
import type { Friend, FriendRequest } from '../types'
/** 当前正在进行的好友申请列表拉取;多端连续多条申请到达时复用同一 Promise避免雪崩重拉 */
let inflightFetchRequests: Promise<void> | null = null
/** 好友通知 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
*
* 负责:
* - 拉取 / 缓存当前登录用户的好友列表 + 申请列表
* - 申请-审批流程apply / agree / refuse+ 备注 / 免打扰 / 联系人置顶 / 拉黑
* - 接收 WebSocket 1201-1210 段位通知,按事件分发到 friendStore 内部各 dispatcher
*/
export const useFriendStore = defineStore('imFriendStore', {
state: () => ({
friends: [] as Friend[],
// 仅 fetchFriends 成功后置位loadFriendsIDB不置位否则后台 SWR 刷新会被缓存命中跳过
loaded: false,
/** 我相关的好友申请列表(含我发起的 + 别人加我的;后端按 id 倒序,前端不再分页) */
friendRequests: [] as FriendRequest[]
}),
getters: {
/** 按 friendUserId 找好友(含已软删的 DISABLE 记录,调用方自行判定) */
getFriend:
(state) =>
(friendUserId: number): Friend | undefined => {
return state.friends.find((f) => f.friendUserId === friendUserId)
},
/** 当前生效的好友列表(过滤掉 DISABLE 软删记录) */
getActiveFriends: (state): Friend[] => {
return state.friends.filter((f) => f.status !== CommonStatusEnum.DISABLE)
},
/** 判断对方是否是当前用户的有效好友(存在 + 非 DISABLE */
isFriend() {
return (friendUserId: number): boolean => {
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 currentUserId = Number(getCurrentUserId() || 0)
return state.friendRequests.filter(
(request) =>
request.handleResult === ImFriendRequestHandleResult.UNHANDLED &&
request.toUserId === currentUserId
).length
}
},
actions: {
// ==================== 本地缓存 ====================
/** 从 IDB 恢复好友列表 */
async loadFriends(): Promise<boolean> {
const userId = getCurrentUserId()
if (!userId) {
return false
}
try {
const cached = await imStorage.getItem<Friend[]>(StorageKeys.friends(userId))
if (!cached || cached.length === 0) {
return false
}
this.friends = cached
return true
} catch (e) {
console.warn('[IM friendStore] 本地好友缓存读取失败', e)
return false
}
},
/** 整桶持久化好友列表(量级有限,不维护增量) */
saveFriends(): void {
const userId = getCurrentUserId()
if (!userId) {
return
}
setQuietly(StorageKeys.friends(userId), this.friends, '[IM friendStore] 本地好友缓存写入失败')
},
// ==================== 远端拉取 ====================
/** 从后端拉取并覆盖本地列表;同步刷新对应私聊会话的展示名 / 头像 + 落 IDB */
async fetchFriends(force = false) {
if (this.loaded && !force) {
return
}
const list = await apiGetMyFriendList()
this.friends = (list || []).map(convertFriend)
this.loaded = true
// 同步 conversationStore 私聊会话的展示名 / 头像 / 免打扰
const conversationStore = useConversationStore()
for (const friend of this.friends) {
conversationStore.updateConversation(ImConversationType.PRIVATE, friend.friendUserId, {
name: getFriendDisplayName(friend),
avatar: friend.avatar,
muted: friend.muted
})
}
this.saveFriends()
},
/** 按 friendUserId 获取详情并合并到本地(保证 nickname / avatar 最新) */
async loadFriendInfo(friendUserId: number) {
try {
const data = await apiGetFriend(friendUserId)
if (!data) {
return
}
this.upsertFriend(convertFriend(data))
} catch (e) {
console.warn('[IM friendStore] loadFriendInfo 失败', e)
}
},
// ==================== 申请-审批 ====================
/** 发起好友申请:成功后等待对方同意(不直接落地为好友) */
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 = ImFriendRequestHandleResult.AGREED
request.handleTime = Date.now()
} else {
// 列表过期场景兜底重拉
// TODO @AI是不是只拉这个人避免拉所有
await this.fetchFriendRequests()
}
},
/** 拒绝一条好友申请 */
async refuseFriendRequest(requestId: number, handleContent?: string) {
await apiRefuseFriendRequest(requestId, handleContent)
const request = this.findFriendRequest(requestId)
if (request) {
request.handleResult = ImFriendRequestHandleResult.REFUSED
request.handleContent = handleContent
request.handleTime = Date.now()
} else {
await this.fetchFriendRequests()
}
},
/** 拉取「我相关」的好友申请列表(页面打开时 / 收到 FRIEND_APPLICATION 时刷新in-flight 期间复用同一 Promise */
async fetchFriendRequests() {
if (inflightFetchRequests) {
return inflightFetchRequests
}
inflightFetchRequests = apiGetMyFriendRequestList()
.then((list) => {
this.friendRequests = (list || []).map(convertFriendRequest)
})
.finally(() => {
inflightFetchRequests = null
})
return inflightFetchRequests
},
/** 按 id 查申请记录;列表是按 id 倒序的小列表O(n) find 即可,不再维护 Map 索引 */
findFriendRequest(requestId: number): FriendRequest | undefined {
return this.friendRequests.find((request) => request.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()
}
},
/** 拉黑好友:本端乐观更新 + 调接口;后端 FRIEND_BLOCK 推到时由 dispatcher 兜底同步多端 */
async blockFriend(friendUserId: number) {
await apiBlockFriend(friendUserId)
const friend = this.getFriend(friendUserId)
if (friend) {
friend.blocked = true
this.saveFriends()
}
},
/** 移出黑名单:本端乐观更新 + 调接口;后端 FRIEND_UNBLOCK 推到时由 dispatcher 兜底同步多端 */
async unblockFriend(friendUserId: number) {
await apiUnblockFriend(friendUserId)
const friend = this.getFriend(friendUserId)
if (friend) {
friend.blocked = false
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) {
const index = this.friends.findIndex((f) => f.friendUserId === friend.friendUserId)
if (index >= 0) {
this.friends[index] = {
...this.friends[index],
...friend,
status: friend.status ?? CommonStatusEnum.ENABLE
}
} else {
this.friends.push({
...friend,
status: friend.status ?? CommonStatusEnum.ENABLE
})
}
const conversationStore = useConversationStore()
const merged = this.getFriend(friend.friendUserId)
conversationStore.updateConversation(ImConversationType.PRIVATE, friend.friendUserId, {
name: merged ? getFriendDisplayName(merged) : friend.nickname,
avatar: friend.avatar,
muted: friend.muted
})
this.saveFriends()
},
/** 本地标记删除WebSocket FRIEND_DELETE 事件触发;同时级联清私聊会话) */
removeFriend(friendUserId: number) {
const friend = this.getFriend(friendUserId)
if (friend) {
friend.status = CommonStatusEnum.DISABLE
friend.deleteTime = Date.now()
friend.blocked = false
}
// 级联清理:把对应的私聊会话也软删,避免会话列表里留着已删好友
const conversationStore = useConversationStore()
conversationStore.removePrivateConversation(friendUserId)
this.saveFriends()
},
// ==================== WebSocket 事件 dispatcher1201-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 = ImFriendRequestHandleResult.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 = ImFriendRequestHandleResult.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.blocked = true
this.saveFriends()
}
},
/** FRIEND_UNBLOCK(1208):移出黑名单;多端同步 */
applyFriendUnblockNotification(payload: FriendNotificationPayload) {
const friend = this.getFriend(payload.friendUserId)
if (friend) {
friend.blocked = false
this.saveFriends()
}
},
/**
* FRIEND_INFO_UPDATED(1209):好友资料变更(昵称 / 头像);重拉详情
* TODO @AI后端暂未实现 1209 推送;待 system 模块改昵称 / 头像时回调触发,本 dispatcher 已就绪
*/
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-memoryIDB 按 userId 分桶天然隔离,回切秒开 */
clear() {
this.friends = []
this.friendRequests = []
this.loaded = false
}
}
})
function convertFriend(vo: ImFriendRespVO): Friend {
return {
id: vo.id,
friendUserId: vo.friendUserId,
nickname: vo.nickname || String(vo.friendUserId),
nicknamePinyin: vo.nicknamePinyin,
avatar: vo.avatar,
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 都得硬刷
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useFriendStore, import.meta.hot))
}