chore: 初始化项目基础结构和资源文件

- 添加项目图标文件(app-icon.png、各平台图标)
- 配置开发环境文件(.env、.nvmrc、.npmrc)
- 添加静态资源文件(背景图片、字体、音频)
- 初始化Tauri后端结构(build.rs、main.rs、模块文件)
- 配置前端项目结构(TypeScript、Vue组件、样式)
- 添加Node.js API服务基础结构
- 配置构建和开发工具(vite、prettier、gitignore)
This commit is contained in:
2026-03-13 10:03:05 +08:00
commit 78af453fe1
357 changed files with 70605 additions and 0 deletions

View File

@ -0,0 +1,108 @@
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 };
}
}