feat(im): 初始化群申请 v0.5:第六把 review(性能 / 健壮性 / 简洁度收口)

后端
- 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
im
YunaiV 2026-05-07 08:13:27 +08:00
parent cb26df3ca1
commit 808ad575fc
4 changed files with 47 additions and 29 deletions

View File

@ -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

View File

@ -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 rcurrprev
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) {

View File

@ -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}条进群申请]` : ''
})

View File

@ -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 内容 */
/**
* MapO(N) ConversationItem N N×M filter
*/
unhandledCountMap(state): Map<number, number> {
const map = new Map<number, number>()
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 1503requestId +
* WS 1503
*
* group_id, user_id / requestId applyContent / inviterUserId
* id fetch update_time
* group_id, user_id requestId applyContent / inviterUserId fetch +
* handleResultHTTP 1505 / 1506returnedRequest
*/
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)]