feat(im): 初始化群申请 v0.0:第一把 review

im
YunaiV 2026-05-06 14:53:47 +08:00
parent 4868d69ed8
commit 3be0daf115
8 changed files with 361 additions and 8 deletions

View File

@ -10,11 +10,13 @@ export interface ImGroupRespVO {
notice?: string // 群公告
banned?: boolean // 是否封禁
mutedAll?: boolean // 是否全群禁言
joinType?: number // 加群方式;参见 ImGroupJoinTypeEnum
bannedTime?: string // 封禁时间
status: number // 群状态0=正常1=已解散)
dissolvedTime?: string // 解散时间
createTime?: string // 创建时间
pinnedMessages?: ImGroupMessageRespVO[] // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空)
pendingRequestCount?: number // 未处理加群申请数;后端关联回填,仅当登录用户是该群群主 / 管理员时非空 TODO @AI看看这里是不是可以不返回
}
// 群消息置顶 / 取消置顶 Request VO
@ -27,6 +29,7 @@ export interface ImGroupMessagePinReqVO {
export interface ImGroupCreateReqVO {
name: string // 群名称
memberUserIds?: number[] // 初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)
joinType?: number // 加群方式;不传默认 0 自由进群 TODO @AI不要写 0而是写对应的枚举
}
// 群更新 Request VO
@ -35,6 +38,7 @@ export interface ImGroupUpdateReqVO {
name?: string // 群名称
avatar?: string // 群头像
notice?: string // 群公告
joinType?: number // 加群方式
}
// 添加 / 撤销群管理员 Request VO

View File

@ -0,0 +1,84 @@
import request from '@/config/axios'
// IM 加群申请 Response VO
export interface ImGroupRequestRespVO {
id: number // 申请编号
groupId: number // 群编号
userId: number // 申请人 / 被邀请人用户编号
inviterUserId?: number // 邀请人NULL 表示用户主动申请
handleResult: number // 处理结果0=未处理1=同意2=拒绝
applyContent?: string // 申请理由
handleContent?: string // 处理理由(拒绝时可选填)
handleUserId?: number // 处理人用户编号
addSource?: number // 加入来源;参见 ImGroupAddSourceEnum
handleTime?: string // 处理时间
createTime: string // 申请创建时间
// 聚合字段
userNickname?: string // 申请人 / 被邀请人昵称
userAvatar?: string // 申请人 / 被邀请人头像
inviterNickname?: string // 邀请人昵称
inviterAvatar?: string // 邀请人头像
groupName?: string // 群名称
groupAvatar?: string // 群头像
}
// IM 加群申请发起 Request VO
export interface ImGroupRequestApplyReqVO {
groupId: number // 群编号
applyContent?: string // 申请理由
addSource?: number // 加入来源
}
// 申请加群
export const applyJoinGroup = (data: ImGroupRequestApplyReqVO) => {
return request.post<number | null>({ url: '/im/group-request/apply', data })
}
// 同意加群申请(群主或管理员)
export const agreeGroupRequest = (id: number | string) => {
return request.put<boolean>({ url: '/im/group-request/agree', params: { id } })
}
// 拒绝加群申请(群主或管理员)
export const refuseGroupRequest = (id: number | string, handleContent?: string) => {
return request.put<boolean>({
url: '/im/group-request/refuse',
params: { id, handleContent }
})
}
// 查询「我相关」的加群申请列表(含我主动申请、我被邀请待审);游标分页
export const getMyGroupRequestList = (limit: number, lastRequestId?: number) => {
const params: Record<string, number> = { limit }
if (lastRequestId != null) {
params.lastRequestId = lastRequestId
}
return request.get<ImGroupRequestRespVO[]>({
url: '/im/group-request/list',
params
})
}
// 查询指定群的未处理加群申请(仅群主或管理员可调);游标分页
export const getPendingGroupRequestList = (
groupId: number | string,
limit: number,
lastRequestId?: number
) => {
const params: Record<string, number | string> = { groupId, limit }
if (lastRequestId != null) {
params.lastRequestId = lastRequestId
}
return request.get<ImGroupRequestRespVO[]>({
url: '/im/group-request/list-pending',
params
})
}
// 按 id 单查「我相关」的申请记录带越权过滤WebSocket 通知到达后用)
export const getMyGroupRequest = (id: number) => {
return request.get<ImGroupRequestRespVO | null>({
url: '/im/group-request/get',
params: { id }
})
}

View File

@ -0,0 +1,24 @@
import request from '@/config/axios'
export interface ImManagerGroupRequestVO {
id: number
groupId: number
groupName?: string
userId: number
userNickname?: string
inviterUserId?: number
inviterNickname?: string
applyContent?: string
addSource?: number
handleResult: number
handleUserId?: number
handleNickname?: string
handleContent?: string
handleTime?: Date
createTime: Date
}
// 获得加群申请分页
export const getManagerGroupRequestPage = (params: PageParam) => {
return request.get({ url: '/im/manager/group-request/page', params })
}

View File

@ -336,5 +336,8 @@ export enum DICT_TYPE {
IM_FRIEND_ADD_SOURCE = 'im_friend_add_source', // IM 好友添加来源
IM_FRIEND_REQUEST_HANDLE_RESULT = 'im_friend_request_handle_result', // IM 好友申请处理结果
IM_GROUP_STATUS = 'im_group_status', // IM 群状态
IM_GROUP_MEMBER_ROLE = 'im_group_member_role' // IM 群成员角色
IM_GROUP_MEMBER_ROLE = 'im_group_member_role', // IM 群成员角色
IM_GROUP_JOIN_TYPE = 'im_group_join_type', // IM 群加群方式
IM_GROUP_ADD_SOURCE = 'im_group_add_source', // IM 加群来源
IM_GROUP_REQUEST_HANDLE_RESULT = 'im_group_request_handle_result' // IM 加群申请处理结果
}

View File

@ -734,7 +734,9 @@ function convertGroup(group: ImGroupRespVO): Group {
ownerUserId: group.ownerUserId,
pinnedMessages: group.pinnedMessages?.map(convertGroupMessageVO),
mutedAll: group.mutedAll,
banned: group.banned
banned: group.banned,
joinType: group.joinType,
pendingRequestCount: group.pendingRequestCount
}
}

