Files
tauri-meeting/node_api/src/modules/meeting/meeting.service.ts
O昵称重要吗O 78af453fe1 chore: 初始化项目基础结构和资源文件
- 添加项目图标文件(app-icon.png、各平台图标)
- 配置开发环境文件(.env、.nvmrc、.npmrc)
- 添加静态资源文件(背景图片、字体、音频)
- 初始化Tauri后端结构(build.rs、main.rs、模块文件)
- 配置前端项目结构(TypeScript、Vue组件、样式)
- 添加Node.js API服务基础结构
- 配置构建和开发工具(vite、prettier、gitignore)
2026-03-13 10:03:05 +08:00

109 lines
4.2 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 { MeetingUser } from './entities/meeting-user.entity';
import type { TokenResponseDto } from './meeting.dto';
@Injectable()
export class MeetingService {
constructor(
private readonly em: EntityManager,
private readonly nacosConfig: NacosConfigService
) {}
/**
* 通过作业 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 };
}
}