From 525281e3f81b32c50168797615a393f3f7424280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=E6=98=B5=E7=A7=B0=E9=87=8D=E8=A6=81=E5=90=97O?= Date: Sat, 14 Mar 2026 22:35:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(meeting):=20=E5=AE=9E=E7=8E=B0=E5=AD=A6?= =?UTF-8?q?=E7=94=9F=E6=8E=92=E5=BA=8F=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96Redis=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将Redis中的学生排序存储从长ID改为短ID - 在监控端按studentListForSort顺序重新排列学生列表 - 添加新加入学生的处理逻辑 - 实现WebSocket消息处理学生排序变更事件 - 更新类型定义支持学生排序变更数据结构 - 优化Redis服务注释说明短ID存储格式 --- .../src/modules/meeting/meeting.service.ts | 16 +++---- .../websocket/meeting-redis.service.ts | 6 +-- .../meeting-room/meeting-room-monitor.vue | 43 +++++++++++++++---- .../meeting-room/meeting-room-teacher.vue | 8 ++++ src/views/meeting/meeting-room/types.ts | 14 ++++++ 5 files changed, 67 insertions(+), 20 deletions(-) diff --git a/node_api/src/modules/meeting/meeting.service.ts b/node_api/src/modules/meeting/meeting.service.ts index f73de7e..a3d25c7 100644 --- a/node_api/src/modules/meeting/meeting.service.ts +++ b/node_api/src/modules/meeting/meeting.service.ts @@ -97,21 +97,21 @@ export class MeetingService { // 如果提供了 roomId,从 Redis 获取排序并应用 if (roomId) { - const orderedIds = await this.redis.getStudentOrder(roomId); - if (orderedIds.length > 0) { - // 创建映射表 + const orderedShortIds = await this.redis.getStudentOrder(roomId); + if (orderedShortIds.length > 0) { + // 创建短 ID 到学生的映射 const studentMap = new Map(); finalResults.forEach((student) => { - studentMap.set(student.longId, student); + studentMap.set(student.shortId, student); }); - // 按 Redis 中的顺序重新排列 + // 按 Redis 中的短 ID 顺序重新排列 const orderedResults: StudentDetailInfo[] = []; - for (const longId of orderedIds) { - const student = studentMap.get(longId); + for (const shortId of orderedShortIds) { + const student = studentMap.get(shortId); if (student) { orderedResults.push(student); - studentMap.delete(longId); // 移除已添加的学生 + studentMap.delete(shortId); // 移除已添加的学生 } } diff --git a/node_api/src/modules/websocket/meeting-redis.service.ts b/node_api/src/modules/websocket/meeting-redis.service.ts index 254d0f9..2eb2eed 100644 --- a/node_api/src/modules/websocket/meeting-redis.service.ts +++ b/node_api/src/modules/websocket/meeting-redis.service.ts @@ -74,8 +74,8 @@ export class MeetingRedisService { * 用途:存储学生列表的排序顺序 * 数据结构:List * Key 格式:meeting:student-order:{roomId} - * List 内容:按顺序存储学生的长 ID (字符串) - * 适用场景:教师端拖动排序后,监控端按相同顺序显示 + * List 内容:按顺序存储学生的短 ID (数字字符串) + * 适用场景:教师端拖动排序后,监控端和学生端按相同顺序显示 * 过期时间:24 小时 */ STUDENT_ORDER: 'meeting:student-order:', @@ -518,7 +518,7 @@ export class MeetingRedisService { /** * 获取学生排序顺序 * @param roomId - 房间 ID - * @returns 学生长 ID 数组 (按排序顺序),未设置返回空数组 + * @returns 学生短 ID 数组 (按排序顺序),未设置返回空数组 */ async getStudentOrder(roomId: string): Promise { const key = `${this.KEY_PREFIX.STUDENT_ORDER}${roomId}`; diff --git a/src/views/meeting/meeting-room/meeting-room-monitor.vue b/src/views/meeting/meeting-room/meeting-room-monitor.vue index da5576d..a80d407 100644 --- a/src/views/meeting/meeting-room/meeting-room-monitor.vue +++ b/src/views/meeting/meeting-room/meeting-room-monitor.vue @@ -173,7 +173,31 @@ */ const studentUsers = computed(() => { const users = meeting.remoteUsers.value as (IAgoraRTCRemoteUser & { userName?: string })[]; - return users.filter((u) => (teacherUid.value ? u.uid !== teacherUid.value : true)); + const filteredUsers = users.filter((u) => (teacherUid.value ? u.uid !== teacherUid.value : true)); + + // 按照 studentListForSort 中的顺序重新排列 + if (studentListForSort.value.length > 0) { + // 创建 shortId -> user 的映射 + const userMap = new Map(); + filteredUsers.forEach((user) => { + userMap.set(Number(user.uid), user); + }); + + // 按照 studentListForSort 的顺序重新排列 + const sortedUsers: typeof filteredUsers = []; + studentListForSort.value.forEach((student) => { + const user = userMap.get(student.shortId); + if (user) { + sortedUsers.push(user); + userMap.delete(student.shortId); + } + }); + + // 添加 studentListForSort 中没有的用户 (新加入的学生) + return [...sortedUsers, ...Array.from(userMap.values())]; + } + + return filteredUsers; }); /** @@ -285,14 +309,15 @@ selectedUserUid.value = packet.data.targetUid; break; } - // 学生排序变更事件 (类型定义待添加) - // case 'sev_student_order_changed': { - // const newOrder = packet.data.studentShortIds; - // if (Array.isArray(newOrder) && newOrder.length > 0) { - // console.log('[监控端] 学生排序已更新:', newOrder); - // } - // break; - // } + // 学生排序变更事件 + case 'sev_student_order_changed': { + const newOrder = packet.data.studentShortIds; + if (Array.isArray(newOrder) && newOrder.length > 0) { + console.log('[监控端] 学生排序已更新,重新获取学生列表'); + fetchStudentList(); + } + break; + } case 'sev_class_started': { classStatus.value = 'in_class'; console.log('[监控端] 收到上课消息'); diff --git a/src/views/meeting/meeting-room/meeting-room-teacher.vue b/src/views/meeting/meeting-room/meeting-room-teacher.vue index 36e2cb4..f6076c0 100644 --- a/src/views/meeting/meeting-room/meeting-room-teacher.vue +++ b/src/views/meeting/meeting-room/meeting-room-teacher.vue @@ -518,6 +518,14 @@ } break; } + case 'sev_student_order_changed': { + const newOrder = packet.data.studentShortIds; + if (Array.isArray(newOrder) && newOrder.length > 0) { + console.log('[教师端] 学生排序已更新,重新获取学生列表'); + fetchStudentList(); + } + break; + } case 'sev_class_started': { classStatus.value = 'in_class'; diff --git a/src/views/meeting/meeting-room/types.ts b/src/views/meeting/meeting-room/types.ts index 815c9e4..1f37b6a 100644 --- a/src/views/meeting/meeting-room/types.ts +++ b/src/views/meeting/meeting-room/types.ts @@ -323,6 +323,19 @@ export interface OnSetMainVideoData extends OnControlMessageData { targetUid: number; } +/** + * 控制消息数据 - 学生排序变更 + */ +export interface OnStudentOrderChangedData extends OnControlMessageData { + /** 学生短 ID 数组 (按排序顺序) */ + studentShortIds: number[]; +} + +/** + * WebSocket 消息包 - 服务端下发:学生排序变更 + */ +export type OnStudentOrderChangedPacket = WebSocketPacket<'sev_student_order_changed', OnStudentOrderChangedData>; + /** * WebSocket 消息包 - 客户端接收:加入房间成功响应 */ @@ -402,6 +415,7 @@ export type DownlinkControlMessagePacket = | OnScreenShareStartedPacket | OnScreenShareStoppedPacket | OnSetMainVideoPacket + | OnStudentOrderChangedPacket | OnUnmuteAudioPacket | OnUnmuteVideoPacket;