import { Injectable } from '@nestjs/common'; import { Role, RtcTokenBuilder } from '../../plugins/shengwang/RtcTokenBuilder2'; 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'; /** * 学生信息接口 */ 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( private readonly em: EntityManager, 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) * @returns 老师长 ID,未查询到返回 null */ public async getTeacherLongUserIdByHomeworkId(homeworkId: number): Promise { const id = Number(homeworkId); if (!Number.isFinite(id) || id <= 0) { return null; } const sql = 'SELECT B.TeacherId AS teacherId FROM icr_homework AS A LEFT JOIN scs_studentgroup AS B ON A.StudentGroupId = B.Id WHERE A.Id = ? LIMIT 1'; const rows = (await this.em.getConnection().execute(sql, [id])) as Array<{ teacherId?: number | string }>; const teacherIdRaw = rows?.[0]?.teacherId; const teacherId = teacherIdRaw === undefined ? NaN : Number(teacherIdRaw); if (!Number.isFinite(teacherId) || teacherId <= 0) { return null; } return teacherId; } /** * 通过作业 ID 获取老师的短 UID * @param homeworkId - 作业 ID(JoinRoomData.homeworkId) * @returns 老师短 UID,未查询到返回 null */ public async getTeacherShortUidByHomeworkId(homeworkId: number): Promise { const teacherLongUserId = await this.getTeacherLongUserIdByHomeworkId(homeworkId); if (!teacherLongUserId) { return null; } return await this.getOrAssignShortUid(teacherLongUserId); } /** * 获取或分配用户的短 UID * @param longUserId - 用户的真实 ID(bigint) * @returns 分配的短 UID(数据库自增 ID) */ public async getOrAssignShortUid(longUserId: number | string): Promise { const userId = typeof longUserId === 'string' ? BigInt(longUserId) : BigInt(longUserId); // 尝试获取已存在的记录 const existingUser = await this.em.findOne(MeetingUser, { longUserId: Number(userId) }); if (existingUser) { // 复用已有的短 UID console.log(`[MeetingService] 用户 ${longUserId} 复用短 UID: ${existingUser.id}`); return existingUser.id; } // 创建新记录(数据库自增 ID 会自动分配) try { 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; } catch (error: any) { // 处理并发导致的唯一索引冲突 (MySQL Error 1062: Duplicate entry) if (error.code === 'ER_DUP_ENTRY' || error.message?.includes('Duplicate entry')) { console.warn(`[MeetingService] 检测到并发创建冲突,重新获取用户 ${longUserId}`); this.em.clear(); // 清除当前上下文,防止缓存干扰 const retryUser = await this.em.findOne(MeetingUser, { longUserId: Number(userId) }); if (retryUser) { return retryUser.id; } } throw error; } } public generateToken(channelName: string, uid: number): TokenResponseDto | null { // 从 Nacos 配置获取 appId 和 appCertificate const agoraConfig = this.nacosConfig.getDefaultAgoraAppConfig(); const now = Date.now(); const role = Role.PUBLISHER; const tokenExpirationInSecond = 60 * 60 * 24; // 24 小时 const privilegeExpirationInSecond = tokenExpirationInSecond; const token = RtcTokenBuilder.buildTokenWithUid( agoraConfig.appId, agoraConfig.appCertificate, channelName, // 使用清理后的频道名 uid, role, tokenExpirationInSecond, privilegeExpirationInSecond ); if (!token) { return null; } const expiresAt = now + tokenExpirationInSecond * 1000; return { appid: agoraConfig.appId, rtcToken: token, expiresAt }; } }