feat(im): 初始化群申请 v0.4:第五把 review(多轮 finding 修复 + 通知静默化)

- 邀请路径写 addSource=INVITE;群主 / 管理员邀请绕过审批;inviteGroupMember 入参去重
- getGroupRequest 越权校验加成员有效状态判断;新增 list-by-group 接口
- 申请列表按 update_time 倒序,update(null, wrapper) 路径手动刷 updateTime
- addByRequestId 不再 skip 同 id,复用记录刷新并置顶
- GroupRequestListDialog 单群模式订阅 store 增量同步;GroupMemberAddDialog 审批分支文案区分
- ConversationItem 增加 [X 条进群申请] 红字前缀;MessagePanel 顶部胶囊横幅
- 1505 / 1506 通知改静默:同意走群事件渲染系统提示,拒绝不再打扰;清掉 dead inviterUserId 字段
im
YunaiV 2026-05-07 00:51:48 +08:00
parent b2ba42049b
commit cb26df3ca1
4 changed files with 59 additions and 25 deletions

View File

@ -86,6 +86,9 @@ import { useMessage } from '@/hooks/web/useMessage'
import { CommonStatusEnum } from '@/utils/constants'
import { inviteGroupMember } from '@/api/im/group/member'
import { useUserStore } from '@/store/modules/user'
import { ImGroupMemberRole } from '@/views/im/utils/constants'
import { useGroupStore } from '../../store/groupStore'
import FriendItem from '../friend/FriendItem.vue'
import type { FriendLite } from '../../types'
import type { GroupMemberLite } from './GroupMember.vue'
@ -116,6 +119,8 @@ const emit = defineEmits<{
}>()
const message = useMessage()
const userStore = useUserStore()
const groupStore = useGroupStore()
/** 弹窗显隐:把父侧 v-model 转双向计算 */
const visible = computed({
@ -123,6 +128,24 @@ const visible = computed({
set: (value) => emit('update:modelValue', value)
})
/**
* 是否走审批群开启 joinApproval + 当前用户是普通成员群主 / 管理员邀请绕过审批对齐后端
*
* 用于切换提交后的提示文案审批分支后端只创建待审批记录没把人拉进群提示等待审批更准确
*/
const willGoApproval = computed(() => {
if (!props.groupId) {
return false
}
const group = groupStore.getGroup(props.groupId)
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 searchText = ref('')
const submitting = ref(false)
const workingFriends = ref<FriendCheckable[]>([]) // 工作副本(带 checked / disabled 标记 prop 隔离
@ -196,7 +219,8 @@ async function handleOk() {
submitting.value = true
try {
await inviteGroupMember({ groupId: props.groupId, memberUserIds })
message.success('邀请成功')
//
message.success(willGoApproval.value ? '邀请已发起,等待群主 / 管理员审批' : '邀请成功')
emit('reload', memberUserIds)
visible.value = false
} finally {

View File

@ -203,6 +203,32 @@ watch(
{ immediate: true }
)
/**
* 单群模式下订阅 store 中归属本群的未处理列表变化 / 减都 refetch 一次拿到最新 handleResult
*
* 触发场景 WS 1503 收到新申请 store 头部 unshift 其他管理员 / 远端处理 store 移除该项
* 本端 agreeRequest / refuseRequest 内部也会 removeByRequestId 从而触发fetchList 拉到的 handleResult
* updateLocalResult 写的一致不冲突仅多一次网络请求可接受
*/
// TODO @AI rcurrprev
watch(
() =>
props.groupId && visible.value
? groupRequestStore.unhandledList
.filter((r) => r.groupId === props.groupId)
.map((r) => r.id)
.join(',')
: null,
(curr, prev) => {
if (curr === null || prev === undefined || curr === prev) {
return
}
if (props.groupId) {
void fetchList(props.groupId)
}
}
)
async function fetchList(groupId: number) {
loading.value = true
try {

View File

@ -51,17 +51,18 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
this.loaded = true
},
/** WS 收到 1503按 requestId 单查 + push 进列表头payload 已带申请方昵称 / 头像可减一次回查 */
/**
* WS 1503 requestId +
*
* group_id, user_id / requestId applyContent / inviterUserId
* id fetch update_time
*/
async addByRequestId(requestId: number) {
const exists = this.unhandledList.some((r) => r.id === requestId)
if (exists) {
return
}
const request = await apiGetMyGroupRequest(requestId)
if (!request) {
return
}
this.unhandledList.unshift(request)
this.unhandledList = [request, ...this.unhandledList.filter((r) => r.id !== requestId)]
},
/** WS 收到 1505 / 1506 或本端处理完一条:按 requestId 从列表移除 */

View File

@ -1,5 +1,4 @@
import { defineStore, acceptHMRUpdate } from 'pinia'
import { ElNotification } from 'element-plus'
import { store } from '@/store'
import { getRefreshToken } from '@/utils/auth'
import { useUserStore } from '@/store/modules/user'
@ -565,39 +564,23 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
*
* ImPrivateMessageDTO.ofGroupNotification
* - 1503admin push unhandledList
* - 1505 / 1506 admin unhandledList toast
* - 1505 / 1506admin unhandledList 1509 / 1510
*/
handleGroupRequestNotification(websocketMessage: ImPrivateMessageDTO) {
const payload = JSON.parse(websocketMessage.content || '{}') as {
requestId?: number
groupId?: number
userId?: number
handleContent?: string
}
if (!payload.requestId) {
return
}
const groupRequestStore = useGroupRequestStore()
const userStore = useUserStore()
const myId = Number(userStore.getUser?.id) || 0
switch (websocketMessage.type) {
case ImMessageType.GROUP_REQUEST_RECEIVED:
groupRequestStore.addByRequestId(payload.requestId).catch(() => undefined)
break
case ImMessageType.GROUP_REQUEST_APPROVED:
groupRequestStore.removeByRequestId(payload.requestId)
if (payload.userId === myId) {
ElNotification.success({ title: '入群申请已通过', message: '可以开始群聊了' })
}
break
case ImMessageType.GROUP_REQUEST_REJECTED:
groupRequestStore.removeByRequestId(payload.requestId)
if (payload.userId === myId) {
ElNotification.warning({
title: '入群申请被拒绝',
message: payload.handleContent || ''
})
}
break
default:
break