feat: v1.1.0 迭代更新

Server:
- 添加房间密码验证 POST /rooms/:id/join
- 添加玩家踢出 POST /rooms/:id/kick/:playerId
- 添加房间过期自动清理(30分钟无活动)
- 添加流量统计 GET /traffic
- 添加token认证中间件保护写操作API
- 房间详情返回玩家列表

Client:
- 添加设置持久化(electron-store)
- 添加设置页面(玩家名、本地端口、自动重连、托盘最小化)
- 添加系统托盘支持(最小化到托盘、右键菜单)
- 添加最近连接服务器记录
- 连接成功自动保存服务器地址
- 加入房间自动填充默认端口
This commit is contained in:
FunMC
2026-02-22 23:38:41 +08:00
parent b17679cec6
commit 9649519745
9 changed files with 411 additions and 3 deletions

View File

@@ -37,6 +37,9 @@ export class Room {
public password?: string;
public nodeId: string;
public createdAt: Date;
public lastActivity: Date;
public bytesIn: number = 0;
public bytesOut: number = 0;
public players: Map<string, Player> = new Map();
private playerSockets: Map<string, net.Socket> = new Map();
@@ -61,6 +64,37 @@ export class Room {
this.nodeId = options.nodeId;
this.password = options.password;
this.createdAt = new Date();
this.lastActivity = new Date();
}
checkPassword(pwd?: string): boolean {
if (!this.password) return true;
return this.password === pwd;
}
touch(): void {
this.lastActivity = new Date();
}
addTraffic(bytesIn: number, bytesOut: number): void {
this.bytesIn += bytesIn;
this.bytesOut += bytesOut;
this.touch();
}
kickPlayer(playerId: string, reason?: string): boolean {
const player = this.players.get(playerId);
if (!player) return false;
logger.info(`Player ${player.name} kicked from room ${this.name}: ${reason || 'no reason'}`);
try { player.socket.destroy(); } catch (e) { /* ignore */ }
this.players.delete(playerId);
this.playerSockets.delete(playerId);
return true;
}
isExpired(maxIdleMs: number): boolean {
if (this.currentPlayers > 0) return false;
return Date.now() - this.lastActivity.getTime() > maxIdleMs;
}
get currentPlayers(): number {
@@ -93,6 +127,14 @@ export class Room {
}
}
getPlayerList(): { id: string; name: string; joinedAt: Date }[] {
return Array.from(this.players.values()).map(p => ({
id: p.id,
name: p.name,
joinedAt: p.joinedAt,
}));
}
toInfo(): RoomInfo {
return {
id: this.id,
@@ -184,4 +226,36 @@ export class RoomManager {
}
return total;
}
getTrafficStats(): { totalBytesIn: number; totalBytesOut: number } {
let totalBytesIn = 0;
let totalBytesOut = 0;
for (const [, room] of this.rooms) {
totalBytesIn += room.bytesIn;
totalBytesOut += room.bytesOut;
}
return { totalBytesIn, totalBytesOut };
}
cleanupExpiredRooms(maxIdleMs: number = 30 * 60 * 1000): number {
let cleaned = 0;
for (const [id, room] of this.rooms) {
if (room.isExpired(maxIdleMs)) {
logger.info(`Room ${room.name} (${id}) expired, cleaning up`);
room.destroy();
this.rooms.delete(id);
cleaned++;
}
}
return cleaned;
}
startCleanupTimer(intervalMs: number = 5 * 60 * 1000, maxIdleMs: number = 30 * 60 * 1000): void {
setInterval(() => {
const cleaned = this.cleanupExpiredRooms(maxIdleMs);
if (cleaned > 0) {
logger.info(`Cleaned up ${cleaned} expired rooms`);
}
}, intervalMs);
}
}