diff --git a/node_api/src/modules/meeting/meeting.controller.ts b/node_api/src/modules/meeting/meeting.controller.ts index d30aca5..9f6af7f 100644 --- a/node_api/src/modules/meeting/meeting.controller.ts +++ b/node_api/src/modules/meeting/meeting.controller.ts @@ -5,7 +5,7 @@ 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 { TokenResponseDto } from './meeting.dto'; +import { StudentListResponseDto, TokenResponseDto } from './meeting.dto'; import { MeetingRedisService } from '../websocket/meeting-redis.service'; /** @@ -70,7 +70,7 @@ export class MeetingController { } /** - * 从房间黑名单移除指定用户(短 UID) + * 从房间黑名单移除指定用户 (短 UID) */ @Delete('blacklist/:roomId/:shortUid') @HttpCode(HttpStatus.OK) @@ -89,4 +89,24 @@ export class MeetingController { await this.redis.removeFromBlacklist(roomId, shortUid); return new OkResult(true); } + + /** + * 通过作业 ID 查询学生详细信息列表 + */ + @Get('students/:homeworkId') + @HttpCode(HttpStatus.OK) + @ApiParam({ name: 'homeworkId', description: '作业 ID', example: '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); + return new OkResult({ + total: students.length, + students, + }); + } } diff --git a/node_api/src/modules/meeting/meeting.dto.ts b/node_api/src/modules/meeting/meeting.dto.ts index 7d2940c..27841e9 100644 --- a/node_api/src/modules/meeting/meeting.dto.ts +++ b/node_api/src/modules/meeting/meeting.dto.ts @@ -7,9 +7,34 @@ export class TokenResponseDto { @ApiProperty({ description: '应用 ID', example: '1234567890' }) public appid!: string; - @ApiProperty({ description: '声网RTC的Token', example: '0061234567890abcdef...' }) + @ApiProperty({ description: '声网 RTC 的 Token', example: '0061234567890abcdef...' }) public rtcToken!: string; @ApiProperty({ description: '过期时间戳', example: 1704067200000 }) public expiresAt!: number; } + +/** + * 学生详细信息响应 DTO + */ +export class StudentDetailResponseDto { + @ApiProperty({ description: '学生长 ID (数据库 ID)', example: 1234567890 }) + longId!: number; + + @ApiProperty({ description: '学生短 ID (声网短 ID)', example: 1001 }) + shortId!: number; + + @ApiProperty({ description: '学生姓名', example: '张三' }) + studentName!: string; +} + +/** + * 学生列表响应 DTO + */ +export class StudentListResponseDto { + @ApiProperty({ description: '学生总数', example: 30 }) + total!: number; + + @ApiProperty({ description: '学生列表', type: [StudentDetailResponseDto] }) + students!: StudentDetailResponseDto[]; +} diff --git a/node_api/src/modules/meeting/meeting.service.ts b/node_api/src/modules/meeting/meeting.service.ts index 3859fa6..1ef0ba5 100644 --- a/node_api/src/modules/meeting/meeting.service.ts +++ b/node_api/src/modules/meeting/meeting.service.ts @@ -5,6 +5,28 @@ import { EntityManager } from '@mikro-orm/core'; import type { TokenResponseDto } from './meeting.dto'; import { MeetingUser } from '@/entities/MeetingUser'; +/** + * 学生信息接口 + */ +export interface StudentInfo { + /** 学生 ID */ + studentId: number; + /** 学生姓名 */ + studentName: string; +} + +/** + * 学生详细信息接口 (包含长短 ID) + */ +export interface StudentDetailInfo { + /** 学生长 ID (数据库 ID) */ + longId: number; + /** 学生短 ID (声网短 ID) */ + shortId: number; + /** 学生姓名 */ + studentName: string; +} + @Injectable() export class MeetingService { constructor( @@ -12,6 +34,96 @@ export class MeetingService { private readonly nacosConfig: NacosConfigService ) {} + /** + * 通过作业 ID 获取学生详细信息列表 (包含长短 ID) + * @param homeworkId - 作业 ID + * @returns 学生详细信息列表,包含长 ID、短 ID 和姓名 + */ + public async getStudentDetailsByHomeworkId(homeworkId: number): Promise { + const id = Number(homeworkId); + if (!Number.isFinite(id) || id <= 0) { + return []; + } + + // 查询学生信息并批量分配缺失的短 ID + const sql = ` + SELECT u.Id AS longId, u.Name AS studentName, mu.id AS shortId + FROM icr_homework h + INNER JOIN scs_studentgroup sg ON h.StudentGroupId = sg.Id + INNER JOIN scs_studentgroupdetail sgd ON sg.Id = sgd.StudentGroupId + INNER JOIN scs_user u ON sgd.StudentId = u.Id + LEFT JOIN meeting_user mu ON u.Id = mu.longUserId + WHERE h.Id = ${id} + `; + + const rows = await this.em.getConnection().execute(sql); + const results = rows as Array<{ longId: number | string; studentName: string; shortId: number | null }>; + + // 批量插入缺失的短 ID + const missingIds = results.filter((row) => row.shortId === null).map((row) => Number(row.longId)); + if (missingIds.length > 0) { + // 批量插入缺失的短 ID + await this.em.getConnection().execute(` + INSERT INTO meeting_user (longUserId, createdAt) + VALUES ${missingIds.map((longUserId) => `(${longUserId}, NOW())`).join(',')} + ON DUPLICATE KEY UPDATE longUserId = longUserId + `); + + // 查询新分配的短 ID + const newUsers = await this.em.getConnection().execute(` + SELECT longUserId, id FROM meeting_user WHERE longUserId IN (${missingIds.join(',')}) + `); + const shortIdMap = new Map(); + (newUsers as Array<{ longUserId: number; id: number }>).forEach((user) => { + shortIdMap.set(user.longUserId, user.id); + }); + + // 更新缺失的短 ID + results.forEach((row) => { + if (row.shortId === null) { + row.shortId = shortIdMap.get(Number(row.longId)) ?? null; + } + }); + } + + // 转换为最终结果 + return results.map((row) => ({ + longId: Number(row.longId), + studentName: row.studentName || '', + shortId: row.shortId ?? 0, + })); + } + + /** + * 通过作业 ID 获取学生列表 + * @param homeworkId - 作业 ID + * @returns 学生列表,包含学生 ID 和姓名 + */ + public async getStudentsByHomeworkId(homeworkId: number): Promise { + const id = Number(homeworkId); + if (!Number.isFinite(id) || id <= 0) { + return []; + } + + // 使用单条 SQL JOIN 查询 (性能最优) + const sql = ` + SELECT + u.Id AS studentId, + u.Name AS studentName + FROM icr_homework h + INNER JOIN scs_studentgroup sg ON h.StudentGroupId = sg.Id + INNER JOIN scs_studentgroupdetail sgd ON sg.Id = sgd.StudentGroupId + INNER JOIN scs_user u ON sgd.StudentId = u.Id + WHERE h.Id = ${id} + `; + + const rows = await this.em.getConnection().execute(sql); + return (rows as Array<{ studentId: number | string; studentName: string }>).map((row) => ({ + studentId: Number(row.studentId), + studentName: row.studentName || '', + })); + } + /** * 通过作业 ID 查询老师的长 ID * @param homeworkId - 作业 ID(JoinRoomData.homeworkId)