Files
tauri-meeting/node_api/src/modules/meeting/meeting.service.ts
O昵称重要吗O ff6dde5f4d feat(meeting): 添加通过作业ID查询学生详细信息功能
- 在MeetingController中新增getStudentDetails方法,支持通过作业ID查询学生信息
- 添加StudentDetailResponseDto和StudentListResponseDto数据传输对象
- 实现getStudentDetailsByHomeworkId服务方法,支持长短ID映射和自动分配
- 优化SQL查询逻辑,支持批量插入缺失的短ID
- 添加相应的API文档注解和参数验证
2026-03-14 14:52:18 +08:00

221 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<StudentDetailInfo[]> {
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<number, number>();
(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<StudentInfo[]> {
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 - 作业 IDJoinRoomData.homeworkId
* @returns 老师长 ID未查询到返回 null
*/
public async getTeacherLongUserIdByHomeworkId(homeworkId: number): Promise<number | null> {
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 - 作业 IDJoinRoomData.homeworkId
* @returns 老师短 UID未查询到返回 null
*/
public async getTeacherShortUidByHomeworkId(homeworkId: number): Promise<number | null> {
const teacherLongUserId = await this.getTeacherLongUserIdByHomeworkId(homeworkId);
if (!teacherLongUserId) {
return null;
}
return await this.getOrAssignShortUid(teacherLongUserId);
}
/**
* 获取或分配用户的短 UID
* @param longUserId - 用户的真实 IDbigint
* @returns 分配的短 UID数据库自增 ID
*/
public async getOrAssignShortUid(longUserId: number | string): Promise<number> {
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 };
}
}