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:
14
node_api/src/modules/meeting/entities/meeting-user.entity.ts
Normal file
14
node_api/src/modules/meeting/entities/meeting-user.entity.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Entity, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy';
|
||||
|
||||
@Entity({ tableName: 'meeting_user' })
|
||||
export class MeetingUser {
|
||||
@PrimaryKey({ type: 'number' })
|
||||
public id!: number;
|
||||
|
||||
@Unique()
|
||||
@Property({ type: 'bigint', fieldName: 'long_user_Id' })
|
||||
public longUserId!: number;
|
||||
|
||||
@Property({ type: 'date' })
|
||||
public createdAt: Date = new Date();
|
||||
}
|
||||
92
node_api/src/modules/meeting/meeting.controller.ts
Normal file
92
node_api/src/modules/meeting/meeting.controller.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common';
|
||||
import { ApiParam, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||
import { FailResult, OkResult } from '../../common/dto/result.dto';
|
||||
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 { MeetingRedisService } from '../websocket/meeting-redis.service';
|
||||
|
||||
/**
|
||||
* 黑名单用户 DTO
|
||||
*/
|
||||
class BlacklistUserDto {
|
||||
@ApiProperty({ description: '短 UID', example: 1001 })
|
||||
shortUid?: number;
|
||||
|
||||
@ApiProperty({ description: '用户名称', example: '张三' })
|
||||
userName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会议控制器
|
||||
* 处理会议相关的 API 请求
|
||||
*/
|
||||
@ApiTags('Meeting')
|
||||
@Controller('meeting')
|
||||
export class MeetingController {
|
||||
public constructor(
|
||||
private readonly meetingService: MeetingService,
|
||||
private readonly redis: MeetingRedisService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 生成声网 RTC Token
|
||||
*/
|
||||
@Get('get-token/:channelName/:uid')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiCustomOkResponse({
|
||||
summary: '声网 RTC Token',
|
||||
model: TokenResponseDto,
|
||||
apiDescription: '声网 RTC Token 信息',
|
||||
resDescription: '声网 RTC Token 信息包含appid',
|
||||
})
|
||||
@ApiParam({ name: 'channelName', description: '需要加入的频道名(格式: `n_课程名称`)', example: 'n_1234567890' })
|
||||
@ApiParam({ name: 'uid', description: '用户的短 UID(0~4294967295)', example: '1001' })
|
||||
public async getToken(@Param('channelName', new NonEmptyStringPipe('channelName')) channelName: string, @Param('uid', new Uint32Pipe('uid')) uid: number) {
|
||||
const result = this.meetingService.generateToken(channelName, uid);
|
||||
if (!result) {
|
||||
return new FailResult('生成 Token 失败');
|
||||
}
|
||||
return new OkResult(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询房间黑名单(包含短 UID 和名称的数组)
|
||||
*/
|
||||
@Get('blacklist/:roomId')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiParam({ name: 'roomId', description: '服务端房间 ID(Socket.IO 房间名)' })
|
||||
@ApiCustomOkResponse({
|
||||
summary: '房间黑名单',
|
||||
model: [BlacklistUserDto],
|
||||
apiDescription: '房间黑名单(包含短 UID 和名称的数组)',
|
||||
resDescription: '房间黑名单(包含短 UID 和名称的数组)',
|
||||
})
|
||||
public async getBlacklist(@Param('roomId', new NonEmptyStringPipe('roomId')) roomId: string) {
|
||||
const list = await this.redis.getBlacklist(roomId);
|
||||
return new OkResult(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从房间黑名单移除指定用户(短 UID)
|
||||
*/
|
||||
@Delete('blacklist/:roomId/:shortUid')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiParam({ name: 'roomId', description: '服务端房间 ID(Socket.IO 房间名)' })
|
||||
@ApiParam({ name: 'shortUid', description: '用户短 UID', example: '1001' })
|
||||
@ApiCustomOkResponse({
|
||||
summary: '从房间黑名单移除用户',
|
||||
model: Boolean,
|
||||
apiDescription: '从房间黑名单移除指定用户(短 UID)',
|
||||
resDescription: '是否成功移除用户',
|
||||
})
|
||||
public async removeFromBlacklist(
|
||||
@Param('roomId', new NonEmptyStringPipe('roomId')) roomId: string,
|
||||
@Param('shortUid', new Uint32Pipe('shortUid')) shortUid: number
|
||||
) {
|
||||
await this.redis.removeFromBlacklist(roomId, shortUid);
|
||||
return new OkResult(true);
|
||||
}
|
||||
}
|
||||
15
node_api/src/modules/meeting/meeting.dto.ts
Normal file
15
node_api/src/modules/meeting/meeting.dto.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* Token 响应数据 DTO
|
||||
*/
|
||||
export class TokenResponseDto {
|
||||
@ApiProperty({ description: '应用 ID', example: '1234567890' })
|
||||
public appid!: string;
|
||||
|
||||
@ApiProperty({ description: '声网RTC的Token', example: '0061234567890abcdef...' })
|
||||
public rtcToken!: string;
|
||||
|
||||
@ApiProperty({ description: '过期时间戳', example: 1704067200000 })
|
||||
public expiresAt!: number;
|
||||
}
|
||||
12
node_api/src/modules/meeting/meeting.module.ts
Normal file
12
node_api/src/modules/meeting/meeting.module.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { MeetingController } from './meeting.controller';
|
||||
import { MeetingService } from './meeting.service';
|
||||
import { WebsocketModule } from '../websocket/websocket.module';
|
||||
|
||||
@Module({
|
||||
controllers: [MeetingController],
|
||||
providers: [MeetingService],
|
||||
imports: [forwardRef(() => WebsocketModule)],
|
||||
exports: [MeetingService],
|
||||
})
|
||||
export class MeetingModule {}
|
||||
108
node_api/src/modules/meeting/meeting.service.ts
Normal file
108
node_api/src/modules/meeting/meeting.service.ts
Normal 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 - 作业 ID(JoinRoomData.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 - 作业 ID(JoinRoomData.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 - 用户的真实 ID(bigint)
|
||||
* @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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user