View File

@ -119,6 +119,8 @@ export interface Group {
pinnedMessages?: Message[] // 群置顶消息列表
mutedAll?: boolean // 是否全群禁言
banned?: boolean // 是否被管理员封禁
joinType?: number // 加群方式;参见 ImGroupJoinType
pendingRequestCount?: number // 未处理加群申请数;后端关联回填,仅当登录用户是该群群主 / 管理员时非空
// ========== 前端扩展字段user-per-group 维度) ==========
silent?: boolean // 是否免打扰。从当前用户的 GroupMember 回填
@ -230,4 +232,5 @@ export interface GroupLite {
showImageThumb?: string
memberCount?: number
ownerId?: number
joinType?: number // 加群方式;参见 ImGroupJoinType
}

View File

@ -0,0 +1,190 @@
<template>
<ContentWrap>
<!-- 搜索栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="80px"
>
<el-form-item label="群编号" prop="groupId">
<el-input v-model="queryParams.groupId" placeholder="请输入群编号" clearable class="!w-240px" />
</el-form-item>
<el-form-item label="申请人" prop="userId">
<UserSelectV2 v-model="queryParams.userId" placeholder="请选择申请人" class="!w-240px" />
</el-form-item>
<el-form-item label="邀请人" prop="inviterUserId">
<UserSelectV2 v-model="queryParams.inviterUserId" placeholder="请选择邀请人" class="!w-240px" />
</el-form-item>
<el-form-item label="处理结果" prop="handleResult">
<el-select
v-model="queryParams.handleResult"
placeholder="请选择处理结果"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IM_GROUP_REQUEST_HANDLE_RESULT)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="加入来源" prop="addSource">
<el-select
v-model="queryParams.addSource"
placeholder="请选择加入来源"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IM_GROUP_ADD_SOURCE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="编号" align="center" prop="id" width="100" />
<el-table-column label="群" align="center" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span>{{ row.groupName || '-' }}</span>
<span class="text-gray-400 ml-5px">({{ row.groupId }})</span>
</template>
</el-table-column>
<el-table-column label="申请人 / 被邀请人" align="center" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span>{{ row.userNickname || '-' }}</span>
<span class="text-gray-400 ml-5px">({{ row.userId }})</span>
</template>
</el-table-column>
<el-table-column label="邀请人" align="center" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
<template v-if="row.inviterUserId">
<span>{{ row.inviterNickname || '-' }}</span>
<span class="text-gray-400 ml-5px">({{ row.inviterUserId }})</span>
</template>
<span v-else class="text-gray-400">主动申请</span>
</template>
</el-table-column>
<el-table-column label="申请理由" align="center" prop="applyContent" min-width="160" show-overflow-tooltip />
<el-table-column label="加入来源" align="center" prop="addSource" width="120">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.IM_GROUP_ADD_SOURCE" :value="row.addSource" />
</template>
</el-table-column>
<el-table-column label="处理结果" align="center" prop="handleResult" width="100">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.IM_GROUP_REQUEST_HANDLE_RESULT" :value="row.handleResult" />
</template>
</el-table-column>
<el-table-column label="处理人" align="center" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
<template v-if="row.handleUserId">
<span>{{ row.handleNickname || '-' }}</span>
<span class="text-gray-400 ml-5px">({{ row.handleUserId }})</span>
</template>
<span v-else class="text-gray-400">-</span>
</template>
</el-table-column>
<el-table-column label="处理理由" align="center" prop="handleContent" min-width="140" show-overflow-tooltip />
<el-table-column
label="处理时间"
align="center"
prop="handleTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</template>
<script lang="ts" setup>
import { dateFormatter } from '@/utils/formatTime'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as ManagerGroupRequestApi from '@/api/im/manager/group/request'
import UserSelectV2 from '@/views/system/user/components/UserSelectV2.vue'
defineOptions({ name: 'ImGroupRequest' })
const loading = ref(true) //
const total = ref(0) //
const list = ref<ManagerGroupRequestApi.ImManagerGroupRequestVO[]>([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
groupId: undefined as number | undefined,
userId: undefined as number | undefined,
inviterUserId: undefined as number | undefined,
handleResult: undefined as number | undefined,
addSource: undefined as number | undefined,
createTime: [] as string[]
})
const queryFormRef = ref() //
/** 查询加群申请分页 */
const getList = async () => {
loading.value = true
try {
const data = await ManagerGroupRequestApi.getManagerGroupRequestPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>

View File

@ -7,6 +7,7 @@ export const ImMessageType = {
VIDEO: 104, // 视频(对应 OpenIM Video=104
FILE: 105, // 文件(对应 OpenIM File=105
CARD: 108, // 名片(对应 OpenIM Card=108
FACE: 115, // 表情贴图(对应 OpenIM Face=115Unicode emoji 仍走 TEXT
// ========== 信号类2101 / 2200 直接复用 OpenIM 段位编号2201 自有扩展) ==========
RECALL: 2101, // 撤回(对应 OpenIM RevokeNotification=2101
RECEIPT: 2200, // 回执(对应 OpenIM HasReadReceipt=2200
@ -25,14 +26,14 @@ export const ImMessageType = {
// ========== 群事件1501-1520 直接复用 OpenIM 段位编号1530+ 自有扩展段) ==========
GROUP_CREATE: 1501, // 群创建
GROUP_INFO_UPDATE: 1502, // 群信息变更NAME / NOTICE 之外字段兜底)
// 1503 GROUP_JOIN_APPLICATION TODO 未实现:入群申请
GROUP_REQUEST_RECEIVED: 1503, // 收到新的入群申请(私聊定向推送给群主 + 全部管理员)
GROUP_MEMBER_QUIT: 1504, // 成员退群
// 1505 GROUP_APPLICATION_ACCEPTED TODO 未实现:入群申请通过
// 1506 GROUP_APPLICATION_REJECTED TODO 未实现:入群申请拒绝
GROUP_REQUEST_APPROVED: 1505, // 入群申请被同意(私聊推给申请人 + 群主 + 全部管理员)
GROUP_REQUEST_REJECTED: 1506, // 入群申请被拒绝(同上)
GROUP_OWNER_TRANSFER: 1507, // 群主转让
GROUP_MEMBER_KICK: 1508, // 成员被移出
GROUP_MEMBER_INVITE: 1509, // 成员加入
// 1510 GROUP_MEMBER_ENTER TODO 未实现:自由进群
GROUP_MEMBER_ENTER: 1510, // 自由进群FREE 模式或申请通过后;全员广播)
GROUP_DISSOLVE: 1511, // 群解散
GROUP_MEMBER_MUTED: 1512, // 单成员禁言
GROUP_MEMBER_CANCEL_MUTED: 1513, // 单成员取消禁言
@ -64,6 +65,15 @@ export function isFriendNotification(type: number): boolean {
return type >= ImMessageType.FRIEND_REQUEST_APPROVED && type <= ImMessageType.FRIEND_UPDATE
}
/** 判断是否「加群申请通知事件」1503/1505/1506 走私聊通道,按段位识别 */
export function isGroupRequestNotification(type: number): boolean {
return (
type === ImMessageType.GROUP_REQUEST_RECEIVED
|| type === ImMessageType.GROUP_REQUEST_APPROVED
|| type === ImMessageType.GROUP_REQUEST_REJECTED
)
}
/** 判断是否「会话内的好友事件气泡」FRIEND_ADD / FRIEND_DELETE 直接渲染成灰色提示,与群事件同处理 */
export function isFriendChatTip(type: number): boolean {
return type === ImMessageType.FRIEND_ADD || type === ImMessageType.FRIEND_DELETE
@ -78,7 +88,7 @@ export function isFriendChatTip(type: number): boolean {
* 3. lastType / @ ConversationItem normal
* 4. MessageItem.vue canPin normal /
*
* CARD1/2/3 4 =
* CARD/ FACE1/2/3 4 =
*/
const ImMessageTypeNormals: number[] = [
ImMessageType.TEXT,
@ -86,7 +96,8 @@ const ImMessageTypeNormals: number[] = [
ImMessageType.FILE,
ImMessageType.VOICE,
ImMessageType.VIDEO,
ImMessageType.CARD
ImMessageType.CARD,
ImMessageType.FACE
]
/** 判断是否"普通消息" */
@ -146,6 +157,35 @@ export const ImGroupMemberRole = {
NORMAL: 3 // 普通成员
} as const
/** 加群方式(对齐后端 ImGroupJoinTypeEnum */
export const ImGroupJoinType = {
FREE: 0, // 自由进群
APPLY: 1, // 申请需审批,邀请直进
APPLY_AND_NORMAL_INVITE: 2 // 申请、及普通成员邀请均需审批
} as const
/** 加群方式文案 */
export const IM_GROUP_JOIN_TYPE_LABELS: Record<number, string> = {
[ImGroupJoinType.FREE]: '自由进群',
[ImGroupJoinType.APPLY]: '申请需审批',
[ImGroupJoinType.APPLY_AND_NORMAL_INVITE]: '申请、及普通成员邀请均需审批'
}
/** 加群来源(对齐后端 ImGroupAddSourceEnum */
export const ImGroupAddSource = {
SEARCH: 1, // 搜索
INVITE: 2, // 邀请
QR_CODE: 3, // 扫码
SHARE_LINK: 4 // 分享链接
} as const
/** 加群申请处理结果(对齐后端 ImGroupRequestHandleResultEnum */
export const ImGroupRequestHandleResult = {
UNHANDLED: 0, // 未处理
AGREED: 1, // 同意
REFUSED: 2 // 拒绝
} as const
/** 好友添加来源(对齐后端 ImFriendAddSourceEnum */
export const ImFriendAddSource = {
SEARCH: 1, // 搜索
@ -176,6 +216,9 @@ export const GROUP_MESSAGE_PULL_SIZE = 100
/** 「我相关」好友申请列表的单次拉取条数(游标分页 page size前端控制 */
export const FRIEND_REQUEST_PAGE_SIZE = 100
/** 「我相关」加群申请列表的单次拉取条数 */
export const GROUP_REQUEST_PAGE_SIZE = 100
/** 消息之间渲染「时间分隔条」的阈值10 分钟 */
export const TIME_TIP_GAP_MS = 10 * 60 * 1000