feat(meeting): 实现学生排序功能并优化Redis存储

- 将Redis中的学生排序存储从长ID改为短ID
- 在监控端按studentListForSort顺序重新排列学生列表
- 添加新加入学生的处理逻辑
- 实现WebSocket消息处理学生排序变更事件
- 更新类型定义支持学生排序变更数据结构
- 优化Redis服务注释说明短ID存储格式
This commit is contained in:
2026-03-14 22:35:30 +08:00
parent 5873cda8f5
commit 525281e3f8
5 changed files with 67 additions and 20 deletions

View File

@ -97,21 +97,21 @@ export class MeetingService {
// 如果提供了 roomId从 Redis 获取排序并应用 // 如果提供了 roomId从 Redis 获取排序并应用
if (roomId) { if (roomId) {
const orderedIds = await this.redis.getStudentOrder(roomId); const orderedShortIds = await this.redis.getStudentOrder(roomId);
if (orderedIds.length > 0) { if (orderedShortIds.length > 0) {
// 创建映射 // 创建短 ID 到学生的映射
const studentMap = new Map<number, StudentDetailInfo>(); const studentMap = new Map<number, StudentDetailInfo>();
finalResults.forEach((student) => { finalResults.forEach((student) => {
studentMap.set(student.longId, student); studentMap.set(student.shortId, student);
}); });
// 按 Redis 中的顺序重新排列 // 按 Redis 中的短 ID 顺序重新排列
const orderedResults: StudentDetailInfo[] = []; const orderedResults: StudentDetailInfo[] = [];
for (const longId of orderedIds) { for (const shortId of orderedShortIds) {
const student = studentMap.get(longId); const student = studentMap.get(shortId);
if (student) { if (student) {
orderedResults.push(student); orderedResults.push(student);
studentMap.delete(longId); // 移除已添加的学生 studentMap.delete(shortId); // 移除已添加的学生
} }
} }

View File

@ -74,8 +74,8 @@ export class MeetingRedisService {
* 用途:存储学生列表的排序顺序 * 用途:存储学生列表的排序顺序
* 数据结构List * 数据结构List
* Key 格式meeting:student-order:{roomId} * Key 格式meeting:student-order:{roomId}
* List 内容:按顺序存储学生的 ID (字符串) * List 内容:按顺序存储学生的 ID (数字字符串)
* 适用场景:教师端拖动排序后,监控端按相同顺序显示 * 适用场景:教师端拖动排序后,监控端和学生端按相同顺序显示
* 过期时间24 小时 * 过期时间24 小时
*/ */
STUDENT_ORDER: 'meeting:student-order:', STUDENT_ORDER: 'meeting:student-order:',
@ -518,7 +518,7 @@ export class MeetingRedisService {
/** /**
* 获取学生排序顺序 * 获取学生排序顺序
* @param roomId - 房间 ID * @param roomId - 房间 ID
* @returns 学生 ID 数组 (按排序顺序),未设置返回空数组 * @returns 学生 ID 数组 (按排序顺序),未设置返回空数组
*/ */
async getStudentOrder(roomId: string): Promise<number[]> { async getStudentOrder(roomId: string): Promise<number[]> {
const key = `${this.KEY_PREFIX.STUDENT_ORDER}${roomId}`; const key = `${this.KEY_PREFIX.STUDENT_ORDER}${roomId}`;

View File

@ -173,7 +173,31 @@
*/ */
const studentUsers = computed(() => { const studentUsers = computed(() => {
const users = meeting.remoteUsers.value as (IAgoraRTCRemoteUser & { userName?: string })[]; 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<number, typeof filteredUsers[0]>();
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; selectedUserUid.value = packet.data.targetUid;
break; break;
} }
// 学生排序变更事件 (类型定义待添加) // 学生排序变更事件
// case 'sev_student_order_changed': { case 'sev_student_order_changed': {
// const newOrder = packet.data.studentShortIds; const newOrder = packet.data.studentShortIds;
// if (Array.isArray(newOrder) && newOrder.length > 0) { if (Array.isArray(newOrder) && newOrder.length > 0) {
// console.log('[监控端] 学生排序已更新:', newOrder); console.log('[监控端] 学生排序已更新,重新获取学生列表');
// } fetchStudentList();
// break; }
// } break;
}
case 'sev_class_started': { case 'sev_class_started': {
classStatus.value = 'in_class'; classStatus.value = 'in_class';
console.log('[监控端] 收到上课消息'); console.log('[监控端] 收到上课消息');

View File

@ -518,6 +518,14 @@
} }
break; 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': { case 'sev_class_started': {
classStatus.value = 'in_class'; classStatus.value = 'in_class';

View File

@ -323,6 +323,19 @@ export interface OnSetMainVideoData extends OnControlMessageData {
targetUid: number; targetUid: number;
} }
/**
* 控制消息数据 - 学生排序变更
*/
export interface OnStudentOrderChangedData extends OnControlMessageData {
/** 学生短 ID 数组 (按排序顺序) */
studentShortIds: number[];
}
/**
* WebSocket 消息包 - 服务端下发:学生排序变更
*/
export type OnStudentOrderChangedPacket = WebSocketPacket<'sev_student_order_changed', OnStudentOrderChangedData>;
/** /**
* WebSocket 消息包 - 客户端接收:加入房间成功响应 * WebSocket 消息包 - 客户端接收:加入房间成功响应
*/ */
@ -402,6 +415,7 @@ export type DownlinkControlMessagePacket =
| OnScreenShareStartedPacket | OnScreenShareStartedPacket
| OnScreenShareStoppedPacket | OnScreenShareStoppedPacket
| OnSetMainVideoPacket | OnSetMainVideoPacket
| OnStudentOrderChangedPacket
| OnUnmuteAudioPacket | OnUnmuteAudioPacket
| OnUnmuteVideoPacket; | OnUnmuteVideoPacket;