From 808ad575fc7d7d16d5cad7678eaea1e986829aed Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 7 May 2026 08:13:27 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(im):=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E7=BE=A4=E7=94=B3=E8=AF=B7=20v0.5=EF=BC=9A=E7=AC=AC?= =?UTF-8?q?=E5=85=AD=E6=8A=8A=20review=EF=BC=88=E6=80=A7=E8=83=BD=20/=20?= =?UTF-8?q?=E5=81=A5=E5=A3=AE=E6=80=A7=20/=20=E7=AE=80=E6=B4=81=E5=BA=A6?= =?UTF-8?q?=E6=94=B6=E5=8F=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端 - createInviteRequestList N+1 → 3 SQL:批量 select IN + update IN + insertBatch;20 人邀请从 40 RTT 降到 3 RTT - service 不再出现 mybatis:复用记录的 update(null, wrapper) 下沉到 Im{Group,Friend}RequestMapper.update*Reset helper - inviteGroupMember 入参去重切 hutool:CollUtil.subtractToList(CollUtil.distinct(...), activeMemberUserIds) - 删除 dead 字段 inviterUserId(GroupRequestApprovedNotification / GroupRequestRejectedNotification):前端不再消费 前端 - 1505 / 1506 通知改静默:同意走群事件 1509 / 1510 渲染系统提示,拒绝不再打扰 - 修竞态:addByRequestId 校验 handleResult === UNHANDLED,避免 1503 在途时被 1505 / 1506 抢先后又把已处理记录塞回未处理列表 - 修复 dialog 复用记录刷新:watch key 含 inviterUserId / applyContent,同 id 不同内容也触发 refetch;actingId 期间跳过避免本端动作多余 RTT - 修复 willGoApproval 误报:group.ownerUserId 兜底群主;members 未到位时保守按非审批处理 - unhandledCountMap memoized getter:O(N) 扫一次缓存到 Map,ConversationItem 直读 Map 消除 O(N×M) 重复 filter --- .../components/group/GroupMemberAddDialog.vue | 25 ++++++++++------ .../group/GroupRequestListDialog.vue | 19 ++++++------ .../conversation/ConversationItem.vue | 2 +- src/views/im/home/store/groupRequestStore.ts | 30 ++++++++++++------- 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/views/im/home/components/group/GroupMemberAddDialog.vue b/src/views/im/home/components/group/GroupMemberAddDialog.vue index 07001b45f..4c91318b6 100644 --- a/src/views/im/home/components/group/GroupMemberAddDialog.vue +++ b/src/views/im/home/components/group/GroupMemberAddDialog.vue @@ -128,11 +128,7 @@ const visible = computed({ set: (value) => emit('update:modelValue', value) }) -/** - * 是否走审批:群开启 joinApproval + 当前用户是普通成员;群主 / 管理员邀请绕过审批(对齐后端) - * - * 用于切换提交后的提示文案:审批分支后端只创建待审批记录,没把人拉进群,提示「等待审批」更准确 - */ +/** 是否走审批:群开启 joinApproval + 当前用户是普通成员;群主 / 管理员邀请直进,不落审批记录 */ const willGoApproval = computed(() => { if (!props.groupId) { return false @@ -141,9 +137,20 @@ const willGoApproval = computed(() => { if (!group?.joinApproval) { return false } - const myId = Number(userStore.getUser?.id) || 0 - const myRole = props.members.find((m) => m.userId === myId)?.role - return myRole !== ImGroupMemberRole.OWNER && myRole !== ImGroupMemberRole.ADMIN + const myId = Number(userStore.getUser?.id) + if (!myId) { + return false + } + // 群主直判,避开 members 异步加载的窗口;admin 仍依赖 members + if (group.ownerUserId === myId) { + return false + } + // members 未到位时无法判定 admin,保守按非审批处理,宁可漏报「等待审批」也不误报给真实管理员 + const myRole = props.members.find((member) => member.userId === myId)?.role + if (myRole == null) { + return false + } + return myRole !== ImGroupMemberRole.ADMIN }) const searchText = ref('') @@ -219,7 +226,7 @@ async function handleOk() { submitting.value = true try { await inviteGroupMember({ groupId: props.groupId, memberUserIds }) - // 审批分支后端仅落待审批记录,没把人拉进群;普通入群直接邀请成功 + // 审批分支:后端仅落审批记录,未入群 message.success(willGoApproval.value ? '邀请已发起,等待群主 / 管理员审批' : '邀请成功') emit('reload', memberUserIds) visible.value = false diff --git a/src/views/im/home/components/group/GroupRequestListDialog.vue b/src/views/im/home/components/group/GroupRequestListDialog.vue index 59d2bff74..b58b13158 100644 --- a/src/views/im/home/components/group/GroupRequestListDialog.vue +++ b/src/views/im/home/components/group/GroupRequestListDialog.vue @@ -204,23 +204,24 @@ watch( ) /** - * 单群模式下订阅 store 中归属本群的未处理列表变化:增 / 减都 refetch 一次拿到最新 handleResult + * 单群模式下订阅 store 中归属本群的未处理列表变化:远端事件(WS 1503 新申请 / 其他管理员处理)触发时 refetch + * 拿最新 handleResult;本端 agree / refuse 期间 actingId 锁住,跳过本端动作引发的 store 变化避免冗余 RTT * - * 触发场景:① WS 1503 收到新申请 → store 头部 unshift;② 其他管理员 / 远端处理 → store 移除该项 - * 本端 agreeRequest / refuseRequest 内部也会 removeByRequestId 从而触发;fetchList 拉到的 handleResult - * 与 updateLocalResult 写的一致,不冲突;仅多一次网络请求,可接受 + * key 不能只 join id:复用旧记录时同一 requestId 的 applyContent / inviterUserId 会刷新但 id 不变,必须把内容字段也纳入触发 */ -// TODO @AI:减少缩写,类似 r、curr、prev;还是完整点,不会有啥影响的; watch( () => props.groupId && visible.value ? groupRequestStore.unhandledList - .filter((r) => r.groupId === props.groupId) - .map((r) => r.id) + .filter((request) => request.groupId === props.groupId) + .map((request) => `${request.id}:${request.inviterUserId ?? ''}:${request.applyContent ?? ''}`) .join(',') : null, - (curr, prev) => { - if (curr === null || prev === undefined || curr === prev) { + (current, previous) => { + if (current === null || previous === undefined || current === previous) { + return + } + if (actingId.value !== null) { return } if (props.groupId) { diff --git a/src/views/im/home/pages/conversation/components/conversation/ConversationItem.vue b/src/views/im/home/pages/conversation/components/conversation/ConversationItem.vue index 4c10e5cf4..d756a0d79 100644 --- a/src/views/im/home/pages/conversation/components/conversation/ConversationItem.vue +++ b/src/views/im/home/pages/conversation/components/conversation/ConversationItem.vue @@ -187,7 +187,7 @@ const requestText = computed(() => { if (!isGroup.value) { return '' } - const count = groupRequestStore.getUnhandledCountByGroupId(props.conversation.targetId) + const count = groupRequestStore.unhandledCountMap.get(props.conversation.targetId) ?? 0 return count > 0 ? `[${count}条进群申请]` : '' }) diff --git a/src/views/im/home/store/groupRequestStore.ts b/src/views/im/home/store/groupRequestStore.ts index 8cdea382d..edf629be0 100644 --- a/src/views/im/home/store/groupRequestStore.ts +++ b/src/views/im/home/store/groupRequestStore.ts @@ -8,6 +8,7 @@ import { refuseGroupRequest as apiRefuseGroupRequest, type ImGroupRequestRespVO } from '@/api/im/group/request' +import { ImGroupRequestHandleResult } from '@/views/im/utils/constants' /** * IM 加群申请 Store @@ -31,12 +32,21 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', { }), getters: { - /** 指定群下的未处理申请数;横幅红点 */ - getUnhandledCountByGroupId: - (state) => - (groupId: number): number => - state.unhandledList.filter((r) => r.groupId === groupId).length, - /** 指定群下的未处理申请列表;Drawer 内容 */ + /** + * 各群下未处理申请数的 Map;O(N) 扫一次缓存供 ConversationItem 等 N 处复用,避免 N×M 重复 filter + */ + unhandledCountMap(state): Map { + const map = new Map() + for (const request of state.unhandledList) { + map.set(request.groupId, (map.get(request.groupId) ?? 0) + 1) + } + return map + }, + /** 指定群下的未处理申请数 */ + getUnhandledCountByGroupId(): (groupId: number) => number { + return (groupId: number) => this.unhandledCountMap.get(groupId) ?? 0 + }, + /** 指定群下的未处理申请列表 */ getUnhandledListByGroupId: (state) => (groupId: number): ImGroupRequestRespVO[] => @@ -52,14 +62,14 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', { }, /** - * WS 收到 1503:按 requestId 单查 + 排到列表头 + * WS 收到 1503:拉最新内容并置顶 * - * 同一对 group_id, user_id 的申请记录会被复用,再次申请 / 邀请时 requestId 不变但 applyContent / inviterUserId - * 等会刷新;不能因 id 已存在就跳过,必须 fetch 最新内容并把它顶到最前面(与后端 update_time 倒序对齐) + * 同一对 group_id, user_id 复用记录时 requestId 不变但 applyContent / inviterUserId 会刷新,所以无条件 fetch + 排到头部 + * 校验 handleResult:HTTP 在途时若已收到 1505 / 1506,returnedRequest 可能已是已处理状态,不能再塞回未处理列表 */ async addByRequestId(requestId: number) { const request = await apiGetMyGroupRequest(requestId) - if (!request) { + if (!request || request.handleResult !== ImGroupRequestHandleResult.UNHANDLED) { return } this.unhandledList = [request, ...this.unhandledList.filter((r) => r.id !== requestId)]