feat(meeting): 添加学生列表排序功能
- 实现学生列表拖拽排序并保存到 Redis - 添加 WebSocket 消息类型支持学生排序变更 - 在教师端界面添加学生列表排序按钮和对话框 - 修改数据传输方式以支持排序信息同步 - 更新 API 接口以传递房间 ID 获取排序数据 - 添加 Redis 存储学生排序顺序的功能
This commit is contained in:
@ -1,18 +1,14 @@
|
||||
import { HttpException, Injectable, type PipeTransform, UnprocessableEntityException } from '@nestjs/common';
|
||||
import { Injectable, type PipeTransform, UnprocessableEntityException } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class NonEmptyStringPipe implements PipeTransform<string, string> {
|
||||
public constructor(
|
||||
private readonly name?: string,
|
||||
private readonly code: 422 | 423 = 422
|
||||
) {}
|
||||
/**
|
||||
* 静态 transform 方法 (用于装饰器)
|
||||
* @param value - 待验证的值
|
||||
*/
|
||||
public transform(value: unknown): string {
|
||||
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||
const message = `${this.name ?? '参数'}不能为空`;
|
||||
if (this.code === 422) {
|
||||
throw new UnprocessableEntityException(message);
|
||||
}
|
||||
throw new HttpException(message, 423);
|
||||
throw new UnprocessableEntityException('参数不能为空');
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
@ -1,43 +1,25 @@
|
||||
import { HttpException, Injectable, type PipeTransform, UnprocessableEntityException } from '@nestjs/common';
|
||||
import { Injectable, type PipeTransform, UnprocessableEntityException } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class Uint32Pipe implements PipeTransform<string, number> {
|
||||
public constructor(
|
||||
private readonly name?: string,
|
||||
private readonly code: 422 | 423 = 422
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 静态 transform 方法 (用于装饰器)
|
||||
* @param value - 待验证的值
|
||||
*/
|
||||
public transform(value: unknown): number {
|
||||
const field = this.name ?? '参数';
|
||||
if (value === undefined || value === null || value === '') {
|
||||
const message = `${field}不能为空`;
|
||||
if (this.code === 422) {
|
||||
throw new UnprocessableEntityException(message);
|
||||
}
|
||||
throw new HttpException(message, 423);
|
||||
throw new UnprocessableEntityException('参数不能为空');
|
||||
}
|
||||
|
||||
if ((typeof value === 'string' && !/^\d+$/.test(value)) || isNaN(Number(value))) {
|
||||
const message = `${field}必须是数字`;
|
||||
if (this.code === 422) {
|
||||
throw new UnprocessableEntityException(message);
|
||||
}
|
||||
throw new HttpException(message, 423);
|
||||
throw new UnprocessableEntityException('参数必须是数字');
|
||||
}
|
||||
|
||||
const num = Number(value);
|
||||
if (!Number.isSafeInteger(num)) {
|
||||
const message = `${field}超出安全整数范围`;
|
||||
if (this.code === 422) {
|
||||
throw new UnprocessableEntityException(message);
|
||||
}
|
||||
throw new HttpException(message, 423);
|
||||
throw new UnprocessableEntityException('参数超出安全整数范围');
|
||||
} else if (num < 0 || num > 4294967295) {
|
||||
const message = `${field}必须在0到4294967295之间`;
|
||||
if (this.code === 422) {
|
||||
throw new UnprocessableEntityException(message);
|
||||
}
|
||||
throw new HttpException(message, 423);
|
||||
throw new UnprocessableEntityException('参数必须在 0 到 4294967295 之间');
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { defineConfig } from '@mikro-orm/mysql';
|
||||
import { MeetingUser } from '../modules/meeting/entities/meeting-user.entity';
|
||||
import { MeetingUser } from '../entities/MeetingUser';
|
||||
|
||||
const config = defineConfig({
|
||||
dbName: process.env.DB_NAME || 'scs',
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common';
|
||||
import { ApiParam, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, ParseIntPipe, Post, Query } from '@nestjs/common';
|
||||
import { ApiParam, ApiProperty, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { FailResult, OkResult } from '../../common/dto/result.dto';
|
||||
import { MeetingService } from './meeting.service';
|
||||
import { NonEmptyStringPipe } from '@/common/pipes/non-empty-string.pipe';
|
||||
import { Uint32Pipe } from '@/common/pipes/uint32.pipe';
|
||||
import { ApiCustomOkResponse } from '@/common/decorators/swagger.decorator';
|
||||
import { StudentListResponseDto, TokenResponseDto } from './meeting.dto';
|
||||
import { SaveStudentOrderDto, StudentListResponseDto, TokenResponseDto } from './meeting.dto';
|
||||
import { MeetingRedisService } from '../websocket/meeting-redis.service';
|
||||
import { MeetingWebSocketGateway } from '../websocket/meeting.websocket';
|
||||
|
||||
/**
|
||||
* 黑名单用户 DTO
|
||||
@ -28,7 +29,8 @@ class BlacklistUserDto {
|
||||
export class MeetingController {
|
||||
public constructor(
|
||||
private readonly meetingService: MeetingService,
|
||||
private readonly redis: MeetingRedisService
|
||||
private readonly redis: MeetingRedisService,
|
||||
private readonly websocketGateway: MeetingWebSocketGateway // 注入 WebSocket Gateway
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -44,7 +46,7 @@ export class MeetingController {
|
||||
})
|
||||
@ApiParam({ name: 'channelName', description: '需要加入的频道名(格式: `n_课程名称`)', example: 'n_1234567890' })
|
||||
@ApiParam({ name: 'uid', description: '用户的短 UID(0~4294967295)', example: '1001' })
|
||||
public async getToken(@Param('channelName', new NonEmptyStringPipe('channelName')) channelName: string, @Param('uid', new Uint32Pipe('uid')) uid: number) {
|
||||
public async getToken(@Param('channelName', NonEmptyStringPipe) channelName: string, @Param('uid', Uint32Pipe) uid: number) {
|
||||
const result = this.meetingService.generateToken(channelName, uid);
|
||||
if (!result) {
|
||||
return new FailResult('生成 Token 失败');
|
||||
@ -64,7 +66,7 @@ export class MeetingController {
|
||||
apiDescription: '房间黑名单(包含短 UID 和名称的数组)',
|
||||
resDescription: '房间黑名单(包含短 UID 和名称的数组)',
|
||||
})
|
||||
public async getBlacklist(@Param('roomId', new NonEmptyStringPipe('roomId')) roomId: string) {
|
||||
public async getBlacklist(@Param('roomId', NonEmptyStringPipe) roomId: string) {
|
||||
const list = await this.redis.getBlacklist(roomId);
|
||||
return new OkResult(list);
|
||||
}
|
||||
@ -82,10 +84,7 @@ export class MeetingController {
|
||||
apiDescription: '从房间黑名单移除指定用户(短 UID)',
|
||||
resDescription: '是否成功移除用户',
|
||||
})
|
||||
public async removeFromBlacklist(
|
||||
@Param('roomId', new NonEmptyStringPipe('roomId')) roomId: string,
|
||||
@Param('shortUid', new Uint32Pipe('shortUid')) shortUid: number
|
||||
) {
|
||||
public async removeFromBlacklist(@Param('roomId', NonEmptyStringPipe) roomId: string, @Param('shortUid', Uint32Pipe) shortUid: number) {
|
||||
await this.redis.removeFromBlacklist(roomId, shortUid);
|
||||
return new OkResult(true);
|
||||
}
|
||||
@ -93,20 +92,57 @@ export class MeetingController {
|
||||
/**
|
||||
* 通过作业 ID 查询学生详细信息列表
|
||||
*/
|
||||
@Get('students/:homeworkId')
|
||||
@Get('students')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiParam({ name: 'homeworkId', description: '作业 ID', example: '1234567890' })
|
||||
@ApiQuery({ name: 'homeworkId', description: '作业 ID', example: '1234567890' })
|
||||
@ApiQuery({ name: 'roomId', description: '房间 ID (可选,用于获取 Redis 中的排序)', required: false, example: 'n_1234567890' })
|
||||
@ApiCustomOkResponse({
|
||||
summary: '学生详细信息列表',
|
||||
model: StudentListResponseDto,
|
||||
apiDescription: '学生详细信息列表,包含长 ID、短 ID (可能为 null) 和姓名',
|
||||
resDescription: '学生详细信息列表',
|
||||
})
|
||||
public async getStudentDetails(@Param('homeworkId', new Uint32Pipe('homeworkId')) homeworkId: number) {
|
||||
const students = await this.meetingService.getStudentDetailsByHomeworkId(homeworkId);
|
||||
public async getStudentDetails(@Query('homeworkId', ParseIntPipe) homeworkId: number, @Query('roomId', NonEmptyStringPipe) roomId?: string) {
|
||||
const students = await this.meetingService.getStudentDetailsByHomeworkId(homeworkId, roomId);
|
||||
return new OkResult({
|
||||
total: students.length,
|
||||
students,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存学生排序顺序
|
||||
*/
|
||||
@Post('student-order')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiCustomOkResponse({
|
||||
summary: '保存学生排序顺序',
|
||||
model: Boolean,
|
||||
apiDescription: '保存学生排序顺序到 Redis 并发送给监控端',
|
||||
resDescription: '是否成功保存',
|
||||
})
|
||||
public async saveStudentOrder(@Body() dto: SaveStudentOrderDto) {
|
||||
// 1. 保存到 Redis
|
||||
await this.meetingService.saveStudentOrder(dto.roomId, dto.studentLongIds);
|
||||
|
||||
// 2. 获取房间内所有 socket 并过滤出监控端 (monitor 模式)
|
||||
const ns = this.websocketGateway.getServer();
|
||||
if (ns) {
|
||||
const sockets = await ns.in(dto.roomId).fetchSockets();
|
||||
const monitorSocketIds = sockets.filter((socket) => socket.data.joinMode === 'monitor').map((socket) => socket.id);
|
||||
|
||||
if (monitorSocketIds.length > 0) {
|
||||
// 发送给所有监控端的 socket
|
||||
ns.to(monitorSocketIds).emit('message', {
|
||||
type: 'sev_student_order_changed',
|
||||
data: {
|
||||
fromRoomId: dto.roomId,
|
||||
studentLongIds: dto.studentLongIds,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new OkResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,3 +38,14 @@ export class StudentListResponseDto {
|
||||
@ApiProperty({ description: '学生列表', type: [StudentDetailResponseDto] })
|
||||
students!: StudentDetailResponseDto[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存学生排序请求 DTO
|
||||
*/
|
||||
export class SaveStudentOrderDto {
|
||||
@ApiProperty({ description: '房间 ID', example: 'room_123' })
|
||||
roomId!: string;
|
||||
|
||||
@ApiProperty({ description: '学生长 ID 数组 (按排序顺序)', example: [1234567890, 1234567891, 1234567892] })
|
||||
studentLongIds!: number[];
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { NacosConfigService } from '../../plugins/nacos/nacos-config.service';
|
||||
import { EntityManager } from '@mikro-orm/core';
|
||||
import type { TokenResponseDto } from './meeting.dto';
|
||||
import { MeetingUser } from '@/entities/MeetingUser';
|
||||
import { MeetingRedisService } from '../websocket/meeting-redis.service';
|
||||
|
||||
/**
|
||||
* 学生信息接口
|
||||
@ -31,7 +32,8 @@ export interface StudentDetailInfo {
|
||||
export class MeetingService {
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly nacosConfig: NacosConfigService
|
||||
private readonly nacosConfig: NacosConfigService,
|
||||
private readonly redis: MeetingRedisService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -39,7 +41,7 @@ export class MeetingService {
|
||||
* @param homeworkId - 作业 ID
|
||||
* @returns 学生详细信息列表,包含长 ID、短 ID 和姓名
|
||||
*/
|
||||
public async getStudentDetailsByHomeworkId(homeworkId: number): Promise<StudentDetailInfo[]> {
|
||||
public async getStudentDetailsByHomeworkId(homeworkId: number, roomId?: string): Promise<StudentDetailInfo[]> {
|
||||
const id = Number(homeworkId);
|
||||
if (!Number.isFinite(id) || id <= 0) {
|
||||
return [];
|
||||
@ -87,11 +89,47 @@ export class MeetingService {
|
||||
}
|
||||
|
||||
// 转换为最终结果
|
||||
return results.map((row) => ({
|
||||
let finalResults = results.map((row) => ({
|
||||
longId: Number(row.longId),
|
||||
studentName: row.studentName || '',
|
||||
shortId: row.shortId ?? 0,
|
||||
}));
|
||||
|
||||
// 如果提供了 roomId,从 Redis 获取排序并应用
|
||||
if (roomId) {
|
||||
const orderedIds = await this.redis.getStudentOrder(roomId);
|
||||
if (orderedIds.length > 0) {
|
||||
// 创建映射表
|
||||
const studentMap = new Map<number, StudentDetailInfo>();
|
||||
finalResults.forEach((student) => {
|
||||
studentMap.set(student.longId, student);
|
||||
});
|
||||
|
||||
// 按 Redis 中的顺序重新排列
|
||||
const orderedResults: StudentDetailInfo[] = [];
|
||||
for (const longId of orderedIds) {
|
||||
const student = studentMap.get(longId);
|
||||
if (student) {
|
||||
orderedResults.push(student);
|
||||
studentMap.delete(longId); // 移除已添加的学生
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 Redis 中没有的学生 (新加入的学生)
|
||||
finalResults = [...orderedResults, ...Array.from(studentMap.values())];
|
||||
}
|
||||
}
|
||||
|
||||
return finalResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存学生排序顺序
|
||||
* @param roomId - 房间 ID
|
||||
* @param studentLongIds - 学生长 ID 数组 (按排序顺序)
|
||||
*/
|
||||
public async saveStudentOrder(roomId: string, studentLongIds: number[]): Promise<void> {
|
||||
await this.redis.setStudentOrder(roomId, studentLongIds);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -171,7 +209,7 @@ export class MeetingService {
|
||||
if (existingUser) {
|
||||
// 复用已有的短 UID
|
||||
console.log(`[MeetingService] 用户 ${longUserId} 复用短 UID: ${existingUser.id}`);
|
||||
return existingUser.id;
|
||||
return Number(existingUser.id);
|
||||
}
|
||||
|
||||
// 创建新记录(数据库自增 ID 会自动分配)
|
||||
@ -179,7 +217,7 @@ export class MeetingService {
|
||||
const newUser = this.em.create(MeetingUser, { longUserId: Number(userId), createdAt: new Date() });
|
||||
await this.em.flush();
|
||||
console.log(`[MeetingService] 为用户 ${longUserId} 分配短 UID: ${newUser.id}`);
|
||||
return newUser.id;
|
||||
return Number(newUser.id);
|
||||
} catch (error: any) {
|
||||
// 处理并发导致的唯一索引冲突 (MySQL Error 1062: Duplicate entry)
|
||||
if (error.code === 'ER_DUP_ENTRY' || error.message?.includes('Duplicate entry')) {
|
||||
@ -187,7 +225,7 @@ export class MeetingService {
|
||||
this.em.clear(); // 清除当前上下文,防止缓存干扰
|
||||
const retryUser = await this.em.findOne(MeetingUser, { longUserId: Number(userId) });
|
||||
if (retryUser) {
|
||||
return retryUser.id;
|
||||
return Number(retryUser.id);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
|
||||
@ -59,7 +59,7 @@ export class MeetingRedisService {
|
||||
* 用途:存储房间的全局状态信息
|
||||
* 数据结构:Hash
|
||||
* Key 格式:meeting:room:{roomId}
|
||||
* Hash 字段:
|
||||
* Hash 字段:
|
||||
* - classStatus: 课堂状态(not_started | in_class | finished)
|
||||
* - speakerUid: 当前主讲人短 UID(可选)
|
||||
* - teacherUid: 老师短 UID(可选)
|
||||
@ -68,6 +68,17 @@ export class MeetingRedisService {
|
||||
* 过期时间:24 小时
|
||||
*/
|
||||
ROOM_STATE: 'meeting:room:',
|
||||
|
||||
/**
|
||||
* 学生排序 Key
|
||||
* 用途:存储学生列表的排序顺序
|
||||
* 数据结构:List
|
||||
* Key 格式:meeting:student-order:{roomId}
|
||||
* List 内容:按顺序存储学生的长 ID (字符串)
|
||||
* 适用场景:教师端拖动排序后,监控端按相同顺序显示
|
||||
* 过期时间:24 小时
|
||||
*/
|
||||
STUDENT_ORDER: 'meeting:student-order:',
|
||||
};
|
||||
|
||||
constructor(private readonly redisService: RedisService) {
|
||||
@ -382,7 +393,9 @@ export class MeetingRedisService {
|
||||
const socketEntries = Array.isArray(currentState.socketEntries) ? currentState.socketEntries : [];
|
||||
const nextEntries = socketEntries.length > 0 ? socketEntries.filter((e) => e.socketId !== socketId) : [];
|
||||
const nextSocketIds =
|
||||
nextEntries.length > 0 ? [...new Set(nextEntries.map((e) => e.socketId).filter(Boolean))] : (currentState.socketIds || []).filter((id) => id !== socketId);
|
||||
nextEntries.length > 0
|
||||
? [...new Set(nextEntries.map((e) => e.socketId).filter(Boolean))]
|
||||
: (currentState.socketIds || []).filter((id) => id !== socketId);
|
||||
|
||||
if (nextSocketIds.length > 0) {
|
||||
const hm: Record<string, string> = { socketIds: JSON.stringify(nextSocketIds) };
|
||||
@ -486,6 +499,42 @@ export class MeetingRedisService {
|
||||
return !socketChecks.includes(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置学生排序顺序
|
||||
* @param roomId - 房间 ID
|
||||
* @param studentLongIds - 学生长 ID 数组 (按排序顺序)
|
||||
*/
|
||||
async setStudentOrder(roomId: string, studentLongIds: number[]): Promise<void> {
|
||||
const key = `${this.KEY_PREFIX.STUDENT_ORDER}${roomId}`;
|
||||
const client = this.getClient();
|
||||
await client.del(key); // 先删除旧数据
|
||||
if (studentLongIds.length > 0) {
|
||||
const stringIds = studentLongIds.map((id) => String(id));
|
||||
await client.rpush(key, ...stringIds);
|
||||
await client.expire(key, 24 * 60 * 60);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学生排序顺序
|
||||
* @param roomId - 房间 ID
|
||||
* @returns 学生长 ID 数组 (按排序顺序),未设置返回空数组
|
||||
*/
|
||||
async getStudentOrder(roomId: string): Promise<number[]> {
|
||||
const key = `${this.KEY_PREFIX.STUDENT_ORDER}${roomId}`;
|
||||
const ids = await this.getClient().lrange(key, 0, -1);
|
||||
return ids.map((id) => Number(id)).filter((id) => Number.isFinite(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理学生排序
|
||||
* @param roomId - 房间 ID
|
||||
*/
|
||||
async clearStudentOrder(roomId: string): Promise<void> {
|
||||
const key = `${this.KEY_PREFIX.STUDENT_ORDER}${roomId}`;
|
||||
await this.getClient().del(key);
|
||||
}
|
||||
|
||||
// ==================== 清理 ====================
|
||||
|
||||
/**
|
||||
|
||||
@ -62,6 +62,14 @@ export class MeetingWebSocketGateway implements OnGatewayConnection, OnGatewayDi
|
||||
this.logger.info({}, message, 'MeetingWebSocket');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Socket.IO Server 实例
|
||||
* @returns Socket.IO Namespace 实例
|
||||
*/
|
||||
public getServer(): MeetingNamespace | null {
|
||||
return this.server;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找目标用户的所有 Socket(本节点 + 跨节点)
|
||||
* @param roomId - 服务端房间 ID(Socket.IO 房间名)
|
||||
@ -239,7 +247,7 @@ export class MeetingWebSocketGateway implements OnGatewayConnection, OnGatewayDi
|
||||
const roomId = courseRoomId;
|
||||
|
||||
// 从鉴权后的 user 中获取 shortUid(鉴权通过就有短 ID)
|
||||
const shortUid = socket.data.user?.shortUid;
|
||||
const shortUid = Number(socket.data.user?.shortUid || 0);
|
||||
const userName = socket.data.user?.userName || data.userName || '用户';
|
||||
const isHost = socket.data.user?.role !== 0;
|
||||
const joinMode: JoinMode =
|
||||
@ -697,4 +705,36 @@ export class MeetingWebSocketGateway implements OnGatewayConnection, OnGatewayDi
|
||||
this.server?.to(data.roomId).emit('message', { type: 'sev_stop_screen_share', data: { fromRoomId: data.roomId, targetUid, screenUid } });
|
||||
this.log(`创建者停止投屏:roomId=${data.roomId}, shortUid=${targetUid}, screenUid=${screenUid}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理学生排序消息 (仅创建者)
|
||||
*/
|
||||
@SubscribeMessage<ClientToServerMessageType>('client_student_order_changed')
|
||||
public async handleStudentOrderChanged(
|
||||
@MessageBody() data: { roomId: string; studentLongIds: number[] },
|
||||
@ConnectedSocket() socket: MeetingSocket
|
||||
): Promise<void> {
|
||||
if (!data?.roomId || !Array.isArray(data.studentLongIds)) {
|
||||
return;
|
||||
}
|
||||
const isHost = socket.data.user?.role !== 0;
|
||||
if (!isHost) {
|
||||
this.logger.warn({}, `[会议] 非创建者尝试设置学生排序:userId=${socket.data.user?.userId}`, 'MeetingWebSocket');
|
||||
return;
|
||||
}
|
||||
// 保存到 Redis
|
||||
await this.redisService.setStudentOrder(data.roomId, data.studentLongIds);
|
||||
// 获取房间内所有 socket 并过滤出监控端 (monitor 模式)
|
||||
const ns = this.server;
|
||||
if (ns) {
|
||||
const sockets = await ns.in(data.roomId).fetchSockets();
|
||||
const monitorSocketIds = sockets.filter((s) => s.data.joinMode === 'monitor').map((s) => s.id);
|
||||
|
||||
if (monitorSocketIds.length > 0) {
|
||||
// 发送给所有监控端的 socket
|
||||
ns.to(monitorSocketIds).emit('message', { type: 'sev_student_order_changed', data: { fromRoomId: data.roomId, studentLongIds: data.studentLongIds } });
|
||||
}
|
||||
}
|
||||
this.log(`创建者更新了学生排序:roomId=${data.roomId}, 学生数量=${data.studentLongIds.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,6 +214,13 @@ export interface MeetingRoomEndTimestampUpdatedData {
|
||||
endTimestamp: number;
|
||||
}
|
||||
|
||||
export interface MeetingStudentOrderChangedData {
|
||||
/** 来源房间 */
|
||||
fromRoomId: string;
|
||||
/** 学生长 ID 数组 (按排序顺序) */
|
||||
studentLongIds: number[];
|
||||
}
|
||||
|
||||
export type MeetingDownlinkPacket =
|
||||
/** 统一错误包(WsExceptionFilter/业务侧主动 emit 的错误) */
|
||||
| ErrorMessage
|
||||
@ -237,6 +244,8 @@ export type MeetingDownlinkPacket =
|
||||
| MeetingWsMessagePacket<'sev_start_screen_share', MeetingScreenShareNotifyData>
|
||||
/** 停止投屏通知 */
|
||||
| MeetingWsMessagePacket<'sev_stop_screen_share', MeetingScreenShareNotifyData>
|
||||
/** 学生排序变更通知 */
|
||||
| MeetingWsMessagePacket<'sev_student_order_changed', MeetingStudentOrderChangedData>
|
||||
/** 解除禁麦通知 */
|
||||
| MeetingWsMessagePacket<'sev_unmute_audio', MeetingControlData>
|
||||
/** 解除禁视频通知 */
|
||||
@ -265,6 +274,7 @@ export type ClientToServerMessageType =
|
||||
| 'client_start_class' // 开始课程(仅创建者)
|
||||
| 'client_start_screen_share' // 开始投屏
|
||||
| 'client_stop_screen_share' // 停止投屏
|
||||
| 'client_student_order_changed' // 学生排序变更(仅创建者)
|
||||
| 'client_unmute_audio' // 解除禁麦(仅创建者)
|
||||
| 'client_unmute_video' // 解除禁视频(仅创建者)
|
||||
| 'client_renew_room_end_timestamp'; // 续期房间结束时间(仅创建者)
|
||||
@ -283,6 +293,7 @@ export type ServerToClientMessageType =
|
||||
| 'sev_set_main_video' // 设置主屏通知
|
||||
| 'sev_start_screen_share' // 开始投屏通知(广播给所有人)
|
||||
| 'sev_stop_screen_share' // 停止投屏通知(广播给所有人)
|
||||
| 'sev_student_order_changed' // 学生排序变更通知(广播给监控端)
|
||||
| 'sev_unmute_audio' // 解除禁麦通知(转发给目标用户)
|
||||
| 'sev_unmute_video' // 解除禁视频通知(转发给目标用户)
|
||||
| 'sev_room_will_expire' // 房间即将到期提醒(仅老师端)
|
||||
|
||||
Reference in New Issue
Block a user